From 525008ea8b3e7896d3c5d286ac9d2f828b20a6ba Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Sat, 11 May 2024 19:17:45 +0300 Subject: [PATCH 01/23] Interfaces folder and namespace --- src/Ocelot.Provider.Consul/Consul.cs | 1 + src/Ocelot.Provider.Consul/ConsulClientFactory.cs | 4 +++- .../ConsulFileConfigurationRepository.cs | 1 + src/Ocelot.Provider.Consul/ConsulProviderFactory.cs | 1 + .../{ => Interfaces}/IConsulClientFactory.cs | 2 +- src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs | 1 + .../Consul/ConsulFileConfigurationRepositoryTests.cs | 1 + .../Consul/ConsulServiceDiscoveryProviderTests.cs | 1 + test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs | 1 + 9 files changed, 11 insertions(+), 2 deletions(-) rename src/Ocelot.Provider.Consul/{ => Interfaces}/IConsulClientFactory.cs (68%) diff --git a/src/Ocelot.Provider.Consul/Consul.cs b/src/Ocelot.Provider.Consul/Consul.cs index 273fca2ab..e6665e965 100644 --- a/src/Ocelot.Provider.Consul/Consul.cs +++ b/src/Ocelot.Provider.Consul/Consul.cs @@ -1,5 +1,6 @@ using Ocelot.Infrastructure.Extensions; using Ocelot.Logging; +using Ocelot.Provider.Consul.Interfaces; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; diff --git a/src/Ocelot.Provider.Consul/ConsulClientFactory.cs b/src/Ocelot.Provider.Consul/ConsulClientFactory.cs index 4a7478c59..f7c5c0c0c 100644 --- a/src/Ocelot.Provider.Consul/ConsulClientFactory.cs +++ b/src/Ocelot.Provider.Consul/ConsulClientFactory.cs @@ -1,4 +1,6 @@ -namespace Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; + +namespace Ocelot.Provider.Consul; public class ConsulClientFactory : IConsulClientFactory { diff --git a/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs b/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs index 2f9569362..c95146f46 100644 --- a/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs +++ b/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs @@ -5,6 +5,7 @@ using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.Logging; +using Ocelot.Provider.Consul.Interfaces; using Ocelot.Responses; using System.Text; diff --git a/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs b/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs index c769f49c0..785938374 100644 --- a/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs +++ b/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Logging; +using Ocelot.Provider.Consul.Interfaces; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.Provider.Consul; diff --git a/src/Ocelot.Provider.Consul/IConsulClientFactory.cs b/src/Ocelot.Provider.Consul/Interfaces/IConsulClientFactory.cs similarity index 68% rename from src/Ocelot.Provider.Consul/IConsulClientFactory.cs rename to src/Ocelot.Provider.Consul/Interfaces/IConsulClientFactory.cs index 3ee3a2b25..0fe12aa08 100644 --- a/src/Ocelot.Provider.Consul/IConsulClientFactory.cs +++ b/src/Ocelot.Provider.Consul/Interfaces/IConsulClientFactory.cs @@ -1,4 +1,4 @@ -namespace Ocelot.Provider.Consul; +namespace Ocelot.Provider.Consul.Interfaces; public interface IConsulClientFactory { diff --git a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs index dac7aecff..4e52feb46 100644 --- a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Ocelot.Configuration.Repository; using Ocelot.DependencyInjection; +using Ocelot.Provider.Consul.Interfaces; namespace Ocelot.Provider.Consul; diff --git a/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs index 2b2ee6557..0f8eaf192 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs @@ -5,6 +5,7 @@ using Ocelot.Configuration.File; using Ocelot.Logging; using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; using Ocelot.Responses; using System.Text; diff --git a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs index 7df0a2c60..bec41094a 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs @@ -5,6 +5,7 @@ using Newtonsoft.Json; using Ocelot.Logging; using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; using Ocelot.Values; using ConsulProvider = Ocelot.Provider.Consul.Consul; diff --git a/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs b/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs index f71b4ffe5..d7b676a23 100644 --- a/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs @@ -3,6 +3,7 @@ using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.UnitTests.Consul; From 6ab5f8bde698d11b9c02f1639af5faaf2abd0db6 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Sat, 11 May 2024 20:29:50 +0300 Subject: [PATCH 02/23] `IConsulServiceBuilder` interface vs `ConsulServiceBuilder` class --- src/Ocelot.Provider.Consul/Consul.cs | 27 ++++++++------- .../ConsulProviderFactory.cs | 3 +- .../ConsulServiceBuilder.cs | 34 +++++++++++++++++++ .../Interfaces/IConsulServiceBuilder.cs | 8 +++++ .../ConsulServiceDiscoveryProviderTests.cs | 8 +++-- 5 files changed, 63 insertions(+), 17 deletions(-) create mode 100644 src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs create mode 100644 src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs diff --git a/src/Ocelot.Provider.Consul/Consul.cs b/src/Ocelot.Provider.Consul/Consul.cs index e6665e965..9c34d834f 100644 --- a/src/Ocelot.Provider.Consul/Consul.cs +++ b/src/Ocelot.Provider.Consul/Consul.cs @@ -1,4 +1,5 @@ -using Ocelot.Infrastructure.Extensions; +using Microsoft.Extensions.Configuration; +using Ocelot.Infrastructure.Extensions; using Ocelot.Logging; using Ocelot.Provider.Consul.Interfaces; using Ocelot.ServiceDiscovery.Providers; @@ -12,34 +13,34 @@ public class Consul : IServiceDiscoveryProvider private readonly ConsulRegistryConfiguration _config; private readonly IConsulClient _consul; private readonly IOcelotLogger _logger; + private readonly IConsulServiceBuilder _serviceBuilder; - public Consul(ConsulRegistryConfiguration config, IOcelotLoggerFactory factory, IConsulClientFactory clientFactory) + public Consul( + ConsulRegistryConfiguration config, + IOcelotLoggerFactory factory, + IConsulClientFactory clientFactory, + IConsulServiceBuilder serviceBuilder) { _config = config; _consul = clientFactory.Get(_config); _logger = factory.CreateLogger(); + _serviceBuilder = serviceBuilder; } public async Task> GetAsync() { - var queryResult = await _consul.Health.Service(_config.KeyOfServiceInConsul, string.Empty, true); - var services = new List(); + var queryResult = await _consul.Health.Service(_config.KeyOfServiceInConsul, string.Empty, true); foreach (var serviceEntry in queryResult.Response) { var service = serviceEntry.Service; if (IsValid(service)) { - var nodes = await _consul.Catalog.Nodes(); - if (nodes.Response == null) - { - services.Add(BuildService(serviceEntry, null)); - } - else + var item = await _serviceBuilder.BuildServiceAsync(_consul, _config, serviceEntry); + if (item != null) { - var serviceNode = nodes.Response.FirstOrDefault(n => n.Address == service.Address); - services.Add(BuildService(serviceEntry, serviceNode)); + services.Add(item); } } else @@ -49,7 +50,7 @@ public async Task> GetAsync() } } - return services.ToList(); + return services; } private static Service BuildService(ServiceEntry serviceEntry, Node serviceNode) diff --git a/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs b/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs index 785938374..b65e4ddaa 100644 --- a/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs +++ b/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs @@ -23,11 +23,12 @@ private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provide { var factory = provider.GetService(); var consulFactory = provider.GetService(); + var serviceBuilder = provider.GetService(); var consulRegistryConfiguration = new ConsulRegistryConfiguration( config.Scheme, config.Host, config.Port, route.ServiceName, config.Token); - var consulProvider = new Consul(consulRegistryConfiguration, factory, consulFactory); + var consulProvider = new Consul(consulRegistryConfiguration, factory, consulFactory, serviceBuilder); if (PollConsul.Equals(config.Type, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs b/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs new file mode 100644 index 000000000..f0542b048 --- /dev/null +++ b/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs @@ -0,0 +1,34 @@ +using Ocelot.Infrastructure.Extensions; +using Ocelot.Provider.Consul.Interfaces; +using Ocelot.Values; + +namespace Ocelot.Provider.Consul; + +public class ConsulServiceBuilder : IConsulServiceBuilder +{ + public async Task BuildServiceAsync(IConsulClient client, ConsulRegistryConfiguration configuration, ServiceEntry entry) + { + var nodes = await client.Catalog.Nodes(); + Node serviceNode = nodes?.Response?.FirstOrDefault(n => n.Address == entry.Service.Address); + return CreateService(entry, serviceNode); + } + + private static Service CreateService(ServiceEntry serviceEntry, Node serviceNode) + { + var service = serviceEntry.Service; + return new Service( + service.Service, + new ServiceHostAndPort( + serviceNode == null ? service.Address : serviceNode.Name, + service.Port), + service.ID, + GetVersionFromStrings(service.Tags), + service.Tags ?? Enumerable.Empty()); + } + + private const string VersionPrefix = "version-"; + + private static string GetVersionFromStrings(IEnumerable strings) + => strings?.FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal)) + .TrimStart(VersionPrefix); +} diff --git a/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs b/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs new file mode 100644 index 000000000..a14d1322f --- /dev/null +++ b/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs @@ -0,0 +1,8 @@ +using Ocelot.Values; + +namespace Ocelot.Provider.Consul.Interfaces; + +public interface IConsulServiceBuilder +{ + Task BuildServiceAsync(IConsulClient client, ConsulRegistryConfiguration configuration, ServiceEntry entry); +} diff --git a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs index bec41094a..467f0edf6 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs @@ -26,6 +26,7 @@ public class ConsulServiceDiscoveryProviderTests : UnitTest, IDisposable private readonly Mock _logger; private string _receivedToken; private readonly IConsulClientFactory _clientFactory; + private readonly IConsulServiceBuilder _serviceBuilder; public ConsulServiceDiscoveryProviderTests() { @@ -36,12 +37,13 @@ public ConsulServiceDiscoveryProviderTests() _fakeConsulServiceDiscoveryUrl = $"{_consulScheme}://{_consulHost}:{_port}"; _serviceEntries = new List(); _factory = new Mock(); - _clientFactory = new ConsulClientFactory(); _logger = new Mock(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, _serviceName, null); - _provider = new ConsulProvider(config, _factory.Object, _clientFactory); + _clientFactory = new ConsulClientFactory(); + _serviceBuilder = new ConsulServiceBuilder(); + _provider = new ConsulProvider(config, _factory.Object, _clientFactory, _serviceBuilder); } [Fact] @@ -71,7 +73,7 @@ public void should_use_token() { var token = "test token"; var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, _serviceName, token); - _provider = new ConsulProvider(config, _factory.Object, _clientFactory); + _provider = new ConsulProvider(config, _factory.Object, _clientFactory, _serviceBuilder); var serviceEntryOne = new ServiceEntry { From cfeecff46fb448fd2f560c02376b0538829b47ab Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Tue, 14 May 2024 13:05:29 +0300 Subject: [PATCH 03/23] Inject services into `ConsulServiceBuilder` --- src/Ocelot.Provider.Consul/Consul.cs | 49 +++++++------------ .../ConsulProviderFactory.cs | 11 +++-- .../ConsulServiceBuilder.cs | 34 +++++++++++-- .../Interfaces/IConsulServiceBuilder.cs | 3 +- .../OcelotBuilderExtensions.cs | 2 + .../ConsulServiceDiscoveryProviderTests.cs | 2 +- 6 files changed, 62 insertions(+), 39 deletions(-) diff --git a/src/Ocelot.Provider.Consul/Consul.cs b/src/Ocelot.Provider.Consul/Consul.cs index 9c34d834f..09c2da936 100644 --- a/src/Ocelot.Provider.Consul/Consul.cs +++ b/src/Ocelot.Provider.Consul/Consul.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.Configuration; -using Ocelot.Infrastructure.Extensions; -using Ocelot.Logging; +using Ocelot.Logging; using Ocelot.Provider.Consul.Interfaces; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; @@ -9,7 +7,6 @@ namespace Ocelot.Provider.Consul; public class Consul : IServiceDiscoveryProvider { - private const string VersionPrefix = "version-"; private readonly ConsulRegistryConfiguration _config; private readonly IConsulClient _consul; private readonly IOcelotLogger _logger; @@ -27,17 +24,22 @@ public Consul( _serviceBuilder = serviceBuilder; } - public async Task> GetAsync() + public virtual async Task> GetAsync() { var services = new List(); - var queryResult = await _consul.Health.Service(_config.KeyOfServiceInConsul, string.Empty, true); + var entriesTask = _consul.Health.Service(_config.KeyOfServiceInConsul, string.Empty, true); + var nodesTask = _consul.Catalog.Nodes(); - foreach (var serviceEntry in queryResult.Response) + await Task.WhenAll(entriesTask, nodesTask); + + var entries = entriesTask.Result.Response; + var nodes = nodesTask.Result.Response; + + foreach (var serviceEntry in entries) { - var service = serviceEntry.Service; - if (IsValid(service)) + if (IsValid(serviceEntry)) { - var item = await _serviceBuilder.BuildServiceAsync(_consul, _config, serviceEntry); + var item = _serviceBuilder.BuildService(serviceEntry, nodes); if (item != null) { services.Add(item); @@ -45,6 +47,7 @@ public async Task> GetAsync() } else { + var service = serviceEntry.Service; _logger.LogWarning( () => $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."); } @@ -53,26 +56,12 @@ public async Task> GetAsync() return services; } - private static Service BuildService(ServiceEntry serviceEntry, Node serviceNode) + protected virtual bool IsValid(ServiceEntry entry) { - var service = serviceEntry.Service; - return new Service( - service.Service, - new ServiceHostAndPort( - serviceNode == null ? service.Address : serviceNode.Name, - service.Port), - service.ID, - GetVersionFromStrings(service.Tags), - service.Tags ?? Enumerable.Empty()); + var address = entry.Service.Address; + return !string.IsNullOrEmpty(address) + && !address.Contains($"{Uri.UriSchemeHttp}://") + && !address.Contains($"{Uri.UriSchemeHttps}://") + && entry.Service.Port > 0; } - - private static bool IsValid(AgentService service) - => !string.IsNullOrEmpty(service.Address) - && !service.Address.Contains($"{Uri.UriSchemeHttp}://") - && !service.Address.Contains($"{Uri.UriSchemeHttps}://") - && service.Port > 0; - - private static string GetVersionFromStrings(IEnumerable strings) - => strings?.FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal)) - .TrimStart(VersionPrefix); } diff --git a/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs b/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs index b65e4ddaa..00c2715ee 100644 --- a/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs +++ b/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs @@ -18,17 +18,20 @@ public static class ConsulProviderFactory public static ServiceDiscoveryFinderDelegate Get { get; } = CreateProvider; + private static ConsulRegistryConfiguration configuration; + private static ConsulRegistryConfiguration ConfigurationGetter() => configuration; + public static Func GetConfiguration { get; } = ConfigurationGetter; + private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route) { var factory = provider.GetService(); var consulFactory = provider.GetService(); - var serviceBuilder = provider.GetService(); - var consulRegistryConfiguration = new ConsulRegistryConfiguration( - config.Scheme, config.Host, config.Port, route.ServiceName, config.Token); + configuration = new ConsulRegistryConfiguration(config.Scheme, config.Host, config.Port, route.ServiceName, config.Token); + var serviceBuilder = provider.GetService(); - var consulProvider = new Consul(consulRegistryConfiguration, factory, consulFactory, serviceBuilder); + var consulProvider = new Consul(configuration, factory, consulFactory, serviceBuilder); if (PollConsul.Equals(config.Type, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs b/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs index f0542b048..f99e83d4b 100644 --- a/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs +++ b/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs @@ -1,4 +1,5 @@ using Ocelot.Infrastructure.Extensions; +using Ocelot.Logging; using Ocelot.Provider.Consul.Interfaces; using Ocelot.Values; @@ -6,10 +7,37 @@ namespace Ocelot.Provider.Consul; public class ConsulServiceBuilder : IConsulServiceBuilder { - public async Task BuildServiceAsync(IConsulClient client, ConsulRegistryConfiguration configuration, ServiceEntry entry) + private readonly ConsulRegistryConfiguration _configuration; + private readonly IConsulClient _client; + private readonly IOcelotLogger _logger; + + public ConsulServiceBuilder( + Func configurationFactory, + IConsulClientFactory clientFactory, + IOcelotLoggerFactory loggerFactory) + { + _configuration = configurationFactory.Invoke(); + _client = clientFactory.Get(_configuration); + _logger = loggerFactory.CreateLogger(); + } + + public virtual Service BuildService(ServiceEntry entry, IEnumerable nodes) + { + ArgumentNullException.ThrowIfNull(entry); + nodes ??= _client.Catalog.Nodes().Result?.Response; + return BuildServiceInternal(entry, nodes); + } + + public virtual async Task BuildServiceAsync(ServiceEntry entry, IEnumerable nodes) + { + ArgumentNullException.ThrowIfNull(entry); + nodes ??= (await _client.Catalog.Nodes())?.Response; + return BuildServiceInternal(entry, nodes); + } + + protected virtual Service BuildServiceInternal(ServiceEntry entry, IEnumerable nodes) { - var nodes = await client.Catalog.Nodes(); - Node serviceNode = nodes?.Response?.FirstOrDefault(n => n.Address == entry.Service.Address); + var serviceNode = nodes?.FirstOrDefault(n => n.Address == entry.Service.Address); return CreateService(entry, serviceNode); } diff --git a/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs b/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs index a14d1322f..0823dd266 100644 --- a/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs +++ b/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs @@ -4,5 +4,6 @@ namespace Ocelot.Provider.Consul.Interfaces; public interface IConsulServiceBuilder { - Task BuildServiceAsync(IConsulClient client, ConsulRegistryConfiguration configuration, ServiceEntry entry); + Service BuildService(ServiceEntry entry, IEnumerable nodes); + Task BuildServiceAsync(ServiceEntry entry, IEnumerable nodes); } diff --git a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs index 4e52feb46..6151b2562 100644 --- a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs @@ -12,7 +12,9 @@ public static IOcelotBuilder AddConsul(this IOcelotBuilder builder) { builder.Services .AddSingleton(ConsulProviderFactory.Get) + .AddSingleton(ConsulProviderFactory.GetConfiguration) .AddSingleton() + .AddSingleton() .RemoveAll(typeof(IFileConfigurationPollerOptions)) .AddSingleton(); return builder; diff --git a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs index 467f0edf6..eccb77170 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs @@ -42,7 +42,7 @@ public ConsulServiceDiscoveryProviderTests() _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, _serviceName, null); _clientFactory = new ConsulClientFactory(); - _serviceBuilder = new ConsulServiceBuilder(); + _serviceBuilder = new ConsulServiceBuilder(() => config, _clientFactory, _factory.Object); _provider = new ConsulProvider(config, _factory.Object, _clientFactory, _serviceBuilder); } From 6449bc40820d6fe6221e7fc09307151cd894ca2a Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Wed, 15 May 2024 15:39:50 +0300 Subject: [PATCH 04/23] Extend `IConsulServiceBuilder` interface --- src/Ocelot.Provider.Consul/Consul.cs | 48 ++++------- .../ConsulServiceBuilder.cs | 85 +++++++++++++------ .../Interfaces/IConsulServiceBuilder.cs | 6 +- .../ConsulServiceDiscoveryProviderTests.cs | 1 + 4 files changed, 80 insertions(+), 60 deletions(-) diff --git a/src/Ocelot.Provider.Consul/Consul.cs b/src/Ocelot.Provider.Consul/Consul.cs index 09c2da936..27b5b4422 100644 --- a/src/Ocelot.Provider.Consul/Consul.cs +++ b/src/Ocelot.Provider.Consul/Consul.cs @@ -7,7 +7,7 @@ namespace Ocelot.Provider.Consul; public class Consul : IServiceDiscoveryProvider { - private readonly ConsulRegistryConfiguration _config; + private readonly ConsulRegistryConfiguration _configuration; private readonly IConsulClient _consul; private readonly IOcelotLogger _logger; private readonly IConsulServiceBuilder _serviceBuilder; @@ -18,50 +18,38 @@ public Consul( IConsulClientFactory clientFactory, IConsulServiceBuilder serviceBuilder) { - _config = config; - _consul = clientFactory.Get(_config); + _configuration = config; + _consul = clientFactory.Get(_configuration); _logger = factory.CreateLogger(); _serviceBuilder = serviceBuilder; } public virtual async Task> GetAsync() { - var services = new List(); - var entriesTask = _consul.Health.Service(_config.KeyOfServiceInConsul, string.Empty, true); + var entriesTask = _consul.Health.Service(_configuration.KeyOfServiceInConsul, string.Empty, true); var nodesTask = _consul.Catalog.Nodes(); await Task.WhenAll(entriesTask, nodesTask); - var entries = entriesTask.Result.Response; - var nodes = nodesTask.Result.Response; + var entries = entriesTask.Result.Response ?? Array.Empty(); + var nodes = nodesTask.Result.Response ?? Array.Empty(); + var services = new List(); - foreach (var serviceEntry in entries) + if (entries.Length != 0) + { + _logger.LogDebug(() => $"{nameof(Consul)} Provider: Found total {entries.Length} service entries for '{_configuration.KeyOfServiceInConsul}' service."); + _logger.LogDebug(() => $"{nameof(Consul)} Provider: Found total {nodes.Length} catalog nodes."); + var collection = BuildServices(entries, nodes); + services.AddRange(collection); + } + else { - if (IsValid(serviceEntry)) - { - var item = _serviceBuilder.BuildService(serviceEntry, nodes); - if (item != null) - { - services.Add(item); - } - } - else - { - var service = serviceEntry.Service; - _logger.LogWarning( - () => $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."); - } + _logger.LogWarning(() => $"{nameof(Consul)} Provider: No service entries found for '{_configuration.KeyOfServiceInConsul}' service!"); } return services; } - protected virtual bool IsValid(ServiceEntry entry) - { - var address = entry.Service.Address; - return !string.IsNullOrEmpty(address) - && !address.Contains($"{Uri.UriSchemeHttp}://") - && !address.Contains($"{Uri.UriSchemeHttps}://") - && entry.Service.Port > 0; - } + protected virtual IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes) + => _serviceBuilder.BuildServices(entries, nodes); } diff --git a/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs b/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs index f99e83d4b..d261f8b5e 100644 --- a/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs +++ b/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs @@ -21,42 +21,71 @@ public ConsulServiceBuilder( _logger = loggerFactory.CreateLogger(); } - public virtual Service BuildService(ServiceEntry entry, IEnumerable nodes) - { - ArgumentNullException.ThrowIfNull(entry); - nodes ??= _client.Catalog.Nodes().Result?.Response; - return BuildServiceInternal(entry, nodes); - } + public ConsulRegistryConfiguration Configuration => _configuration; - public virtual async Task BuildServiceAsync(ServiceEntry entry, IEnumerable nodes) + public virtual bool IsValid(ServiceEntry entry) { - ArgumentNullException.ThrowIfNull(entry); - nodes ??= (await _client.Catalog.Nodes())?.Response; - return BuildServiceInternal(entry, nodes); + var address = entry.Service.Address; + return !string.IsNullOrEmpty(address) + && !address.Contains($"{Uri.UriSchemeHttp}://") + && !address.Contains($"{Uri.UriSchemeHttps}://") + && entry.Service.Port > 0; } - protected virtual Service BuildServiceInternal(ServiceEntry entry, IEnumerable nodes) + public virtual IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes) { - var serviceNode = nodes?.FirstOrDefault(n => n.Address == entry.Service.Address); - return CreateService(entry, serviceNode); - } + ArgumentNullException.ThrowIfNull(entries); + var services = new List(); - private static Service CreateService(ServiceEntry serviceEntry, Node serviceNode) - { - var service = serviceEntry.Service; - return new Service( - service.Service, - new ServiceHostAndPort( - serviceNode == null ? service.Address : serviceNode.Name, - service.Port), - service.ID, - GetVersionFromStrings(service.Tags), - service.Tags ?? Enumerable.Empty()); + foreach (var serviceEntry in entries) + { + var service = serviceEntry.Service; + if (IsValid(serviceEntry)) + { + var serviceNode = nodes?.FirstOrDefault(n => n.Address == service.Address); + var item = CreateService(serviceEntry, serviceNode); + if (item != null) + { + services.Add(item); + } + } + else + { + _logger.LogWarning( + () => $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."); + } + } + + return services; } - private const string VersionPrefix = "version-"; + public virtual Service CreateService(ServiceEntry entry, Node node) + => new( + GetServiceName(entry, node), + GetServiceHostAndPort(entry, node), + GetServiceId(entry, node), + GetServiceVersion(entry, node), + GetServiceTags(entry, node) + ); - private static string GetVersionFromStrings(IEnumerable strings) - => strings?.FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal)) + protected virtual string GetServiceName(ServiceEntry entry, Node node) + => entry.Service.Service; + + protected virtual ServiceHostAndPort GetServiceHostAndPort(ServiceEntry entry, Node node) + => new( + downstreamHost: node != null ? node.Name : entry.Service.Address, + downstreamPort: entry.Service.Port); + + protected virtual string GetServiceId(ServiceEntry entry, Node serviceNode) + => entry.Service.ID; + + protected virtual string GetServiceVersion(ServiceEntry entry, Node serviceNode) + => entry.Service.Tags? + .FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal)) .TrimStart(VersionPrefix); + + protected virtual IEnumerable GetServiceTags(ServiceEntry entry, Node serviceNode) + => entry.Service.Tags ?? Enumerable.Empty(); + + private const string VersionPrefix = "version-"; } diff --git a/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs b/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs index 0823dd266..0555b0144 100644 --- a/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs +++ b/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs @@ -4,6 +4,8 @@ namespace Ocelot.Provider.Consul.Interfaces; public interface IConsulServiceBuilder { - Service BuildService(ServiceEntry entry, IEnumerable nodes); - Task BuildServiceAsync(ServiceEntry entry, IEnumerable nodes); + ConsulRegistryConfiguration Configuration { get; } + bool IsValid(ServiceEntry entry); + IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes); + Service CreateService(ServiceEntry serviceEntry, Node serviceNode); } diff --git a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs index eccb77170..ff69b85af 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs @@ -40,6 +40,7 @@ public ConsulServiceDiscoveryProviderTests() _logger = new Mock(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, _serviceName, null); _clientFactory = new ConsulClientFactory(); _serviceBuilder = new ConsulServiceBuilder(() => config, _clientFactory, _factory.Object); From c6eeebef4e0d7083a350bbe35d6fc9e28d2983ce Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Wed, 15 May 2024 16:08:17 +0300 Subject: [PATCH 05/23] Finalize design of the customization. No JSON options because `ServiceDiscoveryProvider` are generalized for all types of providers --- src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs b/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs index d261f8b5e..8cebb07f4 100644 --- a/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs +++ b/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs @@ -22,6 +22,8 @@ public ConsulServiceBuilder( } public ConsulRegistryConfiguration Configuration => _configuration; + protected IConsulClient Client => _client; + protected IOcelotLogger Logger => _logger; public virtual bool IsValid(ServiceEntry entry) { @@ -42,7 +44,7 @@ public virtual IEnumerable BuildServices(ServiceEntry[] entries, Node[] var service = serviceEntry.Service; if (IsValid(serviceEntry)) { - var serviceNode = nodes?.FirstOrDefault(n => n.Address == service.Address); + var serviceNode = GetNode(serviceEntry, nodes); var item = CreateService(serviceEntry, serviceNode); if (item != null) { @@ -59,6 +61,9 @@ public virtual IEnumerable BuildServices(ServiceEntry[] entries, Node[] return services; } + protected virtual Node GetNode(ServiceEntry entry, Node[] nodes) + => nodes?.FirstOrDefault(n => n.Address == entry?.Service?.Address); + public virtual Service CreateService(ServiceEntry entry, Node node) => new( GetServiceName(entry, node), @@ -73,8 +78,11 @@ protected virtual string GetServiceName(ServiceEntry entry, Node node) protected virtual ServiceHostAndPort GetServiceHostAndPort(ServiceEntry entry, Node node) => new( - downstreamHost: node != null ? node.Name : entry.Service.Address, - downstreamPort: entry.Service.Port); + GetDownstreamHost(entry, node), + entry.Service.Port); + + protected virtual string GetDownstreamHost(ServiceEntry entry, Node node) + => node != null ? node.Name : entry.Service.Address; protected virtual string GetServiceId(ServiceEntry entry, Node serviceNode) => entry.Service.ID; From 903388396cc307476a41aa4a3412c1263334df25 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Wed, 15 May 2024 19:07:49 +0300 Subject: [PATCH 06/23] Remove BDDfy in favor of AAA pattern --- .../ConsulServiceDiscoveryProviderTests.cs | 84 +++++++++++-------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs index ff69b85af..7d7b71314 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs @@ -48,8 +48,9 @@ public ConsulServiceDiscoveryProviderTests() } [Fact] - public void should_return_service_from_consul() + public void Should_return_service_from_consul() { + // Arrange var serviceEntryOne = new ServiceEntry { Service = new AgentService @@ -61,21 +62,23 @@ public void should_return_service_from_consul() Tags = Array.Empty(), }, }; + GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName); + GivenTheServicesAreRegisteredWithConsul(serviceEntryOne); - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) - .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .When(x => WhenIGetTheServices()) - .Then(x => ThenTheCountIs(1)) - .BDDfy(); + // Act + WhenIGetTheServices(); + + // Assert + ThenTheCountIs(1); } [Fact] - public void should_use_token() + public void Should_use_token() { + // Arrange var token = "test token"; var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, _serviceName, token); _provider = new ConsulProvider(config, _factory.Object, _clientFactory, _serviceBuilder); - var serviceEntryOne = new ServiceEntry { Service = new AgentService @@ -87,63 +90,74 @@ public void should_use_token() Tags = Array.Empty(), }, }; + GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName); + GivenTheServicesAreRegisteredWithConsul(serviceEntryOne); + + // Act + WhenIGetTheServices(); - this.Given(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) - .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .When(_ => WhenIGetTheServices()) - .Then(_ => ThenTheCountIs(1)) - .And(_ => ThenTheTokenIs(token)) - .BDDfy(); + // Assert + ThenTheCountIs(1); + ThenTheTokenIs(token); } [Fact] - public void should_not_return_services_with_invalid_address() + public void Should_not_return_services_with_invalid_address() { + // Arrange var serviceEntryOne = GivenService(address: "http://localhost", port: 50881) .ToServiceEntry(); var serviceEntryTwo = GivenService(address: "http://localhost", port: 50888) .ToServiceEntry(); + GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName); + GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo); - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) - .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .When(x => WhenIGetTheServices()) - .Then(x => ThenTheCountIs(0)) - .And(x => ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(serviceEntryOne, serviceEntryTwo)) - .BDDfy(); + // Act + WhenIGetTheServices(); + + // Assert + ThenTheCountIs(0); + ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(serviceEntryOne, serviceEntryTwo); } [Fact] - public void should_not_return_services_with_empty_address() + public void Should_not_return_services_with_empty_address() { + // Arrange var serviceEntryOne = GivenService(port: 50881) .WithAddress(string.Empty) .ToServiceEntry(); var serviceEntryTwo = GivenService(port: 50888) .WithAddress(null) .ToServiceEntry(); + GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName); + GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo); + + // Act + WhenIGetTheServices(); - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) - .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .When(x => WhenIGetTheServices()) - .Then(x => ThenTheCountIs(0)) - .And(x => ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(serviceEntryOne, serviceEntryTwo)) - .BDDfy(); + // Assert + ThenTheCountIs(0); + ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(serviceEntryOne, serviceEntryTwo); } [Fact] - public void should_not_return_services_with_invalid_port() + public void Should_not_return_services_with_invalid_port() { + // Arrange var serviceEntryOne = GivenService(port: -1) .ToServiceEntry(); var serviceEntryTwo = GivenService(port: 0) .ToServiceEntry(); + GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName); + GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo); + + // Act + WhenIGetTheServices(); - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) - .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .When(x => WhenIGetTheServices()) - .Then(x => ThenTheCountIs(0)) - .And(x => ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(serviceEntryOne, serviceEntryTwo)) - .BDDfy(); + // Assert + ThenTheCountIs(0); + ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(serviceEntryOne, serviceEntryTwo); } private AgentService GivenService(string address = null, int? port = null, string id = null, string[] tags = null) From 320e8f9aa74b8e5891dc9519907d66d8058d1b4d Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Wed, 15 May 2024 20:36:14 +0300 Subject: [PATCH 07/23] Refactor original unit tests --- .../ConsulServiceDiscoveryProviderTests.cs | 369 ++++++++---------- 1 file changed, 160 insertions(+), 209 deletions(-) diff --git a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs index 7d7b71314..47b644bad 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs @@ -6,236 +6,187 @@ using Ocelot.Logging; using Ocelot.Provider.Consul; using Ocelot.Provider.Consul.Interfaces; -using Ocelot.Values; +using System.Runtime.CompilerServices; using ConsulProvider = Ocelot.Provider.Consul.Consul; -namespace Ocelot.UnitTests.Consul +namespace Ocelot.UnitTests.Consul; + +public sealed class ConsulServiceDiscoveryProviderTests : UnitTest, IDisposable { - public class ConsulServiceDiscoveryProviderTests : UnitTest, IDisposable + private readonly int _port; + private readonly string _consulHost; + private readonly string _consulScheme; + private readonly string _fakeConsulServiceDiscoveryUrl; + private readonly List _consulServiceEntries; + private readonly Mock _factory; + private readonly Mock _logger; + private IConsulClientFactory _clientFactory; + private IConsulServiceBuilder _serviceBuilder; + private ConsulRegistryConfiguration _config; + private IWebHost _fakeConsulBuilder; + private ConsulProvider _provider; + private string _receivedToken; + + public ConsulServiceDiscoveryProviderTests() { - private IWebHost _fakeConsulBuilder; - private readonly List _serviceEntries; - private ConsulProvider _provider; - private readonly string _serviceName; - private readonly int _port; - private readonly string _consulHost; - private readonly string _consulScheme; - private readonly string _fakeConsulServiceDiscoveryUrl; - private List _services; - private readonly Mock _factory; - private readonly Mock _logger; - private string _receivedToken; - private readonly IConsulClientFactory _clientFactory; - private readonly IConsulServiceBuilder _serviceBuilder; - - public ConsulServiceDiscoveryProviderTests() - { - _serviceName = "test"; - _port = 8500; - _consulHost = "localhost"; - _consulScheme = "http"; - _fakeConsulServiceDiscoveryUrl = $"{_consulScheme}://{_consulHost}:{_port}"; - _serviceEntries = new List(); - _factory = new Mock(); - _logger = new Mock(); - _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, _serviceName, null); - _clientFactory = new ConsulClientFactory(); - _serviceBuilder = new ConsulServiceBuilder(() => config, _clientFactory, _factory.Object); - _provider = new ConsulProvider(config, _factory.Object, _clientFactory, _serviceBuilder); - } + _port = 8500; + _consulHost = "localhost"; + _consulScheme = "http"; + _fakeConsulServiceDiscoveryUrl = $"{_consulScheme}://{_consulHost}:{_port}"; + _consulServiceEntries = new List(); + _factory = new Mock(); + _logger = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + } - [Fact] - public void Should_return_service_from_consul() - { - // Arrange - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = _serviceName, - Address = "localhost", - Port = 50881, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName); - GivenTheServicesAreRegisteredWithConsul(serviceEntryOne); - - // Act - WhenIGetTheServices(); - - // Assert - ThenTheCountIs(1); - } + public void Dispose() + { + _fakeConsulBuilder?.Dispose(); + } - [Fact] - public void Should_use_token() - { - // Arrange - var token = "test token"; - var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, _serviceName, token); - _provider = new ConsulProvider(config, _factory.Object, _clientFactory, _serviceBuilder); - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = _serviceName, - Address = "localhost", - Port = 50881, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName); - GivenTheServicesAreRegisteredWithConsul(serviceEntryOne); - - // Act - WhenIGetTheServices(); - - // Assert - ThenTheCountIs(1); - ThenTheTokenIs(token); - } + private void Arrange([CallerMemberName] string serviceName = null) + { + _config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, serviceName, null); + _clientFactory = new ConsulClientFactory(); + _serviceBuilder = new ConsulServiceBuilder(() => _config, _clientFactory, _factory.Object); + _provider = new ConsulProvider(_config, _factory.Object, _clientFactory, _serviceBuilder); + } - [Fact] - public void Should_not_return_services_with_invalid_address() - { - // Arrange - var serviceEntryOne = GivenService(address: "http://localhost", port: 50881) - .ToServiceEntry(); - var serviceEntryTwo = GivenService(address: "http://localhost", port: 50888) - .ToServiceEntry(); - GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName); - GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo); - - // Act - WhenIGetTheServices(); - - // Assert - ThenTheCountIs(0); - ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(serviceEntryOne, serviceEntryTwo); - } + [Fact] + public async Task Should_return_service_from_consul() + { + Arrange(); + var service1 = GivenService(50881); + _consulServiceEntries.Add(service1.ToServiceEntry()); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); - [Fact] - public void Should_not_return_services_with_empty_address() - { - // Arrange - var serviceEntryOne = GivenService(port: 50881) - .WithAddress(string.Empty) - .ToServiceEntry(); - var serviceEntryTwo = GivenService(port: 50888) - .WithAddress(null) - .ToServiceEntry(); - GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName); - GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo); - - // Act - WhenIGetTheServices(); - - // Assert - ThenTheCountIs(0); - ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(serviceEntryOne, serviceEntryTwo); - } + // Act + var actual = await _provider.GetAsync(); - [Fact] - public void Should_not_return_services_with_invalid_port() - { - // Arrange - var serviceEntryOne = GivenService(port: -1) - .ToServiceEntry(); - var serviceEntryTwo = GivenService(port: 0) - .ToServiceEntry(); - GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName); - GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo); - - // Act - WhenIGetTheServices(); - - // Assert - ThenTheCountIs(0); - ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(serviceEntryOne, serviceEntryTwo); - } + // Assert + actual.ShouldNotBeNull().Count.ShouldBe(1); + } - private AgentService GivenService(string address = null, int? port = null, string id = null, string[] tags = null) - => new() - { - Service = _serviceName, - Address = address ?? "localhost", - Port = port ?? 123, - ID = id ?? Guid.NewGuid().ToString(), - Tags = tags ?? Array.Empty(), - }; - - private void ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(params ServiceEntry[] serviceEntries) - { - foreach (var entry in serviceEntries) - { - var service = entry.Service; - var expected = $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."; - _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == expected)), Times.Once); - } - } + [Fact] + public async Task Should_use_token() + { + Arrange(); + const string token = "test token"; + var service1 = GivenService(50881); + _consulServiceEntries.Add(service1.ToServiceEntry()); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, nameof(Should_use_token), token); + _provider = new ConsulProvider(config, _factory.Object, _clientFactory, _serviceBuilder); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().Count.ShouldBe(1); + _receivedToken.ShouldBe(token); + } - private void ThenTheCountIs(int count) - { - _services.Count.ShouldBe(count); - } + [Fact] + public async Task Should_not_return_services_with_invalid_address() + { + Arrange(); + var service1 = GivenService(50881, "http://localhost"); + var service2 = GivenService(50888, "http://localhost"); + _consulServiceEntries.Add(service1.ToServiceEntry()); + _consulServiceEntries.Add(service2.ToServiceEntry()); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().Count.ShouldBe(0); + ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(); + } - private void WhenIGetTheServices() - { - _services = _provider.GetAsync().GetAwaiter().GetResult(); - } + [Fact] + public async Task Should_not_return_services_with_empty_address() + { + Arrange(); + var service1 = GivenService(50881).WithAddress(string.Empty); + var service2 = GivenService(50888).WithAddress(null); + _consulServiceEntries.Add(service1.ToServiceEntry()); + _consulServiceEntries.Add(service2.ToServiceEntry()); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().Count.ShouldBe(0); + ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(); + } - private void ThenTheTokenIs(string token) - { - _receivedToken.ShouldBe(token); - } + [Fact] + public async Task Should_not_return_services_with_invalid_port() + { + Arrange(); + var service1 = GivenService(-1); + var service2 = GivenService(0); + _consulServiceEntries.Add(service1.ToServiceEntry()); + _consulServiceEntries.Add(service2.ToServiceEntry()); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().Count.ShouldBe(0); + ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(); + } - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + private static AgentService GivenService(int port, string address = null, string id = null, string[] tags = null, [CallerMemberName] string serviceName = null) => new() + { + Service = serviceName, + Address = address ?? "localhost", + Port = port, + ID = id ?? Guid.NewGuid().ToString(), + Tags = tags ?? Array.Empty(), + }; + + private void ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning() + { + foreach (var entry in _consulServiceEntries) { - foreach (var serviceEntry in serviceEntries) - { - _serviceEntries.Add(serviceEntry); - } + var service = entry.Service; + var expected = $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."; + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == expected)), Times.Once); } + } - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) - { - _fakeConsulBuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => + private void GivenThereIsAFakeConsulServiceDiscoveryProvider([CallerMemberName] string serviceName = "test") + { + _fakeConsulBuilder = new WebHostBuilder() + .UseUrls(_fakeConsulServiceDiscoveryUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_fakeConsulServiceDiscoveryUrl) + .Configure(app => + { + app.Run(async context => { - app.Run(async context => + if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") { - if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") + if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) { - if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) - { - _receivedToken = values.First(); - } - - var json = JsonConvert.SerializeObject(_serviceEntries); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); + _receivedToken = values.First(); } - }); - }) - .Build(); - - _fakeConsulBuilder.Start(); - } - public void Dispose() - { - _fakeConsulBuilder?.Dispose(); - } + var json = JsonConvert.SerializeObject(_consulServiceEntries); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + }) + .Build(); + _fakeConsulBuilder.Start(); } } From f624333cffba6db0dedaa73758cb61ca576a0a66 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Thu, 16 May 2024 18:47:44 +0300 Subject: [PATCH 08/23] Rename to `DefaultConsulServiceBuilder` --- ...nsulServiceBuilder.cs => DefaultConsulServiceBuilder.cs} | 6 +++--- src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs | 2 +- .../Consul/ConsulServiceDiscoveryProviderTests.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/Ocelot.Provider.Consul/{ConsulServiceBuilder.cs => DefaultConsulServiceBuilder.cs} (95%) diff --git a/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs b/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs similarity index 95% rename from src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs rename to src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs index 8cebb07f4..adc05dc1f 100644 --- a/src/Ocelot.Provider.Consul/ConsulServiceBuilder.cs +++ b/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs @@ -5,20 +5,20 @@ namespace Ocelot.Provider.Consul; -public class ConsulServiceBuilder : IConsulServiceBuilder +public class DefaultConsulServiceBuilder : IConsulServiceBuilder { private readonly ConsulRegistryConfiguration _configuration; private readonly IConsulClient _client; private readonly IOcelotLogger _logger; - public ConsulServiceBuilder( + public DefaultConsulServiceBuilder( Func configurationFactory, IConsulClientFactory clientFactory, IOcelotLoggerFactory loggerFactory) { _configuration = configurationFactory.Invoke(); _client = clientFactory.Get(_configuration); - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory.CreateLogger(); } public ConsulRegistryConfiguration Configuration => _configuration; diff --git a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs index 6151b2562..dfb5cc371 100644 --- a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs @@ -14,7 +14,7 @@ public static IOcelotBuilder AddConsul(this IOcelotBuilder builder) .AddSingleton(ConsulProviderFactory.Get) .AddSingleton(ConsulProviderFactory.GetConfiguration) .AddSingleton() - .AddSingleton() + .AddSingleton() .RemoveAll(typeof(IFileConfigurationPollerOptions)) .AddSingleton(); return builder; diff --git a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs index 47b644bad..36b1ee9c5 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs @@ -38,7 +38,7 @@ public ConsulServiceDiscoveryProviderTests() _logger = new Mock(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); } public void Dispose() @@ -50,7 +50,7 @@ private void Arrange([CallerMemberName] string serviceName = null) { _config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, serviceName, null); _clientFactory = new ConsulClientFactory(); - _serviceBuilder = new ConsulServiceBuilder(() => _config, _clientFactory, _factory.Object); + _serviceBuilder = new DefaultConsulServiceBuilder(() => _config, _clientFactory, _factory.Object); _provider = new ConsulProvider(_config, _factory.Object, _clientFactory, _serviceBuilder); } From 042388e0183bbc69786509c3c6c2947ecfe60941 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Fri, 17 May 2024 15:44:47 +0300 Subject: [PATCH 09/23] Update src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update `IsValid(ServiceEntry entry)` Co-authored-by: Raynald Messié --- .../DefaultConsulServiceBuilder.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs b/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs index adc05dc1f..7c5b07a2e 100644 --- a/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs +++ b/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs @@ -25,14 +25,15 @@ public DefaultConsulServiceBuilder( protected IConsulClient Client => _client; protected IOcelotLogger Logger => _logger; - public virtual bool IsValid(ServiceEntry entry) - { - var address = entry.Service.Address; - return !string.IsNullOrEmpty(address) - && !address.Contains($"{Uri.UriSchemeHttp}://") - && !address.Contains($"{Uri.UriSchemeHttps}://") - && entry.Service.Port > 0; - } + public virtual bool IsValid(ServiceEntry entry) +{ + string address = entry.Service.Address; + return !string.IsNullOrEmpty(address) + && !address.StartsWith(Uri.UriSchemeHttp + "://", StringComparison.OrdinalIgnoreCase) + && !address.StartsWith(Uri.UriSchemeHttps + "://", StringComparison.OrdinalIgnoreCase) + && entry.Service.Port > 0; +} + public virtual IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes) { From 4bf7f7c30c94a1e2ee5472eceaedb87304cd8b1e Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Fri, 17 May 2024 15:57:54 +0300 Subject: [PATCH 10/23] Code review by @RaynaldM --- .../DefaultConsulServiceBuilder.cs | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs b/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs index 7c5b07a2e..76d17d693 100644 --- a/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs +++ b/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs @@ -25,24 +25,31 @@ public DefaultConsulServiceBuilder( protected IConsulClient Client => _client; protected IOcelotLogger Logger => _logger; - public virtual bool IsValid(ServiceEntry entry) -{ - string address = entry.Service.Address; - return !string.IsNullOrEmpty(address) - && !address.StartsWith(Uri.UriSchemeHttp + "://", StringComparison.OrdinalIgnoreCase) - && !address.StartsWith(Uri.UriSchemeHttps + "://", StringComparison.OrdinalIgnoreCase) - && entry.Service.Port > 0; -} + public virtual bool IsValid(ServiceEntry entry) + { + var service = entry.Service; + var address = service.Address; + bool valid = !string.IsNullOrEmpty(address) + && !address.StartsWith(Uri.UriSchemeHttp + "://", StringComparison.OrdinalIgnoreCase) + && !address.StartsWith(Uri.UriSchemeHttps + "://", StringComparison.OrdinalIgnoreCase) + && service.Port > 0; + + if (!valid) + { + _logger.LogWarning( + () => $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."); + } + return valid; + } public virtual IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes) { ArgumentNullException.ThrowIfNull(entries); - var services = new List(); + var services = new List(entries.Length); foreach (var serviceEntry in entries) { - var service = serviceEntry.Service; if (IsValid(serviceEntry)) { var serviceNode = GetNode(serviceEntry, nodes); @@ -52,11 +59,6 @@ public virtual IEnumerable BuildServices(ServiceEntry[] entries, Node[] services.Add(item); } } - else - { - _logger.LogWarning( - () => $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."); - } } return services; From 944b78d5d0e894c29bc35b07ec6f31c8dd80ae18 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Fri, 17 May 2024 16:23:20 +0300 Subject: [PATCH 11/23] Read the node instance from `ServiceEntry`. If it is null, search for a node in the common collection. --- src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs b/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs index 76d17d693..4da549144 100644 --- a/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs +++ b/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs @@ -65,7 +65,7 @@ public virtual IEnumerable BuildServices(ServiceEntry[] entries, Node[] } protected virtual Node GetNode(ServiceEntry entry, Node[] nodes) - => nodes?.FirstOrDefault(n => n.Address == entry?.Service?.Address); + => entry.Node ?? nodes?.FirstOrDefault(n => n.Address == entry?.Service?.Address); public virtual Service CreateService(ServiceEntry entry, Node node) => new( From 722b95e13dcea1b0a17ae0dc6df97673cb623e22 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Sat, 18 May 2024 12:57:49 +0300 Subject: [PATCH 12/23] Refactor `OcelotBuilderExtensionsTests` --- .../Consul/OcelotBuilderExtensionsTests.cs | 87 +++++++------------ 1 file changed, 32 insertions(+), 55 deletions(-) diff --git a/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs index b9c7532c0..62a4cffda 100644 --- a/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs @@ -5,69 +5,46 @@ using Ocelot.Provider.Consul; using System.Reflection; -namespace Ocelot.UnitTests.Consul -{ - public class OcelotBuilderExtensionsTests : UnitTest - { - private readonly IServiceCollection _services; - private readonly IConfiguration _configRoot; - private IOcelotBuilder _ocelotBuilder; - private Exception _ex; - - public OcelotBuilderExtensionsTests() - { - _configRoot = new ConfigurationRoot(new List()); - _services = new ServiceCollection(); - _services.AddSingleton(GetHostingEnvironment()); - _services.AddSingleton(_configRoot); - } +namespace Ocelot.UnitTests.Consul; - private static IWebHostEnvironment GetHostingEnvironment() - { - var environment = new Mock(); - environment - .Setup(e => e.ApplicationName) - .Returns(typeof(OcelotBuilderExtensionsTests).GetTypeInfo().Assembly.GetName().Name); +public class OcelotBuilderExtensionsTests : UnitTest +{ + private readonly IServiceCollection _services; + private readonly IConfiguration _configRoot; - return environment.Object; - } + public OcelotBuilderExtensionsTests() + { + _configRoot = new ConfigurationRoot(new List()); + _services = new ServiceCollection(); + _services.AddSingleton(GetHostingEnvironment()); + _services.AddSingleton(_configRoot); + } - [Fact] - public void should_set_up_consul() - { - this.Given(x => WhenISetUpOcelotServices()) - .When(x => WhenISetUpConsul()) - .Then(x => ThenAnExceptionIsntThrown()) - .BDDfy(); - } + private static IWebHostEnvironment GetHostingEnvironment() + { + var environment = new Mock(); + environment.Setup(e => e.ApplicationName) + .Returns(typeof(OcelotBuilderExtensionsTests).GetTypeInfo().Assembly.GetName().Name); + return environment.Object; + } - private void WhenISetUpOcelotServices() + [Fact] + public void ShouldSetUpConsul() + { + // Arrange + Exception ex = null; + try { - try - { - _ocelotBuilder = _services.AddOcelot(_configRoot); - } - catch (Exception e) - { - _ex = e; - } + // Act + var ocelotBuilder = _services.AddOcelot(_configRoot); + ocelotBuilder.AddConsul().AddConfigStoredInConsul(); } - - private void WhenISetUpConsul() + catch (Exception e) { - try - { - _ocelotBuilder.AddConsul().AddConfigStoredInConsul(); - } - catch (Exception e) - { - _ex = e; - } + ex = e; } - private void ThenAnExceptionIsntThrown() - { - _ex.ShouldBeNull(); - } + // Assert + ex.ShouldBeNull(); } } From abf5b77eadd0cd7a5c5eee6ea7eb8840a68e209b Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Sat, 18 May 2024 14:05:14 +0300 Subject: [PATCH 13/23] The generic `AddConsul(IOcelotBuilder)` method --- .../OcelotBuilderExtensions.cs | 33 ++++++++++ .../Consul/OcelotBuilderExtensionsTests.cs | 63 +++++++++++++++++-- 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs index dfb5cc371..0c064f780 100644 --- a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs @@ -8,6 +8,18 @@ namespace Ocelot.Provider.Consul; public static class OcelotBuilderExtensions { + /// + /// Integrates Consul service discovery into the DI, atop the existing Ocelot services. + /// + /// + /// Default services: + /// + /// The service is an instance of . + /// The service is an instance of . + /// + /// + /// The Ocelot Builder instance, default. + /// The reference to the same extended object. public static IOcelotBuilder AddConsul(this IOcelotBuilder builder) { builder.Services @@ -20,6 +32,27 @@ public static IOcelotBuilder AddConsul(this IOcelotBuilder builder) return builder; } + /// + /// Integrates Consul service discovery into the DI, atop the existing Ocelot services, with service builder overriding. + /// + /// + /// Services to override: + /// + /// The service has been substituted with a instance. + /// + /// + /// The service builder type. + /// The Ocelot Builder instance, default. + /// The reference to the same extended object. + public static IOcelotBuilder AddConsul(this IOcelotBuilder builder) + where TServiceBuilder : class, IConsulServiceBuilder + { + AddConsul(builder).Services + .RemoveAll() + .AddSingleton(typeof(IConsulServiceBuilder), typeof(TServiceBuilder)); + return builder; + } + public static IOcelotBuilder AddConfigStoredInConsul(this IOcelotBuilder builder) { builder.Services diff --git a/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs index 62a4cffda..c1a2ed096 100644 --- a/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs @@ -1,8 +1,11 @@ -using Microsoft.AspNetCore.Hosting; +using Consul; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; +using Ocelot.Values; using System.Reflection; namespace Ocelot.UnitTests.Consul; @@ -29,15 +32,15 @@ private static IWebHostEnvironment GetHostingEnvironment() } [Fact] - public void ShouldSetUpConsul() + public void AddConsul_ShouldSetUpConsul() { // Arrange Exception ex = null; try { // Act - var ocelotBuilder = _services.AddOcelot(_configRoot); - ocelotBuilder.AddConsul().AddConfigStoredInConsul(); + var builder = _services.AddOcelot(_configRoot); + builder.AddConsul(); } catch (Exception e) { @@ -47,4 +50,56 @@ public void ShouldSetUpConsul() // Assert ex.ShouldBeNull(); } + + [Fact] + public void AddConfigStoredInConsul_ShouldSetUpConsul() + { + // Arrange + Exception ex = null; + try + { + // Act + var builder = _services.AddOcelot(_configRoot); + builder.AddConsul().AddConfigStoredInConsul(); + } + catch (Exception e) + { + ex = e; + } + + // Assert + ex.ShouldBeNull(); + } + + [Fact] + public void AddConsulGeneric_TServiceBuilder_ShouldSetUpConsul() + { + // Arrange + Exception ex = null; + IOcelotBuilder builder = null; + try + { + // Act + builder = _services + .AddOcelot(_configRoot) + .AddConsul(); + } + catch (Exception e) + { + ex = e; + } + + // Assert + ex.ShouldBeNull(); + builder.ShouldNotBeNull(); + builder.Services.SingleOrDefault(s => s.ServiceType == typeof(IConsulServiceBuilder)).ShouldNotBeNull(); + } +} + +internal class FakeConsulServiceBuilder : IConsulServiceBuilder +{ + public ConsulRegistryConfiguration Configuration => throw new NotImplementedException(); + public IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes) => throw new NotImplementedException(); + public Service CreateService(ServiceEntry serviceEntry, Node serviceNode) => throw new NotImplementedException(); + public bool IsValid(ServiceEntry entry) => throw new NotImplementedException(); } From 20d73916e1b5cbb54389c1fe751ba80c1f609b09 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Sat, 18 May 2024 17:06:26 +0300 Subject: [PATCH 14/23] Rename to `ConsulTests` --- ...{ConsulServiceDiscoveryProviderTests.cs => ConsulTests.cs} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename test/Ocelot.UnitTests/Consul/{ConsulServiceDiscoveryProviderTests.cs => ConsulTests.cs} (98%) diff --git a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/ConsulTests.cs similarity index 98% rename from test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs rename to test/Ocelot.UnitTests/Consul/ConsulTests.cs index 36b1ee9c5..58a0ab0bf 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.Consul; -public sealed class ConsulServiceDiscoveryProviderTests : UnitTest, IDisposable +public sealed class ConsulTests : UnitTest, IDisposable { private readonly int _port; private readonly string _consulHost; @@ -27,7 +27,7 @@ public sealed class ConsulServiceDiscoveryProviderTests : UnitTest, IDisposable private ConsulProvider _provider; private string _receivedToken; - public ConsulServiceDiscoveryProviderTests() + public ConsulTests() { _port = 8500; _consulHost = "localhost"; From 972091f7625bff0d570c9e6aea29629eaf9a034c Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Sat, 18 May 2024 21:39:55 +0300 Subject: [PATCH 15/23] Unit tests: 100% coverage --- .../DefaultConsulServiceBuilder.cs | 15 +- test/Ocelot.UnitTests/Consul/ConsulTests.cs | 17 ++ .../DefaultConsulServiceBuilderTests.cs | 200 ++++++++++++++++++ 3 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 test/Ocelot.UnitTests/Consul/DefaultConsulServiceBuilderTests.cs diff --git a/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs b/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs index 4da549144..7526bea65 100644 --- a/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs +++ b/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs @@ -65,7 +65,7 @@ public virtual IEnumerable BuildServices(ServiceEntry[] entries, Node[] } protected virtual Node GetNode(ServiceEntry entry, Node[] nodes) - => entry.Node ?? nodes?.FirstOrDefault(n => n.Address == entry?.Service?.Address); + => entry?.Node ?? nodes?.FirstOrDefault(n => n.Address == entry?.Service?.Address); public virtual Service CreateService(ServiceEntry entry, Node node) => new( @@ -87,15 +87,16 @@ protected virtual ServiceHostAndPort GetServiceHostAndPort(ServiceEntry entry, N protected virtual string GetDownstreamHost(ServiceEntry entry, Node node) => node != null ? node.Name : entry.Service.Address; - protected virtual string GetServiceId(ServiceEntry entry, Node serviceNode) + protected virtual string GetServiceId(ServiceEntry entry, Node node) => entry.Service.ID; - protected virtual string GetServiceVersion(ServiceEntry entry, Node serviceNode) - => entry.Service.Tags? - .FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal)) - .TrimStart(VersionPrefix); + protected virtual string GetServiceVersion(ServiceEntry entry, Node node) + => entry.Service.Tags + ?.FirstOrDefault(tag => tag.StartsWith(VersionPrefix, StringComparison.Ordinal)) + ?.TrimStart(VersionPrefix) + ?? string.Empty; - protected virtual IEnumerable GetServiceTags(ServiceEntry entry, Node serviceNode) + protected virtual IEnumerable GetServiceTags(ServiceEntry entry, Node node) => entry.Service.Tags ?? Enumerable.Empty(); private const string VersionPrefix = "version-"; diff --git a/test/Ocelot.UnitTests/Consul/ConsulTests.cs b/test/Ocelot.UnitTests/Consul/ConsulTests.cs index 58a0ab0bf..b9009d488 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulTests.cs @@ -142,6 +142,23 @@ public async Task Should_not_return_services_with_invalid_port() ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(); } + [Fact] + public async Task GetAsync_NoEntries_ShouldLogWarning() + { + Arrange(); + _consulServiceEntries.Clear(); // NoEntries + _logger.Setup(x => x.LogWarning(It.IsAny>())).Verifiable(); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().ShouldBeEmpty(); + var expected = $"Consul Provider: No service entries found for '{nameof(GetAsync_NoEntries_ShouldLogWarning)}' service!"; + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == expected)), Times.Once); + } + private static AgentService GivenService(int port, string address = null, string id = null, string[] tags = null, [CallerMemberName] string serviceName = null) => new() { Service = serviceName, diff --git a/test/Ocelot.UnitTests/Consul/DefaultConsulServiceBuilderTests.cs b/test/Ocelot.UnitTests/Consul/DefaultConsulServiceBuilderTests.cs new file mode 100644 index 000000000..25dc8d950 --- /dev/null +++ b/test/Ocelot.UnitTests/Consul/DefaultConsulServiceBuilderTests.cs @@ -0,0 +1,200 @@ +using Castle.Components.DictionaryAdapter.Xml; +using Consul; +using Ocelot.Logging; +using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Xml.Linq; + +namespace Ocelot.UnitTests.Consul; + +public sealed class DefaultConsulServiceBuilderTests +{ + private DefaultConsulServiceBuilder sut; + private readonly Func configurationFactory; + private readonly Mock clientFactory; + private readonly Mock loggerFactory; + private readonly Mock logger; + private ConsulRegistryConfiguration _configuration; + + private ConsulRegistryConfiguration GetConfiguration() => _configuration; + + public DefaultConsulServiceBuilderTests() + { + configurationFactory = GetConfiguration; + clientFactory = new(); + clientFactory.Setup(x => x.Get(It.IsAny())) + .Returns(new ConsulClient()); + logger = new(); + loggerFactory = new(); + loggerFactory.Setup(x => x.CreateLogger()) + .Returns(logger.Object); + } + + private void Arrange([CallerMemberName] string testName = null) + { + _configuration = new(null, null, 0, testName, null); + sut = new DefaultConsulServiceBuilder(configurationFactory, clientFactory.Object, loggerFactory.Object); + } + + [Fact] + public void Ctor_PrivateMembers_PropertiesAreInitialized() + { + Arrange(); + var methodClient = sut.GetType().GetProperty("Client", BindingFlags.NonPublic | BindingFlags.Instance); + var methodLogger = sut.GetType().GetProperty("Logger", BindingFlags.NonPublic | BindingFlags.Instance); + + // Act + var actualConfiguration = sut.Configuration; + var actualClient = methodClient.GetValue(sut); + var actualLogger = methodLogger.GetValue(sut); + + // Assert + actualConfiguration.ShouldNotBeNull().ShouldBe(_configuration); + actualClient.ShouldNotBeNull(); + actualLogger.ShouldNotBeNull(); + } + + private static Type Me { get; } = typeof(DefaultConsulServiceBuilder); + private static MethodInfo GetNode { get; } = Me.GetMethod("GetNode", BindingFlags.NonPublic | BindingFlags.Instance); + + [Fact] + public void GetNode_EntryBranch_ReturnsEntryNode() + { + Arrange(); + Node node = new() { Name = nameof(GetNode_EntryBranch_ReturnsEntryNode) }; + ServiceEntry entry = new() { Node = node }; + + // Act + var actual = GetNode.Invoke(sut, new object[] { entry, null }) as Node; + + // Assert + actual.ShouldNotBeNull().ShouldBe(node); + actual.Name.ShouldBe(node.Name); + } + + [Fact] + public void GetNode_NodesBranch_ReturnsNodeFromCollection() + { + Arrange(); + ServiceEntry entry = new() + { + Node = null, + Service = new() { Address = nameof(GetNode_NodesBranch_ReturnsNodeFromCollection) }, + }; + Node[] nodes = null; + + // Act, Assert: nodes is null + var actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; + actual.ShouldBeNull(); + + // Arrange, Act, Assert: nodes has items, happy path + var node = new Node { Address = nameof(GetNode_NodesBranch_ReturnsNodeFromCollection) }; + nodes = new[] { node }; + actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; + actual.ShouldNotBeNull().ShouldBe(node); + actual.Address.ShouldBe(entry.Service.Address); + + // Arrange, Act, Assert: nodes has items, some nulls in entry + entry.Service.Address = null; + actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; + actual.ShouldBeNull(); + + entry.Service = null; + actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; + actual.ShouldBeNull(); + + entry = null; + actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; + actual.ShouldBeNull(); + } + + private static MethodInfo GetDownstreamHost { get; } = Me.GetMethod("GetDownstreamHost", BindingFlags.NonPublic | BindingFlags.Instance); + + [Fact] + public void GetDownstreamHost_BothBranches_NameOrAddress() + { + Arrange(); + + // Arrange, Act, Assert: node branch + ServiceEntry entry = new() + { + Service = new() { Address = nameof(GetDownstreamHost_BothBranches_NameOrAddress) }, + }; + var node = new Node { Name = "test1" }; + var actual = GetDownstreamHost.Invoke(sut, new object[] { entry, node }) as string; + actual.ShouldNotBeNull().ShouldBe("test1"); + + // Arrange, Act, Assert: entry branch + node = null; + actual = GetDownstreamHost.Invoke(sut, new object[] { entry, node }) as string; + actual.ShouldNotBeNull().ShouldBe(nameof(GetDownstreamHost_BothBranches_NameOrAddress)); + } + + private static MethodInfo GetServiceVersion { get; } = Me.GetMethod("GetServiceVersion", BindingFlags.NonPublic | BindingFlags.Instance); + + [Fact] + public void GetServiceVersion_TagsIsNull_EmptyString() + { + Arrange(); + + // Arrange, Act, Assert: collection is null + ServiceEntry entry = new() + { + Service = new() { Tags = null }, + }; + Node node = null; + var actual = GetServiceVersion.Invoke(sut, new object[] { entry, node }) as string; + actual.ShouldBe(string.Empty); + + // Arrange, Act, Assert: collection has no version tag + entry.Service.Tags = new[] { "test" }; + actual = GetServiceVersion.Invoke(sut, new object[] { entry, node }) as string; + actual.ShouldBe(string.Empty); + } + + [Fact] + public void GetServiceVersion_HasTags_HappyPath() + { + Arrange(); + + // Arrange + var tags = new string[] { "test", "version-v2" }; + ServiceEntry entry = new() + { + Service = new() { Tags = tags }, + }; + Node node = null; + + // Act + var actual = GetServiceVersion.Invoke(sut, new object[] { entry, node }) as string; + + // Assert + actual.ShouldBe("v2"); + } + + private static MethodInfo GetServiceTags { get; } = Me.GetMethod("GetServiceTags", BindingFlags.NonPublic | BindingFlags.Instance); + + [Fact] + public void GetServiceTags_BothBranches() + { + Arrange(); + + // Arrange, Act, Assert: collection is null + ServiceEntry entry = new() + { + Service = new() { Tags = null }, + }; + Node node = null; + var actual = GetServiceTags.Invoke(sut, new object[] { entry, node }) as IEnumerable; + actual.ShouldNotBeNull().ShouldBeEmpty(); + + // Arrange, Act, Assert: happy path + entry.Service.Tags = new string[] { "1", "2", "3" }; + actual = GetServiceTags.Invoke(sut, new object[] { entry, node }) as IEnumerable; + actual.ShouldNotBeNull().ShouldNotBeEmpty(); + actual.Count().ShouldBe(3); + actual.ShouldContain("3"); + } +} From 7ac24d77e1a9d88c62f1e50c3e84395feadef874 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Mon, 20 May 2024 16:30:23 +0300 Subject: [PATCH 16/23] Move to `ServiceDiscovery` folder --- .../ConsulConfigurationInConsulTests.cs | 942 +++++++++--------- .../ConsulWebSocketTests.cs | 678 ++++++------- 2 files changed, 810 insertions(+), 810 deletions(-) rename test/Ocelot.AcceptanceTests/{ => ServiceDiscovery}/ConsulConfigurationInConsulTests.cs (97%) rename test/Ocelot.AcceptanceTests/{ => ServiceDiscovery}/ConsulWebSocketTests.cs (97%) diff --git a/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulConfigurationInConsulTests.cs similarity index 97% rename from test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs rename to test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulConfigurationInConsulTests.cs index 978626c14..d97d98c09 100644 --- a/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulConfigurationInConsulTests.cs @@ -6,474 +6,474 @@ using Ocelot.Cache; using Ocelot.Configuration.File; using System.Text; - -namespace Ocelot.AcceptanceTests -{ - public class ConsulConfigurationInConsulTests : IDisposable - { - private IWebHost _builder; - private readonly Steps _steps; - private IWebHost _fakeConsulBuilder; - private FileConfiguration _config; - private readonly List _consulServices; - - public ConsulConfigurationInConsulTests() - { - _consulServices = new List(); - _steps = new Steps(); - } - - [Fact] - public void should_return_response_200_with_simple_url() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = servicePort, - }, - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", string.Empty, 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_load_configuration_out_of_consul() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - - var consulConfig = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/status", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = servicePort, - }, - }, - UpstreamPathTemplate = "/cs/status", - UpstreamHttpMethod = new List {"Get"}, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) - .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", "/status", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_load_configuration_out_of_consul_if_it_is_changed() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - - var consulConfig = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/status", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = servicePort, - }, - }, - UpstreamPathTemplate = "/cs/status", - UpstreamHttpMethod = new List {"Get"}, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - var secondConsulConfig = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/status", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = servicePort, - }, - }, - UpstreamPathTemplate = "/cs/status/awesome", - UpstreamHttpMethod = new List {"Get"}, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) - .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", "/status", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .When(x => GivenTheConsulConfigurationIs(secondConsulConfig)) - .Then(x => ThenTheConfigIsUpdatedInOcelot()) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes_and_rate_limit() - { - var consulPort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = downstreamServicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - - var consulConfig = new FileConfiguration - { - DynamicRoutes = new List - { - new() - { - ServiceName = serviceName, - RateLimitRule = new FileRateLimitRule - { - EnableRateLimiting = true, - ClientWhitelist = new List(), - Limit = 3, - Period = "1s", - PeriodTimespan = 1000, - }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - RateLimitOptions = new FileRateLimitOptions - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = string.Empty, - RateLimitCounterPrefix = string.Empty, - HttpStatusCode = 428, - }, - DownstreamScheme = "http", - }, - }; - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) - .And(x => GivenTheConsulConfigurationIs(consulConfig)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 2)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .BDDfy(); - } - - private void ThenTheConfigIsUpdatedInOcelot() - { - var result = Wait.WaitFor(20000).Until(() => - { - try - { - _steps.WhenIGetUrlOnTheApiGateway("/cs/status/awesome"); - _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK); - _steps.ThenTheResponseBodyShouldBe("Hello from Laura"); - return true; - } - catch (Exception) - { - return false; - } - }); - result.ShouldBeTrue(); - } - - private void GivenTheConsulConfigurationIs(FileConfiguration config) - { - _config = config; - } - - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) - { - foreach (var serviceEntry in serviceEntries) - { - _consulServices.Add(serviceEntry); - } - } - - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) - { - _fakeConsulBuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.Run(async context => - { - if (context.Request.Method.ToLower() == "get" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") - { - var json = JsonConvert.SerializeObject(_config); - - var bytes = Encoding.UTF8.GetBytes(json); - - var base64 = Convert.ToBase64String(bytes); - - var kvp = new FakeConsulGetResponse(base64); - json = JsonConvert.SerializeObject(new[] { kvp }); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") - { - try - { - var reader = new StreamReader(context.Request.Body); - - // Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. - // var json = reader.ReadToEnd(); - var json = await reader.ReadToEndAsync(); - - _config = JsonConvert.DeserializeObject(json); - - var response = JsonConvert.SerializeObject(true); - - await context.Response.WriteAsync(response); - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } - } - else if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") - { - var json = JsonConvert.SerializeObject(_consulServices); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - }) - .Build(); - - _fakeConsulBuilder.Start(); - } - - public class FakeConsulGetResponse - { - public FakeConsulGetResponse(string value) - { - Value = value; - } - - public int CreateIndex => 100; - public int ModifyIndex => 200; - public int LockIndex => 200; - public string Key => "InternalConfiguration"; - public int Flags => 0; - public string Value { get; } - public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e"; - } - - private void GivenThereIsAServiceRunningOn(string url, string basePath, int statusCode, string responseBody) - { - _builder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.UsePathBase(basePath); - - app.Run(async context => - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - }) - .Build(); - - _builder.Start(); - } - - public void Dispose() - { - _builder?.Dispose(); - _steps.Dispose(); - } - - private class FakeCache : IOcelotCache - { - public void Add(string key, FileConfiguration value, TimeSpan ttl, string region) - { - throw new NotImplementedException(); - } - - public FileConfiguration Get(string key, string region) - { - throw new NotImplementedException(); - } - - public void ClearRegion(string region) - { - throw new NotImplementedException(); - } - - public void AddAndDelete(string key, FileConfiguration value, TimeSpan ttl, string region) - { - throw new NotImplementedException(); - } - } - } -} + +namespace Ocelot.AcceptanceTests.ServiceDiscovery +{ + public class ConsulConfigurationInConsulTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + private IWebHost _fakeConsulBuilder; + private FileConfiguration _config; + private readonly List _consulServices; + + public ConsulConfigurationInConsulTests() + { + _consulServices = new List(); + _steps = new Steps(); + } + + [Fact] + public void should_return_response_200_with_simple_url() + { + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + + this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", string.Empty, 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_load_configuration_out_of_consul() + { + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + + var consulConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/status", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/cs/status", + UpstreamHttpMethod = new List {"Get"}, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) + .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", "/status", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_load_configuration_out_of_consul_if_it_is_changed() + { + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + + var consulConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/status", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/cs/status", + UpstreamHttpMethod = new List {"Get"}, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var secondConsulConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/status", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/cs/status/awesome", + UpstreamHttpMethod = new List {"Get"}, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) + .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", "/status", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(x => GivenTheConsulConfigurationIs(secondConsulConfig)) + .Then(x => ThenTheConfigIsUpdatedInOcelot()) + .BDDfy(); + } + + [Fact] + public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes_and_rate_limit() + { + var consulPort = PortFinder.GetRandomPort(); + const string serviceName = "web"; + var downstreamServicePort = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = downstreamServicePort, + ID = "web_90_0_2_224_8080", + Tags = new[] { "version-v1" }, + }, + }; + + var consulConfig = new FileConfiguration + { + DynamicRoutes = new List + { + new() + { + ServiceName = serviceName, + RateLimitRule = new FileRateLimitRule + { + EnableRateLimiting = true, + ClientWhitelist = new List(), + Limit = 3, + Period = "1s", + PeriodTimespan = 1000, + }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + RateLimitOptions = new FileRateLimitOptions + { + ClientIdHeader = "ClientId", + DisableRateLimitHeaders = false, + QuotaExceededMessage = string.Empty, + RateLimitCounterPrefix = string.Empty, + HttpStatusCode = 428, + }, + DownstreamScheme = "http", + }, + }; + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) + .And(x => GivenTheConsulConfigurationIs(consulConfig)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 2)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) + .BDDfy(); + } + + private void ThenTheConfigIsUpdatedInOcelot() + { + var result = Wait.WaitFor(20000).Until(() => + { + try + { + _steps.WhenIGetUrlOnTheApiGateway("/cs/status/awesome"); + _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK); + _steps.ThenTheResponseBodyShouldBe("Hello from Laura"); + return true; + } + catch (Exception) + { + return false; + } + }); + result.ShouldBeTrue(); + } + + private void GivenTheConsulConfigurationIs(FileConfiguration config) + { + _config = config; + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach (var serviceEntry in serviceEntries) + { + _consulServices.Add(serviceEntry); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) + { + _fakeConsulBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + if (context.Request.Method.ToLower() == "get" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") + { + var json = JsonConvert.SerializeObject(_config); + + var bytes = Encoding.UTF8.GetBytes(json); + + var base64 = Convert.ToBase64String(bytes); + + var kvp = new FakeConsulGetResponse(base64); + json = JsonConvert.SerializeObject(new[] { kvp }); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") + { + try + { + var reader = new StreamReader(context.Request.Body); + + // Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. + // var json = reader.ReadToEnd(); + var json = await reader.ReadToEndAsync(); + + _config = JsonConvert.DeserializeObject(json); + + var response = JsonConvert.SerializeObject(true); + + await context.Response.WriteAsync(response); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + else if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") + { + var json = JsonConvert.SerializeObject(_consulServices); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + }) + .Build(); + + _fakeConsulBuilder.Start(); + } + + public class FakeConsulGetResponse + { + public FakeConsulGetResponse(string value) + { + Value = value; + } + + public int CreateIndex => 100; + public int ModifyIndex => 200; + public int LockIndex => 200; + public string Key => "InternalConfiguration"; + public int Flags => 0; + public string Value { get; } + public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e"; + } + + private void GivenThereIsAServiceRunningOn(string url, string basePath, int statusCode, string responseBody) + { + _builder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.UsePathBase(basePath); + + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _builder.Start(); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + + private class FakeCache : IOcelotCache + { + public void Add(string key, FileConfiguration value, TimeSpan ttl, string region) + { + throw new NotImplementedException(); + } + + public FileConfiguration Get(string key, string region) + { + throw new NotImplementedException(); + } + + public void ClearRegion(string region) + { + throw new NotImplementedException(); + } + + public void AddAndDelete(string key, FileConfiguration value, TimeSpan ttl, string region) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulWebSocketTests.cs similarity index 97% rename from test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs rename to test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulWebSocketTests.cs index dceafc0d4..9afa1b154 100644 --- a/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulWebSocketTests.cs @@ -5,343 +5,343 @@ using Ocelot.WebSockets; using System.Net.WebSockets; using System.Text; - -namespace Ocelot.AcceptanceTests -{ - public class ConsulWebSocketTests : IDisposable - { - private readonly List _secondRecieved; - private readonly List _firstRecieved; - private readonly List _serviceEntries; - private readonly Steps _steps; - private readonly ServiceHandler _serviceHandler; - - public ConsulWebSocketTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - _firstRecieved = new List(); - _secondRecieved = new List(); - _serviceEntries = new List(); - } - - [Fact] - public void ShouldProxyWebsocketInputToDownstreamServiceAndUseServiceDiscoveryAndLoadBalancer() - { - var downstreamPort = PortFinder.GetRandomPort(); - var downstreamHost = "localhost"; - - var secondDownstreamPort = PortFinder.GetRandomPort(); - var secondDownstreamHost = "localhost"; - - var serviceName = "websockets"; - var consulPort = PortFinder.GetRandomPort(); - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = downstreamHost, - Port = downstreamPort, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = secondDownstreamHost, - Port = secondDownstreamPort, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - var config = new FileConfiguration - { - Routes = new List - { - new() - { - UpstreamPathTemplate = "/", - DownstreamPathTemplate = "/ws", - DownstreamScheme = "ws", - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "RoundRobin" }, - ServiceName = serviceName, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - Type = "consul", - }, - }, - }; - - this.Given(_ => _steps.GivenThereIsAConfiguration(config)) - .And(_ => _steps.StartFakeOcelotWithWebSocketsWithConsul()) - .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws")) - .And(_ => StartSecondFakeDownstreamService($"http://{secondDownstreamHost}:{secondDownstreamPort}", "/ws")) - .When(_ => WhenIStartTheClients()) - .Then(_ => ThenBothDownstreamServicesAreCalled()) - .BDDfy(); - } - - private void ThenBothDownstreamServicesAreCalled() - { - _firstRecieved.Count.ShouldBe(10); - _firstRecieved.ForEach(x => - { - x.ShouldBe("test"); - }); - - _secondRecieved.Count.ShouldBe(10); - _secondRecieved.ForEach(x => - { - x.ShouldBe("chocolate"); - }); - } - - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) - { - foreach (var serviceEntry in serviceEntries) - { - _serviceEntries.Add(serviceEntry); - } - } - - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") - { - var json = JsonConvert.SerializeObject(_serviceEntries); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - } - - private async Task WhenIStartTheClients() - { - var firstClient = StartClient("ws://localhost:5000/"); - - var secondClient = StartSecondClient("ws://localhost:5000/"); - - await Task.WhenAll(firstClient, secondClient); - } - - private async Task StartClient(string url) - { - IClientWebSocket client = new ClientWebSocketProxy(); - - await client.ConnectAsync(new Uri(url), CancellationToken.None); - - var sending = Task.Run(async () => - { - var line = "test"; - for (var i = 0; i < 10; i++) - { - var bytes = Encoding.UTF8.GetBytes(line); - - await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, - CancellationToken.None); - await Task.Delay(10); - } - - await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - }); - - var receiving = Task.Run(async () => - { - var buffer = new byte[1024 * 4]; - - while (true) - { - var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - if (result.MessageType == WebSocketMessageType.Text) - { - _firstRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); - } - else if (result.MessageType == WebSocketMessageType.Close) - { - if (client.State != WebSocketState.Closed) - { - // Last version, the client state is CloseReceived - // Valid states are: Open, CloseReceived, CloseSent - await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - } - - break; - } - } - }); - - await Task.WhenAll(sending, receiving); - } - - private async Task StartSecondClient(string url) - { - await Task.Delay(500); - - IClientWebSocket client = new ClientWebSocketProxy(); - - await client.ConnectAsync(new Uri(url), CancellationToken.None); - - var sending = Task.Run(async () => - { - var line = "test"; - for (var i = 0; i < 10; i++) - { - var bytes = Encoding.UTF8.GetBytes(line); - - await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, - CancellationToken.None); - await Task.Delay(10); - } - - await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - }); - - var receiving = Task.Run(async () => - { - var buffer = new byte[1024 * 4]; - - while (true) - { - var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - if (result.MessageType == WebSocketMessageType.Text) - { - _secondRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); - } - else if (result.MessageType == WebSocketMessageType.Close) - { - if (client.State != WebSocketState.Closed) - { - // Last version, the client state is CloseReceived - // Valid states are: Open, CloseReceived, CloseSent - await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - } - - break; - } - } - }); - - await Task.WhenAll(sending, receiving); - } - - private async Task StartFakeDownstreamService(string url, string path) - { - await _serviceHandler.StartFakeDownstreamService(url, async (context, next) => - { - if (context.Request.Path == path) - { - if (context.WebSockets.IsWebSocketRequest) - { - var webSocket = await context.WebSockets.AcceptWebSocketAsync(); - await Echo(webSocket); - } - else - { - context.Response.StatusCode = 400; - } - } - else - { - await next(); - } - }); - } - - private async Task StartSecondFakeDownstreamService(string url, string path) - { - await _serviceHandler.StartFakeDownstreamService(url, async (context, next) => - { - if (context.Request.Path == path) - { - if (context.WebSockets.IsWebSocketRequest) - { - var webSocket = await context.WebSockets.AcceptWebSocketAsync(); - await Message(webSocket); - } - else - { - context.Response.StatusCode = 400; - } - } - else - { - await next(); - } - }); - } - - private static async Task Echo(WebSocket webSocket) - { - try - { - var buffer = new byte[1024 * 4]; - - var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - while (!result.CloseStatus.HasValue) - { - await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); - - result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - } - - await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - - private static async Task Message(WebSocket webSocket) - { - try - { - var buffer = new byte[1024 * 4]; - - var bytes = Encoding.UTF8.GetBytes("chocolate"); - - var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - while (!result.CloseStatus.HasValue) - { - await webSocket.SendAsync(new ArraySegment(bytes), result.MessageType, result.EndOfMessage, CancellationToken.None); - - result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - } - - await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - - public void Dispose() - { - _serviceHandler?.Dispose(); + +namespace Ocelot.AcceptanceTests.ServiceDiscovery +{ + public class ConsulWebSocketTests : IDisposable + { + private readonly List _secondRecieved; + private readonly List _firstRecieved; + private readonly List _serviceEntries; + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public ConsulWebSocketTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + _firstRecieved = new List(); + _secondRecieved = new List(); + _serviceEntries = new List(); + } + + [Fact] + public void ShouldProxyWebsocketInputToDownstreamServiceAndUseServiceDiscoveryAndLoadBalancer() + { + var downstreamPort = PortFinder.GetRandomPort(); + var downstreamHost = "localhost"; + + var secondDownstreamPort = PortFinder.GetRandomPort(); + var secondDownstreamHost = "localhost"; + + var serviceName = "websockets"; + var consulPort = PortFinder.GetRandomPort(); + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = downstreamHost, + Port = downstreamPort, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + var serviceEntryTwo = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = secondDownstreamHost, + Port = secondDownstreamPort, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + + var config = new FileConfiguration + { + Routes = new List + { + new() + { + UpstreamPathTemplate = "/", + DownstreamPathTemplate = "/ws", + DownstreamScheme = "ws", + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "RoundRobin" }, + ServiceName = serviceName, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + Type = "consul", + }, + }, + }; + + this.Given(_ => _steps.GivenThereIsAConfiguration(config)) + .And(_ => _steps.StartFakeOcelotWithWebSocketsWithConsul()) + .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws")) + .And(_ => StartSecondFakeDownstreamService($"http://{secondDownstreamHost}:{secondDownstreamPort}", "/ws")) + .When(_ => WhenIStartTheClients()) + .Then(_ => ThenBothDownstreamServicesAreCalled()) + .BDDfy(); + } + + private void ThenBothDownstreamServicesAreCalled() + { + _firstRecieved.Count.ShouldBe(10); + _firstRecieved.ForEach(x => + { + x.ShouldBe("test"); + }); + + _secondRecieved.Count.ShouldBe(10); + _secondRecieved.ForEach(x => + { + x.ShouldBe("chocolate"); + }); + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach (var serviceEntry in serviceEntries) + { + _serviceEntries.Add(serviceEntry); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") + { + var json = JsonConvert.SerializeObject(_serviceEntries); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + } + + private async Task WhenIStartTheClients() + { + var firstClient = StartClient("ws://localhost:5000/"); + + var secondClient = StartSecondClient("ws://localhost:5000/"); + + await Task.WhenAll(firstClient, secondClient); + } + + private async Task StartClient(string url) + { + IClientWebSocket client = new ClientWebSocketProxy(); + + await client.ConnectAsync(new Uri(url), CancellationToken.None); + + var sending = Task.Run(async () => + { + var line = "test"; + for (var i = 0; i < 10; i++) + { + var bytes = Encoding.UTF8.GetBytes(line); + + await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, + CancellationToken.None); + await Task.Delay(10); + } + + await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + }); + + var receiving = Task.Run(async () => + { + var buffer = new byte[1024 * 4]; + + while (true) + { + var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + if (result.MessageType == WebSocketMessageType.Text) + { + _firstRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); + } + else if (result.MessageType == WebSocketMessageType.Close) + { + if (client.State != WebSocketState.Closed) + { + // Last version, the client state is CloseReceived + // Valid states are: Open, CloseReceived, CloseSent + await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + } + + break; + } + } + }); + + await Task.WhenAll(sending, receiving); + } + + private async Task StartSecondClient(string url) + { + await Task.Delay(500); + + IClientWebSocket client = new ClientWebSocketProxy(); + + await client.ConnectAsync(new Uri(url), CancellationToken.None); + + var sending = Task.Run(async () => + { + var line = "test"; + for (var i = 0; i < 10; i++) + { + var bytes = Encoding.UTF8.GetBytes(line); + + await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, + CancellationToken.None); + await Task.Delay(10); + } + + await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + }); + + var receiving = Task.Run(async () => + { + var buffer = new byte[1024 * 4]; + + while (true) + { + var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + if (result.MessageType == WebSocketMessageType.Text) + { + _secondRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); + } + else if (result.MessageType == WebSocketMessageType.Close) + { + if (client.State != WebSocketState.Closed) + { + // Last version, the client state is CloseReceived + // Valid states are: Open, CloseReceived, CloseSent + await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + } + + break; + } + } + }); + + await Task.WhenAll(sending, receiving); + } + + private async Task StartFakeDownstreamService(string url, string path) + { + await _serviceHandler.StartFakeDownstreamService(url, async (context, next) => + { + if (context.Request.Path == path) + { + if (context.WebSockets.IsWebSocketRequest) + { + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await Echo(webSocket); + } + else + { + context.Response.StatusCode = 400; + } + } + else + { + await next(); + } + }); + } + + private async Task StartSecondFakeDownstreamService(string url, string path) + { + await _serviceHandler.StartFakeDownstreamService(url, async (context, next) => + { + if (context.Request.Path == path) + { + if (context.WebSockets.IsWebSocketRequest) + { + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await Message(webSocket); + } + else + { + context.Response.StatusCode = 400; + } + } + else + { + await next(); + } + }); + } + + private static async Task Echo(WebSocket webSocket) + { + try + { + var buffer = new byte[1024 * 4]; + + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + private static async Task Message(WebSocket webSocket) + { + try + { + var buffer = new byte[1024 * 4]; + + var bytes = Encoding.UTF8.GetBytes("chocolate"); + + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(bytes), result.MessageType, result.EndOfMessage, CancellationToken.None); + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + public void Dispose() + { + _serviceHandler?.Dispose(); _steps.Dispose(); - GC.SuppressFinalize(this); - } - } -} + GC.SuppressFinalize(this); + } + } +} From c9f093cd8d43312425ce987a0117b6cb0cd56e9f Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Tue, 21 May 2024 16:22:04 +0300 Subject: [PATCH 17/23] DRY: GivenServiceEntry, GivenRoute --- .../ConsulServiceDiscoveryTests.cs | 280 ++++-------------- 1 file changed, 61 insertions(+), 219 deletions(-) diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs index d25c2075b..e3719954a 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs @@ -2,6 +2,8 @@ using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Ocelot.Configuration.File; +using Ocelot.LoadBalancer.LoadBalancers; +using System.Runtime.CompilerServices; using System.Text.RegularExpressions; namespace Ocelot.AcceptanceTests.ServiceDiscovery @@ -39,43 +41,14 @@ public void should_use_consul_service_discovery_and_load_balance_request() var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort1, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort2, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - + var serviceEntryOne = GivenServiceEntry(servicePort1, serviceName: serviceName); + var serviceEntryTwo = GivenServiceEntry(servicePort2, serviceName: serviceName); var configuration = new FileConfiguration { Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, + { + GivenRoute(serviceName: serviceName), + }, GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider @@ -107,32 +80,13 @@ public void should_handle_request_to_consul_for_downstream_service_and_make_requ const string serviceName = "web"; var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - + var serviceEntryOne = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); var configuration = new FileConfiguration { Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, + { + GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }), + }, GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider @@ -163,18 +117,7 @@ public void should_handle_request_to_consul_for_downstream_service_and_make_requ var downstreamServicePort = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = downstreamServicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - + var serviceEntryOne = GivenServiceEntry(downstreamServicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); var configuration = new FileConfiguration { GlobalConfiguration = new FileGlobalConfiguration @@ -209,36 +152,15 @@ public void should_handle_request_to_consul_for_downstream_service_and_make_requ [Fact] public void should_use_consul_service_discovery_and_load_balance_request_no_re_routes() { + const string serviceName = "product"; var consulPort = PortFinder.GetRandomPort(); - var serviceName = "product"; var serviceOnePort = PortFinder.GetRandomPort(); var serviceTwoPort = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{serviceOnePort}"; var downstreamServiceTwoUrl = $"http://localhost:{serviceTwoPort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = serviceOnePort, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = serviceTwoPort, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - + var serviceEntryOne = GivenServiceEntry(serviceOnePort, serviceName: serviceName); + var serviceEntryTwo = GivenServiceEntry(serviceTwoPort, serviceName: serviceName); var configuration = new FileConfiguration { GlobalConfiguration = new FileGlobalConfiguration @@ -269,38 +191,19 @@ public void should_use_consul_service_discovery_and_load_balance_request_no_re_r [Fact] public void should_use_token_to_make_request_to_consul() { + const string serviceName = "web"; var token = "abctoken"; var consulPort = PortFinder.GetRandomPort(); - var serviceName = "web"; var servicePort = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - + var serviceEntryOne = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); var configuration = new FileConfiguration { Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, + { + GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }), + }, GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider @@ -328,50 +231,21 @@ public void should_use_token_to_make_request_to_consul() [Fact] public void should_send_request_to_service_after_it_becomes_available_in_consul() { + const string serviceName = "product"; var consulPort = PortFinder.GetRandomPort(); - var serviceName = "product"; var servicePort1 = PortFinder.GetRandomPort(); var servicePort2 = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort1, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort2, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - + var serviceEntryOne = GivenServiceEntry(servicePort1, serviceName: serviceName); + var serviceEntryTwo = GivenServiceEntry(servicePort2, serviceName: serviceName); var configuration = new FileConfiguration { Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, + { + GivenRoute(serviceName: serviceName), + }, GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider @@ -412,32 +286,13 @@ public void should_handle_request_to_poll_consul_for_downstream_service_and_make var downstreamServicePort = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = downstreamServicePort, - ID = $"web_90_0_2_224_{downstreamServicePort}", - Tags = new[] { "version-v1" }, - }, - }; - + var serviceEntryOne = GivenServiceEntry(downstreamServicePort, "localhost", $"web_90_0_2_224_{downstreamServicePort}", new[] { "version-v1" }, serviceName); var configuration = new FileConfiguration { Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, + { + GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }), + }, GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider @@ -457,7 +312,7 @@ public void should_handle_request_to_poll_consul_for_downstream_service_and_make .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); @@ -489,53 +344,14 @@ public void Should_use_consul_service_discovery_based_on_upstream_host(string lo var responseBodyUS = "Phone chargers with US plug"; var responseBodyEU = "Phone chargers with EU plug"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryUS = new ServiceEntry - { - Service = new AgentService - { - Service = serviceNameUS, - Address = "localhost", - Port = servicePortUS, - ID = Guid.NewGuid().ToString(), - Tags = new string[] { "US" }, - }, - }; - var serviceEntryEU = new ServiceEntry - { - Service = new AgentService - { - Service = serviceNameEU, - Address = "localhost", - Port = servicePortEU, - ID = Guid.NewGuid().ToString(), - Tags = new string[] { "EU" }, - }, - }; - + var serviceEntryUS = GivenServiceEntry(servicePortUS, serviceName: serviceNameUS, tags: new[] { "US" }); + var serviceEntryEU = GivenServiceEntry(servicePortEU, serviceName: serviceNameEU, tags: new[] { "EU" }); var configuration = new FileConfiguration { Routes = new() { - new() - { - DownstreamPathTemplate = "/products", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new() { "Get" }, - UpstreamHost = upstreamHostUS, - ServiceName = serviceNameUS, - LoadBalancerOptions = new() { Type = loadBalancerType }, - }, - new() - { - DownstreamPathTemplate = "/products", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new() {"Get" }, - UpstreamHost = upstreamHostEU, - ServiceName = serviceNameEU, - LoadBalancerOptions = new() { Type = loadBalancerType }, - }, + GivenRoute("/products", "/", serviceNameUS, loadBalancerType, upstreamHostUS), + GivenRoute("/products", "/", serviceNameEU, loadBalancerType, upstreamHostEU), }, GlobalConfiguration = new() { @@ -575,6 +391,32 @@ public void Should_use_consul_service_discovery_based_on_upstream_host(string lo .BDDfy(); } + private static ServiceEntry GivenServiceEntry(int port, string address = null, string id = null, string[] tags = null, [CallerMemberName] string serviceName = null) + { + return new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = address ?? "localhost", + Port = port, + ID = id ?? Guid.NewGuid().ToString(), + Tags = tags ?? Array.Empty(), + }, + }; + } + + private static FileRoute GivenRoute(string downstream = null, string upstream = null, [CallerMemberName] string serviceName = null, string loadBalancerType = null, string upstreamHost = null, string[] httpMethods = null) => new() + { + DownstreamPathTemplate = downstream ?? "/", + DownstreamScheme = Uri.UriSchemeHttp, + UpstreamPathTemplate = upstream ?? "/", + UpstreamHttpMethod = httpMethods != null ? new(httpMethods) : new() { HttpMethods.Get }, + UpstreamHost = upstreamHost, + ServiceName = serviceName, + LoadBalancerOptions = new() { Type = loadBalancerType ?? nameof(LeastConnection) }, + }; + private void ThenTheTokenIs(string token) { _receivedToken.ShouldBe(token); From c60f40183de5a8be0fc85702c11540aed2a7fbb5 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Tue, 21 May 2024 16:24:19 +0300 Subject: [PATCH 18/23] Convert to file-scoped namespace --- .../ConsulServiceDiscoveryTests.cs | 961 +++++++++--------- 1 file changed, 480 insertions(+), 481 deletions(-) diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs index e3719954a..9ed46f5a7 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs @@ -6,581 +6,580 @@ using System.Runtime.CompilerServices; using System.Text.RegularExpressions; -namespace Ocelot.AcceptanceTests.ServiceDiscovery +namespace Ocelot.AcceptanceTests.ServiceDiscovery; + +public class ConsulServiceDiscoveryTests : IDisposable { - public class ConsulServiceDiscoveryTests : IDisposable + private readonly Steps _steps; + private readonly List _consulServices; + private int _counterOne; + private int _counterTwo; + private int _counterConsul; + private static readonly object SyncLock = new(); + private string _downstreamPath; + private string _receivedToken; + private readonly ServiceHandler _serviceHandler; + private readonly ServiceHandler _serviceHandler2; + private readonly ServiceHandler _consulHandler; + + public ConsulServiceDiscoveryTests() { - private readonly Steps _steps; - private readonly List _consulServices; - private int _counterOne; - private int _counterTwo; - private int _counterConsul; - private static readonly object SyncLock = new(); - private string _downstreamPath; - private string _receivedToken; - private readonly ServiceHandler _serviceHandler; - private readonly ServiceHandler _serviceHandler2; - private readonly ServiceHandler _consulHandler; - - public ConsulServiceDiscoveryTests() - { - _serviceHandler = new ServiceHandler(); - _serviceHandler2 = new ServiceHandler(); - _consulHandler = new ServiceHandler(); - _steps = new Steps(); - _consulServices = new List(); - } + _serviceHandler = new ServiceHandler(); + _serviceHandler2 = new ServiceHandler(); + _consulHandler = new ServiceHandler(); + _steps = new Steps(); + _consulServices = new List(); + } - [Fact] - public void should_use_consul_service_discovery_and_load_balance_request() + [Fact] + public void should_use_consul_service_discovery_and_load_balance_request() + { + var consulPort = PortFinder.GetRandomPort(); + var servicePort1 = PortFinder.GetRandomPort(); + var servicePort2 = PortFinder.GetRandomPort(); + var serviceName = "product"; + var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; + var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = GivenServiceEntry(servicePort1, serviceName: serviceName); + var serviceEntryTwo = GivenServiceEntry(servicePort2, serviceName: serviceName); + var configuration = new FileConfiguration { - var consulPort = PortFinder.GetRandomPort(); - var servicePort1 = PortFinder.GetRandomPort(); - var servicePort2 = PortFinder.GetRandomPort(); - var serviceName = "product"; - var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; - var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = GivenServiceEntry(servicePort1, serviceName: serviceName); - var serviceEntryTwo = GivenServiceEntry(servicePort2, serviceName: serviceName); - var configuration = new FileConfiguration + Routes = new List { - Routes = new List - { - GivenRoute(serviceName: serviceName), - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); - var configuration = new FileConfiguration + GivenRoute(serviceName: serviceName), + }, + GlobalConfiguration = new FileGlobalConfiguration { - Routes = new List + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { - GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }), + Scheme = "http", + Host = "localhost", + Port = consulPort, }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; + }, + }; - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/home")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) .BDDfy(); - } + } - [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes() + [Fact] + public void should_handle_request_to_consul_for_downstream_service_and_make_request() + { + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + const string serviceName = "web"; + var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); + var configuration = new FileConfiguration { - var consulPort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = GivenServiceEntry(downstreamServicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); - var configuration = new FileConfiguration + Routes = new List { - GlobalConfiguration = new FileGlobalConfiguration + GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }), + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - DownstreamScheme = "http", - HttpHandlerOptions = new FileHttpHandlerOptions - { - AllowAutoRedirect = true, - UseCookieContainer = true, - UseTracing = false, - }, + Scheme = "http", + Host = "localhost", + Port = consulPort, }, - }; + }, + }; - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/web/something")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/home")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } - [Fact] - public void should_use_consul_service_discovery_and_load_balance_request_no_re_routes() + [Fact] + public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes() + { + var consulPort = PortFinder.GetRandomPort(); + const string serviceName = "web"; + var downstreamServicePort = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = GivenServiceEntry(downstreamServicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); + var configuration = new FileConfiguration { - const string serviceName = "product"; - var consulPort = PortFinder.GetRandomPort(); - var serviceOnePort = PortFinder.GetRandomPort(); - var serviceTwoPort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{serviceOnePort}"; - var downstreamServiceTwoUrl = $"http://localhost:{serviceTwoPort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = GivenServiceEntry(serviceOnePort, serviceName: serviceName); - var serviceEntryTwo = GivenServiceEntry(serviceTwoPort, serviceName: serviceName); - var configuration = new FileConfiguration + GlobalConfiguration = new FileGlobalConfiguration { - GlobalConfiguration = new FileGlobalConfiguration + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - DownstreamScheme = "http", + Scheme = "http", + Host = "localhost", + Port = consulPort, }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) - .BDDfy(); - } + DownstreamScheme = "http", + HttpHandlerOptions = new FileHttpHandlerOptions + { + AllowAutoRedirect = true, + UseCookieContainer = true, + UseTracing = false, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/web/something")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } - [Fact] - public void should_use_token_to_make_request_to_consul() + [Fact] + public void should_use_consul_service_discovery_and_load_balance_request_no_re_routes() + { + const string serviceName = "product"; + var consulPort = PortFinder.GetRandomPort(); + var serviceOnePort = PortFinder.GetRandomPort(); + var serviceTwoPort = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{serviceOnePort}"; + var downstreamServiceTwoUrl = $"http://localhost:{serviceTwoPort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = GivenServiceEntry(serviceOnePort, serviceName: serviceName); + var serviceEntryTwo = GivenServiceEntry(serviceTwoPort, serviceName: serviceName); + var configuration = new FileConfiguration { - const string serviceName = "web"; - var token = "abctoken"; - var consulPort = PortFinder.GetRandomPort(); - var servicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); - var configuration = new FileConfiguration + GlobalConfiguration = new FileGlobalConfiguration { - Routes = new List - { - GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }), - }, - GlobalConfiguration = new FileGlobalConfiguration + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - Token = token, - }, + Scheme = "http", + Host = "localhost", + Port = consulPort, }, - }; - - this.Given(_ => GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(_ => _steps.GivenThereIsAConfiguration(configuration)) - .And(_ => _steps.GivenOcelotIsRunningWithConsul()) - .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/home")) - .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(_ => ThenTheTokenIs(token)) - .BDDfy(); - } + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + DownstreamScheme = "http", + }, + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) + .BDDfy(); + } - [Fact] - public void should_send_request_to_service_after_it_becomes_available_in_consul() + [Fact] + public void should_use_token_to_make_request_to_consul() + { + const string serviceName = "web"; + var token = "abctoken"; + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); + var configuration = new FileConfiguration { - const string serviceName = "product"; - var consulPort = PortFinder.GetRandomPort(); - var servicePort1 = PortFinder.GetRandomPort(); - var servicePort2 = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; - var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = GivenServiceEntry(servicePort1, serviceName: serviceName); - var serviceEntryTwo = GivenServiceEntry(servicePort2, serviceName: serviceName); - var configuration = new FileConfiguration + Routes = new List { - Routes = new List - { - GivenRoute(serviceName: serviceName), - }, - GlobalConfiguration = new FileGlobalConfiguration + GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }), + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, + Scheme = "http", + Host = "localhost", + Port = consulPort, + Token = token, }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .And(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) - .And(x => WhenIRemoveAService(serviceEntryTwo)) - .And(x => GivenIResetCounters()) - .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .And(x => ThenOnlyOneServiceHasBeenCalled()) - .And(x => WhenIAddAServiceBackIn(serviceEntryTwo)) - .And(x => GivenIResetCounters()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) - .BDDfy(); - } + }, + }; - [Fact] - public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request() + this.Given(_ => GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(_ => _steps.GivenThereIsAConfiguration(configuration)) + .And(_ => _steps.GivenOcelotIsRunningWithConsul()) + .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/home")) + .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(_ => ThenTheTokenIs(token)) + .BDDfy(); + } + + [Fact] + public void should_send_request_to_service_after_it_becomes_available_in_consul() + { + const string serviceName = "product"; + var consulPort = PortFinder.GetRandomPort(); + var servicePort1 = PortFinder.GetRandomPort(); + var servicePort2 = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; + var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = GivenServiceEntry(servicePort1, serviceName: serviceName); + var serviceEntryTwo = GivenServiceEntry(servicePort2, serviceName: serviceName); + var configuration = new FileConfiguration { - var consulPort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = GivenServiceEntry(downstreamServicePort, "localhost", $"web_90_0_2_224_{downstreamServicePort}", new[] { "version-v1" }, serviceName); - var configuration = new FileConfiguration + Routes = new List { - Routes = new List - { - GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }), - }, - GlobalConfiguration = new FileGlobalConfiguration + GivenRoute(serviceName: serviceName), + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - Type = "PollConsul", - PollingInterval = 0, - Namespace = string.Empty, - }, + Scheme = "http", + Host = "localhost", + Port = consulPort, }, - }; + }, + }; - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .And(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) + .And(x => WhenIRemoveAService(serviceEntryTwo)) + .And(x => GivenIResetCounters()) + .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .And(x => ThenOnlyOneServiceHasBeenCalled()) + .And(x => WhenIAddAServiceBackIn(serviceEntryTwo)) + .And(x => GivenIResetCounters()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) .BDDfy(); - } + } - [Theory] - [Trait("PR", "1944")] - [Trait("Issues", "849 1496")] - [InlineData("LeastConnection")] - [InlineData("RoundRobin")] - [InlineData("NoLoadBalancer")] - [InlineData("CookieStickySessions")] - public void Should_use_consul_service_discovery_based_on_upstream_host(string loadBalancerType) + [Fact] + public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request() + { + var consulPort = PortFinder.GetRandomPort(); + const string serviceName = "web"; + var downstreamServicePort = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = GivenServiceEntry(downstreamServicePort, "localhost", $"web_90_0_2_224_{downstreamServicePort}", new[] { "version-v1" }, serviceName); + var configuration = new FileConfiguration { - // Simulate two DIFFERENT downstream services (e.g. product services for US and EU markets) - // with different ServiceNames (e.g. product-us and product-eu), - // UpstreamHost is used to determine which ServiceName to use when making a request to Consul (e.g. Host: us-shop goes to product-us) - var consulPort = PortFinder.GetRandomPort(); - var servicePortUS = PortFinder.GetRandomPort(); - var servicePortEU = PortFinder.GetRandomPort(); - var serviceNameUS = "product-us"; - var serviceNameEU = "product-eu"; - var downstreamServiceUrlUS = $"http://localhost:{servicePortUS}"; - var downstreamServiceUrlEU = $"http://localhost:{servicePortEU}"; - var upstreamHostUS = "us-shop"; - var upstreamHostEU = "eu-shop"; - var publicUrlUS = $"http://{upstreamHostUS}"; - var publicUrlEU = $"http://{upstreamHostEU}"; - var responseBodyUS = "Phone chargers with US plug"; - var responseBodyEU = "Phone chargers with EU plug"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryUS = GivenServiceEntry(servicePortUS, serviceName: serviceNameUS, tags: new[] { "US" }); - var serviceEntryEU = GivenServiceEntry(servicePortEU, serviceName: serviceNameEU, tags: new[] { "EU" }); - var configuration = new FileConfiguration + Routes = new List { - Routes = new() - { - GivenRoute("/products", "/", serviceNameUS, loadBalancerType, upstreamHostUS), - GivenRoute("/products", "/", serviceNameEU, loadBalancerType, upstreamHostEU), - }, - GlobalConfiguration = new() + GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }), + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { - ServiceDiscoveryProvider = new() - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, + Scheme = "http", + Host = "localhost", + Port = consulPort, + Type = "PollConsul", + PollingInterval = 0, + Namespace = string.Empty, }, - }; - - // Ocelot request for http://us-shop/ should find 'product-us' in Consul, call /products and return "Phone chargers with US plug" - // Ocelot request for http://eu-shop/ should find 'product-eu' in Consul, call /products and return "Phone chargers with EU plug" - this.Given(x => x._serviceHandler.GivenThereIsAServiceRunningOn(downstreamServiceUrlUS, "/products", MapGet("/products", responseBodyUS))) - .And(x => x._serviceHandler2.GivenThereIsAServiceRunningOn(downstreamServiceUrlEU, "/products", MapGet("/products", responseBodyEU))) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryUS, serviceEntryEU)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul(publicUrlUS, publicUrlEU)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop for the first time") - .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(1)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyUS)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop for the first time") - .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(2)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyEU)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop again") - .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(3)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyUS)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop again") - .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(4)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyEU)) - .BDDfy(); - } + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } - private static ServiceEntry GivenServiceEntry(int port, string address = null, string id = null, string[] tags = null, [CallerMemberName] string serviceName = null) + [Theory] + [Trait("PR", "1944")] + [Trait("Issues", "849 1496")] + [InlineData("LeastConnection")] + [InlineData("RoundRobin")] + [InlineData("NoLoadBalancer")] + [InlineData("CookieStickySessions")] + public void Should_use_consul_service_discovery_based_on_upstream_host(string loadBalancerType) + { + // Simulate two DIFFERENT downstream services (e.g. product services for US and EU markets) + // with different ServiceNames (e.g. product-us and product-eu), + // UpstreamHost is used to determine which ServiceName to use when making a request to Consul (e.g. Host: us-shop goes to product-us) + var consulPort = PortFinder.GetRandomPort(); + var servicePortUS = PortFinder.GetRandomPort(); + var servicePortEU = PortFinder.GetRandomPort(); + var serviceNameUS = "product-us"; + var serviceNameEU = "product-eu"; + var downstreamServiceUrlUS = $"http://localhost:{servicePortUS}"; + var downstreamServiceUrlEU = $"http://localhost:{servicePortEU}"; + var upstreamHostUS = "us-shop"; + var upstreamHostEU = "eu-shop"; + var publicUrlUS = $"http://{upstreamHostUS}"; + var publicUrlEU = $"http://{upstreamHostEU}"; + var responseBodyUS = "Phone chargers with US plug"; + var responseBodyEU = "Phone chargers with EU plug"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryUS = GivenServiceEntry(servicePortUS, serviceName: serviceNameUS, tags: new[] { "US" }); + var serviceEntryEU = GivenServiceEntry(servicePortEU, serviceName: serviceNameEU, tags: new[] { "EU" }); + var configuration = new FileConfiguration { - return new ServiceEntry + Routes = new() + { + GivenRoute("/products", "/", serviceNameUS, loadBalancerType, upstreamHostUS), + GivenRoute("/products", "/", serviceNameEU, loadBalancerType, upstreamHostEU), + }, + GlobalConfiguration = new() { - Service = new AgentService + ServiceDiscoveryProvider = new() { - Service = serviceName, - Address = address ?? "localhost", - Port = port, - ID = id ?? Guid.NewGuid().ToString(), - Tags = tags ?? Array.Empty(), + Scheme = "http", + Host = "localhost", + Port = consulPort, }, - }; - } + }, + }; + + // Ocelot request for http://us-shop/ should find 'product-us' in Consul, call /products and return "Phone chargers with US plug" + // Ocelot request for http://eu-shop/ should find 'product-eu' in Consul, call /products and return "Phone chargers with EU plug" + this.Given(x => x._serviceHandler.GivenThereIsAServiceRunningOn(downstreamServiceUrlUS, "/products", MapGet("/products", responseBodyUS))) + .And(x => x._serviceHandler2.GivenThereIsAServiceRunningOn(downstreamServiceUrlEU, "/products", MapGet("/products", responseBodyEU))) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryUS, serviceEntryEU)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul(publicUrlUS, publicUrlEU)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop for the first time") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(1)) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyUS)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop for the first time") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(2)) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyEU)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop again") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(3)) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyUS)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop again") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(4)) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyEU)) + .BDDfy(); + } - private static FileRoute GivenRoute(string downstream = null, string upstream = null, [CallerMemberName] string serviceName = null, string loadBalancerType = null, string upstreamHost = null, string[] httpMethods = null) => new() + private static ServiceEntry GivenServiceEntry(int port, string address = null, string id = null, string[] tags = null, [CallerMemberName] string serviceName = null) + { + return new ServiceEntry { - DownstreamPathTemplate = downstream ?? "/", - DownstreamScheme = Uri.UriSchemeHttp, - UpstreamPathTemplate = upstream ?? "/", - UpstreamHttpMethod = httpMethods != null ? new(httpMethods) : new() { HttpMethods.Get }, - UpstreamHost = upstreamHost, - ServiceName = serviceName, - LoadBalancerOptions = new() { Type = loadBalancerType ?? nameof(LeastConnection) }, + Service = new AgentService + { + Service = serviceName, + Address = address ?? "localhost", + Port = port, + ID = id ?? Guid.NewGuid().ToString(), + Tags = tags ?? Array.Empty(), + }, }; + } - private void ThenTheTokenIs(string token) - { - _receivedToken.ShouldBe(token); - } + private static FileRoute GivenRoute(string downstream = null, string upstream = null, [CallerMemberName] string serviceName = null, string loadBalancerType = null, string upstreamHost = null, string[] httpMethods = null) => new() + { + DownstreamPathTemplate = downstream ?? "/", + DownstreamScheme = Uri.UriSchemeHttp, + UpstreamPathTemplate = upstream ?? "/", + UpstreamHttpMethod = httpMethods != null ? new(httpMethods) : new() { HttpMethods.Get }, + UpstreamHost = upstreamHost, + ServiceName = serviceName, + LoadBalancerOptions = new() { Type = loadBalancerType ?? nameof(LeastConnection) }, + }; + + private void ThenTheTokenIs(string token) + { + _receivedToken.ShouldBe(token); + } - private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo) - { - _consulServices.Add(serviceEntryTwo); - } + private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo) + { + _consulServices.Add(serviceEntryTwo); + } - private void ThenOnlyOneServiceHasBeenCalled() - { - _counterOne.ShouldBe(10); - _counterTwo.ShouldBe(0); - } + private void ThenOnlyOneServiceHasBeenCalled() + { + _counterOne.ShouldBe(10); + _counterTwo.ShouldBe(0); + } - private void WhenIRemoveAService(ServiceEntry serviceEntryTwo) - { - _consulServices.Remove(serviceEntryTwo); - } + private void WhenIRemoveAService(ServiceEntry serviceEntryTwo) + { + _consulServices.Remove(serviceEntryTwo); + } - private void GivenIResetCounters() - { - _counterOne = 0; - _counterTwo = 0; - _counterConsul = 0; - } + private void GivenIResetCounters() + { + _counterOne = 0; + _counterTwo = 0; + _counterConsul = 0; + } - private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) - { - _counterOne.ShouldBeInRange(bottom, top); - _counterOne.ShouldBeInRange(bottom, top); - } + private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) + { + _counterOne.ShouldBeInRange(bottom, top); + _counterOne.ShouldBeInRange(bottom, top); + } - private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) + private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) + { + var total = _counterOne + _counterTwo; + total.ShouldBe(expected); + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach (var serviceEntry in serviceEntries) { - var total = _counterOne + _counterTwo; - total.ShouldBe(expected); + _consulServices.Add(serviceEntry); } + } - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) + { + _consulHandler.GivenThereIsAServiceRunningOn(url, async context => { - foreach (var serviceEntry in serviceEntries) + if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) { - _consulServices.Add(serviceEntry); + _receivedToken = values.First(); } - } - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) - { - _consulHandler.GivenThereIsAServiceRunningOn(url, async context => + // Parse the request path to get the service name + var pathMatch = Regex.Match(context.Request.Path.Value, "/v1/health/service/(?[^/]+)"); + if (pathMatch.Success) { - if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) - { - _receivedToken = values.First(); - } - - // Parse the request path to get the service name - var pathMatch = Regex.Match(context.Request.Path.Value, "/v1/health/service/(?[^/]+)"); - if (pathMatch.Success) - { - _counterConsul++; - - // Use the parsed service name to filter the registered Consul services - var serviceName = pathMatch.Groups["serviceName"].Value; - var services = _consulServices.Where(x => x.Service.Service == serviceName).ToList(); - var json = JsonConvert.SerializeObject(services); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - } + _counterConsul++; + + // Use the parsed service name to filter the registered Consul services + var serviceName = pathMatch.Groups["serviceName"].Value; + var services = _consulServices.Where(x => x.Service.Service == serviceName).ToList(); + var json = JsonConvert.SerializeObject(services); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + } - private void ThenConsulShouldHaveBeenCalledTimes(int expected) - { - _counterConsul.ShouldBe(expected); - } + private void ThenConsulShouldHaveBeenCalledTimes(int expected) + { + _counterConsul.ShouldBe(expected); + } - private void GivenProductServiceOneIsRunning(string url, int statusCode) + private void GivenProductServiceOneIsRunning(string url, int statusCode) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + try { - try - { - string response; - lock (SyncLock) - { - _counterOne++; - response = _counterOne.ToString(); - } - - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(response); - } - catch (Exception exception) + string response; + lock (SyncLock) { - await context.Response.WriteAsync(exception.StackTrace); + _counterOne++; + response = _counterOne.ToString(); } - }); - } - private void GivenProductServiceTwoIsRunning(string url, int statusCode) - { - _serviceHandler2.GivenThereIsAServiceRunningOn(url, async context => + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (Exception exception) { - try - { - string response; - lock (SyncLock) - { - _counterTwo++; - response = _counterTwo.ToString(); - } - - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(response); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + private void GivenProductServiceTwoIsRunning(string url, int statusCode) + { + _serviceHandler2.GivenThereIsAServiceRunningOn(url, async context => { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + try { - _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPath != basePath) + string response; + lock (SyncLock) { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); + _counterTwo++; + response = _counterTwo.ToString(); } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); - } - private RequestDelegate MapGet(string path, string responseBody) => async context => + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => { - var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - if (downstreamPath == path) + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) { - context.Response.StatusCode = 200; - await context.Response.WriteAsync(responseBody); + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); } else { - context.Response.StatusCode = 404; - await context.Response.WriteAsync("Not Found"); + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); } - }; + }); + } - public void Dispose() + private RequestDelegate MapGet(string path, string responseBody) => async context => + { + var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + if (downstreamPath == path) { - _serviceHandler?.Dispose(); - _serviceHandler2?.Dispose(); - _consulHandler?.Dispose(); - _steps.Dispose(); + context.Response.StatusCode = 200; + await context.Response.WriteAsync(responseBody); } + else + { + context.Response.StatusCode = 404; + await context.Response.WriteAsync("Not Found"); + } + }; + + public void Dispose() + { + _serviceHandler?.Dispose(); + _serviceHandler2?.Dispose(); + _consulHandler?.Dispose(); + _steps.Dispose(); } } From 8f341cb7b1d0042031ef5f73d6ab20624059420c Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Tue, 21 May 2024 16:32:08 +0300 Subject: [PATCH 19/23] Inherit from `Steps` --- .../ConsulServiceDiscoveryTests.cs | 109 +++++++++--------- 1 file changed, 53 insertions(+), 56 deletions(-) diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs index 9ed46f5a7..3af7e8dbf 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs @@ -8,9 +8,8 @@ namespace Ocelot.AcceptanceTests.ServiceDiscovery; -public class ConsulServiceDiscoveryTests : IDisposable +public sealed class ConsulServiceDiscoveryTests : Steps, IDisposable { - private readonly Steps _steps; private readonly List _consulServices; private int _counterOne; private int _counterTwo; @@ -27,10 +26,16 @@ public ConsulServiceDiscoveryTests() _serviceHandler = new ServiceHandler(); _serviceHandler2 = new ServiceHandler(); _consulHandler = new ServiceHandler(); - _steps = new Steps(); _consulServices = new List(); } + public override void Dispose() + { + _serviceHandler?.Dispose(); + _serviceHandler2?.Dispose(); + _consulHandler?.Dispose(); + } + [Fact] public void should_use_consul_service_discovery_and_load_balance_request() { @@ -64,9 +69,9 @@ public void should_use_consul_service_discovery_and_load_balance_request() .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) .BDDfy(); @@ -101,11 +106,11 @@ public void should_handle_request_to_consul_for_downstream_service_and_make_requ this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/home")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGateway("/home")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } @@ -141,11 +146,11 @@ public void should_handle_request_to_consul_for_downstream_service_and_make_requ this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/web/something")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGateway("/web/something")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } @@ -180,9 +185,9 @@ public void should_use_consul_service_discovery_and_load_balance_request_no_re_r .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50)) .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) .BDDfy(); @@ -219,11 +224,11 @@ public void should_use_token_to_make_request_to_consul() this.Given(_ => GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(_ => _steps.GivenThereIsAConfiguration(configuration)) - .And(_ => _steps.GivenOcelotIsRunningWithConsul()) - .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/home")) - .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(_ => GivenThereIsAConfiguration(configuration)) + .And(_ => GivenOcelotIsRunningWithConsul()) + .When(_ => WhenIGetUrlOnTheApiGateway("/home")) + .Then(_ => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => ThenTheResponseBodyShouldBe("Hello from Laura")) .And(_ => ThenTheTokenIs(token)) .BDDfy(); } @@ -261,18 +266,18 @@ public void should_send_request_to_service_after_it_becomes_available_in_consul( .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .And(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) .And(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) .And(x => WhenIRemoveAService(serviceEntryTwo)) .And(x => GivenIResetCounters()) - .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .And(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) .And(x => ThenOnlyOneServiceHasBeenCalled()) .And(x => WhenIAddAServiceBackIn(serviceEntryTwo)) .And(x => GivenIResetCounters()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) .BDDfy(); @@ -310,11 +315,11 @@ public void should_handle_request_to_poll_consul_for_downstream_service_and_make this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } @@ -370,24 +375,24 @@ public void Should_use_consul_service_discovery_based_on_upstream_host(string lo .And(x => x._serviceHandler2.GivenThereIsAServiceRunningOn(downstreamServiceUrlEU, "/products", MapGet("/products", responseBodyEU))) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryUS, serviceEntryEU)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul(publicUrlUS, publicUrlEU)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop for the first time") + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul(publicUrlUS, publicUrlEU)) + .When(x => WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop for the first time") .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(1)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyUS)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop for the first time") + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(responseBodyUS)) + .When(x => WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop for the first time") .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(2)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyEU)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop again") + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(responseBodyEU)) + .When(x => WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop again") .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(3)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyUS)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop again") + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(responseBodyUS)) + .When(x => WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop again") .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(4)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyEU)) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(responseBodyEU)) .BDDfy(); } @@ -574,12 +579,4 @@ private RequestDelegate MapGet(string path, string responseBody) => async contex await context.Response.WriteAsync("Not Found"); } }; - - public void Dispose() - { - _serviceHandler?.Dispose(); - _serviceHandler2?.Dispose(); - _consulHandler?.Dispose(); - _steps.Dispose(); - } } From aa292fa2394d6ad46b5f6622854708bee7041d77 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Tue, 21 May 2024 18:36:32 +0300 Subject: [PATCH 20/23] Refactor acceptance tests --- .../ConsulServiceDiscoveryTests.cs | 354 ++++++------------ 1 file changed, 120 insertions(+), 234 deletions(-) diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs index 3af7e8dbf..17e1c2498 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using Ocelot.Configuration.File; using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Provider.Consul; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; @@ -37,37 +38,19 @@ public override void Dispose() } [Fact] - public void should_use_consul_service_discovery_and_load_balance_request() + public void Should_use_consul_service_discovery_and_load_balance_request() { + const string serviceName = "product"; var consulPort = PortFinder.GetRandomPort(); - var servicePort1 = PortFinder.GetRandomPort(); - var servicePort2 = PortFinder.GetRandomPort(); - var serviceName = "product"; - var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; - var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = GivenServiceEntry(servicePort1, serviceName: serviceName); - var serviceEntryTwo = GivenServiceEntry(servicePort2, serviceName: serviceName); - var configuration = new FileConfiguration - { - Routes = new List - { - GivenRoute(serviceName: serviceName), - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var serviceEntryOne = GivenServiceEntry(port1, serviceName: serviceName); + var serviceEntryTwo = GivenServiceEntry(port2, serviceName: serviceName); + var route = GivenRoute(serviceName: serviceName); + var configuration = GivenServiceDiscovery(consulPort, route); + this.Given(x => x.GivenProductServiceOneIsRunning(DownstreamUrl(port1), 200)) + .And(x => x.GivenProductServiceTwoIsRunning(DownstreamUrl(port2), 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningWithConsul()) @@ -78,33 +61,16 @@ public void should_use_consul_service_discovery_and_load_balance_request() } [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request() + public void Should_handle_request_to_consul_for_downstream_service_and_make_request() { + const string serviceName = "web"; var consulPort = PortFinder.GetRandomPort(); var servicePort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; var serviceEntryOne = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); - var configuration = new FileConfiguration - { - Routes = new List - { - GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }), - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + var route = GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }); + var configuration = GivenServiceDiscovery(consulPort, route); + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningWithConsul()) @@ -115,37 +81,25 @@ public void should_handle_request_to_consul_for_downstream_service_and_make_requ } [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes() + public void Should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes() { - var consulPort = PortFinder.GetRandomPort(); const string serviceName = "web"; - var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = GivenServiceEntry(downstreamServicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); - var configuration = new FileConfiguration + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + var serviceEntry = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); + + var configuration = GivenServiceDiscovery(consulPort); + configuration.GlobalConfiguration.DownstreamScheme = "http"; + configuration.GlobalConfiguration.HttpHandlerOptions = new() { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - DownstreamScheme = "http", - HttpHandlerOptions = new FileHttpHandlerOptions - { - AllowAutoRedirect = true, - UseCookieContainer = true, - UseTracing = false, - }, - }, + AllowAutoRedirect = true, + UseCookieContainer = true, + UseTracing = false, }; - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/something", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningWithConsul()) .When(x => WhenIGetUrlOnTheApiGateway("/web/something")) @@ -155,36 +109,23 @@ public void should_handle_request_to_consul_for_downstream_service_and_make_requ } [Fact] - public void should_use_consul_service_discovery_and_load_balance_request_no_re_routes() + public void Should_use_consul_service_discovery_and_load_balance_request_no_re_routes() { const string serviceName = "product"; var consulPort = PortFinder.GetRandomPort(); - var serviceOnePort = PortFinder.GetRandomPort(); - var serviceTwoPort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{serviceOnePort}"; - var downstreamServiceTwoUrl = $"http://localhost:{serviceTwoPort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = GivenServiceEntry(serviceOnePort, serviceName: serviceName); - var serviceEntryTwo = GivenServiceEntry(serviceTwoPort, serviceName: serviceName); - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - DownstreamScheme = "http", - }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var serviceEntry1 = GivenServiceEntry(port1, serviceName: serviceName); + var serviceEntry2 = GivenServiceEntry(port2, serviceName: serviceName); + + var configuration = GivenServiceDiscovery(consulPort); + configuration.GlobalConfiguration.LoadBalancerOptions = new() { Type = nameof(LeastConnection) }; + configuration.GlobalConfiguration.DownstreamScheme = "http"; + + this.Given(x => x.GivenProductServiceOneIsRunning(DownstreamUrl(port1), 200)) + .And(x => x.GivenProductServiceTwoIsRunning(DownstreamUrl(port2), 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry1, serviceEntry2)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningWithConsul()) .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50)) @@ -194,36 +135,21 @@ public void should_use_consul_service_discovery_and_load_balance_request_no_re_r } [Fact] - public void should_use_token_to_make_request_to_consul() + public void Should_use_token_to_make_request_to_consul() { const string serviceName = "web"; - var token = "abctoken"; + const string token = "abctoken"; var consulPort = PortFinder.GetRandomPort(); var servicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); - var configuration = new FileConfiguration - { - Routes = new List - { - GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }), - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - Token = token, - }, - }, - }; + var serviceEntry = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); + var route = GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }); + + var configuration = GivenServiceDiscovery(consulPort, route); + configuration.GlobalConfiguration.ServiceDiscoveryProvider.Token = token; - this.Given(_ => GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + this.Given(_ => GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", 200, "Hello from Laura")) + .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntry)) .And(_ => GivenThereIsAConfiguration(configuration)) .And(_ => GivenOcelotIsRunningWithConsul()) .When(_ => WhenIGetUrlOnTheApiGateway("/home")) @@ -234,48 +160,30 @@ public void should_use_token_to_make_request_to_consul() } [Fact] - public void should_send_request_to_service_after_it_becomes_available_in_consul() + public void Should_send_request_to_service_after_it_becomes_available_in_consul() { const string serviceName = "product"; var consulPort = PortFinder.GetRandomPort(); - var servicePort1 = PortFinder.GetRandomPort(); - var servicePort2 = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; - var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = GivenServiceEntry(servicePort1, serviceName: serviceName); - var serviceEntryTwo = GivenServiceEntry(servicePort2, serviceName: serviceName); - var configuration = new FileConfiguration - { - Routes = new List - { - GivenRoute(serviceName: serviceName), - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var serviceEntry1 = GivenServiceEntry(port1, serviceName: serviceName); + var serviceEntry2 = GivenServiceEntry(port2, serviceName: serviceName); + var route = GivenRoute(serviceName: serviceName); + var configuration = GivenServiceDiscovery(consulPort, route); + this.Given(x => x.GivenProductServiceOneIsRunning(DownstreamUrl(port1), 200)) + .And(x => x.GivenProductServiceTwoIsRunning(DownstreamUrl(port2), 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry1, serviceEntry2)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningWithConsul()) .And(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) .And(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) - .And(x => WhenIRemoveAService(serviceEntryTwo)) + .And(x => WhenIRemoveAService(serviceEntry2)) .And(x => GivenIResetCounters()) .And(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) .And(x => ThenOnlyOneServiceHasBeenCalled()) - .And(x => WhenIAddAServiceBackIn(serviceEntryTwo)) + .And(x => WhenIAddAServiceBackIn(serviceEntry2)) .And(x => GivenIResetCounters()) .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) @@ -284,37 +192,23 @@ public void should_send_request_to_service_after_it_becomes_available_in_consul( } [Fact] - public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request() + public void Should_handle_request_to_poll_consul_for_downstream_service_and_make_request() { - var consulPort = PortFinder.GetRandomPort(); const string serviceName = "web"; - var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = GivenServiceEntry(downstreamServicePort, "localhost", $"web_90_0_2_224_{downstreamServicePort}", new[] { "version-v1" }, serviceName); - var configuration = new FileConfiguration - { - Routes = new List - { - GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }), - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - Type = "PollConsul", - PollingInterval = 0, - Namespace = string.Empty, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + var serviceEntry = GivenServiceEntry(servicePort, "localhost", $"web_90_0_2_224_{servicePort}", new[] { "version-v1" }, serviceName); + var route = GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }); + var configuration = GivenServiceDiscovery(consulPort, route); + + var sd = configuration.GlobalConfiguration.ServiceDiscoveryProvider; + sd.Type = nameof(PollConsul); + sd.PollingInterval = 0; + sd.Namespace = string.Empty; + + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningWithConsul()) .When(x => WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) @@ -335,45 +229,28 @@ public void Should_use_consul_service_discovery_based_on_upstream_host(string lo // Simulate two DIFFERENT downstream services (e.g. product services for US and EU markets) // with different ServiceNames (e.g. product-us and product-eu), // UpstreamHost is used to determine which ServiceName to use when making a request to Consul (e.g. Host: us-shop goes to product-us) + const string serviceNameUS = "product-us"; + const string serviceNameEU = "product-eu"; var consulPort = PortFinder.GetRandomPort(); var servicePortUS = PortFinder.GetRandomPort(); var servicePortEU = PortFinder.GetRandomPort(); - var serviceNameUS = "product-us"; - var serviceNameEU = "product-eu"; - var downstreamServiceUrlUS = $"http://localhost:{servicePortUS}"; - var downstreamServiceUrlEU = $"http://localhost:{servicePortEU}"; - var upstreamHostUS = "us-shop"; - var upstreamHostEU = "eu-shop"; + const string upstreamHostUS = "us-shop"; + const string upstreamHostEU = "eu-shop"; var publicUrlUS = $"http://{upstreamHostUS}"; var publicUrlEU = $"http://{upstreamHostEU}"; - var responseBodyUS = "Phone chargers with US plug"; - var responseBodyEU = "Phone chargers with EU plug"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + const string responseBodyUS = "Phone chargers with US plug"; + const string responseBodyEU = "Phone chargers with EU plug"; var serviceEntryUS = GivenServiceEntry(servicePortUS, serviceName: serviceNameUS, tags: new[] { "US" }); var serviceEntryEU = GivenServiceEntry(servicePortEU, serviceName: serviceNameEU, tags: new[] { "EU" }); - var configuration = new FileConfiguration - { - Routes = new() - { - GivenRoute("/products", "/", serviceNameUS, loadBalancerType, upstreamHostUS), - GivenRoute("/products", "/", serviceNameEU, loadBalancerType, upstreamHostEU), - }, - GlobalConfiguration = new() - { - ServiceDiscoveryProvider = new() - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; + var routeUS = GivenRoute("/products", "/", serviceNameUS, loadBalancerType, upstreamHostUS); + var routeEU = GivenRoute("/products", "/", serviceNameEU, loadBalancerType, upstreamHostEU); + var configuration = GivenServiceDiscovery(consulPort, routeUS, routeEU); // Ocelot request for http://us-shop/ should find 'product-us' in Consul, call /products and return "Phone chargers with US plug" // Ocelot request for http://eu-shop/ should find 'product-eu' in Consul, call /products and return "Phone chargers with EU plug" - this.Given(x => x._serviceHandler.GivenThereIsAServiceRunningOn(downstreamServiceUrlUS, "/products", MapGet("/products", responseBodyUS))) - .And(x => x._serviceHandler2.GivenThereIsAServiceRunningOn(downstreamServiceUrlEU, "/products", MapGet("/products", responseBodyEU))) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + this.Given(x => x._serviceHandler.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePortUS), "/products", MapGet("/products", responseBodyUS))) + .And(x => x._serviceHandler2.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePortEU), "/products", MapGet("/products", responseBodyEU))) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryUS, serviceEntryEU)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningWithConsul(publicUrlUS, publicUrlEU)) @@ -396,20 +273,17 @@ public void Should_use_consul_service_discovery_based_on_upstream_host(string lo .BDDfy(); } - private static ServiceEntry GivenServiceEntry(int port, string address = null, string id = null, string[] tags = null, [CallerMemberName] string serviceName = null) + private static ServiceEntry GivenServiceEntry(int port, string address = null, string id = null, string[] tags = null, [CallerMemberName] string serviceName = null) => new() { - return new ServiceEntry + Service = new AgentService { - Service = new AgentService - { - Service = serviceName, - Address = address ?? "localhost", - Port = port, - ID = id ?? Guid.NewGuid().ToString(), - Tags = tags ?? Array.Empty(), - }, - }; - } + Service = serviceName, + Address = address ?? "localhost", + Port = port, + ID = id ?? Guid.NewGuid().ToString(), + Tags = tags ?? Array.Empty(), + }, + }; private static FileRoute GivenRoute(string downstream = null, string upstream = null, [CallerMemberName] string serviceName = null, string loadBalancerType = null, string upstreamHost = null, string[] httpMethods = null) => new() { @@ -422,6 +296,18 @@ private static ServiceEntry GivenServiceEntry(int port, string address = null, s LoadBalancerOptions = new() { Type = loadBalancerType ?? nameof(LeastConnection) }, }; + private static FileConfiguration GivenServiceDiscovery(int consulPort, params FileRoute[] routes) + { + var config = GivenConfiguration(routes); + config.GlobalConfiguration.ServiceDiscoveryProvider = new() + { + Scheme = Uri.UriSchemeHttp, + Host = "localhost", + Port = consulPort, + }; + return config; + } + private void ThenTheTokenIs(string token) { _receivedToken.ShouldBe(token); @@ -565,17 +451,17 @@ private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int }); } - private RequestDelegate MapGet(string path, string responseBody) => async context => + private static RequestDelegate MapGet(string path, string responseBody) => async context => { var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; if (downstreamPath == path) { - context.Response.StatusCode = 200; + context.Response.StatusCode = (int)HttpStatusCode.OK; await context.Response.WriteAsync(responseBody); } else { - context.Response.StatusCode = 404; + context.Response.StatusCode = (int)HttpStatusCode.NotFound; await context.Response.WriteAsync("Not Found"); } }; From 8dcaf7433a90d14d6eb16ef5404ed24e1e5d406d Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Fri, 24 May 2024 20:35:38 +0300 Subject: [PATCH 21/23] Acceptance test for #954 user scenario --- .../ConsulServiceDiscoveryTests.cs | 165 ++++++++++++------ 1 file changed, 112 insertions(+), 53 deletions(-) diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs index 17e1c2498..b98f93c0d 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs @@ -1,9 +1,13 @@ using Consul; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Logging; using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; @@ -12,9 +16,11 @@ namespace Ocelot.AcceptanceTests.ServiceDiscovery; public sealed class ConsulServiceDiscoveryTests : Steps, IDisposable { private readonly List _consulServices; + private readonly List _consulNodes; private int _counterOne; private int _counterTwo; private int _counterConsul; + private int _counterNodes; private static readonly object SyncLock = new(); private string _downstreamPath; private string _receivedToken; @@ -27,7 +33,8 @@ public ConsulServiceDiscoveryTests() _serviceHandler = new ServiceHandler(); _serviceHandler2 = new ServiceHandler(); _consulHandler = new ServiceHandler(); - _consulServices = new List(); + _consulServices = new(); + _consulNodes = new(); } public override void Dispose() @@ -69,15 +76,15 @@ public void Should_handle_request_to_consul_for_downstream_service_and_make_requ var serviceEntryOne = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); var route = GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }); var configuration = GivenServiceDiscovery(consulPort, route); - this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunningWithConsul()) - .When(x => WhenIGetUrlOnTheApiGateway("/home")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", HttpStatusCode.OK, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGateway("/home")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); } [Fact] @@ -97,15 +104,15 @@ public void Should_handle_request_to_consul_for_downstream_service_and_make_requ UseTracing = false, }; - this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/something", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) - .And(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunningWithConsul()) - .When(x => WhenIGetUrlOnTheApiGateway("/web/something")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/something", HttpStatusCode.OK, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGateway("/web/something")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); } [Fact] @@ -147,7 +154,7 @@ public void Should_use_token_to_make_request_to_consul() var configuration = GivenServiceDiscovery(consulPort, route); configuration.GlobalConfiguration.ServiceDiscoveryProvider.Token = token; - this.Given(_ => GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", 200, "Hello from Laura")) + this.Given(_ => GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", HttpStatusCode.OK, "Hello from Laura")) .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntry)) .And(_ => GivenThereIsAConfiguration(configuration)) @@ -206,24 +213,24 @@ public void Should_handle_request_to_poll_consul_for_downstream_service_and_make sd.PollingInterval = 0; sd.Namespace = string.Empty; - this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) - .And(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunningWithConsul()) - .When(x => WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", HttpStatusCode.OK, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); } [Theory] [Trait("PR", "1944")] - [Trait("Issues", "849 1496")] - [InlineData("LeastConnection")] - [InlineData("RoundRobin")] - [InlineData("NoLoadBalancer")] - [InlineData("CookieStickySessions")] + [Trait("Bugs", "849 1496")] + [InlineData(nameof(LeastConnection))] + [InlineData(nameof(RoundRobin))] + [InlineData(nameof(NoLoadBalancer))] + [InlineData(nameof(CookieStickySessions))] public void Should_use_consul_service_discovery_based_on_upstream_host(string loadBalancerType) { // Simulate two DIFFERENT downstream services (e.g. product services for US and EU markets) @@ -273,6 +280,55 @@ public void Should_use_consul_service_discovery_based_on_upstream_host(string lo .BDDfy(); } + [Fact] + [Trait("Bug", "954")] + public void Should_return_service_address_by_overridden_service_builder_when_there_is_a_node() + { + const string serviceName = "OpenTestService"; + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); // 9999 + var serviceEntry = GivenServiceEntry(servicePort, + id: "OPEN_TEST_01", + serviceName: serviceName, + tags: new[] { serviceName }); + var serviceNode = new Node() { Name = "n1" }; // cornerstone of the bug + serviceEntry.Node = serviceNode; + var route = GivenRoute("/api/{url}", "/open/{url}", serviceName, httpMethods: new[] { "POST", "GET" }); + var configuration = GivenServiceDiscovery(consulPort, route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", HttpStatusCode.OK, "Hello from Raman")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) + .And(x => x.GivenTheServiceNodesAreRegisteredWithConsul(serviceNode)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) // default services registration results with the bug: "n1" host issue + .When(x => WhenIGetUrlOnTheApiGateway("/open/home")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) + .And(x => ThenTheResponseBodyShouldBe("")) + .And(x => ThenConsulShouldHaveBeenCalledTimes(1)) + .And(x => ThenConsulNodesShouldHaveBeenCalledTimes(1)) + + // Override default service builder + .Given(x => GivenOcelotIsRunningWithServices(WithOverriddenConsulServiceBuilder)) + .When(x => WhenIGetUrlOnTheApiGateway("/open/home")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Raman")) + .And(x => ThenConsulShouldHaveBeenCalledTimes(2)) + .And(x => ThenConsulNodesShouldHaveBeenCalledTimes(2)) + .BDDfy(); + } + + private static void WithOverriddenConsulServiceBuilder(IServiceCollection services) + => services.AddOcelot().AddConsul(); + + public class MyConsulServiceBuilder : DefaultConsulServiceBuilder + { + public MyConsulServiceBuilder(Func configurationFactory, IConsulClientFactory clientFactory, IOcelotLoggerFactory loggerFactory) + : base(configurationFactory, clientFactory, loggerFactory) { } + + protected override string GetDownstreamHost(ServiceEntry entry, Node node) => entry.Service.Address; + } + private static ServiceEntry GivenServiceEntry(int port, string address = null, string id = null, string[] tags = null, [CallerMemberName] string serviceName = null) => new() { Service = new AgentService @@ -304,6 +360,7 @@ private static FileConfiguration GivenServiceDiscovery(int consulPort, params Fi Scheme = Uri.UriSchemeHttp, Host = "localhost", Port = consulPort, + Type = nameof(Provider.Consul.Consul), }; return config; } @@ -313,9 +370,9 @@ private void ThenTheTokenIs(string token) _receivedToken.ShouldBe(token); } - private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo) + private void WhenIAddAServiceBackIn(ServiceEntry serviceEntry) { - _consulServices.Add(serviceEntryTwo); + _consulServices.Add(serviceEntry); } private void ThenOnlyOneServiceHasBeenCalled() @@ -324,9 +381,9 @@ private void ThenOnlyOneServiceHasBeenCalled() _counterTwo.ShouldBe(0); } - private void WhenIRemoveAService(ServiceEntry serviceEntryTwo) + private void WhenIRemoveAService(ServiceEntry serviceEntry) { - _consulServices.Remove(serviceEntryTwo); + _consulServices.Remove(serviceEntry); } private void GivenIResetCounters() @@ -348,13 +405,8 @@ private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) total.ShouldBe(expected); } - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) - { - foreach (var serviceEntry in serviceEntries) - { - _consulServices.Add(serviceEntry); - } - } + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) => _consulServices.AddRange(serviceEntries); + private void GivenTheServiceNodesAreRegisteredWithConsul(params Node[] nodes) => _consulNodes.AddRange(nodes); private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) { @@ -377,14 +429,21 @@ private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) var json = JsonConvert.SerializeObject(services); context.Response.Headers.Append("Content-Type", "application/json"); await context.Response.WriteAsync(json); + return; + } + + if (context.Request.Path.Value == "/v1/catalog/nodes") + { + _counterNodes++; + var json = JsonConvert.SerializeObject(_consulNodes); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); } }); } - private void ThenConsulShouldHaveBeenCalledTimes(int expected) - { - _counterConsul.ShouldBe(expected); - } + private void ThenConsulShouldHaveBeenCalledTimes(int expected) => _counterConsul.ShouldBe(expected); + private void ThenConsulNodesShouldHaveBeenCalledTimes(int expected) => _counterNodes.ShouldBe(expected); private void GivenProductServiceOneIsRunning(string url, int statusCode) { @@ -432,7 +491,7 @@ private void GivenProductServiceTwoIsRunning(string url, int statusCode) }); } - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, HttpStatusCode statusCode, string responseBody) { _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => { @@ -440,12 +499,12 @@ private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int if (_downstreamPath != basePath) { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync("Downstream path doesn't match base path"); } else { - context.Response.StatusCode = statusCode; + context.Response.StatusCode = (int)statusCode; await context.Response.WriteAsync(responseBody); } }); From 8073a9947c66eb32623b53d8e859c031ac1235b2 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Sat, 25 May 2024 18:35:15 +0300 Subject: [PATCH 22/23] Move "Store Configuration in Consul" to "Service Discovery" --- docs/features/configuration.rst | 68 ++++------------------ docs/features/servicediscovery.rst | 91 ++++++++++++++++++++++++++---- 2 files changed, 90 insertions(+), 69 deletions(-) diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 4447dac3d..5a29a6b48 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -216,62 +216,14 @@ For example: Examining the code within the `ConfigurationBuilderExtensions class `_ would be helpful for gaining a better understanding of the signatures of the overloaded methods [#f2]_. -Store Configuration in Consul ------------------------------ +Store Configuration in `Consul`_ +-------------------------------- -The first thing you need to do is install the `NuGet package `_ that provides `Consul `_ support in Ocelot. +As a developer, if you have enabled :doc:`../features/servicediscovery` with `Consul`_ support in Ocelot, you may choose to manage your configuration saving to the *Consul* `KV store`_. -.. code-block:: powershell +Beyond the traditional methods of storing configuration in a file vs folder (:ref:`config-merging-files`), or in-memory (:ref:`config-merging-tomemory`), you also have the alternative to utilize the `Consul`_ server's storage capabilities. - Install-Package Ocelot.Provider.Consul - -Then you add the following when you register your services Ocelot will attempt to store and retrieve its configuration in Consul KV store. -In order to register Consul services we must call the ``AddConsul()`` and ``AddConfigStoredInConsul()`` extensions using the ``OcelotBuilder`` being returned by ``AddOcelot()`` [#f3]_ like below: - -.. code-block:: csharp - - services.AddOcelot() - .AddConsul() - .AddConfigStoredInConsul(); - -You also need to add the following to your `ocelot.json`_. This is how Ocelot finds your Consul agent and interacts to load and store the configuration from Consul. - -.. code-block:: json - - "GlobalConfiguration": { - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 9500 - } - } - -The team decided to create this feature after working on the Raft consensus algorithm and finding out its super hard. -Why not take advantage of the fact Consul already gives you this! -We guess it means if you want to use Ocelot to its fullest, you take on Consul as a dependency for now. - -This feature has a `3 seconds `_ TTL cache before making a new request to your local Consul agent. - -.. _config-consul-key: - -Consul Configuration Key [#f4]_ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If you are using Consul for configuration (or other providers in the future), you might want to key your configurations: so you can have multiple configurations. - -In order to specify the key you need to set the **ConfigurationKey** property in the **ServiceDiscoveryProvider** options of the configuration JSON file e.g. - -.. code-block:: json - - "GlobalConfiguration": { - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 9500, - "ConfigurationKey": "Ocelot_A" - } - } - -In this example Ocelot will use ``Ocelot_A`` as the key for your configuration when looking it up in Consul. -If you do not set the **ConfigurationKey**, Ocelot will use the string ``InternalConfiguration`` as the key. +For further details on managing Ocelot configurations via a Consul instance, please consult the ":ref:`sd-consul-configuration-in-kv`" section. Follow Redirects aka HttpHandlerOptions --------------------------------------- @@ -417,7 +369,7 @@ Ocelot allows you to choose the HTTP version it will use to make the proxy reque .. _config-version-policy: -DownstreamHttpVersionPolicy [#f5]_ +DownstreamHttpVersionPolicy [#f3]_ ---------------------------------- This routing property enables the configuration of the ``VersionPolicy`` property within ``HttpRequestMessage`` objects for downstream HTTP requests. @@ -557,12 +509,12 @@ Now, the route metadata can be accessed through the `DownstreamRoute` object: .. [#f1] ":ref:`config-merging-files`" feature was requested in `issue 296 `_, since then we extended it in `issue 1216 `_ (PR `1227 `_) as ":ref:`config-merging-tomemory`" subfeature which was released as a part of version `23.2`_. .. [#f2] ":ref:`config-merging-tomemory`" subfeature is based on the ``MergeOcelotJson`` enumeration type with values: ``ToFile`` and ``ToMemory``. The 1st one is implicit by default, and the second one is exactly what you need when merging to memory. See more details on implementations in the `ConfigurationBuilderExtensions`_ class. -.. [#f3] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. -.. [#f4] ":ref:`config-consul-key`" feature was requested in `issue 346 `_ as a part of version `7.0.0 `_. -.. [#f5] ":ref:`config-version-policy`" feature was requested in `issue 1672 `_ as a part of version `24.0`_. +.. [#f3] ":ref:`config-version-policy`" feature was requested in `issue 1672 `_ as a part of version `23.3`_. .. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0 .. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 -.. _24.0: https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0 +.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/ocelot.json .. _ConfigurationBuilderExtensions: https://github.com/ThreeMammals/Ocelot/blob/develop/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs +.. _Consul: https://www.consul.io/ +.. _KV Store: https://developer.hashicorp.com/consul/docs/dynamic-app-config/kv diff --git a/docs/features/servicediscovery.rst b/docs/features/servicediscovery.rst index 6d9d8d4ef..2b97090dd 100644 --- a/docs/features/servicediscovery.rst +++ b/docs/features/servicediscovery.rst @@ -9,28 +9,85 @@ At the moment this is only supported in the **GlobalConfiguration** section, whi Consul ------ - | **Namespace**: `Ocelot.Provider.Consul `_ + | **Namespace**: ``Ocelot.Provider.Consul`` -The first thing you need to do is install the `Ocelot.Provider.Consul `__ package that provides `Consul `_ support in Ocelot: +The first thing you need to do is install the `Ocelot.Provider.Consul `_ package that provides `Consul`_ support in Ocelot: .. code-block:: powershell Install-Package Ocelot.Provider.Consul -Then add the following to your ``ConfigureServices`` method: +To register *Consul* services, you must invoke the ``AddConsul()`` extension using the ``OcelotBuilder`` returned by ``AddOcelot()`` [#f1]_. +Therefore, include the following in your ``ConfigureServices`` method: .. code-block:: csharp services.AddOcelot() .AddConsul(); -Currently there are 2 types of Consul *service discovery* providers: ``Consul`` and ``PollConsul``. -The default provider is ``Consul``, which means that if ``ConsulProviderFactory`` cannot read, understand, or parse the **Type** property of the ``ServiceProviderConfiguration`` object, then a ``Consul`` provider instance is created by the factory. +Currently there are 2 types of *Consul* service discovery providers: ``Consul`` and ``PollConsul``. +The default provider is ``Consul``, which means that if ``ConsulProviderFactory`` cannot read, understand, or parse the **Type** property of the ``ServiceProviderConfiguration`` object, +then a :ref:`sd-consul-provider` instance is created by the factory. -Explore these types of providers and understand the differences in the subsections below. +Explore these types of providers and understand the differences in the subsections: :ref:`sd-consul-provider` and :ref:`sd-pollconsul-provider`. -Consul Provider Type -^^^^^^^^^^^^^^^^^^^^ +.. _sd-consul-configuration-in-kv: + +Configuration in `KV Store`_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Add the following when you register your services Ocelot will attempt to store and retrieve its :doc:`../features/configuration` in *Consul* `KV Store`_: + +.. code-block:: csharp + + services.AddOcelot() + .AddConsul() + .AddConfigStoredInConsul(); // ! + +You also need to add the following to your `ocelot.json`_. +This is how Ocelot finds your *Consul* agent and interacts to load and store the configuration from *Consul*. + +.. code-block:: json + + "GlobalConfiguration": { + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 9500 + } + } + +The team decided to create this feature after working on the Raft consensus algorithm and finding out its super hard. +Why not take advantage of the fact Consul already gives you this! +We guess it means if you want to use Ocelot to its fullest, you take on Consul as a dependency for now. + + **Note!** This feature has a `3 seconds TTL`_ cache before making a new request to your local *Consul* agent. + +.. _sd-consul-configuration-key: + +Consul Configuration Key [#f2]_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you are using *Consul* for :doc:`../features/configuration` (or other providers in the future), you might want to key your configurations: so you can have multiple configurations. + +In order to specify the key you need to set the **ConfigurationKey** property in the **ServiceDiscoveryProvider** options of the configuration JSON file e.g. + +.. code-block:: json + + "GlobalConfiguration": { + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 9500, + "ConfigurationKey": "Ocelot_A" // ! + } + } + +In this example Ocelot will use ``Ocelot_A`` as the key for your configuration when looking it up in *Consul*. +If you do not set the **ConfigurationKey**, Ocelot will use the string ``InternalConfiguration`` as the key. + +.. _sd-consul-provider: + +``Consul`` Provider +^^^^^^^^^^^^^^^^^^^ | **Class**: `Ocelot.Provider.Consul.Consul `_ @@ -67,8 +124,10 @@ If no load balancer is specified, Ocelot will not load balance requests. When this is set up Ocelot will lookup the downstream host and port from the *service discovery* provider and load balance requests across any available services. -PollConsul Provider Type -^^^^^^^^^^^^^^^^^^^^^^^^ +.. _sd-pollconsul-provider: + +``PollConsul`` Provider +^^^^^^^^^^^^^^^^^^^^^^^ | **Class**: `Ocelot.Provider.Consul.PollConsul `_ @@ -98,7 +157,7 @@ Service Definition Your services need to be added to Consul something like below (C# style but hopefully this make sense)... The only important thing to note is not to add ``http`` or ``https`` to the ``Address`` field. We have been contacted before about not accepting scheme in ``Address``. -After reading `this `_ we do not think the scheme should be in there. +After reading `Agents Overview `_ and `Define services `_ docs we do not think the **scheme** should be in there. In C# @@ -410,3 +469,13 @@ After this, you need to add the ``IServiceDiscoveryProviderFactory`` interface t Note that in this case the Ocelot pipeline will not use ``ServiceDiscoveryProviderFactory`` by default. Additionally, you do not need to specify ``"Type": "MyServiceDiscoveryProvider"`` in the **ServiceDiscoveryProvider** properties of the **GlobalConfiguration** settings. But you can leave this ``Type`` option for compatibility between both designs. + +"""" + +.. [#f1] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. +.. [#f2] *"Consul Configuration Key"* feature was requested in `issue 346 `_ as a part of version `7.0.0 `_. + +.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/ocelot.json +.. _Consul: https://www.consul.io/ +.. _KV Store: https://developer.hashicorp.com/consul/docs/dynamic-app-config/kv +.. _3 seconds TTL: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+TimeSpan.FromSeconds%283%29&type=code From 7bb76e363279cc0ec3eef157759f45fd252921c1 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Sat, 25 May 2024 21:12:57 +0300 Subject: [PATCH 23/23] Feature docs --- docs/features/servicediscovery.rst | 75 +++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/docs/features/servicediscovery.rst b/docs/features/servicediscovery.rst index 2b97090dd..a4e4f96c5 100644 --- a/docs/features/servicediscovery.rst +++ b/docs/features/servicediscovery.rst @@ -23,7 +23,7 @@ Therefore, include the following in your ``ConfigureServices`` method: .. code-block:: csharp services.AddOcelot() - .AddConsul(); + .AddConsul(); // or .AddConsul() Currently there are 2 types of *Consul* service discovery providers: ``Consul`` and ``PollConsul``. The default provider is ``Consul``, which means that if ``ConsulProviderFactory`` cannot read, understand, or parse the **Type** property of the ``ServiceProviderConfiguration`` object, @@ -199,6 +199,68 @@ In order so this to work you must add the additional property below: Ocelot will add this token to the Consul client that it uses to make requests and that is then used for every request. +.. _sd-consul-service-builder: + +Consul Service Builder [#f3]_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + | **Interface**: ``IConsulServiceBuilder`` + | **Implementation**: ``DefaultConsulServiceBuilder`` + +The Ocelot community has consistently reported, both in the past and presently, issues with *Consul* services (such as connectivity) due to a variety of *Consul* agent definitions. +Some DevOps engineers prefer to group services as *Consul* `catalog nodes`_ by customizing the assignment of host names to node names, +while others focus on defining agent services with pure IP addresses as hosts, which relates to the `954`_ bug dilemma. + +Since version `13.5.2`_, the building of service downstream host/port in PR `909`_ has been altered to favor the node name as the host over the agent service address IP. + +Version `23.3`_ saw the introduction of a customization feature that allows control over the service building process through the ``DefaultConsulServiceBuilder`` class. +This class has virtual methods that can be overridden to meet the needs of developers and DevOps. + +The present logic in the ``DefaultConsulServiceBuilder`` class is as follows: + +.. code-block:: csharp + + protected virtual string GetDownstreamHost(ServiceEntry entry, Node node) + => node != null ? node.Name : entry.Service.Address; + +Some DevOps engineers choose to ignore node names, opting instead for abstract identifiers rather than actual hostnames. +Our team, however, advocates for the assignment of real hostnames or IP addresses to node names, upholding this as a best practice. +If this approach does not align with your needs, or if you prefer not to spend time detailing your nodes for downstream services, you might consider defining agent services without node names. +In such cases within a *Consul* setup, you would need to override the behavior of the ``DefaultConsulServiceBuilder`` class. +For further details, refer to the subsequent section below. + +.. _sd-addconsul-generic-method: + +``AddConsul`` method +""""""""""""""""""""""" + + | **Signature**: ``IOcelotBuilder AddConsul(this IOcelotBuilder builder)`` + +Overriding the ``DefaultConsulServiceBuilder`` behavior involves two steps: defining a new class that inherits from the ``IConsulServiceBuilder`` interface, +and then injecting this new behavior into DI using the ``AddConsul`` helper. +However, the quickest and most streamlined approach is to inherit directly from the ``DefaultConsulServiceBuilder`` class, which offers greater flexibility. + +**First**, we need to define a new service building class: + +.. code-block:: csharp + + public class MyConsulServiceBuilder : DefaultConsulServiceBuilder + { + public MyConsulServiceBuilder(Func configurationFactory, IConsulClientFactory clientFactory, IOcelotLoggerFactory loggerFactory) + : base(configurationFactory, clientFactory, loggerFactory) { } + // I want to use the agent service IP address as the downstream hostname + protected override string GetDownstreamHost(ServiceEntry entry, Node node) => entry.Service.Address; + } + +**Second**, we must inject the new behavior into DI, as demonstrated in the Ocelot versus Consul setup: + +.. code-block:: csharp + + services.AddOcelot() + .AddConsul(); + +You can refer to `the acceptance test`_ in the repository for an example. + Eureka ------ @@ -473,9 +535,18 @@ But you can leave this ``Type`` option for compatibility between both designs. """" .. [#f1] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. -.. [#f2] *"Consul Configuration Key"* feature was requested in `issue 346 `_ as a part of version `7.0.0 `_. +.. [#f2] *"Consul Configuration Key"* feature was requested in issue `346`_ as a part of version `7.0.0`_. +.. [#f3] Customization of *"Consul Service Builder"* was implemented as a part of bug `954`_ fixing and the feature was delivered in version `23.3`_. .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/ocelot.json .. _Consul: https://www.consul.io/ .. _KV Store: https://developer.hashicorp.com/consul/docs/dynamic-app-config/kv .. _3 seconds TTL: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+TimeSpan.FromSeconds%283%29&type=code +.. _catalog nodes: https://developer.hashicorp.com/consul/api-docs/catalog#list-nodes +.. _the acceptance test: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+Should_return_service_address_by_overridden_service_builder_when_there_is_a_node&type=code +.. _346: https://github.com/ThreeMammals/Ocelot/issues/346 +.. _909: https://github.com/ThreeMammals/Ocelot/pull/909 +.. _954: https://github.com/ThreeMammals/Ocelot/issues/954 +.. _7.0.0: https://github.com/ThreeMammals/Ocelot/releases/tag/7.0.0 +.. _13.5.2: https://github.com/ThreeMammals/Ocelot/releases/tag/13.5.2 +.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0