From 8845d1b98d2c3b0bd633ebd2b526b923687edd33 Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 29 Feb 2024 09:44:41 +0100 Subject: [PATCH] #849 #1496 Extend the route key format used for load balancing making it unique (#1944) * Fix the route key format used for load balancing The old key format did not contain enough information to disambiguate routes based on an UpstreamHost. This was especially problematic when a ServiceName was used in conjuction with Service Discovery, instead of DownstreamHostAndPorts configuration. Resolves #1496 * Update tests * Amend test * Remove empty key parts Co-authored-by: Raman Maksimchuk * Amend * Make route keys uniform, keep empty parts * Fix wrong usage of dictionary TryGetValue * Coalesce empty strings or white space, use fallback values * Add back host and ports * Remove redundant load balancer type check * Fix TryGetValue with null argument * Revert removal of balancer type check, still relevant * Improve helper local functions * Fix outdated comment * Add acceptance test * Refactor RouteKeyCreator class * Add developer's XML docs * Add TODO * Convert fact to theory * Review unit tests * Fix incorrect usages of ServiceHandler, make sure all are correctly Disposed * Fix services responding to wrong path * Add Consul counter * Reduce summary length * Rename OkResponse->MapGet * Refactor `LoadBalancerHouse` class because of DRY principle --------- Co-authored-by: Raman Maksimchuk --- .../Configuration/Creator/RouteKeyCreator.cs | 93 ++++++-- .../LoadBalancers/LoadBalancerHouse.cs | 53 +++-- .../ServiceDiscoveryTests.cs | 198 +++++++++++++++--- test/Ocelot.AcceptanceTests/Steps.cs | 7 +- .../Configuration/RouteKeyCreatorTests.cs | 88 ++++++-- 5 files changed, 353 insertions(+), 86 deletions(-) diff --git a/src/Ocelot/Configuration/Creator/RouteKeyCreator.cs b/src/Ocelot/Configuration/Creator/RouteKeyCreator.cs index 93ff6f0aa..8bc5debc9 100644 --- a/src/Ocelot/Configuration/Creator/RouteKeyCreator.cs +++ b/src/Ocelot/Configuration/Creator/RouteKeyCreator.cs @@ -1,17 +1,86 @@ using Ocelot.Configuration.File; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.LoadBalancers; -namespace Ocelot.Configuration.Creator -{ - public class RouteKeyCreator : IRouteKeyCreator +namespace Ocelot.Configuration.Creator; + +public class RouteKeyCreator : IRouteKeyCreator +{ + /// + /// Creates the unique key based on the route properties for load balancing etc. + /// + /// + /// Key template: + /// + /// UpstreamHttpMethod|UpstreamPathTemplate|UpstreamHost|DownstreamHostAndPorts|ServiceNamespace|ServiceName|LoadBalancerType|LoadBalancerKey + /// + /// + /// The route object. + /// A object containing the key. + public string Create(FileRoute fileRoute) { - public string Create(FileRoute fileRoute) => IsStickySession(fileRoute) - ? $"{nameof(CookieStickySessions)}:{fileRoute.LoadBalancerOptions.Key}" - : $"{fileRoute.UpstreamPathTemplate}|{string.Join(',', fileRoute.UpstreamHttpMethod)}|{string.Join(',', fileRoute.DownstreamHostAndPorts.Select(x => $"{x.Host}:{x.Port}"))}"; + var isStickySession = fileRoute.LoadBalancerOptions is + { + Type: nameof(CookieStickySessions), + Key.Length: > 0 + }; + + if (isStickySession) + { + return $"{nameof(CookieStickySessions)}:{fileRoute.LoadBalancerOptions.Key}"; + } + + var upstreamHttpMethods = Csv(fileRoute.UpstreamHttpMethod); + var downstreamHostAndPorts = Csv(fileRoute.DownstreamHostAndPorts.Select(downstream => $"{downstream.Host}:{downstream.Port}")); + + var keyBuilder = new StringBuilder() + + // UpstreamHttpMethod and UpstreamPathTemplate are required + .AppendNext(upstreamHttpMethods) + .AppendNext(fileRoute.UpstreamPathTemplate) + + // Other properties are optional, replace undefined values with defaults to aid debugging + .AppendNext(Coalesce(fileRoute.UpstreamHost, "no-host")) + + .AppendNext(Coalesce(downstreamHostAndPorts, "no-host-and-port")) + .AppendNext(Coalesce(fileRoute.ServiceNamespace, "no-svc-ns")) + .AppendNext(Coalesce(fileRoute.ServiceName, "no-svc-name")) + .AppendNext(Coalesce(fileRoute.LoadBalancerOptions.Type, "no-lb-type")) + .AppendNext(Coalesce(fileRoute.LoadBalancerOptions.Key, "no-lb-key")); - private static bool IsStickySession(FileRoute fileRoute) => - !string.IsNullOrEmpty(fileRoute.LoadBalancerOptions.Type) - && !string.IsNullOrEmpty(fileRoute.LoadBalancerOptions.Key) - && fileRoute.LoadBalancerOptions.Type == nameof(CookieStickySessions); - } + return keyBuilder.ToString(); + } + + /// + /// Helper function to convert multiple strings into a comma-separated string. + /// + /// The collection of strings to join by comma separator. + /// A in the comma-separated format. + private static string Csv(IEnumerable values) => string.Join(',', values); + + /// + /// Helper function to return the first non-null-or-whitespace string. + /// + /// The 1st string to check. + /// The 2nd string to check. + /// A which is not empty. + private static string Coalesce(string first, string second) => string.IsNullOrWhiteSpace(first) ? second : first; +} + +internal static class RouteKeyCreatorHelpers +{ + /// + /// Helper function to append a string to the key builder, separated by a pipe. + /// + /// The builder of the key. + /// The next word to add. + /// The reference to the builder. + public static StringBuilder AppendNext(this StringBuilder builder, string next) + { + if (builder.Length > 0) + { + builder.Append('|'); + } + + return builder.Append(next); + } } diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs index 18a380a48..4a3dc798f 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs @@ -1,5 +1,5 @@ using Ocelot.Configuration; -using Ocelot.Responses; +using Ocelot.Responses; namespace Ocelot.LoadBalancer.LoadBalancers { @@ -18,45 +18,40 @@ public Response Get(DownstreamRoute route, ServiceProviderConfigu { try { - Response result; - if (_loadBalancers.TryGetValue(route.LoadBalancerKey, out var loadBalancer)) - { - loadBalancer = _loadBalancers[route.LoadBalancerKey]; - + { + // TODO Fix ugly reflection issue of dymanic detection in favor of static type property if (route.LoadBalancerOptions.Type != loadBalancer.GetType().Name) - { - result = _factory.Get(route, config); - if (result.IsError) - { - return new ErrorResponse(result.Errors); - } - - loadBalancer = result.Data; - AddLoadBalancer(route.LoadBalancerKey, loadBalancer); + { + return GetResponse(route, config); } return new OkResponse(loadBalancer); } - result = _factory.Get(route, config); - - if (result.IsError) - { - return new ErrorResponse(result.Errors); - } - - loadBalancer = result.Data; - AddLoadBalancer(route.LoadBalancerKey, loadBalancer); - return new OkResponse(loadBalancer); + return GetResponse(route, config); } catch (Exception ex) { - return new ErrorResponse(new List - { - new UnableToFindLoadBalancerError($"unabe to find load balancer for {route.LoadBalancerKey} exception is {ex}"), - }); + return new ErrorResponse( + [ + new UnableToFindLoadBalancerError($"Unable to find load balancer for '{route.LoadBalancerKey}'. Exception: {ex};"), + ]); } + } + + private Response GetResponse(DownstreamRoute route, ServiceProviderConfiguration config) + { + var result = _factory.Get(route, config); + + if (result.IsError) + { + return new ErrorResponse(result.Errors); + } + + var loadBalancer = result.Data; + AddLoadBalancer(route.LoadBalancerKey, loadBalancer); + return new OkResponse(loadBalancer); } private void AddLoadBalancer(string key, ILoadBalancer loadBalancer) diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs index 79e0dd505..c37333cf2 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs @@ -1,7 +1,8 @@ -using Consul; -using Microsoft.AspNetCore.Http; -using Newtonsoft.Json; -using Ocelot.Configuration.File; +using Consul; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using System.Text.RegularExpressions; namespace Ocelot.AcceptanceTests { @@ -11,14 +12,19 @@ public class ServiceDiscoveryTests : IDisposable 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 ServiceDiscoveryTests() { _serviceHandler = new ServiceHandler(); + _serviceHandler2 = new ServiceHandler(); + _consulHandler = new ServiceHandler(); _steps = new Steps(); _consulServices = new List(); } @@ -83,7 +89,7 @@ public void should_use_consul_service_discovery_and_load_balance_request() this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningWithConsul()) @@ -139,7 +145,7 @@ 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, serviceName)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningWithConsul()) @@ -190,7 +196,7 @@ 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, serviceName)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningWithConsul()) @@ -250,7 +256,7 @@ public void should_use_consul_service_discovery_and_load_balance_request_no_re_r this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningWithConsul()) @@ -308,7 +314,7 @@ public void should_use_token_to_make_request_to_consul() }; this.Given(_ => GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) .And(_ => _steps.GivenThereIsAConfiguration(configuration)) .And(_ => _steps.GivenOcelotIsRunningWithConsul()) @@ -379,7 +385,7 @@ public void should_send_request_to_service_after_it_becomes_available_in_consul( this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningWithConsul()) @@ -447,7 +453,7 @@ 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, serviceName)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningWithConsul()) @@ -455,11 +461,123 @@ public void should_handle_request_to_poll_consul_for_downstream_service_and_make .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); - } - - private void ThenTheTokenIs(string token) - { - _receivedToken.ShouldBe(token); + } + + [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 = new ServiceEntry + { + Service = new AgentService + { + Service = serviceNameUS, + Address = "localhost", + Port = servicePortUS, + ID = Guid.NewGuid().ToString(), + Tags = ["US"], + }, + }; + var serviceEntryEU = new ServiceEntry + { + Service = new AgentService + { + Service = serviceNameEU, + Address = "localhost", + Port = servicePortEU, + ID = Guid.NewGuid().ToString(), + Tags = ["EU"], + }, + }; + + var configuration = new FileConfiguration + { + Routes = + [ + new() + { + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = ["Get"], + UpstreamHost = upstreamHostUS, + ServiceName = serviceNameUS, + LoadBalancerOptions = new() { Type = loadBalancerType }, + }, + new() + { + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = ["Get"], + UpstreamHost = upstreamHostEU, + ServiceName = serviceNameEU, + LoadBalancerOptions = new() { Type = loadBalancerType }, + }, + ], + GlobalConfiguration = new() + { + ServiceDiscoveryProvider = new() + { + 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 void ThenTheTokenIs(string token) + { + _receivedToken.ShouldBe(token); } private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo) @@ -482,6 +600,7 @@ private void GivenIResetCounters() { _counterOne = 0; _counterTwo = 0; + _counterConsul = 0; } private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) @@ -504,24 +623,36 @@ private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] servi } } - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + _consulHandler.GivenThereIsAServiceRunningOn(url, async context => { - 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(); - } + _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++; - var json = JsonConvert.SerializeObject(_consulServices); + // 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 GivenProductServiceOneIsRunning(string url, int statusCode) { _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => @@ -547,7 +678,7 @@ private void GivenProductServiceOneIsRunning(string url, int statusCode) private void GivenProductServiceTwoIsRunning(string url, int statusCode) { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + _serviceHandler2.GivenThereIsAServiceRunningOn(url, async context => { try { @@ -587,9 +718,26 @@ private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int }); } + 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) + { + 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(); } } diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index de863d99a..98ab33f9b 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -269,10 +269,15 @@ public void GivenOcelotIsRunningWithCustomLoadBalancer( _ocelotClient = _ocelotServer.CreateClient(); } - public void GivenOcelotIsRunningWithConsul() + public void GivenOcelotIsRunningWithConsul(params string[] urlsToListenOn) { _webHostBuilder = new WebHostBuilder(); + if (urlsToListenOn?.Length > 0) + { + _webHostBuilder.UseUrls(urlsToListenOn); + } + _webHostBuilder .ConfigureAppConfiguration((hostingContext, config) => { diff --git a/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs index 54f63dd4c..8326fb5a0 100644 --- a/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs @@ -1,6 +1,6 @@ -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; -using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.LoadBalancer.LoadBalancers; namespace Ocelot.UnitTests.Configuration { @@ -16,7 +16,7 @@ public RouteKeyCreatorTests() } [Fact] - public void should_return_sticky_session_key() + public void Should_return_sticky_session_key() { var route = new FileRoute { @@ -29,35 +29,85 @@ public void should_return_sticky_session_key() this.Given(_ => GivenThe(route)) .When(_ => WhenICreate()) - .Then(_ => ThenTheResultIs($"{nameof(CookieStickySessions)}:{route.LoadBalancerOptions.Key}")) + .Then(_ => ThenTheResultIs("CookieStickySessions:testy")) .BDDfy(); } [Fact] - public void should_return_re_route_key() + public void Should_return_route_key() { var route = new FileRoute { UpstreamPathTemplate = "/api/product", - UpstreamHttpMethod = new List { "GET", "POST", "PUT" }, - DownstreamHostAndPorts = new List + UpstreamHttpMethod = ["GET", "POST", "PUT"], + DownstreamHostAndPorts = + [ + new("localhost", 8080), + new("localhost", 4430), + ], + }; + + this.Given(_ => GivenThe(route)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheResultIs("GET,POST,PUT|/api/product|no-host|localhost:8080,localhost:4430|no-svc-ns|no-svc-name|no-lb-type|no-lb-key")) + .BDDfy(); + } + + [Fact] + public void Should_return_route_key_with_upstream_host() + { + var route = new FileRoute + { + UpstreamHost = "my-host", + UpstreamPathTemplate = "/api/product", + UpstreamHttpMethod = ["GET", "POST", "PUT"], + DownstreamHostAndPorts = + [ + new("localhost", 8080), + new("localhost", 4430), + ], + }; + + this.Given(_ => GivenThe(route)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheResultIs("GET,POST,PUT|/api/product|my-host|localhost:8080,localhost:4430|no-svc-ns|no-svc-name|no-lb-type|no-lb-key")) + .BDDfy(); + } + + [Fact] + public void Should_return_route_key_with_svc_name() + { + var route = new FileRoute + { + UpstreamPathTemplate = "/api/product", + UpstreamHttpMethod = ["GET", "POST", "PUT"], + ServiceName = "products-service", + }; + + this.Given(_ => GivenThe(route)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheResultIs("GET,POST,PUT|/api/product|no-host|no-host-and-port|no-svc-ns|products-service|no-lb-type|no-lb-key")) + .BDDfy(); + } + + [Fact] + public void Should_return_route_key_with_load_balancer_options() + { + var route = new FileRoute + { + UpstreamPathTemplate = "/api/product", + UpstreamHttpMethod = ["GET", "POST", "PUT"], + ServiceName = "products-service", + LoadBalancerOptions = new FileLoadBalancerOptions { - new() - { - Host = "localhost", - Port = 123, - }, - new() - { - Host = "localhost", - Port = 123, - }, + Type = nameof(LeastConnection), + Key = "testy", }, }; this.Given(_ => GivenThe(route)) .When(_ => WhenICreate()) - .Then(_ => ThenTheResultIs($"{route.UpstreamPathTemplate}|{string.Join(',', route.UpstreamHttpMethod)}|{string.Join(',', route.DownstreamHostAndPorts.Select(x => $"{x.Host}:{x.Port}"))}")) + .Then(_ => ThenTheResultIs("GET,POST,PUT|/api/product|no-host|no-host-and-port|no-svc-ns|products-service|LeastConnection|testy")) .BDDfy(); }