From 67932785970048415028480e536949c58f2715b7 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Mon, 30 Apr 2018 18:55:11 +0100 Subject: [PATCH] Feature/sticky sessions (#336) * started messing around with sticky sessions idea * more tests for sticky session thing * more faffing cant make up my mind how to do this * +semver: breaking added sticky session load balancer and changed way load balancer configuration is set by user * #336 made tests BDDFy --- .editorconfig | 2 +- docs/features/loadbalancer.rst | 57 +- .../Builder/DownstreamReRouteBuilder.cs | 18 +- .../FileInternalConfigurationCreator.cs | 480 ++-- src/Ocelot/Configuration/DownstreamReRoute.cs | 123 +- .../File/FileLoadBalancerOptions.cs | 9 + src/Ocelot/Configuration/File/FileReRoute.cs | 109 +- .../Configuration/LoadBalancerOptions.cs | 18 + .../LoadBalancers/CookieStickySessions.cs | 93 + .../LoadBalancers/ILoadBalancer.cs | 25 +- .../LoadBalancers/ILoadBalancerFactory.cs | 20 +- .../LoadBalancers/ILoadBalancerHouse.cs | 22 +- .../LoadBalancers/LeastConnection.cs | 291 +-- .../LoadBalancers/LoadBalancerFactory.cs | 64 +- .../LoadBalancers/LoadBalancerHouse.cs | 112 +- .../LoadBalancers/NoLoadBalancer.cs | 69 +- .../LoadBalancer/LoadBalancers/RoundRobin.cs | 75 +- .../LoadBalancers/StickySession.cs | 18 + .../Middleware/LoadBalancingMiddleware.cs | 126 +- src/Ocelot/Requester/QoS/QosProviderHouse.cs | 10 +- .../LoadBalancerTests.cs | 326 +-- test/Ocelot.AcceptanceTests/RoutingTests.cs | 2032 ++++++++--------- .../ServiceDiscoveryTests.cs | 10 +- test/Ocelot.AcceptanceTests/Steps.cs | 37 +- .../StickySessionsTests.cs | 321 +++ test/Ocelot.AcceptanceTests/WebSocketTests.cs | 4 +- ... FileInternalConfigurationCreatorTests.cs} | 1952 ++++++++-------- .../LoadBalancer/CookieStickySessionsTests.cs | 276 +++ .../LoadBalancer/LeastConnectionTests.cs | 572 ++--- .../LoadBalancer/LoadBalancerFactoryTests.cs | 268 ++- .../LoadBalancer/LoadBalancerHouseTests.cs | 345 +-- .../LoadBalancerMiddlewareTests.cs | 386 ++-- .../LoadBalancer/NoLoadBalancerTests.cs | 152 +- .../LoadBalancer/RoundRobinTests.cs | 143 +- .../Requester/QosProviderHouseTests.cs | 12 +- 35 files changed, 4776 insertions(+), 3801 deletions(-) create mode 100644 src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs create mode 100644 src/Ocelot/Configuration/LoadBalancerOptions.cs create mode 100644 src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessions.cs create mode 100644 src/Ocelot/LoadBalancer/LoadBalancers/StickySession.cs create mode 100644 test/Ocelot.AcceptanceTests/StickySessionsTests.cs rename test/Ocelot.UnitTests/Configuration/{FileConfigurationCreatorTests.cs => FileInternalConfigurationCreatorTests.cs} (92%) create mode 100644 test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs diff --git a/.editorconfig b/.editorconfig index 054123c4..82435303 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,7 @@ root = true [*] -end_of_line = crlf +end_of_line = lf insert_final_newline = true [*.cs] diff --git a/docs/features/loadbalancer.rst b/docs/features/loadbalancer.rst index c2cc0f26..30d56598 100644 --- a/docs/features/loadbalancer.rst +++ b/docs/features/loadbalancer.rst @@ -11,6 +11,8 @@ The type of load balancer available are: NoLoadBalancer - takes the first available service from config or service discovery. + CookieStickySessions - uses a cookie to stick all requests to a specific server. More info below. + You must choose in your configuration which load balancer to use. Configuration @@ -34,7 +36,9 @@ The following shows how to set up multiple downstream services for a ReRoute usi } ], "UpstreamPathTemplate": "/posts/{postId}", - "LoadBalancer": "LeastConnection", + "LoadBalancerOptions": { + "Type": "LeastConnection" + }, "UpstreamHttpMethod": [ "Put", "Delete" ] } @@ -52,9 +56,56 @@ The following shows how to set up a ReRoute using service discovery then select "UpstreamPathTemplate": "/posts/{postId}", "UpstreamHttpMethod": [ "Put" ], "ServiceName": "product", - "LoadBalancer": "LeastConnection", + "LoadBalancerOptions": { + "Type": "LeastConnection" + }, "UseServiceDiscovery": true } When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balance requests across any available services. If you add and remove services from the -service discovery provider (consul) then Ocelot should respect this and stop calling services that have been removed and start calling services that have been added. \ No newline at end of file +service discovery provider (consul) then Ocelot should respect this and stop calling services that have been removed and start calling services that have been added. + +CookieStickySessions +^^^^^^^^^^^^^^^^^^^^ + +I've implemented a really basic sticky session type of load balancer. The scenario it is meant to support is you have a bunch of downstream +servers that don't share session state so if you get more than one request for one of these servers then it should go to the same box each +time or the session state might be incorrect for the given user. This feature was requested in `Issue #322 `_ +though what the user wants is more complicated than just sticky sessions :) anyway I thought this would be a nice feature to have! + +In order to set up CookieStickySessions load balancer you need to do something like the following. + +.. code-block:: json + + { + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "10.0.1.10", + "Port": 5000, + }, + { + "Host": "10.0.1.11", + "Port": 5000, + } + ], + "UpstreamPathTemplate": "/posts/{postId}", + "LoadBalancerOptions": { + "Type": "CookieStickySessions", + "Key": "ASP.NET_SessionId", + "Expiry": 1800000 + }, + "UpstreamHttpMethod": [ "Put", "Delete" ] + } + +The LoadBalancerOptions are Type this needs to be CookieStickySessions, Key this is the key of the cookie you +wish to use for the sticky sessions, Expiry this is how long in milliseconds you want to the session to be stuck for. Remember this +refreshes on every request which is meant to mimick how sessions work usually. + +If you have multiple ReRoutes with the same LoadBalancerOptions then all of those ReRoutes will use the same load balancer for there +subsequent requests. This means the sessions will be stuck across ReRoutes. + +Please note that if you give more than one DownstreamHostAndPort or you are using a Service Discovery provider such as Consul +and this returns more than one service then CookieStickySessions uses round robin to select the next server. This is hard coded at the +moment but could be changed. \ No newline at end of file diff --git a/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs index 15fddb97..225feb6f 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs @@ -24,7 +24,7 @@ namespace Ocelot.Configuration.Builder private bool _isCached; private CacheOptions _fileCacheOptions; private string _downstreamScheme; - private string _loadBalancer; + private LoadBalancerOptions _loadBalancerOptions; private bool _useQos; private QoSOptions _qosOptions; private HttpHandlerOptions _httpHandlerOptions; @@ -41,6 +41,7 @@ namespace Ocelot.Configuration.Builder private List _addHeadersToDownstream; private List _addHeadersToUpstream; private bool _dangerousAcceptAnyServerCertificateValidator; + private string _qosKey; public DownstreamReRouteBuilder() { @@ -62,9 +63,9 @@ namespace Ocelot.Configuration.Builder return this; } - public DownstreamReRouteBuilder WithLoadBalancer(string loadBalancer) + public DownstreamReRouteBuilder WithLoadBalancerOptions(LoadBalancerOptions loadBalancerOptions) { - _loadBalancer = loadBalancer; + _loadBalancerOptions = loadBalancerOptions; return this; } @@ -170,6 +171,12 @@ namespace Ocelot.Configuration.Builder return this; } + public DownstreamReRouteBuilder WithQosKey(string qosKey) + { + _qosKey = qosKey; + return this; + } + public DownstreamReRouteBuilder WithAuthenticationOptions(AuthenticationOptions authenticationOptions) { _authenticationOptions = authenticationOptions; @@ -266,7 +273,7 @@ namespace Ocelot.Configuration.Builder _requestIdHeaderKey, _isCached, _fileCacheOptions, - _loadBalancer, + _loadBalancerOptions, _rateLimitOptions, _routeClaimRequirement, _claimToQueries, @@ -280,7 +287,8 @@ namespace Ocelot.Configuration.Builder _delegatingHandlers, _addHeadersToDownstream, _addHeadersToUpstream, - _dangerousAcceptAnyServerCertificateValidator); + _dangerousAcceptAnyServerCertificateValidator, + _qosKey); } } } diff --git a/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs index 46eec9d6..06789116 100644 --- a/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs @@ -1,229 +1,251 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using Ocelot.Cache; -using Ocelot.Configuration.Builder; -using Ocelot.Configuration.File; -using Ocelot.Configuration.Validator; -using Ocelot.DependencyInjection; -using Ocelot.Logging; -using Ocelot.Responses; - -namespace Ocelot.Configuration.Creator -{ - /// - /// Register as singleton - /// - public class FileInternalConfigurationCreator : IInternalConfigurationCreator - { - private readonly IConfigurationValidator _configurationValidator; - private readonly IOcelotLogger _logger; - private readonly IClaimsToThingCreator _claimsToThingCreator; - private readonly IAuthenticationOptionsCreator _authOptionsCreator; - private readonly IUpstreamTemplatePatternCreator _upstreamTemplatePatternCreator; - private readonly IRequestIdKeyCreator _requestIdKeyCreator; - private readonly IServiceProviderConfigurationCreator _serviceProviderConfigCreator; - private readonly IQoSOptionsCreator _qosOptionsCreator; - private readonly IReRouteOptionsCreator _fileReRouteOptionsCreator; - private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; - private readonly IRegionCreator _regionCreator; - private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; - private readonly IAdministrationPath _adminPath; - private readonly IHeaderFindAndReplaceCreator _headerFAndRCreator; - private readonly IDownstreamAddressesCreator _downstreamAddressesCreator; - - public FileInternalConfigurationCreator( - IConfigurationValidator configurationValidator, - IOcelotLoggerFactory loggerFactory, - IClaimsToThingCreator claimsToThingCreator, - IAuthenticationOptionsCreator authOptionsCreator, - IUpstreamTemplatePatternCreator upstreamTemplatePatternCreator, - IRequestIdKeyCreator requestIdKeyCreator, - IServiceProviderConfigurationCreator serviceProviderConfigCreator, - IQoSOptionsCreator qosOptionsCreator, - IReRouteOptionsCreator fileReRouteOptionsCreator, - IRateLimitOptionsCreator rateLimitOptionsCreator, - IRegionCreator regionCreator, - IHttpHandlerOptionsCreator httpHandlerOptionsCreator, - IAdministrationPath adminPath, - IHeaderFindAndReplaceCreator headerFAndRCreator, - IDownstreamAddressesCreator downstreamAddressesCreator - ) - { - _downstreamAddressesCreator = downstreamAddressesCreator; - _headerFAndRCreator = headerFAndRCreator; - _adminPath = adminPath; - _regionCreator = regionCreator; - _rateLimitOptionsCreator = rateLimitOptionsCreator; - _requestIdKeyCreator = requestIdKeyCreator; - _upstreamTemplatePatternCreator = upstreamTemplatePatternCreator; - _authOptionsCreator = authOptionsCreator; - _configurationValidator = configurationValidator; - _logger = loggerFactory.CreateLogger(); - _claimsToThingCreator = claimsToThingCreator; - _serviceProviderConfigCreator = serviceProviderConfigCreator; - _qosOptionsCreator = qosOptionsCreator; - _fileReRouteOptionsCreator = fileReRouteOptionsCreator; - _httpHandlerOptionsCreator = httpHandlerOptionsCreator; - } - - public async Task> Create(FileConfiguration fileConfiguration) - { - var config = await SetUpConfiguration(fileConfiguration); - return config; - } - - private async Task> SetUpConfiguration(FileConfiguration fileConfiguration) - { - var response = await _configurationValidator.IsValid(fileConfiguration); - - if (response.Data.IsError) - { - return new ErrorResponse(response.Data.Errors); - } - - var reRoutes = new List(); - - foreach (var reRoute in fileConfiguration.ReRoutes) - { - var downstreamReRoute = SetUpDownstreamReRoute(reRoute, fileConfiguration.GlobalConfiguration); - - var ocelotReRoute = SetUpReRoute(reRoute, downstreamReRoute); - - reRoutes.Add(ocelotReRoute); - } - - foreach (var aggregate in fileConfiguration.Aggregates) - { - var ocelotReRoute = SetUpAggregateReRoute(reRoutes, aggregate, fileConfiguration.GlobalConfiguration); - reRoutes.Add(ocelotReRoute); - } - - var serviceProviderConfiguration = _serviceProviderConfigCreator.Create(fileConfiguration.GlobalConfiguration); - - var config = new InternalConfiguration(reRoutes, _adminPath.Path, serviceProviderConfiguration, fileConfiguration.GlobalConfiguration.RequestIdKey); - - return new OkResponse(config); - } - - public ReRoute SetUpAggregateReRoute(List reRoutes, FileAggregateReRoute aggregateReRoute, FileGlobalConfiguration globalConfiguration) - { - var applicableReRoutes = reRoutes - .SelectMany(x => x.DownstreamReRoute) - .Where(r => aggregateReRoute.ReRouteKeys.Contains(r.Key)) - .ToList(); - - if(applicableReRoutes.Count != aggregateReRoute.ReRouteKeys.Count) - { - //todo - log or throw or return error whatever? - } - - //make another re route out of these - var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(aggregateReRoute); - - var reRoute = new ReRouteBuilder() - .WithUpstreamPathTemplate(aggregateReRoute.UpstreamPathTemplate) - .WithUpstreamHttpMethod(aggregateReRoute.UpstreamHttpMethod) - .WithUpstreamTemplatePattern(upstreamTemplatePattern) - .WithDownstreamReRoutes(applicableReRoutes) - .WithUpstreamHost(aggregateReRoute.UpstreamHost) - .WithAggregator(aggregateReRoute.Aggregator) - .Build(); - - return reRoute; - } - - private ReRoute SetUpReRoute(FileReRoute fileReRoute, DownstreamReRoute downstreamReRoutes) - { - var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileReRoute); - - var reRoute = new ReRouteBuilder() - .WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate) - .WithUpstreamHttpMethod(fileReRoute.UpstreamHttpMethod) - .WithUpstreamTemplatePattern(upstreamTemplatePattern) - .WithDownstreamReRoute(downstreamReRoutes) - .WithUpstreamHost(fileReRoute.UpstreamHost) - .Build(); - - return reRoute; - } - - private DownstreamReRoute SetUpDownstreamReRoute(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration) - { - var fileReRouteOptions = _fileReRouteOptionsCreator.Create(fileReRoute); - - var requestIdKey = _requestIdKeyCreator.Create(fileReRoute, globalConfiguration); - - var reRouteKey = CreateReRouteKey(fileReRoute); - - var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileReRoute); - - var authOptionsForRoute = _authOptionsCreator.Create(fileReRoute); - - var claimsToHeaders = _claimsToThingCreator.Create(fileReRoute.AddHeadersToRequest); - - var claimsToClaims = _claimsToThingCreator.Create(fileReRoute.AddClaimsToRequest); - - var claimsToQueries = _claimsToThingCreator.Create(fileReRoute.AddQueriesToRequest); - - var qosOptions = _qosOptionsCreator.Create(fileReRoute); - - var rateLimitOption = _rateLimitOptionsCreator.Create(fileReRoute, globalConfiguration, fileReRouteOptions.EnableRateLimiting); - - var region = _regionCreator.Create(fileReRoute); - - var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileReRoute); - - var hAndRs = _headerFAndRCreator.Create(fileReRoute); - - var downstreamAddresses = _downstreamAddressesCreator.Create(fileReRoute); - - var reRoute = new DownstreamReRouteBuilder() - .WithKey(fileReRoute.Key) - .WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate) - .WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate) - .WithUpstreamHttpMethod(fileReRoute.UpstreamHttpMethod) - .WithUpstreamTemplatePattern(upstreamTemplatePattern) - .WithIsAuthenticated(fileReRouteOptions.IsAuthenticated) - .WithAuthenticationOptions(authOptionsForRoute) - .WithClaimsToHeaders(claimsToHeaders) - .WithClaimsToClaims(claimsToClaims) - .WithRouteClaimsRequirement(fileReRoute.RouteClaimsRequirement) - .WithIsAuthorised(fileReRouteOptions.IsAuthorised) - .WithClaimsToQueries(claimsToQueries) - .WithRequestIdKey(requestIdKey) - .WithIsCached(fileReRouteOptions.IsCached) - .WithCacheOptions(new CacheOptions(fileReRoute.FileCacheOptions.TtlSeconds, region)) - .WithDownstreamScheme(fileReRoute.DownstreamScheme) - .WithLoadBalancer(fileReRoute.LoadBalancer) - .WithDownstreamAddresses(downstreamAddresses) - .WithReRouteKey(reRouteKey) - .WithIsQos(fileReRouteOptions.IsQos) - .WithQosOptions(qosOptions) - .WithEnableRateLimiting(fileReRouteOptions.EnableRateLimiting) - .WithRateLimitOptions(rateLimitOption) - .WithHttpHandlerOptions(httpHandlerOptions) - .WithServiceName(fileReRoute.ServiceName) - .WithUseServiceDiscovery(fileReRoute.UseServiceDiscovery) - .WithUpstreamHeaderFindAndReplace(hAndRs.Upstream) - .WithDownstreamHeaderFindAndReplace(hAndRs.Downstream) - .WithUpstreamHost(fileReRoute.UpstreamHost) - .WithDelegatingHandlers(fileReRoute.DelegatingHandlers) - .WithAddHeadersToDownstream(hAndRs.AddHeadersToDownstream) - .WithAddHeadersToUpstream(hAndRs.AddHeadersToUpstream) - .WithDangerousAcceptAnyServerCertificateValidator(fileReRoute.DangerousAcceptAnyServerCertificateValidator) - .Build(); - - return reRoute; - } - - private string CreateReRouteKey(FileReRoute fileReRoute) - { - //note - not sure if this is the correct key, but this is probably the only unique key i can think of given my poor brain - var loadBalancerKey = $"{fileReRoute.UpstreamPathTemplate}|{string.Join(",", fileReRoute.UpstreamHttpMethod)}"; - return loadBalancerKey; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Ocelot.Cache; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Validator; +using Ocelot.DependencyInjection; +using Ocelot.Logging; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Creator +{ + using LoadBalancer.LoadBalancers; + + /// + /// Register as singleton + /// + public class FileInternalConfigurationCreator : IInternalConfigurationCreator + { + private readonly IConfigurationValidator _configurationValidator; + private readonly IOcelotLogger _logger; + private readonly IClaimsToThingCreator _claimsToThingCreator; + private readonly IAuthenticationOptionsCreator _authOptionsCreator; + private readonly IUpstreamTemplatePatternCreator _upstreamTemplatePatternCreator; + private readonly IRequestIdKeyCreator _requestIdKeyCreator; + private readonly IServiceProviderConfigurationCreator _serviceProviderConfigCreator; + private readonly IQoSOptionsCreator _qosOptionsCreator; + private readonly IReRouteOptionsCreator _fileReRouteOptionsCreator; + private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; + private readonly IRegionCreator _regionCreator; + private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; + private readonly IAdministrationPath _adminPath; + private readonly IHeaderFindAndReplaceCreator _headerFAndRCreator; + private readonly IDownstreamAddressesCreator _downstreamAddressesCreator; + + public FileInternalConfigurationCreator( + IConfigurationValidator configurationValidator, + IOcelotLoggerFactory loggerFactory, + IClaimsToThingCreator claimsToThingCreator, + IAuthenticationOptionsCreator authOptionsCreator, + IUpstreamTemplatePatternCreator upstreamTemplatePatternCreator, + IRequestIdKeyCreator requestIdKeyCreator, + IServiceProviderConfigurationCreator serviceProviderConfigCreator, + IQoSOptionsCreator qosOptionsCreator, + IReRouteOptionsCreator fileReRouteOptionsCreator, + IRateLimitOptionsCreator rateLimitOptionsCreator, + IRegionCreator regionCreator, + IHttpHandlerOptionsCreator httpHandlerOptionsCreator, + IAdministrationPath adminPath, + IHeaderFindAndReplaceCreator headerFAndRCreator, + IDownstreamAddressesCreator downstreamAddressesCreator + ) + { + _downstreamAddressesCreator = downstreamAddressesCreator; + _headerFAndRCreator = headerFAndRCreator; + _adminPath = adminPath; + _regionCreator = regionCreator; + _rateLimitOptionsCreator = rateLimitOptionsCreator; + _requestIdKeyCreator = requestIdKeyCreator; + _upstreamTemplatePatternCreator = upstreamTemplatePatternCreator; + _authOptionsCreator = authOptionsCreator; + _configurationValidator = configurationValidator; + _logger = loggerFactory.CreateLogger(); + _claimsToThingCreator = claimsToThingCreator; + _serviceProviderConfigCreator = serviceProviderConfigCreator; + _qosOptionsCreator = qosOptionsCreator; + _fileReRouteOptionsCreator = fileReRouteOptionsCreator; + _httpHandlerOptionsCreator = httpHandlerOptionsCreator; + } + + public async Task> Create(FileConfiguration fileConfiguration) + { + var config = await SetUpConfiguration(fileConfiguration); + return config; + } + + private async Task> SetUpConfiguration(FileConfiguration fileConfiguration) + { + var response = await _configurationValidator.IsValid(fileConfiguration); + + if (response.Data.IsError) + { + return new ErrorResponse(response.Data.Errors); + } + + var reRoutes = new List(); + + foreach (var reRoute in fileConfiguration.ReRoutes) + { + var downstreamReRoute = SetUpDownstreamReRoute(reRoute, fileConfiguration.GlobalConfiguration); + + var ocelotReRoute = SetUpReRoute(reRoute, downstreamReRoute); + + reRoutes.Add(ocelotReRoute); + } + + foreach (var aggregate in fileConfiguration.Aggregates) + { + var ocelotReRoute = SetUpAggregateReRoute(reRoutes, aggregate, fileConfiguration.GlobalConfiguration); + reRoutes.Add(ocelotReRoute); + } + + var serviceProviderConfiguration = _serviceProviderConfigCreator.Create(fileConfiguration.GlobalConfiguration); + + var config = new InternalConfiguration(reRoutes, _adminPath.Path, serviceProviderConfiguration, fileConfiguration.GlobalConfiguration.RequestIdKey); + + return new OkResponse(config); + } + + public ReRoute SetUpAggregateReRoute(List reRoutes, FileAggregateReRoute aggregateReRoute, FileGlobalConfiguration globalConfiguration) + { + var applicableReRoutes = reRoutes + .SelectMany(x => x.DownstreamReRoute) + .Where(r => aggregateReRoute.ReRouteKeys.Contains(r.Key)) + .ToList(); + + if(applicableReRoutes.Count != aggregateReRoute.ReRouteKeys.Count) + { + //todo - log or throw or return error whatever? + } + + //make another re route out of these + var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(aggregateReRoute); + + var reRoute = new ReRouteBuilder() + .WithUpstreamPathTemplate(aggregateReRoute.UpstreamPathTemplate) + .WithUpstreamHttpMethod(aggregateReRoute.UpstreamHttpMethod) + .WithUpstreamTemplatePattern(upstreamTemplatePattern) + .WithDownstreamReRoutes(applicableReRoutes) + .WithUpstreamHost(aggregateReRoute.UpstreamHost) + .WithAggregator(aggregateReRoute.Aggregator) + .Build(); + + return reRoute; + } + + private ReRoute SetUpReRoute(FileReRoute fileReRoute, DownstreamReRoute downstreamReRoutes) + { + var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileReRoute); + + var reRoute = new ReRouteBuilder() + .WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate) + .WithUpstreamHttpMethod(fileReRoute.UpstreamHttpMethod) + .WithUpstreamTemplatePattern(upstreamTemplatePattern) + .WithDownstreamReRoute(downstreamReRoutes) + .WithUpstreamHost(fileReRoute.UpstreamHost) + .Build(); + + return reRoute; + } + + private DownstreamReRoute SetUpDownstreamReRoute(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration) + { + var fileReRouteOptions = _fileReRouteOptionsCreator.Create(fileReRoute); + + var requestIdKey = _requestIdKeyCreator.Create(fileReRoute, globalConfiguration); + + var reRouteKey = CreateReRouteKey(fileReRoute); + + var qosKey = CreateQosKey(fileReRoute); + + var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileReRoute); + + var authOptionsForRoute = _authOptionsCreator.Create(fileReRoute); + + var claimsToHeaders = _claimsToThingCreator.Create(fileReRoute.AddHeadersToRequest); + + var claimsToClaims = _claimsToThingCreator.Create(fileReRoute.AddClaimsToRequest); + + var claimsToQueries = _claimsToThingCreator.Create(fileReRoute.AddQueriesToRequest); + + var qosOptions = _qosOptionsCreator.Create(fileReRoute); + + var rateLimitOption = _rateLimitOptionsCreator.Create(fileReRoute, globalConfiguration, fileReRouteOptions.EnableRateLimiting); + + var region = _regionCreator.Create(fileReRoute); + + var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileReRoute); + + var hAndRs = _headerFAndRCreator.Create(fileReRoute); + + var downstreamAddresses = _downstreamAddressesCreator.Create(fileReRoute); + + var lbOptions = CreateLoadBalancerOptions(fileReRoute); + + var reRoute = new DownstreamReRouteBuilder() + .WithKey(fileReRoute.Key) + .WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate) + .WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate) + .WithUpstreamHttpMethod(fileReRoute.UpstreamHttpMethod) + .WithUpstreamTemplatePattern(upstreamTemplatePattern) + .WithIsAuthenticated(fileReRouteOptions.IsAuthenticated) + .WithAuthenticationOptions(authOptionsForRoute) + .WithClaimsToHeaders(claimsToHeaders) + .WithClaimsToClaims(claimsToClaims) + .WithRouteClaimsRequirement(fileReRoute.RouteClaimsRequirement) + .WithIsAuthorised(fileReRouteOptions.IsAuthorised) + .WithClaimsToQueries(claimsToQueries) + .WithRequestIdKey(requestIdKey) + .WithIsCached(fileReRouteOptions.IsCached) + .WithCacheOptions(new CacheOptions(fileReRoute.FileCacheOptions.TtlSeconds, region)) + .WithDownstreamScheme(fileReRoute.DownstreamScheme) + .WithLoadBalancerOptions(lbOptions) + .WithDownstreamAddresses(downstreamAddresses) + .WithReRouteKey(reRouteKey) + .WithQosKey(qosKey) + .WithIsQos(fileReRouteOptions.IsQos) + .WithQosOptions(qosOptions) + .WithEnableRateLimiting(fileReRouteOptions.EnableRateLimiting) + .WithRateLimitOptions(rateLimitOption) + .WithHttpHandlerOptions(httpHandlerOptions) + .WithServiceName(fileReRoute.ServiceName) + .WithUseServiceDiscovery(fileReRoute.UseServiceDiscovery) + .WithUpstreamHeaderFindAndReplace(hAndRs.Upstream) + .WithDownstreamHeaderFindAndReplace(hAndRs.Downstream) + .WithUpstreamHost(fileReRoute.UpstreamHost) + .WithDelegatingHandlers(fileReRoute.DelegatingHandlers) + .WithAddHeadersToDownstream(hAndRs.AddHeadersToDownstream) + .WithAddHeadersToUpstream(hAndRs.AddHeadersToUpstream) + .WithDangerousAcceptAnyServerCertificateValidator(fileReRoute.DangerousAcceptAnyServerCertificateValidator) + .Build(); + + return reRoute; + } + + private LoadBalancerOptions CreateLoadBalancerOptions(FileReRoute fileReRoute) + { + return new LoadBalancerOptions(fileReRoute.LoadBalancerOptions.Type, fileReRoute.LoadBalancerOptions.Key, fileReRoute.LoadBalancerOptions.Expiry); + } + + private string CreateReRouteKey(FileReRoute fileReRoute) + { + if (!string.IsNullOrEmpty(fileReRoute.LoadBalancerOptions.Type) && !string.IsNullOrEmpty(fileReRoute.LoadBalancerOptions.Key) && fileReRoute.LoadBalancerOptions.Type == nameof(CookieStickySessions)) + { + return $"{nameof(CookieStickySessions)}:{fileReRoute.LoadBalancerOptions.Key}"; + } + + return CreateQosKey(fileReRoute); + } + + private string CreateQosKey(FileReRoute fileReRoute) + { + //note - not sure if this is the correct key, but this is probably the only unique key i can think of given my poor brain + var loadBalancerKey = $"{fileReRoute.UpstreamPathTemplate}|{string.Join(",", fileReRoute.UpstreamHttpMethod)}"; + return loadBalancerKey; + } + } +} diff --git a/src/Ocelot/Configuration/DownstreamReRoute.cs b/src/Ocelot/Configuration/DownstreamReRoute.cs index 4fe89f22..b4118db2 100644 --- a/src/Ocelot/Configuration/DownstreamReRoute.cs +++ b/src/Ocelot/Configuration/DownstreamReRoute.cs @@ -1,42 +1,43 @@ -using System.Collections.Generic; -using Ocelot.Configuration.Creator; -using Ocelot.Values; - namespace Ocelot.Configuration { + using System.Collections.Generic; + using Creator; + using Values; + public class DownstreamReRoute { public DownstreamReRoute( string key, PathTemplate upstreamPathTemplate, List upstreamHeadersFindAndReplace, - List downstreamHeadersFindAndReplace, - List downstreamAddresses, - string serviceName, - HttpHandlerOptions httpHandlerOptions, - bool useServiceDiscovery, - bool enableEndpointEndpointRateLimiting, - bool isQos, - QoSOptions qosOptionsOptions, - string downstreamScheme, - string requestIdKey, - bool isCached, - CacheOptions cacheOptions, - string loadBalancer, - RateLimitOptions rateLimitOptions, - Dictionary routeClaimsRequirement, - List claimsToQueries, - List claimsToHeaders, - List claimsToClaims, - bool isAuthenticated, - bool isAuthorised, - AuthenticationOptions authenticationOptions, - PathTemplate downstreamPathTemplate, - string reRouteKey, + List downstreamHeadersFindAndReplace, + List downstreamAddresses, + string serviceName, + HttpHandlerOptions httpHandlerOptions, + bool useServiceDiscovery, + bool enableEndpointEndpointRateLimiting, + bool isQos, + QoSOptions qosOptionsOptions, + string downstreamScheme, + string requestIdKey, + bool isCached, + CacheOptions cacheOptions, + LoadBalancerOptions loadBalancerOptions, + RateLimitOptions rateLimitOptions, + Dictionary routeClaimsRequirement, + List claimsToQueries, + List claimsToHeaders, + List claimsToClaims, + bool isAuthenticated, + bool isAuthorised, + AuthenticationOptions authenticationOptions, + PathTemplate downstreamPathTemplate, + string loadBalancerKey, List delegatingHandlers, List addHeadersToDownstream, List addHeadersToUpstream, - bool dangerousAcceptAnyServerCertificateValidator) + bool dangerousAcceptAnyServerCertificateValidator, + string qosKey) { DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; AddHeadersToDownstream = addHeadersToDownstream; @@ -56,7 +57,7 @@ namespace Ocelot.Configuration RequestIdKey = requestIdKey; IsCached = isCached; CacheOptions = cacheOptions; - LoadBalancer = loadBalancer; + LoadBalancerOptions = loadBalancerOptions; RateLimitOptions = rateLimitOptions; RouteClaimsRequirement = routeClaimsRequirement; ClaimsToQueries = claimsToQueries ?? new List(); @@ -66,39 +67,41 @@ namespace Ocelot.Configuration IsAuthorised = isAuthorised; AuthenticationOptions = authenticationOptions; DownstreamPathTemplate = downstreamPathTemplate; - ReRouteKey = reRouteKey; + LoadBalancerKey = loadBalancerKey; AddHeadersToUpstream = addHeadersToUpstream; + QosKey = qosKey; } - public string Key { get; private set; } - public PathTemplate UpstreamPathTemplate { get;private set; } - public List UpstreamHeadersFindAndReplace {get;private set;} - public List DownstreamHeadersFindAndReplace { get; private set; } - public List DownstreamAddresses { get; private set; } - public string ServiceName { get; private set; } - public HttpHandlerOptions HttpHandlerOptions { get; private set; } - public bool UseServiceDiscovery { get; private set; } - public bool EnableEndpointEndpointRateLimiting { get; private set; } - public bool IsQos { get; private set; } - public QoSOptions QosOptionsOptions { get; private set; } - public string DownstreamScheme { get; private set; } - public string RequestIdKey { get; private set; } - public bool IsCached { get; private set; } - public CacheOptions CacheOptions { get; private set; } - public string LoadBalancer { get; private set; } - public RateLimitOptions RateLimitOptions { get; private set; } - public Dictionary RouteClaimsRequirement { get; private set; } - public List ClaimsToQueries { get; private set; } - public List ClaimsToHeaders { get; private set; } - public List ClaimsToClaims { get; private set; } - public bool IsAuthenticated { get; private set; } - public bool IsAuthorised { get; private set; } - public AuthenticationOptions AuthenticationOptions { get; private set; } - public PathTemplate DownstreamPathTemplate { get; private set; } - public string ReRouteKey { get; private set; } - public List DelegatingHandlers {get;private set;} - public List AddHeadersToDownstream {get;private set;} - public List AddHeadersToUpstream { get; private set; } - public bool DangerousAcceptAnyServerCertificateValidator { get; private set; } + public string QosKey { get; } + public string Key { get; } + public PathTemplate UpstreamPathTemplate { get; } + public List UpstreamHeadersFindAndReplace { get; } + public List DownstreamHeadersFindAndReplace { get; } + public List DownstreamAddresses { get; } + public string ServiceName { get; } + public HttpHandlerOptions HttpHandlerOptions { get; } + public bool UseServiceDiscovery { get; } + public bool EnableEndpointEndpointRateLimiting { get; } + public bool IsQos { get; } + public QoSOptions QosOptionsOptions { get; } + public string DownstreamScheme { get; } + public string RequestIdKey { get; } + public bool IsCached { get; } + public CacheOptions CacheOptions { get; } + public LoadBalancerOptions LoadBalancerOptions { get; } + public RateLimitOptions RateLimitOptions { get; } + public Dictionary RouteClaimsRequirement { get; } + public List ClaimsToQueries { get; } + public List ClaimsToHeaders { get; } + public List ClaimsToClaims { get; } + public bool IsAuthenticated { get; } + public bool IsAuthorised { get; } + public AuthenticationOptions AuthenticationOptions { get; } + public PathTemplate DownstreamPathTemplate { get; } + public string LoadBalancerKey { get; } + public List DelegatingHandlers { get; } + public List AddHeadersToDownstream { get; } + public List AddHeadersToUpstream { get; } + public bool DangerousAcceptAnyServerCertificateValidator { get; } } } diff --git a/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs b/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs new file mode 100644 index 00000000..29acb5f6 --- /dev/null +++ b/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs @@ -0,0 +1,9 @@ +namespace Ocelot.Configuration.File +{ + public class FileLoadBalancerOptions + { + public string Type { get; set; } + public string Key { get; set; } + public int Expiry { get; set; } + } +} diff --git a/src/Ocelot/Configuration/File/FileReRoute.cs b/src/Ocelot/Configuration/File/FileReRoute.cs index acc6572a..44f9cd9e 100644 --- a/src/Ocelot/Configuration/File/FileReRoute.cs +++ b/src/Ocelot/Configuration/File/FileReRoute.cs @@ -1,54 +1,55 @@ -using System.Collections.Generic; - -namespace Ocelot.Configuration.File -{ - public class FileReRoute : IReRoute - { - public FileReRoute() - { - UpstreamHttpMethod = new List(); - AddHeadersToRequest = new Dictionary(); - AddClaimsToRequest = new Dictionary(); - RouteClaimsRequirement = new Dictionary(); - AddQueriesToRequest = new Dictionary(); - DownstreamHeaderTransform = new Dictionary(); - FileCacheOptions = new FileCacheOptions(); - QoSOptions = new FileQoSOptions(); - RateLimitOptions = new FileRateLimitRule(); - AuthenticationOptions = new FileAuthenticationOptions(); - HttpHandlerOptions = new FileHttpHandlerOptions(); - UpstreamHeaderTransform = new Dictionary(); - DownstreamHostAndPorts = new List(); - DelegatingHandlers = new List(); - Priority = 1; - } - - public string DownstreamPathTemplate { get; set; } - public string UpstreamPathTemplate { get; set; } - public List UpstreamHttpMethod { get; set; } - public Dictionary AddHeadersToRequest { get; set; } - public Dictionary UpstreamHeaderTransform { get; set; } - public Dictionary DownstreamHeaderTransform { get; set; } - public Dictionary AddClaimsToRequest { get; set; } - public Dictionary RouteClaimsRequirement { get; set; } - public Dictionary AddQueriesToRequest { get; set; } - public string RequestIdKey { get; set; } - public FileCacheOptions FileCacheOptions { get; set; } - public bool ReRouteIsCaseSensitive { get; set; } - public string ServiceName { get; set; } - public string DownstreamScheme {get;set;} - public FileQoSOptions QoSOptions { get; set; } - public string LoadBalancer { get;set; } - public FileRateLimitRule RateLimitOptions { get; set; } - public FileAuthenticationOptions AuthenticationOptions { get; set; } - public FileHttpHandlerOptions HttpHandlerOptions { get; set; } - public bool UseServiceDiscovery { get;set; } - public List DownstreamHostAndPorts {get;set;} - public string UpstreamHost { get; set; } - public string Key { get;set; } - public List DelegatingHandlers {get;set;} - public int Priority { get;set; } - public int Timeout { get; set; } - public bool DangerousAcceptAnyServerCertificateValidator { get; set; } - } -} +using System.Collections.Generic; + +namespace Ocelot.Configuration.File +{ + public class FileReRoute : IReRoute + { + public FileReRoute() + { + UpstreamHttpMethod = new List(); + AddHeadersToRequest = new Dictionary(); + AddClaimsToRequest = new Dictionary(); + RouteClaimsRequirement = new Dictionary(); + AddQueriesToRequest = new Dictionary(); + DownstreamHeaderTransform = new Dictionary(); + FileCacheOptions = new FileCacheOptions(); + QoSOptions = new FileQoSOptions(); + RateLimitOptions = new FileRateLimitRule(); + AuthenticationOptions = new FileAuthenticationOptions(); + HttpHandlerOptions = new FileHttpHandlerOptions(); + UpstreamHeaderTransform = new Dictionary(); + DownstreamHostAndPorts = new List(); + DelegatingHandlers = new List(); + LoadBalancerOptions = new FileLoadBalancerOptions(); + Priority = 1; + } + + public string DownstreamPathTemplate { get; set; } + public string UpstreamPathTemplate { get; set; } + public List UpstreamHttpMethod { get; set; } + public Dictionary AddHeadersToRequest { get; set; } + public Dictionary UpstreamHeaderTransform { get; set; } + public Dictionary DownstreamHeaderTransform { get; set; } + public Dictionary AddClaimsToRequest { get; set; } + public Dictionary RouteClaimsRequirement { get; set; } + public Dictionary AddQueriesToRequest { get; set; } + public string RequestIdKey { get; set; } + public FileCacheOptions FileCacheOptions { get; set; } + public bool ReRouteIsCaseSensitive { get; set; } + public string ServiceName { get; set; } + public string DownstreamScheme {get;set;} + public FileQoSOptions QoSOptions { get; set; } + public FileLoadBalancerOptions LoadBalancerOptions { get; set; } + public FileRateLimitRule RateLimitOptions { get; set; } + public FileAuthenticationOptions AuthenticationOptions { get; set; } + public FileHttpHandlerOptions HttpHandlerOptions { get; set; } + public bool UseServiceDiscovery { get;set; } + public List DownstreamHostAndPorts {get;set;} + public string UpstreamHost { get; set; } + public string Key { get;set; } + public List DelegatingHandlers {get;set;} + public int Priority { get;set; } + public int Timeout { get; set; } + public bool DangerousAcceptAnyServerCertificateValidator { get; set; } + } +} diff --git a/src/Ocelot/Configuration/LoadBalancerOptions.cs b/src/Ocelot/Configuration/LoadBalancerOptions.cs new file mode 100644 index 00000000..fd5b8b35 --- /dev/null +++ b/src/Ocelot/Configuration/LoadBalancerOptions.cs @@ -0,0 +1,18 @@ +namespace Ocelot.Configuration +{ + public class LoadBalancerOptions + { + public LoadBalancerOptions(string type, string key, int expiryInMs) + { + Type = type; + Key = key; + ExpiryInMs = expiryInMs; + } + + public string Type { get; } + + public string Key { get; } + + public int ExpiryInMs { get; } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessions.cs b/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessions.cs new file mode 100644 index 00000000..8721939a --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessions.cs @@ -0,0 +1,93 @@ +namespace Ocelot.LoadBalancer.LoadBalancers +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Ocelot.Middleware; + using Responses; + using Values; + + public class CookieStickySessions : ILoadBalancer, IDisposable + { + private readonly int _expiryInMs; + private readonly string _key; + private readonly ILoadBalancer _loadBalancer; + private readonly ConcurrentDictionary _stored; + private readonly Timer _timer; + private bool _expiring; + + public CookieStickySessions(ILoadBalancer loadBalancer, string key, int expiryInMs) + { + _key = key; + _expiryInMs = expiryInMs; + _loadBalancer = loadBalancer; + _stored = new ConcurrentDictionary(); + _timer = new Timer(x => + { + if (_expiring) + { + return; + } + + _expiring = true; + + Expire(); + + _expiring = false; + }, null, 0, 50); + } + + public void Dispose() + { + _timer?.Dispose(); + } + + public async Task> Lease(DownstreamContext context) + { + var value = context.HttpContext.Request.Cookies[_key]; + + if (!string.IsNullOrEmpty(value) && _stored.ContainsKey(value)) + { + var cached = _stored[value]; + + var updated = new StickySession(cached.HostAndPort, DateTime.UtcNow.AddMilliseconds(_expiryInMs)); + + _stored[value] = updated; + + return new OkResponse(updated.HostAndPort); + } + + var next = await _loadBalancer.Lease(context); + + if (next.IsError) + { + return new ErrorResponse(next.Errors); + } + + if (!string.IsNullOrEmpty(value) && !_stored.ContainsKey(value)) + { + _stored[value] = new StickySession(next.Data, DateTime.UtcNow.AddMilliseconds(_expiryInMs)); + } + + return new OkResponse(next.Data); + } + + public void Release(ServiceHostAndPort hostAndPort) + { + } + + private void Expire() + { + var expired = _stored.Where(x => x.Value.Expiry < DateTime.UtcNow); + + foreach (var expire in expired) + { + _stored.Remove(expire.Key, out _); + _loadBalancer.Release(expire.Value.HostAndPort); + } + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs index 90ea703d..d56367b2 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs @@ -1,12 +1,13 @@ -using System.Threading.Tasks; -using Ocelot.Responses; -using Ocelot.Values; - -namespace Ocelot.LoadBalancer.LoadBalancers -{ - public interface ILoadBalancer - { - Task> Lease(); - void Release(ServiceHostAndPort hostAndPort); - } -} +using System.Threading.Tasks; +using Ocelot.Middleware; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public interface ILoadBalancer + { + Task> Lease(DownstreamContext context); + void Release(ServiceHostAndPort hostAndPort); + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs index f02230ec..93082de6 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs @@ -1,10 +1,10 @@ -using System.Threading.Tasks; -using Ocelot.Configuration; - -namespace Ocelot.LoadBalancer.LoadBalancers -{ - public interface ILoadBalancerFactory - { - Task Get(DownstreamReRoute reRoute, ServiceProviderConfiguration config); - } -} \ No newline at end of file +using System.Threading.Tasks; +using Ocelot.Configuration; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public interface ILoadBalancerFactory + { + Task Get(DownstreamReRoute reRoute, ServiceProviderConfiguration config); + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs index d9d051c2..7f8d7381 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs @@ -1,11 +1,11 @@ -using System.Threading.Tasks; -using Ocelot.Configuration; -using Ocelot.Responses; - -namespace Ocelot.LoadBalancer.LoadBalancers -{ - public interface ILoadBalancerHouse - { - Task> Get(DownstreamReRoute reRoute, ServiceProviderConfiguration config); - } -} \ No newline at end of file +using System.Threading.Tasks; +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public interface ILoadBalancerHouse + { + Task> Get(DownstreamReRoute reRoute, ServiceProviderConfiguration config); + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnection.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnection.cs index 2810e021..59f260ca 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnection.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnection.cs @@ -1,145 +1,146 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Ocelot.Errors; -using Ocelot.Responses; -using Ocelot.Values; - -namespace Ocelot.LoadBalancer.LoadBalancers -{ - public class LeastConnection : ILoadBalancer - { - private readonly Func>> _services; - private readonly List _leases; - private readonly string _serviceName; - private static readonly object _syncLock = new object(); - - public LeastConnection(Func>> services, string serviceName) - { - _services = services; - _serviceName = serviceName; - _leases = new List(); - } - - public async Task> Lease() - { - var services = await _services.Invoke(); - - if (services == null) - { - return new ErrorResponse(new ServicesAreNullError($"services were null for {_serviceName}") ); - } - - if (!services.Any()) - { - return new ErrorResponse(new ServicesAreEmptyError($"services were empty for {_serviceName}")); - } - - lock(_syncLock) - { - //todo - maybe this should be moved somewhere else...? Maybe on a repeater on seperate thread? loop every second and update or something? - UpdateServices(services); - - var leaseWithLeastConnections = GetLeaseWithLeastConnections(); - - _leases.Remove(leaseWithLeastConnections); - - leaseWithLeastConnections = AddConnection(leaseWithLeastConnections); - - _leases.Add(leaseWithLeastConnections); - - return new OkResponse(new ServiceHostAndPort(leaseWithLeastConnections.HostAndPort.DownstreamHost, leaseWithLeastConnections.HostAndPort.DownstreamPort)); - } - } - - public void Release(ServiceHostAndPort hostAndPort) - { - lock(_syncLock) - { - var matchingLease = _leases.FirstOrDefault(l => l.HostAndPort.DownstreamHost == hostAndPort.DownstreamHost - && l.HostAndPort.DownstreamPort == hostAndPort.DownstreamPort); - - if (matchingLease != null) - { - var replacementLease = new Lease(hostAndPort, matchingLease.Connections - 1); - - _leases.Remove(matchingLease); - - _leases.Add(replacementLease); - } - } - } - - private Lease AddConnection(Lease lease) - { - return new Lease(lease.HostAndPort, lease.Connections + 1); - } - - private Lease GetLeaseWithLeastConnections() - { - //now get the service with the least connections? - Lease leaseWithLeastConnections = null; - - for (var i = 0; i < _leases.Count; i++) - { - if (i == 0) - { - leaseWithLeastConnections = _leases[i]; - } - else - { - if (_leases[i].Connections < leaseWithLeastConnections.Connections) - { - leaseWithLeastConnections = _leases[i]; - } - } - } - - return leaseWithLeastConnections; - } - - private Response UpdateServices(List services) - { - if (_leases.Count > 0) - { - var leasesToRemove = new List(); - - foreach (var lease in _leases) - { - var match = services.FirstOrDefault(s => s.HostAndPort.DownstreamHost == lease.HostAndPort.DownstreamHost - && s.HostAndPort.DownstreamPort == lease.HostAndPort.DownstreamPort); - - if (match == null) - { - leasesToRemove.Add(lease); - } - } - - foreach (var lease in leasesToRemove) - { - _leases.Remove(lease); - } - - foreach (var service in services) - { - var exists = _leases.FirstOrDefault(l => l.HostAndPort.DownstreamHost == service.HostAndPort.DownstreamHost && l.HostAndPort.DownstreamPort == service.HostAndPort.DownstreamPort); - - if (exists == null) - { - _leases.Add(new Lease(service.HostAndPort, 0)); - } - } - } - else - { - foreach (var service in services) - { - _leases.Add(new Lease(service.HostAndPort, 0)); - } - } - - return new OkResponse(); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ocelot.Errors; +using Ocelot.Middleware; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class LeastConnection : ILoadBalancer + { + private readonly Func>> _services; + private readonly List _leases; + private readonly string _serviceName; + private static readonly object _syncLock = new object(); + + public LeastConnection(Func>> services, string serviceName) + { + _services = services; + _serviceName = serviceName; + _leases = new List(); + } + + public async Task> Lease(DownstreamContext downstreamContext) + { + var services = await _services.Invoke(); + + if (services == null) + { + return new ErrorResponse(new ServicesAreNullError($"services were null for {_serviceName}") ); + } + + if (!services.Any()) + { + return new ErrorResponse(new ServicesAreEmptyError($"services were empty for {_serviceName}")); + } + + lock(_syncLock) + { + //todo - maybe this should be moved somewhere else...? Maybe on a repeater on seperate thread? loop every second and update or something? + UpdateServices(services); + + var leaseWithLeastConnections = GetLeaseWithLeastConnections(); + + _leases.Remove(leaseWithLeastConnections); + + leaseWithLeastConnections = AddConnection(leaseWithLeastConnections); + + _leases.Add(leaseWithLeastConnections); + + return new OkResponse(new ServiceHostAndPort(leaseWithLeastConnections.HostAndPort.DownstreamHost, leaseWithLeastConnections.HostAndPort.DownstreamPort)); + } + } + + public void Release(ServiceHostAndPort hostAndPort) + { + lock(_syncLock) + { + var matchingLease = _leases.FirstOrDefault(l => l.HostAndPort.DownstreamHost == hostAndPort.DownstreamHost + && l.HostAndPort.DownstreamPort == hostAndPort.DownstreamPort); + + if (matchingLease != null) + { + var replacementLease = new Lease(hostAndPort, matchingLease.Connections - 1); + + _leases.Remove(matchingLease); + + _leases.Add(replacementLease); + } + } + } + + private Lease AddConnection(Lease lease) + { + return new Lease(lease.HostAndPort, lease.Connections + 1); + } + + private Lease GetLeaseWithLeastConnections() + { + //now get the service with the least connections? + Lease leaseWithLeastConnections = null; + + for (var i = 0; i < _leases.Count; i++) + { + if (i == 0) + { + leaseWithLeastConnections = _leases[i]; + } + else + { + if (_leases[i].Connections < leaseWithLeastConnections.Connections) + { + leaseWithLeastConnections = _leases[i]; + } + } + } + + return leaseWithLeastConnections; + } + + private Response UpdateServices(List services) + { + if (_leases.Count > 0) + { + var leasesToRemove = new List(); + + foreach (var lease in _leases) + { + var match = services.FirstOrDefault(s => s.HostAndPort.DownstreamHost == lease.HostAndPort.DownstreamHost + && s.HostAndPort.DownstreamPort == lease.HostAndPort.DownstreamPort); + + if (match == null) + { + leasesToRemove.Add(lease); + } + } + + foreach (var lease in leasesToRemove) + { + _leases.Remove(lease); + } + + foreach (var service in services) + { + var exists = _leases.FirstOrDefault(l => l.HostAndPort.DownstreamHost == service.HostAndPort.DownstreamHost && l.HostAndPort.DownstreamPort == service.HostAndPort.DownstreamPort); + + if (exists == null) + { + _leases.Add(new Lease(service.HostAndPort, 0)); + } + } + } + else + { + foreach (var service in services) + { + _leases.Add(new Lease(service.HostAndPort, 0)); + } + } + + return new OkResponse(); + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs index 29d84ba4..1f0ecc72 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs @@ -1,30 +1,34 @@ -using System.Threading.Tasks; -using Ocelot.Configuration; -using Ocelot.ServiceDiscovery; - -namespace Ocelot.LoadBalancer.LoadBalancers -{ - public class LoadBalancerFactory : ILoadBalancerFactory - { - private readonly IServiceDiscoveryProviderFactory _serviceProviderFactory; - public LoadBalancerFactory(IServiceDiscoveryProviderFactory serviceProviderFactory) - { - _serviceProviderFactory = serviceProviderFactory; - } - - public async Task Get(DownstreamReRoute reRoute, ServiceProviderConfiguration config) - { - var serviceProvider = _serviceProviderFactory.Get(config, reRoute); - - switch (reRoute.LoadBalancer) - { - case "RoundRobin": - return new RoundRobin(async () => await serviceProvider.Get()); - case "LeastConnection": - return new LeastConnection(async () => await serviceProvider.Get(), reRoute.ServiceName); - default: - return new NoLoadBalancer(await serviceProvider.Get()); - } - } - } -} +using System.Threading.Tasks; +using Ocelot.Configuration; +using Ocelot.ServiceDiscovery; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class LoadBalancerFactory : ILoadBalancerFactory + { + private readonly IServiceDiscoveryProviderFactory _serviceProviderFactory; + + public LoadBalancerFactory(IServiceDiscoveryProviderFactory serviceProviderFactory) + { + _serviceProviderFactory = serviceProviderFactory; + } + + public async Task Get(DownstreamReRoute reRoute, ServiceProviderConfiguration config) + { + var serviceProvider = _serviceProviderFactory.Get(config, reRoute); + + switch (reRoute.LoadBalancerOptions?.Type) + { + case nameof(RoundRobin): + return new RoundRobin(async () => await serviceProvider.Get()); + case nameof(LeastConnection): + return new LeastConnection(async () => await serviceProvider.Get(), reRoute.ServiceName); + case nameof(CookieStickySessions): + var loadBalancer = new RoundRobin(async () => await serviceProvider.Get()); + return new CookieStickySessions(loadBalancer, reRoute.LoadBalancerOptions.Key, reRoute.LoadBalancerOptions.ExpiryInMs); + default: + return new NoLoadBalancer(await serviceProvider.Get()); + } + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs index 3d8059b9..8ca3fab1 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs @@ -1,56 +1,56 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading.Tasks; -using Ocelot.Configuration; -using Ocelot.Responses; - -namespace Ocelot.LoadBalancer.LoadBalancers -{ - public class LoadBalancerHouse : ILoadBalancerHouse - { - private readonly ILoadBalancerFactory _factory; - private readonly ConcurrentDictionary _loadBalancers; - - public LoadBalancerHouse(ILoadBalancerFactory factory) - { - _factory = factory; - _loadBalancers = new ConcurrentDictionary(); - } - - public async Task> Get(DownstreamReRoute reRoute, ServiceProviderConfiguration config) - { - try - { - if(_loadBalancers.TryGetValue(reRoute.ReRouteKey, out var loadBalancer)) - { - loadBalancer = _loadBalancers[reRoute.ReRouteKey]; - - if(reRoute.LoadBalancer != loadBalancer.GetType().Name) - { - loadBalancer = await _factory.Get(reRoute, config); - AddLoadBalancer(reRoute.ReRouteKey, loadBalancer); - } - - return new OkResponse(loadBalancer); - } - - loadBalancer = await _factory.Get(reRoute, config); - AddLoadBalancer(reRoute.ReRouteKey, loadBalancer); - return new OkResponse(loadBalancer); - } - catch(Exception ex) - { - return new ErrorResponse(new List() - { - new UnableToFindLoadBalancerError($"unabe to find load balancer for {reRoute.ReRouteKey} exception is {ex}") - }); - } - } - - private void AddLoadBalancer(string key, ILoadBalancer loadBalancer) - { - _loadBalancers.AddOrUpdate(key, loadBalancer, (x, y) => loadBalancer); - } - } -} +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class LoadBalancerHouse : ILoadBalancerHouse + { + private readonly ILoadBalancerFactory _factory; + private readonly ConcurrentDictionary _loadBalancers; + + public LoadBalancerHouse(ILoadBalancerFactory factory) + { + _factory = factory; + _loadBalancers = new ConcurrentDictionary(); + } + + public async Task> Get(DownstreamReRoute reRoute, ServiceProviderConfiguration config) + { + try + { + if(_loadBalancers.TryGetValue(reRoute.LoadBalancerKey, out var loadBalancer)) + { + loadBalancer = _loadBalancers[reRoute.LoadBalancerKey]; + + if(reRoute.LoadBalancerOptions.Type != loadBalancer.GetType().Name) + { + loadBalancer = await _factory.Get(reRoute, config); + AddLoadBalancer(reRoute.LoadBalancerKey, loadBalancer); + } + + return new OkResponse(loadBalancer); + } + + loadBalancer = await _factory.Get(reRoute, config); + AddLoadBalancer(reRoute.LoadBalancerKey, loadBalancer); + return new OkResponse(loadBalancer); + } + catch(Exception ex) + { + return new ErrorResponse(new List() + { + new UnableToFindLoadBalancerError($"unabe to find load balancer for {reRoute.LoadBalancerKey} exception is {ex}") + }); + } + } + + private void AddLoadBalancer(string key, ILoadBalancer loadBalancer) + { + _loadBalancers.AddOrUpdate(key, loadBalancer, (x, y) => loadBalancer); + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs b/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs index 69743c06..89f45545 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs @@ -1,34 +1,35 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Ocelot.Responses; -using Ocelot.Values; - -namespace Ocelot.LoadBalancer.LoadBalancers -{ - public class NoLoadBalancer : ILoadBalancer - { - private readonly List _services; - - public NoLoadBalancer(List services) - { - _services = services; - } - - public async Task> Lease() - { - //todo no point spinning a task up here, also first or default could be null.. - if (_services == null || _services.Count == 0) - { - return new ErrorResponse(new ServicesAreEmptyError("There were no services in NoLoadBalancer")); - } - - var service = await Task.FromResult(_services.FirstOrDefault()); - return new OkResponse(service.HostAndPort); - } - - public void Release(ServiceHostAndPort hostAndPort) - { - } - } -} +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ocelot.Middleware; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class NoLoadBalancer : ILoadBalancer + { + private readonly List _services; + + public NoLoadBalancer(List services) + { + _services = services; + } + + public async Task> Lease(DownstreamContext downstreamContext) + { + //todo no point spinning a task up here, also first or default could be null.. + if (_services == null || _services.Count == 0) + { + return new ErrorResponse(new ServicesAreEmptyError("There were no services in NoLoadBalancer")); + } + + var service = await Task.FromResult(_services.FirstOrDefault()); + return new OkResponse(service.HostAndPort); + } + + public void Release(ServiceHostAndPort hostAndPort) + { + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobin.cs b/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobin.cs index 26d55b1f..15fcf6eb 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobin.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobin.cs @@ -1,37 +1,38 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Ocelot.Responses; -using Ocelot.Values; -using System; - -namespace Ocelot.LoadBalancer.LoadBalancers -{ - public class RoundRobin : ILoadBalancer - { - private readonly Func>> _services; - - private int _last; - - public RoundRobin(Func>> services) - { - _services = services; - } - - public async Task> Lease() - { - var services = await _services.Invoke(); - if (_last >= services.Count) - { - _last = 0; - } - - var next = await Task.FromResult(services[_last]); - _last++; - return new OkResponse(next.HostAndPort); - } - - public void Release(ServiceHostAndPort hostAndPort) - { - } - } -} +using System.Collections.Generic; +using System.Threading.Tasks; +using Ocelot.Responses; +using Ocelot.Values; +using System; +using Ocelot.Middleware; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class RoundRobin : ILoadBalancer + { + private readonly Func>> _services; + + private int _last; + + public RoundRobin(Func>> services) + { + _services = services; + } + + public async Task> Lease(DownstreamContext downstreamContext) + { + var services = await _services.Invoke(); + if (_last >= services.Count) + { + _last = 0; + } + + var next = await Task.FromResult(services[_last]); + _last++; + return new OkResponse(next.HostAndPort); + } + + public void Release(ServiceHostAndPort hostAndPort) + { + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/StickySession.cs b/src/Ocelot/LoadBalancer/LoadBalancers/StickySession.cs new file mode 100644 index 00000000..152bdf4b --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/StickySession.cs @@ -0,0 +1,18 @@ +using System; +using Ocelot.Values; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class StickySession + { + public StickySession(ServiceHostAndPort hostAndPort, DateTime expiry) + { + HostAndPort = hostAndPort; + Expiry = expiry; + } + + public ServiceHostAndPort HostAndPort { get; } + + public DateTime Expiry { get; } + } +} diff --git a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs index f5ddf6f2..fc971a16 100644 --- a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs +++ b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs @@ -1,63 +1,63 @@ -using System; -using System.Threading.Tasks; -using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.Logging; -using Ocelot.Middleware; - -namespace Ocelot.LoadBalancer.Middleware -{ - public class LoadBalancingMiddleware : OcelotMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly ILoadBalancerHouse _loadBalancerHouse; - - public LoadBalancingMiddleware(OcelotRequestDelegate next, - IOcelotLoggerFactory loggerFactory, - ILoadBalancerHouse loadBalancerHouse) - :base(loggerFactory.CreateLogger()) - { - _next = next; - _loadBalancerHouse = loadBalancerHouse; - } - - public async Task Invoke(DownstreamContext context) - { - var loadBalancer = await _loadBalancerHouse.Get(context.DownstreamReRoute, context.ServiceProviderConfiguration); - if(loadBalancer.IsError) - { - Logger.LogDebug("there was an error retriving the loadbalancer, setting pipeline error"); - SetPipelineError(context, loadBalancer.Errors); - return; - } - - var hostAndPort = await loadBalancer.Data.Lease(); - if(hostAndPort.IsError) - { - Logger.LogDebug("there was an error leasing the loadbalancer, setting pipeline error"); - SetPipelineError(context, hostAndPort.Errors); - return; - } - - context.DownstreamRequest.Host = hostAndPort.Data.DownstreamHost; - - if (hostAndPort.Data.DownstreamPort > 0) - { - context.DownstreamRequest.Port = hostAndPort.Data.DownstreamPort; - } - - try - { - await _next.Invoke(context); - } - catch (Exception) - { - Logger.LogDebug("Exception calling next middleware, exception will be thrown to global handler"); - throw; - } - finally - { - loadBalancer.Data.Release(hostAndPort.Data); - } - } - } -} +using System; +using System.Threading.Tasks; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.LoadBalancer.Middleware +{ + public class LoadBalancingMiddleware : OcelotMiddleware + { + private readonly OcelotRequestDelegate _next; + private readonly ILoadBalancerHouse _loadBalancerHouse; + + public LoadBalancingMiddleware(OcelotRequestDelegate next, + IOcelotLoggerFactory loggerFactory, + ILoadBalancerHouse loadBalancerHouse) + :base(loggerFactory.CreateLogger()) + { + _next = next; + _loadBalancerHouse = loadBalancerHouse; + } + + public async Task Invoke(DownstreamContext context) + { + var loadBalancer = await _loadBalancerHouse.Get(context.DownstreamReRoute, context.ServiceProviderConfiguration); + if(loadBalancer.IsError) + { + Logger.LogDebug("there was an error retriving the loadbalancer, setting pipeline error"); + SetPipelineError(context, loadBalancer.Errors); + return; + } + + var hostAndPort = await loadBalancer.Data.Lease(context); + if(hostAndPort.IsError) + { + Logger.LogDebug("there was an error leasing the loadbalancer, setting pipeline error"); + SetPipelineError(context, hostAndPort.Errors); + return; + } + + context.DownstreamRequest.Host = hostAndPort.Data.DownstreamHost; + + if (hostAndPort.Data.DownstreamPort > 0) + { + context.DownstreamRequest.Port = hostAndPort.Data.DownstreamPort; + } + + try + { + await _next.Invoke(context); + } + catch (Exception) + { + Logger.LogDebug("Exception calling next middleware, exception will be thrown to global handler"); + throw; + } + finally + { + loadBalancer.Data.Release(hostAndPort.Data); + } + } + } +} diff --git a/src/Ocelot/Requester/QoS/QosProviderHouse.cs b/src/Ocelot/Requester/QoS/QosProviderHouse.cs index 64e2bb7d..73629e7f 100644 --- a/src/Ocelot/Requester/QoS/QosProviderHouse.cs +++ b/src/Ocelot/Requester/QoS/QosProviderHouse.cs @@ -21,26 +21,26 @@ namespace Ocelot.Requester.QoS { try { - if (_qoSProviders.TryGetValue(reRoute.ReRouteKey, out var qosProvider)) + if (_qoSProviders.TryGetValue(reRoute.QosKey, out var qosProvider)) { if (reRoute.IsQos && qosProvider.CircuitBreaker == null) { qosProvider = _qoSProviderFactory.Get(reRoute); - Add(reRoute.ReRouteKey, qosProvider); + Add(reRoute.QosKey, qosProvider); } - return new OkResponse(_qoSProviders[reRoute.ReRouteKey]); + return new OkResponse(_qoSProviders[reRoute.QosKey]); } qosProvider = _qoSProviderFactory.Get(reRoute); - Add(reRoute.ReRouteKey, qosProvider); + Add(reRoute.QosKey, qosProvider); return new OkResponse(qosProvider); } catch (Exception ex) { return new ErrorResponse(new List() { - new UnableToFindQoSProviderError($"unabe to find qos provider for {reRoute.ReRouteKey}, exception was {ex}") + new UnableToFindQoSProviderError($"unabe to find qos provider for {reRoute.QosKey}, exception was {ex}") }); } } diff --git a/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs b/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs index 14c69f9a..a4ce1767 100644 --- a/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs +++ b/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs @@ -1,163 +1,163 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.File; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.AcceptanceTests -{ - public class LoadBalancerTests : IDisposable - { - private IWebHost _builderOne; - private IWebHost _builderTwo; - private readonly Steps _steps; - private int _counterOne; - private int _counterTwo; - private static readonly object _syncLock = new object(); - - public LoadBalancerTests() - { - _steps = new Steps(); - } - - [Fact] - public void should_load_balance_request() - { - var downstreamServiceOneUrl = "http://localhost:50881"; - var downstreamServiceTwoUrl = "http://localhost:50892"; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - LoadBalancer = "LeastConnection", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 50881 - }, - new FileHostAndPort - { - Host = "localhost", - Port = 50892 - } - } - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - } - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) - .BDDfy(); - } - - private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) - { - _counterOne.ShouldBeInRange(bottom, top); - _counterOne.ShouldBeInRange(bottom, top); - } - - private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) - { - var total = _counterOne + _counterTwo; - total.ShouldBe(expected); - } - - private void GivenProductServiceOneIsRunning(string url, int statusCode) - { - _builderOne = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.Run(async context => - { - try - { - var response = string.Empty; - lock (_syncLock) - { - _counterOne++; - response = _counterOne.ToString(); - } - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(response); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - }) - .Build(); - - _builderOne.Start(); - } - - private void GivenProductServiceTwoIsRunning(string url, int statusCode) - { - _builderTwo = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.Run(async context => - { - try - { - var response = string.Empty; - lock (_syncLock) - { - _counterTwo++; - response = _counterTwo.ToString(); - } - - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(response); - } - catch (System.Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - }) - .Build(); - - _builderTwo.Start(); - } - - public void Dispose() - { - _builderOne?.Dispose(); - _builderTwo?.Dispose(); - _steps.Dispose(); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class LoadBalancerTests : IDisposable + { + private IWebHost _builderOne; + private IWebHost _builderTwo; + private readonly Steps _steps; + private int _counterOne; + private int _counterTwo; + private static readonly object _syncLock = new object(); + + public LoadBalancerTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_load_balance_request() + { + var downstreamServiceOneUrl = "http://localhost:50881"; + var downstreamServiceTwoUrl = "http://localhost:50892"; + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 50881 + }, + new FileHostAndPort + { + Host = "localhost", + Port = 50892 + } + } + } + }, + GlobalConfiguration = new FileGlobalConfiguration() + { + } + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) + .BDDfy(); + } + + private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) + { + _counterOne.ShouldBeInRange(bottom, top); + _counterOne.ShouldBeInRange(bottom, top); + } + + private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) + { + var total = _counterOne + _counterTwo; + total.ShouldBe(expected); + } + + private void GivenProductServiceOneIsRunning(string url, int statusCode) + { + _builderOne = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + try + { + var response = string.Empty; + lock (_syncLock) + { + _counterOne++; + response = _counterOne.ToString(); + } + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + }) + .Build(); + + _builderOne.Start(); + } + + private void GivenProductServiceTwoIsRunning(string url, int statusCode) + { + _builderTwo = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + try + { + var response = string.Empty; + lock (_syncLock) + { + _counterTwo++; + response = _counterTwo.ToString(); + } + + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (System.Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + }) + .Build(); + + _builderTwo.Start(); + } + + public void Dispose() + { + _builderOne?.Dispose(); + _builderTwo?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/RoutingTests.cs index 6ede8524..3ddb4f87 100644 --- a/test/Ocelot.AcceptanceTests/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingTests.cs @@ -1,1016 +1,1016 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.File; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.AcceptanceTests -{ - public class RoutingTests : IDisposable - { - private IWebHost _builder; - private readonly Steps _steps; - private string _downstreamPath; - - public RoutingTests() - { - _steps = new Steps(); - } - - [Fact] - public void should_return_response_404_when_no_configuration_at_all() - { - this.Given(x => _steps.GivenThereIsAConfiguration(new FileConfiguration())) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_forward_slash_and_placeholder_only() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/{url}", - DownstreamScheme = "http", - UpstreamPathTemplate = "/{url}", - UpstreamHttpMethod = new List { "Get" }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_favouring_forward_slash_with_path_route() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/{url}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/{url}", - UpstreamHttpMethod = new List { "Get" }, - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", "/test", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/test")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_favouring_forward_slash() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/{url}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/{url}", - UpstreamHttpMethod = new List { "Get" }, - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_favouring_forward_slash_route_because_it_is_first() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - }, - new FileReRoute - { - DownstreamPathTemplate = "/{url}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/{url}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_nothing_and_placeholder_only() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/{url}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/{url}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_simple_url() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void bug() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/v1/vacancy", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51874, - } - }, - UpstreamPathTemplate = "/vacancy/", - UpstreamHttpMethod = new List { "Options", "Put", "Get", "Post", "Delete" }, - ServiceName = "botCore", - LoadBalancer = "LeastConnection" - }, - new FileReRoute - { - DownstreamPathTemplate = "/api/v1/vacancy/{vacancyId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51874, - } - }, - UpstreamPathTemplate = "/vacancy/{vacancyId}", - UpstreamHttpMethod = new List { "Options", "Put", "Get", "Post", "Delete" }, - ServiceName = "botCore", - LoadBalancer = "LeastConnection" - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51874", "/api/v1/vacancy/1", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/vacancy/1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_when_path_missing_forward_slash_as_first_char() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/products", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_when_host_has_trailing_slash() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/products", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_ok_when_upstream_url_ends_with_forward_slash_but_template_does_not() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/products", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/products/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/products", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_not_found_when_upstream_url_ends_with_forward_slash_but_template_does_not() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/products", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/products", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/products", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - [Fact] - public void should_return_not_found() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/products", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, - QoSOptions = new FileQoSOptions() - { - ExceptionsAllowedBeforeBreaking = 3, - DurationOfBreak = 5, - TimeoutValue = 5000 - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/products", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_complex_url() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/{productId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/products/1", 200, "Some Product")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Some Product")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_complex_url_that_starts_with_placeholder() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/{variantId}/products/{productId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/{variantId}/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/23/products/1", 200, "Some Product")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("23/products/1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Some Product")) - .BDDfy(); - } - - [Fact] - public void should_not_add_trailing_slash_to_downstream_url() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/{productId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/products/1", 200, "Some Product")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/1")) - .Then(x => ThenTheDownstreamUrlPathShouldBe("/api/products/1")) - .BDDfy(); - } - - [Fact] - public void should_return_response_201_with_simple_url() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Post" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201, string.Empty)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) - .BDDfy(); - } - - [Fact] - public void should_return_response_201_with_complex_query_string() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/newThing", - UpstreamPathTemplate = "/newThing", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/newThing", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/newThing?DeviceType=IphoneApp&Browser=moonpigIphone&BrowserString=-&CountryCode=123&DeviceName=iPhone 5 (GSM+CDMA)&OperatingSystem=iPhone OS 7.1.2&BrowserVersion=3708AdHoc&ipAddress=-")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_placeholder_for_final_url_path() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/{urlPath}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/myApp1Name/api/{urlPath}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/products/1", 200, "Some Product")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/myApp1Name/api/products/1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Some Product")) - .BDDfy(); - } - - [Fact] - public void should_return_response_201_with_simple_url_and_multiple_upstream_http_method() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get", "Post" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "", 201, string.Empty)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_simple_url_and_any_upstream_http_method() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List(), - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_404_when_calling_upstream_route_with_no_matching_downstream_re_route_github_issue_134() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/v1/vacancy", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/vacancy/", - UpstreamHttpMethod = new List { "Options", "Put", "Get", "Post", "Delete" }, - ServiceName = "botCore", - LoadBalancer = "LeastConnection" - }, - new FileReRoute - { - DownstreamPathTemplate = "/api/v1/vacancy/{vacancyId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/vacancy/{vacancyId}", - UpstreamHttpMethod = new List { "Options", "Put", "Get", "Post", "Delete" }, - ServiceName = "botCore", - LoadBalancer = "LeastConnection" - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/v1/vacancy/1", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("api/vacancy/1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - [Fact] - public void should_fix_145() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/{url}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51899, - } - }, - UpstreamPathTemplate = "/platform/{url}", - UpstreamHttpMethod = new List { "Get" }, - QoSOptions = new FileQoSOptions { - ExceptionsAllowedBeforeBreaking = 3, - DurationOfBreak = 10, - TimeoutValue = 5000 - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", "/api/swagger/lib/backbone-min.js", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/platform/swagger/lib/backbone-min.js")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => ThenTheDownstreamUrlPathShouldBe("/api/swagger/lib/backbone-min.js")) - .BDDfy(); - } - - [Fact] - public void should_use_priority() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/goods/{url}", - DownstreamScheme = "http", - UpstreamPathTemplate = "/goods/{url}", - UpstreamHttpMethod = new List { "Get" }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 53879, - } - }, - Priority = 0 - }, - new FileReRoute - { - DownstreamPathTemplate = "/goods/delete", - DownstreamScheme = "http", - UpstreamPathTemplate = "/goods/delete", - UpstreamHttpMethod = new List { "Get" }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 52879, - } - }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:52879/", "/goods/delete", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/goods/delete")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_fix_issue_271() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/v1/{everything}", - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/v1/{everything}", - UpstreamHttpMethod = new List { "Get", "Put", "Post" }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 54879, - } - }, - }, - new FileReRoute - { - DownstreamPathTemplate = "/connect/token", - DownstreamScheme = "http", - UpstreamPathTemplate = "/connect/token", - UpstreamHttpMethod = new List { "Post" }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 5001, - } - }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:54879/", "/api/v1/modules/Test", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/v1/modules/Test")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) - { - _builder = new WebHostBuilder() - .UseUrls(baseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .Configure(app => - { - app.UsePathBase(basePath); - app.Run(async context => - { - _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if(_downstreamPath != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); - }) - .Build(); - - _builder.Start(); - } - - internal void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPath) - { - _downstreamPath.ShouldBe(expectedDownstreamPath); - } - - public void Dispose() - { - _builder?.Dispose(); - _steps.Dispose(); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class RoutingTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + private string _downstreamPath; + + public RoutingTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_return_response_404_when_no_configuration_at_all() + { + this.Given(x => _steps.GivenThereIsAConfiguration(new FileConfiguration())) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_forward_slash_and_placeholder_only() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_favouring_forward_slash_with_path_route() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51880, + } + }, + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", "/test", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/test")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_favouring_forward_slash() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51880, + } + }, + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_favouring_forward_slash_route_because_it_is_first() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51880, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + new FileReRoute + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_nothing_and_placeholder_only() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_simple_url() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void bug() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/v1/vacancy", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51874, + } + }, + UpstreamPathTemplate = "/vacancy/", + UpstreamHttpMethod = new List { "Options", "Put", "Get", "Post", "Delete" }, + ServiceName = "botCore", + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" } + }, + new FileReRoute + { + DownstreamPathTemplate = "/api/v1/vacancy/{vacancyId}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51874, + } + }, + UpstreamPathTemplate = "/vacancy/{vacancyId}", + UpstreamHttpMethod = new List { "Options", "Put", "Get", "Post", "Delete" }, + ServiceName = "botCore", + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51874", "/api/v1/vacancy/1", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/vacancy/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_when_path_missing_forward_slash_as_first_char() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_when_host_has_trailing_slash() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_ok_when_upstream_url_ends_with_forward_slash_but_template_does_not() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamPathTemplate = "/products/", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_not_found_when_upstream_url_ends_with_forward_slash_but_template_does_not() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamPathTemplate = "/products", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void should_return_not_found() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + QoSOptions = new FileQoSOptions() + { + ExceptionsAllowedBeforeBreaking = 3, + DurationOfBreak = 5, + TimeoutValue = 5000 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_complex_url() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Some Product")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_complex_url_that_starts_with_placeholder() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/{variantId}/products/{productId}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamPathTemplate = "/{variantId}/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/23/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("23/products/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Some Product")) + .BDDfy(); + } + + [Fact] + public void should_not_add_trailing_slash_to_downstream_url() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/1")) + .Then(x => ThenTheDownstreamUrlPathShouldBe("/api/products/1")) + .BDDfy(); + } + + [Fact] + public void should_return_response_201_with_simple_url() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Post" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201, string.Empty)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) + .BDDfy(); + } + + [Fact] + public void should_return_response_201_with_complex_query_string() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/newThing", + UpstreamPathTemplate = "/newThing", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/newThing", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/newThing?DeviceType=IphoneApp&Browser=moonpigIphone&BrowserString=-&CountryCode=123&DeviceName=iPhone 5 (GSM+CDMA)&OperatingSystem=iPhone OS 7.1.2&BrowserVersion=3708AdHoc&ipAddress=-")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_placeholder_for_final_url_path() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/{urlPath}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamPathTemplate = "/myApp1Name/api/{urlPath}", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/myApp1Name/api/products/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Some Product")) + .BDDfy(); + } + + [Fact] + public void should_return_response_201_with_simple_url_and_multiple_upstream_http_method() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get", "Post" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "", 201, string.Empty)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_simple_url_and_any_upstream_http_method() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List(), + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_404_when_calling_upstream_route_with_no_matching_downstream_re_route_github_issue_134() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/v1/vacancy", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamPathTemplate = "/vacancy/", + UpstreamHttpMethod = new List { "Options", "Put", "Get", "Post", "Delete" }, + ServiceName = "botCore", + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" } + }, + new FileReRoute + { + DownstreamPathTemplate = "/api/v1/vacancy/{vacancyId}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + UpstreamPathTemplate = "/vacancy/{vacancyId}", + UpstreamHttpMethod = new List { "Options", "Put", "Get", "Post", "Delete" }, + ServiceName = "botCore", + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/v1/vacancy/1", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("api/vacancy/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void should_fix_145() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/{url}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51899, + } + }, + UpstreamPathTemplate = "/platform/{url}", + UpstreamHttpMethod = new List { "Get" }, + QoSOptions = new FileQoSOptions { + ExceptionsAllowedBeforeBreaking = 3, + DurationOfBreak = 10, + TimeoutValue = 5000 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", "/api/swagger/lib/backbone-min.js", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/platform/swagger/lib/backbone-min.js")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => ThenTheDownstreamUrlPathShouldBe("/api/swagger/lib/backbone-min.js")) + .BDDfy(); + } + + [Fact] + public void should_use_priority() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/goods/{url}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/goods/{url}", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 53879, + } + }, + Priority = 0 + }, + new FileReRoute + { + DownstreamPathTemplate = "/goods/delete", + DownstreamScheme = "http", + UpstreamPathTemplate = "/goods/delete", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 52879, + } + }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:52879/", "/goods/delete", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/goods/delete")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_fix_issue_271() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/v1/{everything}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/api/v1/{everything}", + UpstreamHttpMethod = new List { "Get", "Put", "Post" }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 54879, + } + }, + }, + new FileReRoute + { + DownstreamPathTemplate = "/connect/token", + DownstreamScheme = "http", + UpstreamPathTemplate = "/connect/token", + UpstreamHttpMethod = new List { "Post" }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 5001, + } + }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:54879/", "/api/v1/modules/Test", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/v1/modules/Test")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if(_downstreamPath != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + }) + .Build(); + + _builder.Start(); + } + + internal void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPath) + { + _downstreamPath.ShouldBe(expectedDownstreamPath); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs index 151bb99c..870ab7e8 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs @@ -61,7 +61,7 @@ namespace Ocelot.AcceptanceTests UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, ServiceName = serviceName, - LoadBalancer = "LeastConnection", + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, UseServiceDiscovery = true, } }, @@ -127,7 +127,7 @@ namespace Ocelot.AcceptanceTests UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, ServiceName = serviceName, - LoadBalancer = "LeastConnection", + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, UseServiceDiscovery = true, } }, @@ -183,7 +183,7 @@ namespace Ocelot.AcceptanceTests UpstreamPathTemplate = "/home", UpstreamHttpMethod = new List { "Get", "Options" }, ServiceName = serviceName, - LoadBalancer = "LeastConnection", + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, UseServiceDiscovery = true, } }, @@ -239,7 +239,7 @@ namespace Ocelot.AcceptanceTests UpstreamPathTemplate = "/home", UpstreamHttpMethod = new List { "Get", "Options" }, ServiceName = serviceName, - LoadBalancer = "LeastConnection", + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, UseServiceDiscovery = true, } }, @@ -308,7 +308,7 @@ namespace Ocelot.AcceptanceTests UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, ServiceName = serviceName, - LoadBalancer = "LeastConnection", + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, UseServiceDiscovery = true, } }, diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index aac5be0d..2742aa60 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -30,6 +30,9 @@ using Ocelot.Middleware.Multiplexer; namespace Ocelot.AcceptanceTests { + using Microsoft.Net.Http.Headers; + using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue; + public class Steps : IDisposable { private TestServer _ocelotServer; @@ -341,7 +344,7 @@ namespace Ocelot.AcceptanceTests internal void GivenIAddCookieToMyRequest(string cookie) { - _ocelotClient.DefaultRequestHeaders.Add("Set-Cookie", cookie); + _ocelotClient.DefaultRequestHeaders.Add("Set-Cookie", cookie); } /// @@ -671,6 +674,14 @@ namespace Ocelot.AcceptanceTests _response = _ocelotClient.GetAsync(url).Result; } + public void WhenIGetUrlOnTheApiGateway(string url, string cookie, string value) + { + var request = _ocelotServer.CreateRequest(url); + request.And(x => { x.Headers.Add("Cookie", new CookieHeaderValue(cookie, value).ToString()); }); + var response = request.GetAsync().Result; + _response = response; + } + public void GivenIAddAHeader(string key, string value) { _ocelotClient.DefaultRequestHeaders.Add(key, value); @@ -690,6 +701,30 @@ namespace Ocelot.AcceptanceTests Task.WaitAll(tasks); } + public async Task WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times, string cookie, string value) + { + var tasks = new Task[times]; + + for (int i = 0; i < times; i++) + { + var urlCopy = url; + tasks[i] = GetForServiceDiscoveryTest(urlCopy, cookie, value); + Thread.Sleep(_random.Next(40, 60)); + } + + Task.WaitAll(tasks); + } + + private async Task GetForServiceDiscoveryTest(string url, string cookie, string value) + { + var request = _ocelotServer.CreateRequest(url); + request.And(x => { x.Headers.Add("Cookie", new CookieHeaderValue(cookie, value).ToString()); }); + var response = await request.GetAsync(); + var content = await response.Content.ReadAsStringAsync(); + int count = int.Parse(content); + count.ShouldBeGreaterThan(0); + } + private async Task GetForServiceDiscoveryTest(string url) { var response = await _ocelotClient.GetAsync(url); diff --git a/test/Ocelot.AcceptanceTests/StickySessionsTests.cs b/test/Ocelot.AcceptanceTests/StickySessionsTests.cs new file mode 100644 index 00000000..254eb16a --- /dev/null +++ b/test/Ocelot.AcceptanceTests/StickySessionsTests.cs @@ -0,0 +1,321 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class StickySessionsTests : IDisposable + { + private IWebHost _builderOne; + private IWebHost _builderTwo; + private readonly Steps _steps; + private int _counterOne; + private int _counterTwo; + private static readonly object _syncLock = new object(); + + public StickySessionsTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_use_same_downstream_host() + { + var downstreamPortOne = 51881; + var downstreamPortTwo = 51892; + var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; + var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + LoadBalancerOptions = new FileLoadBalancerOptions + { + Type = "CookieStickySessions", + Key = "sessionid", + Expiry = 300000 + }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = downstreamPortOne + }, + new FileHostAndPort + { + Host = "localhost", + Port = downstreamPortTwo + } + } + } + } + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10, "sessionid", "123")) + .Then(x => x.ThenTheFirstServiceIsCalled(10)) + .Then(x => x.ThenTheSecondServiceIsCalled(0)) + .BDDfy(); + } + + [Fact] + public void should_use_different_downstream_host_for_different_re_route() + { + var downstreamPortOne = 52881; + var downstreamPortTwo = 52892; + var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; + var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + LoadBalancerOptions = new FileLoadBalancerOptions + { + Type = "CookieStickySessions", + Key = "sessionid", + Expiry = 300000 + }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = downstreamPortOne + }, + new FileHostAndPort + { + Host = "localhost", + Port = downstreamPortTwo + } + } + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/test", + UpstreamHttpMethod = new List { "Get" }, + LoadBalancerOptions = new FileLoadBalancerOptions + { + Type = "CookieStickySessions", + Key = "bestid", + Expiry = 300000 + }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = downstreamPortTwo + }, + new FileHostAndPort + { + Host = "localhost", + Port = downstreamPortOne + } + } + } + } + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/", "sessionid", "123")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/test", "bestid", "123")) + .Then(x => x.ThenTheFirstServiceIsCalled(1)) + .Then(x => x.ThenTheSecondServiceIsCalled(1)) + .BDDfy(); + } + + [Fact] + public void should_use_same_downstream_host_for_different_re_route() + { + var downstreamPortOne = 53881; + var downstreamPortTwo = 53892; + var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; + var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + LoadBalancerOptions = new FileLoadBalancerOptions + { + Type = "CookieStickySessions", + Key = "sessionid", + Expiry = 300000 + }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = downstreamPortOne + }, + new FileHostAndPort + { + Host = "localhost", + Port = downstreamPortTwo + } + } + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/test", + UpstreamHttpMethod = new List { "Get" }, + LoadBalancerOptions = new FileLoadBalancerOptions + { + Type = "CookieStickySessions", + Key = "sessionid", + Expiry = 300000 + }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = downstreamPortTwo + }, + new FileHostAndPort + { + Host = "localhost", + Port = downstreamPortOne + } + } + } + } + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/", "sessionid", "123")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/test", "sessionid", "123")) + .Then(x => x.ThenTheFirstServiceIsCalled(2)) + .Then(x => x.ThenTheSecondServiceIsCalled(0)) + .BDDfy(); + } + + private void ThenTheFirstServiceIsCalled(int expected) + { + _counterOne.ShouldBe(expected); + } + + private void ThenTheSecondServiceIsCalled(int expected) + { + _counterTwo.ShouldBe(expected); + } + + private void GivenProductServiceOneIsRunning(string url, int statusCode) + { + _builderOne = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + try + { + var response = string.Empty; + lock (_syncLock) + { + _counterOne++; + response = _counterOne.ToString(); + } + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + }) + .Build(); + + _builderOne.Start(); + } + + private void GivenProductServiceTwoIsRunning(string url, int statusCode) + { + _builderTwo = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + try + { + var response = string.Empty; + lock (_syncLock) + { + _counterTwo++; + response = _counterTwo.ToString(); + } + + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (System.Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + }) + .Build(); + + _builderTwo.Start(); + } + + public void Dispose() + { + _builderOne?.Dispose(); + _builderTwo?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/WebSocketTests.cs b/test/Ocelot.AcceptanceTests/WebSocketTests.cs index dce2f306..667fb5fd 100644 --- a/test/Ocelot.AcceptanceTests/WebSocketTests.cs +++ b/test/Ocelot.AcceptanceTests/WebSocketTests.cs @@ -101,7 +101,7 @@ namespace Ocelot.AcceptanceTests Port = secondDownstreamPort } }, - LoadBalancer = "RoundRobin" + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "RoundRobin" } } } }; @@ -159,7 +159,7 @@ namespace Ocelot.AcceptanceTests UpstreamPathTemplate = "/", DownstreamPathTemplate = "/ws", DownstreamScheme = "ws", - LoadBalancer = "RoundRobin", + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "RoundRobin" }, ServiceName = serviceName, UseServiceDiscovery = true } diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs similarity index 92% rename from test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs rename to test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs index 40fe4f02..a097f921 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs @@ -1,943 +1,1009 @@ -namespace Ocelot.UnitTests.Configuration -{ - using System.Collections.Generic; - using Moq; - using Ocelot.Cache; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Configuration.Creator; - using Ocelot.Configuration.File; - using Ocelot.Configuration.Validator; - using Ocelot.Logging; - using Ocelot.Responses; - using Shouldly; - using TestStack.BDDfy; - using Xunit; - using Ocelot.DependencyInjection; - using Ocelot.Errors; - using Ocelot.UnitTests.TestData; - using Ocelot.Values; - - public class FileConfigurationCreatorTests - { - private readonly Mock _validator; - private Response _config; - private FileConfiguration _fileConfiguration; - private readonly Mock _logger; - private readonly FileInternalConfigurationCreator _internalConfigurationCreator; - private readonly Mock _claimsToThingCreator; - private readonly Mock _authOptionsCreator; - private readonly Mock _upstreamTemplatePatternCreator; - private readonly Mock _requestIdKeyCreator; - private readonly Mock _serviceProviderConfigCreator; - private readonly Mock _qosOptionsCreator; - private readonly Mock _fileReRouteOptionsCreator; - private readonly Mock _rateLimitOptions; - private readonly Mock _regionCreator; - private readonly Mock _httpHandlerOptionsCreator; - private readonly Mock _adminPath; - private readonly Mock _headerFindAndReplaceCreator; - private readonly Mock _downstreamAddressesCreator; - - public FileConfigurationCreatorTests() - { - _logger = new Mock(); - _validator = new Mock(); - _claimsToThingCreator = new Mock(); - _authOptionsCreator = new Mock(); - _upstreamTemplatePatternCreator = new Mock(); - _requestIdKeyCreator = new Mock(); - _serviceProviderConfigCreator = new Mock(); - _qosOptionsCreator = new Mock(); - _fileReRouteOptionsCreator = new Mock(); - _rateLimitOptions = new Mock(); - _regionCreator = new Mock(); - _httpHandlerOptionsCreator = new Mock(); - _adminPath = new Mock(); - _headerFindAndReplaceCreator = new Mock(); - _downstreamAddressesCreator = new Mock(); - - _internalConfigurationCreator = new FileInternalConfigurationCreator( - _validator.Object, - _logger.Object, - _claimsToThingCreator.Object, - _authOptionsCreator.Object, - _upstreamTemplatePatternCreator.Object, - _requestIdKeyCreator.Object, - _serviceProviderConfigCreator.Object, - _qosOptionsCreator.Object, - _fileReRouteOptionsCreator.Object, - _rateLimitOptions.Object, - _regionCreator.Object, - _httpHandlerOptionsCreator.Object, - _adminPath.Object, - _headerFindAndReplaceCreator.Object, - _downstreamAddressesCreator.Object); - } - - [Fact] - public void should_set_up_aggregate_re_route() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura", - UpstreamHost = "localhost" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom", - UpstreamHost = "localhost", - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Tom", - "Laura" - }, - Aggregator = "asdf" - } - } - }; - - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - var expected = new List(); - - var lauraDownstreamReRoute = new DownstreamReRouteBuilder() - .WithUpstreamHost("localhost") - .WithKey("Laura") - .WithDownstreamPathTemplate("/") - .WithDownstreamScheme("http") - .WithUpstreamHttpMethod(new List() {"Get"}) - .WithDownstreamAddresses(new List() {new DownstreamHostAndPort("localhost", 51878)}) - .Build(); - - var lauraReRoute = new ReRouteBuilder() - .WithUpstreamHttpMethod(new List() { "Get" }) - .WithUpstreamHost("localhost") - .WithUpstreamPathTemplate("/laura") - .WithDownstreamReRoute(lauraDownstreamReRoute) - .Build(); - - expected.Add(lauraReRoute); - - var tomDownstreamReRoute = new DownstreamReRouteBuilder() - .WithUpstreamHost("localhost") - .WithKey("Tom") - .WithDownstreamPathTemplate("/") - .WithDownstreamScheme("http") - .WithUpstreamHttpMethod(new List() { "Get" }) - .WithDownstreamAddresses(new List() { new DownstreamHostAndPort("localhost", 51878) }) - .Build(); - - var tomReRoute = new ReRouteBuilder() - .WithUpstreamHttpMethod(new List() { "Get" }) - .WithUpstreamHost("localhost") - .WithUpstreamPathTemplate("/tom") - .WithDownstreamReRoute(tomDownstreamReRoute) - .Build(); - - expected.Add(tomReRoute); - - var aggregateReReRoute = new ReRouteBuilder() - .WithUpstreamPathTemplate("/") - .WithUpstreamHost("localhost") - .WithDownstreamReRoute(lauraDownstreamReRoute) - .WithDownstreamReRoute(tomDownstreamReRoute) - .WithUpstreamHttpMethod(new List() { "Get" }) - .Build(); - - expected.Add(aggregateReReRoute); - - this.Given(x => x.GivenTheConfigIs(configuration)) - .And(x => x.GivenTheFollowingOptionsAreReturned(new ReRouteOptionsBuilder().Build())) - .And(x => x.GivenTheFollowingIsReturned(serviceProviderConfig)) - .And(x => GivenTheDownstreamAddresses()) - .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) - .And(x => x.GivenTheConfigIsValid()) - .When(x => x.WhenICreateTheConfig()) - .Then(x => x.ThenTheServiceProviderCreatorIsCalledCorrectly()) - .Then(x => x.ThenTheReRoutesAre(expected)) - .BDDfy(); - } - - [Fact] - public void should_call_service_provider_config_creator() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenTheConfigIs(new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = 8500, - } - } - })) - .And(x => x.GivenTheFollowingIsReturned(serviceProviderConfig)) - .And(x => GivenTheDownstreamAddresses()) - .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) - .And(x => x.GivenTheConfigIsValid()) - .When(x => x.WhenICreateTheConfig()) - .Then(x => x.ThenTheServiceProviderCreatorIsCalledCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_call_region_creator() - { - var reRouteOptions = new ReRouteOptionsBuilder() - .Build(); - - this.Given(x => x.GivenTheConfigIs(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "127.0.0.1", - } - }, - UpstreamPathTemplate = "/api/products/{productId}", - DownstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, - FileCacheOptions = new FileCacheOptions - { - Region = "region" - } - } - }, - })) - .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) - .And(x => GivenTheDownstreamAddresses()) - .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) - .And(x => x.GivenTheConfigIsValid()) - .And(x => x.GivenTheFollowingRegionIsReturned("region")) - .When(x => x.WhenICreateTheConfig()) - .Then(x => x.ThenTheRegionCreatorIsCalledCorrectly()) - .And(x => x.ThenTheHeaderFindAndReplaceCreatorIsCalledCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_call_rate_limit_options_creator() - { - var reRouteOptions = new ReRouteOptionsBuilder() - .Build(); - - this.Given(x => x.GivenTheConfigIs(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "127.0.0.1", - } - }, - UpstreamPathTemplate = "/api/products/{productId}", - DownstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, - } - }, - })) - .And(x => x.GivenTheConfigIsValid()) - .And(x => GivenTheDownstreamAddresses()) - .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) - .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) - .When(x => x.WhenICreateTheConfig()) - .Then(x => x.ThenTheRateLimitOptionsCreatorIsCalledCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_call_qos_options_creator() - { - var expected = new QoSOptionsBuilder() - .WithDurationOfBreak(1) - .WithExceptionsAllowedBeforeBreaking(1) - .WithTimeoutValue(1) - .Build(); - - var serviceOptions = new ReRouteOptionsBuilder() - .WithIsQos(true) - .Build(); - - this.Given(x => x.GivenTheConfigIs(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "127.0.0.1", - } - }, - UpstreamPathTemplate = "/api/products/{productId}", - DownstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, - QoSOptions = new FileQoSOptions - { - TimeoutValue = 1, - DurationOfBreak = 1, - ExceptionsAllowedBeforeBreaking = 1 - } - } - }, - })) - .And(x => x.GivenTheConfigIsValid()) - .And(x => GivenTheDownstreamAddresses()) - .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) - .And(x => x.GivenTheFollowingOptionsAreReturned(serviceOptions)) - .And(x => x.GivenTheQosOptionsCreatorReturns(expected)) - .When(x => x.WhenICreateTheConfig()) - .Then(x => x.ThenTheQosOptionsAre(expected)) - .BDDfy(); - } - - [Fact] - public void should_use_downstream_host() - { - var reRouteOptions = new ReRouteOptionsBuilder() - .Build(); - - var downstreamReRoute = new DownstreamReRouteBuilder() - .WithDownstreamAddresses(new List() {new DownstreamHostAndPort("127.0.0.1", 80)}) - .WithDownstreamPathTemplate("/products/{productId}") - .WithUpstreamPathTemplate("/api/products/{productId}") - .WithUpstreamHttpMethod(new List {"Get"}) - .Build(); - - this.Given(x => x.GivenTheConfigIs(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "127.0.0.1", - } - }, - UpstreamPathTemplate = "/api/products/{productId}", - DownstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, - } - }, - })) - .And(x => x.GivenTheConfigIsValid()) - .And(x => GivenTheDownstreamAddresses()) - .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) - .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) - .When(x => x.WhenICreateTheConfig()) - .Then(x => x.ThenTheReRoutesAre(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) - .WithUpstreamPathTemplate("/api/products/{productId}") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build() - })) - .BDDfy(); - } - - [Fact] - public void should_use_downstream_scheme() - { - var reRouteOptions = new ReRouteOptionsBuilder() - .Build(); - - var handlers = new List {"Polly", "Tracer"}; - - var downstreamReRoute = new DownstreamReRouteBuilder() - .WithDownstreamScheme("https") - .WithDownstreamPathTemplate("/products/{productId}") - .WithUpstreamPathTemplate("/api/products/{productId}") - .WithUpstreamHttpMethod(new List {"Get"}) - .WithDelegatingHandlers(handlers) - .Build(); - - this.Given(x => x.GivenTheConfigIs(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamScheme = "https", - UpstreamPathTemplate = "/api/products/{productId}", - DownstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, - DelegatingHandlers = handlers - } - }, - })) - .And(x => x.GivenTheConfigIsValid()) - .And(x => GivenTheDownstreamAddresses()) - .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) - .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) - .When(x => x.WhenICreateTheConfig()) - .Then(x => x.ThenTheReRoutesAre(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) - .WithUpstreamPathTemplate("/api/products/{productId}") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build() - })) - .BDDfy(); - } - - [Fact] - public void should_use_service_discovery_for_downstream_service_host() - { - var reRouteOptions = new ReRouteOptionsBuilder() - .Build(); - - var downstreamReRoute = new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("/products/{productId}") - .WithUpstreamPathTemplate("/api/products/{productId}") - .WithUpstreamHttpMethod(new List {"Get"}) - .WithUseServiceDiscovery(true) - .WithServiceName("ProductService") - .Build(); - - this.Given(x => x.GivenTheConfigIs(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - UpstreamPathTemplate = "/api/products/{productId}", - DownstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, - ReRouteIsCaseSensitive = false, - ServiceName = "ProductService" - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "127.0.0.1" - } - } - })) - .And(x => x.GivenTheConfigIsValid()) - .And(x => GivenTheDownstreamAddresses()) - .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) - .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) - .When(x => x.WhenICreateTheConfig()) - .Then(x => x.ThenTheReRoutesAre(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) - .WithUpstreamPathTemplate("/api/products/{productId}") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build() - })) - .BDDfy(); - } - - [Fact] - public void should_not_use_service_discovery_for_downstream_host_url_when_no_service_name() - { - var reRouteOptions = new ReRouteOptionsBuilder() - .Build(); - - var downstreamReRoute = new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("/products/{productId}") - .WithUpstreamPathTemplate("/api/products/{productId}") - .WithUpstreamHttpMethod(new List {"Get"}) - .WithUseServiceDiscovery(false) - .Build(); - - this.Given(x => x.GivenTheConfigIs(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - UpstreamPathTemplate = "/api/products/{productId}", - DownstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, - ReRouteIsCaseSensitive = false, - } - } - })) - .And(x => x.GivenTheConfigIsValid()) - .And(x => GivenTheDownstreamAddresses()) - .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) - .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) - .When(x => x.WhenICreateTheConfig()) - .Then(x => x.ThenTheReRoutesAre(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) - .WithUpstreamPathTemplate("/api/products/{productId}") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build() - })) - .BDDfy(); - } - - [Fact] - public void should_call_template_pattern_creator_correctly() - { - var reRouteOptions = new ReRouteOptionsBuilder() - .Build(); - - var downstreamReRoute = new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("/products/{productId}") - .WithUpstreamPathTemplate("/api/products/{productId}") - .WithUpstreamHttpMethod(new List {"Get"}) - .WithUpstreamTemplatePattern(new UpstreamPathTemplate("(?i)/api/products/.*/$", 1)) - .Build(); - - this.Given(x => x.GivenTheConfigIs(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - UpstreamPathTemplate = "/api/products/{productId}", - DownstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, - ReRouteIsCaseSensitive = false - } - } - })) - .And(x => x.GivenTheConfigIsValid()) - .And(x => GivenTheDownstreamAddresses()) - .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) - .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) - .And(x => x.GivenTheUpstreamTemplatePatternCreatorReturns("(?i)/api/products/.*/$")) - .When(x => x.WhenICreateTheConfig()) - .Then(x => x.ThenTheReRoutesAre(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) - .WithUpstreamPathTemplate("/api/products/{productId}") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamTemplatePattern(new UpstreamPathTemplate("(?i)/api/products/.*/$", 1)) - .Build() - })) - .BDDfy(); - } - - [Fact] - public void should_call_request_id_creator() - { - var reRouteOptions = new ReRouteOptionsBuilder() - .Build(); - - var downstreamReRoute = new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("/products/{productId}") - .WithUpstreamPathTemplate("/api/products/{productId}") - .WithUpstreamHttpMethod(new List {"Get"}) - .WithRequestIdKey("blahhhh") - .Build(); - - this.Given(x => x.GivenTheConfigIs(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - UpstreamPathTemplate = "/api/products/{productId}", - DownstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, - ReRouteIsCaseSensitive = true - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - RequestIdKey = "blahhhh" - } - })) - .And(x => x.GivenTheConfigIsValid()) - .And(x => GivenTheDownstreamAddresses()) - .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) - .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) - .And(x => x.GivenTheRequestIdCreatorReturns("blahhhh")) - .When(x => x.WhenICreateTheConfig()) - .Then(x => x.ThenTheReRoutesAre(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) - .WithUpstreamPathTemplate("/api/products/{productId}") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build() - })) - .And(x => x.ThenTheRequestIdKeyCreatorIsCalledCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_call_httpHandler_creator() - { - var reRouteOptions = new ReRouteOptionsBuilder() - .Build(); - var httpHandlerOptions = new HttpHandlerOptions(true, true,false); - - this.Given(x => x.GivenTheConfigIs(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "127.0.0.1", - } - }, - UpstreamPathTemplate = "/api/products/{productId}", - DownstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" } - } - }, - })) - .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) - .And(x => GivenTheDownstreamAddresses()) - .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) - .And(x => x.GivenTheConfigIsValid()) - .And(x => x.GivenTheFollowingHttpHandlerOptionsAreReturned(httpHandlerOptions)) - .When(x => x.WhenICreateTheConfig()) - .Then(x => x.ThenTheHttpHandlerOptionsCreatorIsCalledCorrectly()) - .BDDfy(); - } - - [Theory] - [MemberData(nameof(AuthenticationConfigTestData.GetAuthenticationData), MemberType = typeof(AuthenticationConfigTestData))] - public void should_create_with_headers_to_extract(FileConfiguration fileConfig) - { - var reRouteOptions = new ReRouteOptionsBuilder() - .WithIsAuthenticated(true) - .Build(); - - var authenticationOptions = new AuthenticationOptionsBuilder() - .WithAllowedScopes(new List()) - .Build(); - - var downstreamReRoute = new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("/products/{productId}") - .WithUpstreamPathTemplate("/api/products/{productId}") - .WithUpstreamHttpMethod(new List {"Get"}) - .WithAuthenticationOptions(authenticationOptions) - .WithClaimsToHeaders(new List - { - new ClaimToThing("CustomerId", "CustomerId", "", 0), - }) - .Build(); - - var expected = new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) - .WithUpstreamPathTemplate("/api/products/{productId}") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build() - }; - - this.Given(x => x.GivenTheConfigIs(fileConfig)) - .And(x => GivenTheDownstreamAddresses()) - .And(x => x.GivenTheConfigIsValid()) - .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) - .And(x => x.GivenTheAuthOptionsCreatorReturns(authenticationOptions)) - .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) - .And(x => x.GivenTheClaimsToThingCreatorReturns(new List { new ClaimToThing("CustomerId", "CustomerId", "", 0) })) - .When(x => x.WhenICreateTheConfig()) - .Then(x => x.ThenTheReRoutesAre(expected)) - .And(x => x.ThenTheAuthenticationOptionsAre(expected)) - .And(x => x.ThenTheAuthOptionsCreatorIsCalledCorrectly()) - .BDDfy(); - } - - [Theory] - [MemberData(nameof(AuthenticationConfigTestData.GetAuthenticationData), MemberType = typeof(AuthenticationConfigTestData))] - public void should_create_with_authentication_properties(FileConfiguration fileConfig) - { - var reRouteOptions = new ReRouteOptionsBuilder() - .WithIsAuthenticated(true) - .Build(); - - var authenticationOptions = new AuthenticationOptionsBuilder() - .WithAllowedScopes(new List()) - .Build(); - - var downstreamReRoute = new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("/products/{productId}") - .WithUpstreamPathTemplate("/api/products/{productId}") - .WithUpstreamHttpMethod(new List {"Get"}) - .WithAuthenticationOptions(authenticationOptions) - .Build(); - - var expected = new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) - .WithUpstreamPathTemplate("/api/products/{productId}") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build() - }; - - this.Given(x => x.GivenTheConfigIs(fileConfig)) - .And(x => GivenTheDownstreamAddresses()) - .And(x => x.GivenTheConfigIsValid()) - .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) - .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) - .And(x => x.GivenTheAuthOptionsCreatorReturns(authenticationOptions)) - .When(x => x.WhenICreateTheConfig()) - .Then(x => x.ThenTheReRoutesAre(expected)) - .And(x => x.ThenTheAuthenticationOptionsAre(expected)) - .And(x => x.ThenTheAuthOptionsCreatorIsCalledCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_return_validation_errors() - { - var errors = new List {new FileValidationFailedError("some message")}; - - this.Given(x => x.GivenTheConfigIs(new FileConfiguration())) - .And(x => GivenTheDownstreamAddresses()) - .And(x => x.GivenTheConfigIsInvalid(errors)) - .When(x => x.WhenICreateTheConfig()) - .Then(x => x.ThenTheErrorsAreReturned(errors)) - .BDDfy(); - } - - private void GivenTheConfigIsInvalid(List errors) - { - _validator - .Setup(x => x.IsValid(It.IsAny())) - .ReturnsAsync(new OkResponse(new ConfigurationValidationResult(true, errors))); - } - - private void ThenTheErrorsAreReturned(List errors) - { - _config.IsError.ShouldBeTrue(); - _config.Errors[0].ShouldBe(errors[0]); - } - - private void GivenTheFollowingOptionsAreReturned(ReRouteOptions fileReRouteOptions) - { - _fileReRouteOptionsCreator - .Setup(x => x.Create(It.IsAny())) - .Returns(fileReRouteOptions); - } - - private void ThenTheRateLimitOptionsCreatorIsCalledCorrectly() - { - _rateLimitOptions - .Verify(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - } - - private void GivenTheConfigIsValid() - { - _validator - .Setup(x => x.IsValid(It.IsAny())) - .ReturnsAsync(new OkResponse(new ConfigurationValidationResult(false))); - } - - private void GivenTheConfigIs(FileConfiguration fileConfiguration) - { - _fileConfiguration = fileConfiguration; - } - - private void WhenICreateTheConfig() - { - _config = _internalConfigurationCreator.Create(_fileConfiguration).Result; - } - - private void ThenTheReRoutesAre(List expectedReRoutes) - { - for (int i = 0; i < _config.Data.ReRoutes.Count; i++) - { - var result = _config.Data.ReRoutes[i]; - var expected = expectedReRoutes[i]; - - result.DownstreamReRoute.Count.ShouldBe(expected.DownstreamReRoute.Count); - - result.DownstreamReRoute[0].DownstreamPathTemplate.Value.ShouldBe(expected.DownstreamReRoute[0].DownstreamPathTemplate.Value); - result.UpstreamHttpMethod.ShouldBe(expected.UpstreamHttpMethod); - result.UpstreamPathTemplate.Value.ShouldBe(expected.UpstreamPathTemplate.Value); - result.UpstreamTemplatePattern?.Template.ShouldBe(expected.UpstreamTemplatePattern?.Template); - result.DownstreamReRoute[0].ClaimsToClaims.Count.ShouldBe(expected.DownstreamReRoute[0].ClaimsToClaims.Count); - result.DownstreamReRoute[0].ClaimsToHeaders.Count.ShouldBe(expected.DownstreamReRoute[0].ClaimsToHeaders.Count); - result.DownstreamReRoute[0].ClaimsToQueries.Count.ShouldBe(expected.DownstreamReRoute[0].ClaimsToQueries.Count); - result.DownstreamReRoute[0].RequestIdKey.ShouldBe(expected.DownstreamReRoute[0].RequestIdKey); - result.DownstreamReRoute[0].DelegatingHandlers.ShouldBe(expected.DownstreamReRoute[0].DelegatingHandlers); - result.DownstreamReRoute[0].AddHeadersToDownstream.ShouldBe(expected.DownstreamReRoute[0].AddHeadersToDownstream); - result.DownstreamReRoute[0].AddHeadersToUpstream.ShouldBe(expected.DownstreamReRoute[0].AddHeadersToUpstream, "AddHeadersToUpstream should be set"); - } - } - - private void ThenTheAuthenticationOptionsAre(List expectedReRoutes) - { - for (int i = 0; i < _config.Data.ReRoutes.Count; i++) - { - var result = _config.Data.ReRoutes[i].DownstreamReRoute[0].AuthenticationOptions; - var expected = expectedReRoutes[i].DownstreamReRoute[0].AuthenticationOptions; - result.AllowedScopes.ShouldBe(expected.AllowedScopes); - } - } - - private void GivenTheClaimsToThingCreatorReturns(List claimsToThing) - { - _claimsToThingCreator - .Setup(x => x.Create(_fileConfiguration.ReRoutes[0].AddHeadersToRequest)) - .Returns(claimsToThing); - } - - private void GivenTheAuthOptionsCreatorReturns(AuthenticationOptions authOptions) - { - _authOptionsCreator - .Setup(x => x.Create(It.IsAny())) - .Returns(authOptions); - } - - private void ThenTheAuthOptionsCreatorIsCalledCorrectly() - { - _authOptionsCreator - .Verify(x => x.Create(_fileConfiguration.ReRoutes[0]), Times.Once); - } - - private void GivenTheUpstreamTemplatePatternCreatorReturns(string pattern) - { - _upstreamTemplatePatternCreator - .Setup(x => x.Create(It.IsAny())) - .Returns(new UpstreamPathTemplate(pattern, 1)); - } - - private void ThenTheRequestIdKeyCreatorIsCalledCorrectly() - { - _requestIdKeyCreator - .Verify(x => x.Create(_fileConfiguration.ReRoutes[0], _fileConfiguration.GlobalConfiguration), Times.Once); - } - - private void GivenTheRequestIdCreatorReturns(string requestId) - { - _requestIdKeyCreator - .Setup(x => x.Create(It.IsAny(), It.IsAny())) - .Returns(requestId); - } - - private void GivenTheQosOptionsCreatorReturns(QoSOptions qosOptions) - { - _qosOptionsCreator - .Setup(x => x.Create(_fileConfiguration.ReRoutes[0])) - .Returns(qosOptions); - } - - private void ThenTheQosOptionsAre(QoSOptions qosOptions) - { - _config.Data.ReRoutes[0].DownstreamReRoute[0].QosOptionsOptions.DurationOfBreak.ShouldBe(qosOptions.DurationOfBreak); - - _config.Data.ReRoutes[0].DownstreamReRoute[0].QosOptionsOptions.ExceptionsAllowedBeforeBreaking.ShouldBe(qosOptions.ExceptionsAllowedBeforeBreaking); - _config.Data.ReRoutes[0].DownstreamReRoute[0].QosOptionsOptions.TimeoutValue.ShouldBe(qosOptions.TimeoutValue); - } - - private void ThenTheServiceProviderCreatorIsCalledCorrectly() - { - _serviceProviderConfigCreator - .Verify(x => x.Create(_fileConfiguration.GlobalConfiguration), Times.Once); - } - - private void ThenTheHeaderFindAndReplaceCreatorIsCalledCorrectly() - { - _headerFindAndReplaceCreator - .Verify(x => x.Create(It.IsAny()), Times.Once); - } - - private void GivenTheHeaderFindAndReplaceCreatorReturns() - { - _headerFindAndReplaceCreator.Setup(x => x.Create(It.IsAny())).Returns(new HeaderTransformations(new List(), new List(), new List(), new List())); - } - - private void GivenTheFollowingIsReturned(ServiceProviderConfiguration serviceProviderConfiguration) - { - _serviceProviderConfigCreator - .Setup(x => x.Create(It.IsAny())).Returns(serviceProviderConfiguration); - } - - private void GivenTheFollowingRegionIsReturned(string region) - { - _regionCreator - .Setup(x => x.Create(It.IsAny())) - .Returns(region); - } - - private void ThenTheRegionCreatorIsCalledCorrectly() - { - _regionCreator - .Verify(x => x.Create(_fileConfiguration.ReRoutes[0]), Times.Once); - } - - private void GivenTheFollowingHttpHandlerOptionsAreReturned(HttpHandlerOptions httpHandlerOptions) - { - _httpHandlerOptionsCreator.Setup(x => x.Create(It.IsAny())) - .Returns(httpHandlerOptions); - } - - private void ThenTheHttpHandlerOptionsCreatorIsCalledCorrectly() - { - _httpHandlerOptionsCreator.Verify(x => x.Create(_fileConfiguration.ReRoutes[0]), Times.Once()); - } - - private void GivenTheDownstreamAddresses() - { - _downstreamAddressesCreator.Setup(x => x.Create(It.IsAny())).Returns(new List()); - } - } -} +namespace Ocelot.UnitTests.Configuration +{ + using System.Collections.Generic; + using Moq; + using Ocelot.Cache; + using Ocelot.Configuration; + using Ocelot.Configuration.Builder; + using Ocelot.Configuration.Creator; + using Ocelot.Configuration.File; + using Ocelot.Configuration.Validator; + using Ocelot.Logging; + using Ocelot.Responses; + using Shouldly; + using TestStack.BDDfy; + using Xunit; + using Ocelot.DependencyInjection; + using Ocelot.Errors; + using Ocelot.UnitTests.TestData; + using Ocelot.Values; + + public class FileInternalConfigurationCreatorTests + { + private readonly Mock _validator; + private Response _config; + private FileConfiguration _fileConfiguration; + private readonly Mock _logger; + private readonly FileInternalConfigurationCreator _internalConfigurationCreator; + private readonly Mock _claimsToThingCreator; + private readonly Mock _authOptionsCreator; + private readonly Mock _upstreamTemplatePatternCreator; + private readonly Mock _requestIdKeyCreator; + private readonly Mock _serviceProviderConfigCreator; + private readonly Mock _qosOptionsCreator; + private readonly Mock _fileReRouteOptionsCreator; + private readonly Mock _rateLimitOptions; + private readonly Mock _regionCreator; + private readonly Mock _httpHandlerOptionsCreator; + private readonly Mock _adminPath; + private readonly Mock _headerFindAndReplaceCreator; + private readonly Mock _downstreamAddressesCreator; + + public FileInternalConfigurationCreatorTests() + { + _logger = new Mock(); + _validator = new Mock(); + _claimsToThingCreator = new Mock(); + _authOptionsCreator = new Mock(); + _upstreamTemplatePatternCreator = new Mock(); + _requestIdKeyCreator = new Mock(); + _serviceProviderConfigCreator = new Mock(); + _qosOptionsCreator = new Mock(); + _fileReRouteOptionsCreator = new Mock(); + _rateLimitOptions = new Mock(); + _regionCreator = new Mock(); + _httpHandlerOptionsCreator = new Mock(); + _adminPath = new Mock(); + _headerFindAndReplaceCreator = new Mock(); + _downstreamAddressesCreator = new Mock(); + + _internalConfigurationCreator = new FileInternalConfigurationCreator( + _validator.Object, + _logger.Object, + _claimsToThingCreator.Object, + _authOptionsCreator.Object, + _upstreamTemplatePatternCreator.Object, + _requestIdKeyCreator.Object, + _serviceProviderConfigCreator.Object, + _qosOptionsCreator.Object, + _fileReRouteOptionsCreator.Object, + _rateLimitOptions.Object, + _regionCreator.Object, + _httpHandlerOptionsCreator.Object, + _adminPath.Object, + _headerFindAndReplaceCreator.Object, + _downstreamAddressesCreator.Object); + } + + [Fact] + public void should_set_up_sticky_sessions_config() + { + var reRouteOptions = new ReRouteOptionsBuilder() + .Build(); + + var downstreamReRoute = new DownstreamReRouteBuilder() + .WithDownstreamAddresses(new List() { new DownstreamHostAndPort("127.0.0.1", 80) }) + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithReRouteKey("CookieStickySessions:sessionid") + .Build(); + + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "127.0.0.1", + } + }, + LoadBalancerOptions = new FileLoadBalancerOptions + { + Expiry = 10, + Key = "sessionid", + Type = "CookieStickySessions" + }, + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + } + }, + })) + .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheDownstreamAddresses()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) + .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamReRoute(downstreamReRoute) + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_set_up_aggregate_re_route() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51878, + } + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + UpstreamHost = "localhost" + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51880, + } + }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new List { "Get" }, + Key = "Tom", + UpstreamHost = "localhost", + } + }, + Aggregates = new List + { + new FileAggregateReRoute + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + ReRouteKeys = new List + { + "Tom", + "Laura" + }, + Aggregator = "asdf" + } + } + }; + + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + var expected = new List(); + + var lauraDownstreamReRoute = new DownstreamReRouteBuilder() + .WithUpstreamHost("localhost") + .WithKey("Laura") + .WithDownstreamPathTemplate("/") + .WithDownstreamScheme("http") + .WithUpstreamHttpMethod(new List() {"Get"}) + .WithDownstreamAddresses(new List() {new DownstreamHostAndPort("localhost", 51878)}) + .WithReRouteKey("/laura|Get") + .Build(); + + var lauraReRoute = new ReRouteBuilder() + .WithUpstreamHttpMethod(new List() { "Get" }) + .WithUpstreamHost("localhost") + .WithUpstreamPathTemplate("/laura") + .WithDownstreamReRoute(lauraDownstreamReRoute) + .Build(); + + expected.Add(lauraReRoute); + + var tomDownstreamReRoute = new DownstreamReRouteBuilder() + .WithUpstreamHost("localhost") + .WithKey("Tom") + .WithDownstreamPathTemplate("/") + .WithDownstreamScheme("http") + .WithUpstreamHttpMethod(new List() { "Get" }) + .WithDownstreamAddresses(new List() { new DownstreamHostAndPort("localhost", 51878) }) + .WithReRouteKey("/tom|Get") + .Build(); + + var tomReRoute = new ReRouteBuilder() + .WithUpstreamHttpMethod(new List() { "Get" }) + .WithUpstreamHost("localhost") + .WithUpstreamPathTemplate("/tom") + .WithDownstreamReRoute(tomDownstreamReRoute) + .Build(); + + expected.Add(tomReRoute); + + var aggregateReReRoute = new ReRouteBuilder() + .WithUpstreamPathTemplate("/") + .WithUpstreamHost("localhost") + .WithDownstreamReRoute(lauraDownstreamReRoute) + .WithDownstreamReRoute(tomDownstreamReRoute) + .WithUpstreamHttpMethod(new List() { "Get" }) + .Build(); + + expected.Add(aggregateReReRoute); + + this.Given(x => x.GivenTheConfigIs(configuration)) + .And(x => x.GivenTheFollowingOptionsAreReturned(new ReRouteOptionsBuilder().Build())) + .And(x => x.GivenTheFollowingIsReturned(serviceProviderConfig)) + .And(x => GivenTheDownstreamAddresses()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheServiceProviderCreatorIsCalledCorrectly()) + .Then(x => x.ThenTheReRoutesAre(expected)) + .BDDfy(); + } + + [Fact] + public void should_call_service_provider_config_creator() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = 8500, + } + } + })) + .And(x => x.GivenTheFollowingIsReturned(serviceProviderConfig)) + .And(x => GivenTheDownstreamAddresses()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheServiceProviderCreatorIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_call_region_creator() + { + var reRouteOptions = new ReRouteOptionsBuilder() + .Build(); + + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "127.0.0.1", + } + }, + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + FileCacheOptions = new FileCacheOptions + { + Region = "region" + } + } + }, + })) + .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .And(x => GivenTheDownstreamAddresses()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) + .And(x => x.GivenTheConfigIsValid()) + .And(x => x.GivenTheFollowingRegionIsReturned("region")) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheRegionCreatorIsCalledCorrectly()) + .And(x => x.ThenTheHeaderFindAndReplaceCreatorIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_call_rate_limit_options_creator() + { + var reRouteOptions = new ReRouteOptionsBuilder() + .Build(); + + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "127.0.0.1", + } + }, + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + } + }, + })) + .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheDownstreamAddresses()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) + .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheRateLimitOptionsCreatorIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_call_qos_options_creator() + { + var expected = new QoSOptionsBuilder() + .WithDurationOfBreak(1) + .WithExceptionsAllowedBeforeBreaking(1) + .WithTimeoutValue(1) + .Build(); + + var serviceOptions = new ReRouteOptionsBuilder() + .WithIsQos(true) + .Build(); + + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "127.0.0.1", + } + }, + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + QoSOptions = new FileQoSOptions + { + TimeoutValue = 1, + DurationOfBreak = 1, + ExceptionsAllowedBeforeBreaking = 1 + } + } + }, + })) + .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheDownstreamAddresses()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) + .And(x => x.GivenTheFollowingOptionsAreReturned(serviceOptions)) + .And(x => x.GivenTheQosOptionsCreatorReturns(expected)) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheQosOptionsAre(expected)) + .BDDfy(); + } + + [Fact] + public void should_use_downstream_host() + { + var reRouteOptions = new ReRouteOptionsBuilder() + .Build(); + + var downstreamReRoute = new DownstreamReRouteBuilder() + .WithDownstreamAddresses(new List() {new DownstreamHostAndPort("127.0.0.1", 80)}) + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List {"Get"}) + .WithReRouteKey("/api/products/{productId}|Get") + .Build(); + + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "127.0.0.1", + } + }, + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + } + }, + })) + .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheDownstreamAddresses()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) + .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamReRoute(downstreamReRoute) + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_use_downstream_scheme() + { + var reRouteOptions = new ReRouteOptionsBuilder() + .Build(); + + var handlers = new List {"Polly", "Tracer"}; + + var downstreamReRoute = new DownstreamReRouteBuilder() + .WithDownstreamScheme("https") + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List {"Get"}) + .WithDelegatingHandlers(handlers) + .WithReRouteKey("/api/products/{productId}|Get") + .Build(); + + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamScheme = "https", + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + DelegatingHandlers = handlers + } + }, + })) + .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheDownstreamAddresses()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) + .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamReRoute(downstreamReRoute) + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_use_service_discovery_for_downstream_service_host() + { + var reRouteOptions = new ReRouteOptionsBuilder() + .Build(); + + var downstreamReRoute = new DownstreamReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List {"Get"}) + .WithUseServiceDiscovery(true) + .WithServiceName("ProductService") + .WithReRouteKey("/api/products/{productId}|Get") + .Build(); + + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + ReRouteIsCaseSensitive = false, + ServiceName = "ProductService" + } + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "127.0.0.1" + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheDownstreamAddresses()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) + .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamReRoute(downstreamReRoute) + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_not_use_service_discovery_for_downstream_host_url_when_no_service_name() + { + var reRouteOptions = new ReRouteOptionsBuilder() + .Build(); + + var downstreamReRoute = new DownstreamReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List {"Get"}) + .WithUseServiceDiscovery(false) + .WithReRouteKey("/api/products/{productId}|Get") + .Build(); + + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + ReRouteIsCaseSensitive = false, + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheDownstreamAddresses()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) + .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamReRoute(downstreamReRoute) + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_call_template_pattern_creator_correctly() + { + var reRouteOptions = new ReRouteOptionsBuilder() + .Build(); + + var downstreamReRoute = new DownstreamReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List {"Get"}) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("(?i)/api/products/.*/$", 1)) + .WithReRouteKey("/api/products/{productId}|Get") + .Build(); + + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + ReRouteIsCaseSensitive = false + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheDownstreamAddresses()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) + .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .And(x => x.GivenTheUpstreamTemplatePatternCreatorReturns("(?i)/api/products/.*/$")) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamReRoute(downstreamReRoute) + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamTemplatePattern(new UpstreamPathTemplate("(?i)/api/products/.*/$", 1)) + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_call_request_id_creator() + { + var reRouteOptions = new ReRouteOptionsBuilder() + .Build(); + + var downstreamReRoute = new DownstreamReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List {"Get"}) + .WithRequestIdKey("blahhhh") + .WithReRouteKey("/api/products/{productId}|Get") + .Build(); + + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + ReRouteIsCaseSensitive = true + } + }, + GlobalConfiguration = new FileGlobalConfiguration + { + RequestIdKey = "blahhhh" + } + })) + .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheDownstreamAddresses()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) + .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .And(x => x.GivenTheRequestIdCreatorReturns("blahhhh")) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamReRoute(downstreamReRoute) + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build() + })) + .And(x => x.ThenTheRequestIdKeyCreatorIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_call_httpHandler_creator() + { + var reRouteOptions = new ReRouteOptionsBuilder() + .Build(); + var httpHandlerOptions = new HttpHandlerOptions(true, true,false); + + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "127.0.0.1", + } + }, + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" } + } + }, + })) + .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .And(x => GivenTheDownstreamAddresses()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) + .And(x => x.GivenTheConfigIsValid()) + .And(x => x.GivenTheFollowingHttpHandlerOptionsAreReturned(httpHandlerOptions)) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheHttpHandlerOptionsCreatorIsCalledCorrectly()) + .BDDfy(); + } + + [Theory] + [MemberData(nameof(AuthenticationConfigTestData.GetAuthenticationData), MemberType = typeof(AuthenticationConfigTestData))] + public void should_create_with_headers_to_extract(FileConfiguration fileConfig) + { + var reRouteOptions = new ReRouteOptionsBuilder() + .WithIsAuthenticated(true) + .Build(); + + var authenticationOptions = new AuthenticationOptionsBuilder() + .WithAllowedScopes(new List()) + .Build(); + + var downstreamReRoute = new DownstreamReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List {"Get"}) + .WithAuthenticationOptions(authenticationOptions) + .WithClaimsToHeaders(new List + { + new ClaimToThing("CustomerId", "CustomerId", "", 0), + }) + .WithReRouteKey("/api/products/{productId}|Get") + .Build(); + + var expected = new List + { + new ReRouteBuilder() + .WithDownstreamReRoute(downstreamReRoute) + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build() + }; + + this.Given(x => x.GivenTheConfigIs(fileConfig)) + .And(x => GivenTheDownstreamAddresses()) + .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) + .And(x => x.GivenTheAuthOptionsCreatorReturns(authenticationOptions)) + .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .And(x => x.GivenTheClaimsToThingCreatorReturns(new List { new ClaimToThing("CustomerId", "CustomerId", "", 0) })) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(expected)) + .And(x => x.ThenTheAuthenticationOptionsAre(expected)) + .And(x => x.ThenTheAuthOptionsCreatorIsCalledCorrectly()) + .BDDfy(); + } + + [Theory] + [MemberData(nameof(AuthenticationConfigTestData.GetAuthenticationData), MemberType = typeof(AuthenticationConfigTestData))] + public void should_create_with_authentication_properties(FileConfiguration fileConfig) + { + var reRouteOptions = new ReRouteOptionsBuilder() + .WithIsAuthenticated(true) + .Build(); + + var authenticationOptions = new AuthenticationOptionsBuilder() + .WithAllowedScopes(new List()) + .Build(); + + var downstreamReRoute = new DownstreamReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List {"Get"}) + .WithAuthenticationOptions(authenticationOptions) + .WithReRouteKey("/api/products/{productId}|Get") + .Build(); + + var expected = new List + { + new ReRouteBuilder() + .WithDownstreamReRoute(downstreamReRoute) + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build() + }; + + this.Given(x => x.GivenTheConfigIs(fileConfig)) + .And(x => GivenTheDownstreamAddresses()) + .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) + .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .And(x => x.GivenTheAuthOptionsCreatorReturns(authenticationOptions)) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(expected)) + .And(x => x.ThenTheAuthenticationOptionsAre(expected)) + .And(x => x.ThenTheAuthOptionsCreatorIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_return_validation_errors() + { + var errors = new List {new FileValidationFailedError("some message")}; + + this.Given(x => x.GivenTheConfigIs(new FileConfiguration())) + .And(x => GivenTheDownstreamAddresses()) + .And(x => x.GivenTheConfigIsInvalid(errors)) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheErrorsAreReturned(errors)) + .BDDfy(); + } + + private void GivenTheConfigIsInvalid(List errors) + { + _validator + .Setup(x => x.IsValid(It.IsAny())) + .ReturnsAsync(new OkResponse(new ConfigurationValidationResult(true, errors))); + } + + private void ThenTheErrorsAreReturned(List errors) + { + _config.IsError.ShouldBeTrue(); + _config.Errors[0].ShouldBe(errors[0]); + } + + private void GivenTheFollowingOptionsAreReturned(ReRouteOptions fileReRouteOptions) + { + _fileReRouteOptionsCreator + .Setup(x => x.Create(It.IsAny())) + .Returns(fileReRouteOptions); + } + + private void ThenTheRateLimitOptionsCreatorIsCalledCorrectly() + { + _rateLimitOptions + .Verify(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + + private void GivenTheConfigIsValid() + { + _validator + .Setup(x => x.IsValid(It.IsAny())) + .ReturnsAsync(new OkResponse(new ConfigurationValidationResult(false))); + } + + private void GivenTheConfigIs(FileConfiguration fileConfiguration) + { + _fileConfiguration = fileConfiguration; + } + + private void WhenICreateTheConfig() + { + _config = _internalConfigurationCreator.Create(_fileConfiguration).Result; + } + + private void ThenTheReRoutesAre(List expectedReRoutes) + { + for (int i = 0; i < _config.Data.ReRoutes.Count; i++) + { + var result = _config.Data.ReRoutes[i]; + var expected = expectedReRoutes[i]; + + result.DownstreamReRoute.Count.ShouldBe(expected.DownstreamReRoute.Count); + + result.DownstreamReRoute[0].DownstreamPathTemplate.Value.ShouldBe(expected.DownstreamReRoute[0].DownstreamPathTemplate.Value); + result.UpstreamHttpMethod.ShouldBe(expected.UpstreamHttpMethod); + result.UpstreamPathTemplate.Value.ShouldBe(expected.UpstreamPathTemplate.Value); + result.UpstreamTemplatePattern?.Template.ShouldBe(expected.UpstreamTemplatePattern?.Template); + result.DownstreamReRoute[0].ClaimsToClaims.Count.ShouldBe(expected.DownstreamReRoute[0].ClaimsToClaims.Count); + result.DownstreamReRoute[0].ClaimsToHeaders.Count.ShouldBe(expected.DownstreamReRoute[0].ClaimsToHeaders.Count); + result.DownstreamReRoute[0].ClaimsToQueries.Count.ShouldBe(expected.DownstreamReRoute[0].ClaimsToQueries.Count); + result.DownstreamReRoute[0].RequestIdKey.ShouldBe(expected.DownstreamReRoute[0].RequestIdKey); + result.DownstreamReRoute[0].LoadBalancerKey.ShouldBe(expected.DownstreamReRoute[0].LoadBalancerKey); + result.DownstreamReRoute[0].DelegatingHandlers.ShouldBe(expected.DownstreamReRoute[0].DelegatingHandlers); + result.DownstreamReRoute[0].AddHeadersToDownstream.ShouldBe(expected.DownstreamReRoute[0].AddHeadersToDownstream); + result.DownstreamReRoute[0].AddHeadersToUpstream.ShouldBe(expected.DownstreamReRoute[0].AddHeadersToUpstream, "AddHeadersToUpstream should be set"); + } + } + + private void ThenTheAuthenticationOptionsAre(List expectedReRoutes) + { + for (int i = 0; i < _config.Data.ReRoutes.Count; i++) + { + var result = _config.Data.ReRoutes[i].DownstreamReRoute[0].AuthenticationOptions; + var expected = expectedReRoutes[i].DownstreamReRoute[0].AuthenticationOptions; + result.AllowedScopes.ShouldBe(expected.AllowedScopes); + } + } + + private void GivenTheClaimsToThingCreatorReturns(List claimsToThing) + { + _claimsToThingCreator + .Setup(x => x.Create(_fileConfiguration.ReRoutes[0].AddHeadersToRequest)) + .Returns(claimsToThing); + } + + private void GivenTheAuthOptionsCreatorReturns(AuthenticationOptions authOptions) + { + _authOptionsCreator + .Setup(x => x.Create(It.IsAny())) + .Returns(authOptions); + } + + private void ThenTheAuthOptionsCreatorIsCalledCorrectly() + { + _authOptionsCreator + .Verify(x => x.Create(_fileConfiguration.ReRoutes[0]), Times.Once); + } + + private void GivenTheUpstreamTemplatePatternCreatorReturns(string pattern) + { + _upstreamTemplatePatternCreator + .Setup(x => x.Create(It.IsAny())) + .Returns(new UpstreamPathTemplate(pattern, 1)); + } + + private void ThenTheRequestIdKeyCreatorIsCalledCorrectly() + { + _requestIdKeyCreator + .Verify(x => x.Create(_fileConfiguration.ReRoutes[0], _fileConfiguration.GlobalConfiguration), Times.Once); + } + + private void GivenTheRequestIdCreatorReturns(string requestId) + { + _requestIdKeyCreator + .Setup(x => x.Create(It.IsAny(), It.IsAny())) + .Returns(requestId); + } + + private void GivenTheQosOptionsCreatorReturns(QoSOptions qosOptions) + { + _qosOptionsCreator + .Setup(x => x.Create(_fileConfiguration.ReRoutes[0])) + .Returns(qosOptions); + } + + private void ThenTheQosOptionsAre(QoSOptions qosOptions) + { + _config.Data.ReRoutes[0].DownstreamReRoute[0].QosOptionsOptions.DurationOfBreak.ShouldBe(qosOptions.DurationOfBreak); + + _config.Data.ReRoutes[0].DownstreamReRoute[0].QosOptionsOptions.ExceptionsAllowedBeforeBreaking.ShouldBe(qosOptions.ExceptionsAllowedBeforeBreaking); + _config.Data.ReRoutes[0].DownstreamReRoute[0].QosOptionsOptions.TimeoutValue.ShouldBe(qosOptions.TimeoutValue); + } + + private void ThenTheServiceProviderCreatorIsCalledCorrectly() + { + _serviceProviderConfigCreator + .Verify(x => x.Create(_fileConfiguration.GlobalConfiguration), Times.Once); + } + + private void ThenTheHeaderFindAndReplaceCreatorIsCalledCorrectly() + { + _headerFindAndReplaceCreator + .Verify(x => x.Create(It.IsAny()), Times.Once); + } + + private void GivenTheHeaderFindAndReplaceCreatorReturns() + { + _headerFindAndReplaceCreator.Setup(x => x.Create(It.IsAny())).Returns(new HeaderTransformations(new List(), new List(), new List(), new List())); + } + + private void GivenTheFollowingIsReturned(ServiceProviderConfiguration serviceProviderConfiguration) + { + _serviceProviderConfigCreator + .Setup(x => x.Create(It.IsAny())).Returns(serviceProviderConfiguration); + } + + private void GivenTheFollowingRegionIsReturned(string region) + { + _regionCreator + .Setup(x => x.Create(It.IsAny())) + .Returns(region); + } + + private void ThenTheRegionCreatorIsCalledCorrectly() + { + _regionCreator + .Verify(x => x.Create(_fileConfiguration.ReRoutes[0]), Times.Once); + } + + private void GivenTheFollowingHttpHandlerOptionsAreReturned(HttpHandlerOptions httpHandlerOptions) + { + _httpHandlerOptionsCreator.Setup(x => x.Create(It.IsAny())) + .Returns(httpHandlerOptions); + } + + private void ThenTheHttpHandlerOptionsCreatorIsCalledCorrectly() + { + _httpHandlerOptionsCreator.Verify(x => x.Create(_fileConfiguration.ReRoutes[0]), Times.Once()); + } + + private void GivenTheDownstreamAddresses() + { + _downstreamAddressesCreator.Setup(x => x.Create(It.IsAny())).Returns(new List()); + } + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs new file mode 100644 index 00000000..b81ca32e --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs @@ -0,0 +1,276 @@ +namespace Ocelot.UnitTests.LoadBalancer +{ + using System.Threading.Tasks; + using Ocelot.LoadBalancer.LoadBalancers; + using Ocelot.Responses; + using Ocelot.Values; + using Shouldly; + using Xunit; + using Moq; + using Microsoft.AspNetCore.Http; + using System.Collections.Generic; + using System.Collections; + using System.Threading; + using Ocelot.Middleware; + using Ocelot.UnitTests.Responder; + using TestStack.BDDfy; + + public class CookieStickySessionsTests + { + private readonly CookieStickySessions _stickySessions; + private readonly Mock _loadBalancer; + private DownstreamContext _downstreamContext; + private Response _result; + private Response _firstHostAndPort; + private Response _secondHostAndPort; + + public CookieStickySessionsTests() + { + _loadBalancer = new Mock(); + const int defaultExpiryInMs = 100; + _stickySessions = new CookieStickySessions(_loadBalancer.Object, "sessionid", defaultExpiryInMs); + _downstreamContext = new DownstreamContext(new DefaultHttpContext()); + } + + [Fact] + public void should_return_host_and_port() + { + this.Given(_ => GivenTheLoadBalancerReturns()) + .When(_ => WhenILease()) + .Then(_ => ThenTheHostAndPortIsNotNull()) + .BDDfy(); + } + + [Fact] + public void should_return_same_host_and_port() + { + this.Given(_ => GivenTheLoadBalancerReturnsSequence()) + .And(_ => GivenTheDownstreamRequestHasSessionId("321")) + .When(_ => WhenILeaseTwiceInARow()) + .Then(_ => ThenTheFirstAndSecondResponseAreTheSame()) + .BDDfy(); + } + + [Fact] + public void should_return_different_host_and_port_if_load_balancer_does() + { + this.Given(_ => GivenTheLoadBalancerReturnsSequence()) + .When(_ => WhenIMakeTwoRequetsWithDifferentSessionValues()) + .Then(_ => ThenADifferentHostAndPortIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_error() + { + this.Given(_ => GivenTheLoadBalancerReturnsError()) + .When(_ => WhenILease()) + .Then(_ => ThenAnErrorIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_expire_sticky_session() + { + this.Given(_ => GivenTheLoadBalancerReturnsSequence()) + .When(_ => WhenTheStickySessionExpires()) + .Then(_ => ThenANewHostAndPortIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_refresh_sticky_session() + { + this.Given(_ => GivenTheLoadBalancerReturnsSequence()) + .When(_ => WhenIMakeRequestsToKeepRefreshingTheSession()) + .Then(_ => ThenTheSessionIsRefreshed()) + .BDDfy(); + } + + [Fact] + public void should_dispose() + { + _stickySessions.Dispose(); + } + + [Fact] + public void should_release() + { + _stickySessions.Release(new ServiceHostAndPort("", 0)); + } + + private async Task ThenTheSessionIsRefreshed() + { + var postExpireHostAndPort = await _stickySessions.Lease(_downstreamContext); + postExpireHostAndPort.Data.DownstreamHost.ShouldBe("one"); + postExpireHostAndPort.Data.DownstreamPort.ShouldBe(80); + + _loadBalancer + .Verify(x => x.Lease(It.IsAny()), Times.Once); + } + + private async Task WhenIMakeRequestsToKeepRefreshingTheSession() + { + var context = new DefaultHttpContext(); + var cookies = new FakeCookies(); + cookies.AddCookie("sessionid", "321"); + context.Request.Cookies = cookies; + _downstreamContext = new DownstreamContext(context); + + var firstHostAndPort = await _stickySessions.Lease(_downstreamContext); + firstHostAndPort.Data.DownstreamHost.ShouldBe("one"); + firstHostAndPort.Data.DownstreamPort.ShouldBe(80); + + Thread.Sleep(80); + + var secondHostAndPort = await _stickySessions.Lease(_downstreamContext); + secondHostAndPort.Data.DownstreamHost.ShouldBe("one"); + secondHostAndPort.Data.DownstreamPort.ShouldBe(80); + + Thread.Sleep(80); + } + + private async Task ThenANewHostAndPortIsReturned() + { + var postExpireHostAndPort = await _stickySessions.Lease(_downstreamContext); + postExpireHostAndPort.Data.DownstreamHost.ShouldBe("two"); + postExpireHostAndPort.Data.DownstreamPort.ShouldBe(80); + } + + private async Task WhenTheStickySessionExpires() + { + var context = new DefaultHttpContext(); + var cookies = new FakeCookies(); + cookies.AddCookie("sessionid", "321"); + context.Request.Cookies = cookies; + _downstreamContext = new DownstreamContext(context); + + var firstHostAndPort = await _stickySessions.Lease(_downstreamContext); + var secondHostAndPort = await _stickySessions.Lease(_downstreamContext); + + firstHostAndPort.Data.DownstreamHost.ShouldBe("one"); + firstHostAndPort.Data.DownstreamPort.ShouldBe(80); + + secondHostAndPort.Data.DownstreamHost.ShouldBe("one"); + secondHostAndPort.Data.DownstreamPort.ShouldBe(80); + + Thread.Sleep(150); + } + + private void ThenAnErrorIsReturned() + { + _result.IsError.ShouldBeTrue(); + } + + private void GivenTheLoadBalancerReturnsError() + { + _loadBalancer + .Setup(x => x.Lease(It.IsAny())) + .ReturnsAsync(new ErrorResponse(new AnyError())); + } + + private void ThenADifferentHostAndPortIsReturned() + { + _firstHostAndPort.Data.DownstreamHost.ShouldBe("one"); + _firstHostAndPort.Data.DownstreamPort.ShouldBe(80); + _secondHostAndPort.Data.DownstreamHost.ShouldBe("two"); + _secondHostAndPort.Data.DownstreamPort.ShouldBe(80); + } + + private async Task WhenIMakeTwoRequetsWithDifferentSessionValues() + { + var contextOne = new DefaultHttpContext(); + var cookiesOne = new FakeCookies(); + cookiesOne.AddCookie("sessionid", "321"); + contextOne.Request.Cookies = cookiesOne; + var contextTwo = new DefaultHttpContext(); + var cookiesTwo = new FakeCookies(); + cookiesTwo.AddCookie("sessionid", "123"); + contextTwo.Request.Cookies = cookiesTwo; + _firstHostAndPort = await _stickySessions.Lease(new DownstreamContext(contextOne)); + _secondHostAndPort = await _stickySessions.Lease(new DownstreamContext(contextTwo)); + } + + private void GivenTheLoadBalancerReturnsSequence() + { + _loadBalancer + .SetupSequence(x => x.Lease(It.IsAny())) + .ReturnsAsync(new OkResponse(new ServiceHostAndPort("one", 80))) + .ReturnsAsync(new OkResponse(new ServiceHostAndPort("two", 80))); + } + + private void ThenTheFirstAndSecondResponseAreTheSame() + { + _firstHostAndPort.Data.DownstreamHost.ShouldBe(_secondHostAndPort.Data.DownstreamHost); + _firstHostAndPort.Data.DownstreamPort.ShouldBe(_secondHostAndPort.Data.DownstreamPort); + } + + private async Task WhenILeaseTwiceInARow() + { + _firstHostAndPort = await _stickySessions.Lease(_downstreamContext); + _secondHostAndPort = await _stickySessions.Lease(_downstreamContext); + } + + private void GivenTheDownstreamRequestHasSessionId(string value) + { + var context = new DefaultHttpContext(); + var cookies = new FakeCookies(); + cookies.AddCookie("sessionid", value); + context.Request.Cookies = cookies; + _downstreamContext = new DownstreamContext(context); + } + + private void GivenTheLoadBalancerReturns() + { + _loadBalancer + .Setup(x => x.Lease(It.IsAny())) + .ReturnsAsync(new OkResponse(new ServiceHostAndPort("", 80))); + } + + private async Task WhenILease() + { + _result = await _stickySessions.Lease(_downstreamContext); + } + + private void ThenTheHostAndPortIsNotNull() + { + _result.Data.ShouldNotBeNull(); + } + } + + class FakeCookies : IRequestCookieCollection + { + private readonly Dictionary _cookies = new Dictionary(); + + public string this[string key] => _cookies[key]; + + public int Count => _cookies.Count; + + public ICollection Keys => _cookies.Keys; + + public void AddCookie(string key, string value) + { + _cookies[key] = value; + } + + public bool ContainsKey(string key) + { + return _cookies.ContainsKey(key); + } + + public IEnumerator> GetEnumerator() + { + return _cookies.GetEnumerator(); + } + + public bool TryGetValue(string key, out string value) + { + return _cookies.TryGetValue(key, out value); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _cookies.GetEnumerator(); + } + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs index 687f8f40..a995a004 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs @@ -1,284 +1,288 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.Responses; -using Ocelot.Values; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.LoadBalancer -{ - public class LeastConnectionTests - { - private ServiceHostAndPort _hostAndPort; - private Response _result; - private LeastConnection _leastConnection; - private List _services; - private Random _random; - - public LeastConnectionTests() - { - _random = new Random(); - } - - [Fact] - public void should_be_able_to_lease_and_release_concurrently() - { - var serviceName = "products"; - - var availableServices = new List - { - new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), - new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), - }; - - _services = availableServices; - _leastConnection = new LeastConnection(() => Task.FromResult(_services), serviceName); - - var tasks = new Task[100]; - - for(var i = 0; i < tasks.Length; i++) - { - tasks[i] = LeaseDelayAndRelease(); - } - - Task.WaitAll(tasks); - } - - [Fact] - public void should_handle_service_returning_to_available() - { - var serviceName = "products"; - - var availableServices = new List - { - new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), - new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), - }; - - _leastConnection = new LeastConnection(() => Task.FromResult(availableServices), serviceName); - - var hostAndPortOne = _leastConnection.Lease().Result; - hostAndPortOne.Data.DownstreamHost.ShouldBe("127.0.0.1"); - var hostAndPortTwo = _leastConnection.Lease().Result; - hostAndPortTwo.Data.DownstreamHost.ShouldBe("127.0.0.2"); - _leastConnection.Release(hostAndPortOne.Data); - _leastConnection.Release(hostAndPortTwo.Data); - - availableServices = new List - { - new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), - }; - - hostAndPortOne = _leastConnection.Lease().Result; - hostAndPortOne.Data.DownstreamHost.ShouldBe("127.0.0.1"); - hostAndPortTwo = _leastConnection.Lease().Result; - hostAndPortTwo.Data.DownstreamHost.ShouldBe("127.0.0.1"); - _leastConnection.Release(hostAndPortOne.Data); - _leastConnection.Release(hostAndPortTwo.Data); - - availableServices = new List - { - new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), - new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), - }; - - hostAndPortOne = _leastConnection.Lease().Result; - hostAndPortOne.Data.DownstreamHost.ShouldBe("127.0.0.1"); - hostAndPortTwo = _leastConnection.Lease().Result; - hostAndPortTwo.Data.DownstreamHost.ShouldBe("127.0.0.2"); - _leastConnection.Release(hostAndPortOne.Data); - _leastConnection.Release(hostAndPortTwo.Data); - } - - private async Task LeaseDelayAndRelease() - { - var hostAndPort = await _leastConnection.Lease(); - await Task.Delay(_random.Next(1, 100)); - _leastConnection.Release(hostAndPort.Data); - } - - [Fact] - public void should_get_next_url() - { - var serviceName = "products"; - - var hostAndPort = new ServiceHostAndPort("localhost", 80); - - var availableServices = new List - { - new Service(serviceName, hostAndPort, string.Empty, string.Empty, new string[0]) - }; - - this.Given(x => x.GivenAHostAndPort(hostAndPort)) - .And(x => x.GivenTheLoadBalancerStarts(availableServices, serviceName)) - .When(x => x.WhenIGetTheNextHostAndPort()) - .Then(x => x.ThenTheNextHostAndPortIsReturned()) - .BDDfy(); - } - - [Fact] - public void should_serve_from_service_with_least_connections() - { - var serviceName = "products"; - - var availableServices = new List - { - new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), - new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), - new Service(serviceName, new ServiceHostAndPort("127.0.0.3", 80), string.Empty, string.Empty, new string[0]) - }; - - _services = availableServices; - _leastConnection = new LeastConnection(() => Task.FromResult(_services), serviceName); - - var response = _leastConnection.Lease().Result; - - response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); - - response = _leastConnection.Lease().Result; - - response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); - - response = _leastConnection.Lease().Result; - - response.Data.DownstreamHost.ShouldBe(availableServices[2].HostAndPort.DownstreamHost); - } - - [Fact] - public void should_build_connections_per_service() - { - var serviceName = "products"; - - var availableServices = new List - { - new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), - new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), - }; - - _services = availableServices; - _leastConnection = new LeastConnection(() => Task.FromResult(_services), serviceName); - - var response = _leastConnection.Lease().Result; - - response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); - - response = _leastConnection.Lease().Result; - - response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); - - response = _leastConnection.Lease().Result; - - response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); - - response = _leastConnection.Lease().Result; - - response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); - } - - [Fact] - public void should_release_connection() - { - var serviceName = "products"; - - var availableServices = new List - { - new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), - new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), - }; - - _services = availableServices; - _leastConnection = new LeastConnection(() => Task.FromResult(_services), serviceName); - - var response = _leastConnection.Lease().Result; - - response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); - - response = _leastConnection.Lease().Result; - - response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); - - response = _leastConnection.Lease().Result; - - response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); - - response = _leastConnection.Lease().Result; - - response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); - - //release this so 2 should have 1 connection and we should get 2 back as our next host and port - _leastConnection.Release(availableServices[1].HostAndPort); - - response = _leastConnection.Lease().Result; - - response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); - } - - [Fact] - public void should_return_error_if_services_are_null() - { - var serviceName = "products"; - - var hostAndPort = new ServiceHostAndPort("localhost", 80); - this.Given(x => x.GivenAHostAndPort(hostAndPort)) - .And(x => x.GivenTheLoadBalancerStarts(null, serviceName)) - .When(x => x.WhenIGetTheNextHostAndPort()) - .Then(x => x.ThenServiceAreNullErrorIsReturned()) - .BDDfy(); - } - - [Fact] - public void should_return_error_if_services_are_empty() - { - var serviceName = "products"; - - var hostAndPort = new ServiceHostAndPort("localhost", 80); - this.Given(x => x.GivenAHostAndPort(hostAndPort)) - .And(x => x.GivenTheLoadBalancerStarts(new List(), serviceName)) - .When(x => x.WhenIGetTheNextHostAndPort()) - .Then(x => x.ThenServiceAreEmptyErrorIsReturned()) - .BDDfy(); - } - - private void ThenServiceAreNullErrorIsReturned() - { - _result.IsError.ShouldBeTrue(); - _result.Errors[0].ShouldBeOfType(); - } - - private void ThenServiceAreEmptyErrorIsReturned() - { - _result.IsError.ShouldBeTrue(); - _result.Errors[0].ShouldBeOfType(); - } - - private void GivenTheLoadBalancerStarts(List services, string serviceName) - { - _services = services; - _leastConnection = new LeastConnection(() => Task.FromResult(_services), serviceName); - } - - private void WhenTheLoadBalancerStarts(List services, string serviceName) - { - GivenTheLoadBalancerStarts(services, serviceName); - } - - private void GivenAHostAndPort(ServiceHostAndPort hostAndPort) - { - _hostAndPort = hostAndPort; - } - - private void WhenIGetTheNextHostAndPort() - { - _result = _leastConnection.Lease().Result; - } - - private void ThenTheNextHostAndPortIsReturned() - { - _result.Data.DownstreamHost.ShouldBe(_hostAndPort.DownstreamHost); - _result.Data.DownstreamPort.ShouldBe(_hostAndPort.DownstreamPort); - } - } -} +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Middleware; +using Ocelot.Responses; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class LeastConnectionTests + { + private ServiceHostAndPort _hostAndPort; + private Response _result; + private LeastConnection _leastConnection; + private List _services; + private Random _random; + private DownstreamContext _context; + + public LeastConnectionTests() + { + _context = new DownstreamContext(new DefaultHttpContext()); + _random = new Random(); + } + + [Fact] + public void should_be_able_to_lease_and_release_concurrently() + { + var serviceName = "products"; + + var availableServices = new List + { + new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + }; + + _services = availableServices; + _leastConnection = new LeastConnection(() => Task.FromResult(_services), serviceName); + + var tasks = new Task[100]; + + for(var i = 0; i < tasks.Length; i++) + { + tasks[i] = LeaseDelayAndRelease(); + } + + Task.WaitAll(tasks); + } + + [Fact] + public void should_handle_service_returning_to_available() + { + var serviceName = "products"; + + var availableServices = new List + { + new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + }; + + _leastConnection = new LeastConnection(() => Task.FromResult(availableServices), serviceName); + + var hostAndPortOne = _leastConnection.Lease(_context).Result; + hostAndPortOne.Data.DownstreamHost.ShouldBe("127.0.0.1"); + var hostAndPortTwo = _leastConnection.Lease(_context).Result; + hostAndPortTwo.Data.DownstreamHost.ShouldBe("127.0.0.2"); + _leastConnection.Release(hostAndPortOne.Data); + _leastConnection.Release(hostAndPortTwo.Data); + + availableServices = new List + { + new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + }; + + hostAndPortOne = _leastConnection.Lease(_context).Result; + hostAndPortOne.Data.DownstreamHost.ShouldBe("127.0.0.1"); + hostAndPortTwo = _leastConnection.Lease(_context).Result; + hostAndPortTwo.Data.DownstreamHost.ShouldBe("127.0.0.1"); + _leastConnection.Release(hostAndPortOne.Data); + _leastConnection.Release(hostAndPortTwo.Data); + + availableServices = new List + { + new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + }; + + hostAndPortOne = _leastConnection.Lease(_context).Result; + hostAndPortOne.Data.DownstreamHost.ShouldBe("127.0.0.1"); + hostAndPortTwo = _leastConnection.Lease(_context).Result; + hostAndPortTwo.Data.DownstreamHost.ShouldBe("127.0.0.2"); + _leastConnection.Release(hostAndPortOne.Data); + _leastConnection.Release(hostAndPortTwo.Data); + } + + private async Task LeaseDelayAndRelease() + { + var hostAndPort = await _leastConnection.Lease(_context); + await Task.Delay(_random.Next(1, 100)); + _leastConnection.Release(hostAndPort.Data); + } + + [Fact] + public void should_get_next_url() + { + var serviceName = "products"; + + var hostAndPort = new ServiceHostAndPort("localhost", 80); + + var availableServices = new List + { + new Service(serviceName, hostAndPort, string.Empty, string.Empty, new string[0]) + }; + + this.Given(x => x.GivenAHostAndPort(hostAndPort)) + .And(x => x.GivenTheLoadBalancerStarts(availableServices, serviceName)) + .When(x => x.WhenIGetTheNextHostAndPort()) + .Then(x => x.ThenTheNextHostAndPortIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_serve_from_service_with_least_connections() + { + var serviceName = "products"; + + var availableServices = new List + { + new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new ServiceHostAndPort("127.0.0.3", 80), string.Empty, string.Empty, new string[0]) + }; + + _services = availableServices; + _leastConnection = new LeastConnection(() => Task.FromResult(_services), serviceName); + + var response = _leastConnection.Lease(_context).Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease(_context).Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease(_context).Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[2].HostAndPort.DownstreamHost); + } + + [Fact] + public void should_build_connections_per_service() + { + var serviceName = "products"; + + var availableServices = new List + { + new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + }; + + _services = availableServices; + _leastConnection = new LeastConnection(() => Task.FromResult(_services), serviceName); + + var response = _leastConnection.Lease(_context).Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease(_context).Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease(_context).Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease(_context).Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + } + + [Fact] + public void should_release_connection() + { + var serviceName = "products"; + + var availableServices = new List + { + new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + }; + + _services = availableServices; + _leastConnection = new LeastConnection(() => Task.FromResult(_services), serviceName); + + var response = _leastConnection.Lease(_context).Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease(_context).Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease(_context).Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease(_context).Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + + //release this so 2 should have 1 connection and we should get 2 back as our next host and port + _leastConnection.Release(availableServices[1].HostAndPort); + + response = _leastConnection.Lease(_context).Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + } + + [Fact] + public void should_return_error_if_services_are_null() + { + var serviceName = "products"; + + var hostAndPort = new ServiceHostAndPort("localhost", 80); + this.Given(x => x.GivenAHostAndPort(hostAndPort)) + .And(x => x.GivenTheLoadBalancerStarts(null, serviceName)) + .When(x => x.WhenIGetTheNextHostAndPort()) + .Then(x => x.ThenServiceAreNullErrorIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_services_are_empty() + { + var serviceName = "products"; + + var hostAndPort = new ServiceHostAndPort("localhost", 80); + this.Given(x => x.GivenAHostAndPort(hostAndPort)) + .And(x => x.GivenTheLoadBalancerStarts(new List(), serviceName)) + .When(x => x.WhenIGetTheNextHostAndPort()) + .Then(x => x.ThenServiceAreEmptyErrorIsReturned()) + .BDDfy(); + } + + private void ThenServiceAreNullErrorIsReturned() + { + _result.IsError.ShouldBeTrue(); + _result.Errors[0].ShouldBeOfType(); + } + + private void ThenServiceAreEmptyErrorIsReturned() + { + _result.IsError.ShouldBeTrue(); + _result.Errors[0].ShouldBeOfType(); + } + + private void GivenTheLoadBalancerStarts(List services, string serviceName) + { + _services = services; + _leastConnection = new LeastConnection(() => Task.FromResult(_services), serviceName); + } + + private void WhenTheLoadBalancerStarts(List services, string serviceName) + { + GivenTheLoadBalancerStarts(services, serviceName); + } + + private void GivenAHostAndPort(ServiceHostAndPort hostAndPort) + { + _hostAndPort = hostAndPort; + } + + private void WhenIGetTheNextHostAndPort() + { + _result = _leastConnection.Lease(_context).Result; + } + + private void ThenTheNextHostAndPortIsReturned() + { + _result.Data.DownstreamHost.ShouldBe(_hostAndPort.DownstreamHost); + _result.Data.DownstreamPort.ShouldBe(_hostAndPort.DownstreamPort); + } + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs index da439976..703078ce 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs @@ -1,126 +1,142 @@ -using Moq; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.ServiceDiscovery; -using Shouldly; -using System.Collections.Generic; -using Ocelot.ServiceDiscovery.Providers; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.LoadBalancer -{ - public class LoadBalancerFactoryTests - { - private DownstreamReRoute _reRoute; - private LoadBalancerFactory _factory; - private ILoadBalancer _result; - private Mock _serviceProviderFactory; - private Mock _serviceProvider; - private ServiceProviderConfiguration _serviceProviderConfig; - - public LoadBalancerFactoryTests() - { - _serviceProviderFactory = new Mock(); - _serviceProvider = new Mock(); - _factory = new LoadBalancerFactory(_serviceProviderFactory.Object); - } - - [Fact] - public void should_return_no_load_balancer() - { - var reRoute = new DownstreamReRouteBuilder() - .WithUpstreamHttpMethod(new List { "Get" }) - .Build(); - - this.Given(x => x.GivenAReRoute(reRoute)) - .And(x => GivenAServiceProviderConfig(new ServiceProviderConfigurationBuilder().Build())) - .And(x => x.GivenTheServiceProviderFactoryReturns()) - .When(x => x.WhenIGetTheLoadBalancer()) - .Then(x => x.ThenTheLoadBalancerIsReturned()) - .BDDfy(); - } - - [Fact] - public void should_return_round_robin_load_balancer() - { - var reRoute = new DownstreamReRouteBuilder() - .WithLoadBalancer("RoundRobin") - .WithUpstreamHttpMethod(new List {"Get"}) - .Build(); - - this.Given(x => x.GivenAReRoute(reRoute)) - .And(x => GivenAServiceProviderConfig(new ServiceProviderConfigurationBuilder().Build())) - .And(x => x.GivenTheServiceProviderFactoryReturns()) - .When(x => x.WhenIGetTheLoadBalancer()) - .Then(x => x.ThenTheLoadBalancerIsReturned()) - .BDDfy(); - } - - [Fact] - public void should_return_round_least_connection_balancer() - { - var reRoute = new DownstreamReRouteBuilder() - .WithLoadBalancer("LeastConnection") - .WithUpstreamHttpMethod(new List {"Get"}) - .Build(); - - this.Given(x => x.GivenAReRoute(reRoute)) - .And(x => GivenAServiceProviderConfig(new ServiceProviderConfigurationBuilder().Build())) - .And(x => x.GivenTheServiceProviderFactoryReturns()) - .When(x => x.WhenIGetTheLoadBalancer()) - .Then(x => x.ThenTheLoadBalancerIsReturned()) - .BDDfy(); - } - - [Fact] - public void should_call_service_provider() - { - var reRoute = new DownstreamReRouteBuilder() - .WithLoadBalancer("RoundRobin") - .WithUpstreamHttpMethod(new List {"Get"}) - .Build(); - - this.Given(x => x.GivenAReRoute(reRoute)) - .And(x => GivenAServiceProviderConfig(new ServiceProviderConfigurationBuilder().Build())) - .And(x => x.GivenTheServiceProviderFactoryReturns()) - .When(x => x.WhenIGetTheLoadBalancer()) - .Then(x => x.ThenTheServiceProviderIsCalledCorrectly()) - .BDDfy(); - } - - private void GivenAServiceProviderConfig(ServiceProviderConfiguration serviceProviderConfig) - { - _serviceProviderConfig = serviceProviderConfig; - } - - private void GivenTheServiceProviderFactoryReturns() - { - _serviceProviderFactory - .Setup(x => x.Get(It.IsAny(), It.IsAny())) - .Returns(_serviceProvider.Object); - } - - private void ThenTheServiceProviderIsCalledCorrectly() - { - _serviceProviderFactory - .Verify(x => x.Get(It.IsAny(), It.IsAny()), Times.Once); - } - - private void GivenAReRoute(DownstreamReRoute reRoute) - { - _reRoute = reRoute; - } - - private void WhenIGetTheLoadBalancer() - { - _result = _factory.Get(_reRoute, _serviceProviderConfig).Result; - } - - private void ThenTheLoadBalancerIsReturned() - { - _result.ShouldBeOfType(); - } - } -} +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.ServiceDiscovery; +using Shouldly; +using System.Collections.Generic; +using Ocelot.ServiceDiscovery.Providers; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class LoadBalancerFactoryTests + { + private DownstreamReRoute _reRoute; + private readonly LoadBalancerFactory _factory; + private ILoadBalancer _result; + private readonly Mock _serviceProviderFactory; + private readonly Mock _serviceProvider; + private ServiceProviderConfiguration _serviceProviderConfig; + + public LoadBalancerFactoryTests() + { + _serviceProviderFactory = new Mock(); + _serviceProvider = new Mock(); + _factory = new LoadBalancerFactory(_serviceProviderFactory.Object); + } + + [Fact] + public void should_return_no_load_balancer() + { + var reRoute = new DownstreamReRouteBuilder() + .WithUpstreamHttpMethod(new List { "Get" }) + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .And(x => GivenAServiceProviderConfig(new ServiceProviderConfigurationBuilder().Build())) + .And(x => x.GivenTheServiceProviderFactoryReturns()) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_round_robin_load_balancer() + { + var reRoute = new DownstreamReRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("RoundRobin", "", 0)) + .WithUpstreamHttpMethod(new List {"Get"}) + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .And(x => GivenAServiceProviderConfig(new ServiceProviderConfigurationBuilder().Build())) + .And(x => x.GivenTheServiceProviderFactoryReturns()) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_round_least_connection_balancer() + { + var reRoute = new DownstreamReRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("LeastConnection", "", 0)) + .WithUpstreamHttpMethod(new List {"Get"}) + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .And(x => GivenAServiceProviderConfig(new ServiceProviderConfigurationBuilder().Build())) + .And(x => x.GivenTheServiceProviderFactoryReturns()) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_call_service_provider() + { + var reRoute = new DownstreamReRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("RoundRobin", "", 0)) + .WithUpstreamHttpMethod(new List {"Get"}) + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .And(x => GivenAServiceProviderConfig(new ServiceProviderConfigurationBuilder().Build())) + .And(x => x.GivenTheServiceProviderFactoryReturns()) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheServiceProviderIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_return_sticky_session() + { + var reRoute = new DownstreamReRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("CookieStickySessions", "", 0)) + .WithUpstreamHttpMethod(new List {"Get"}) + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .And(x => GivenAServiceProviderConfig(new ServiceProviderConfigurationBuilder().Build())) + .And(x => x.GivenTheServiceProviderFactoryReturns()) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) + .BDDfy(); + } + + private void GivenAServiceProviderConfig(ServiceProviderConfiguration serviceProviderConfig) + { + _serviceProviderConfig = serviceProviderConfig; + } + + private void GivenTheServiceProviderFactoryReturns() + { + _serviceProviderFactory + .Setup(x => x.Get(It.IsAny(), It.IsAny())) + .Returns(_serviceProvider.Object); + } + + private void ThenTheServiceProviderIsCalledCorrectly() + { + _serviceProviderFactory + .Verify(x => x.Get(It.IsAny(), It.IsAny()), Times.Once); + } + + private void GivenAReRoute(DownstreamReRoute reRoute) + { + _reRoute = reRoute; + } + + private void WhenIGetTheLoadBalancer() + { + _result = _factory.Get(_reRoute, _serviceProviderConfig).Result; + } + + private void ThenTheLoadBalancerIsReturned() + { + _result.ShouldBeOfType(); + } + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs index cdd63d5a..ffb84518 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs @@ -1,163 +1,182 @@ -using System; -using System.Threading.Tasks; -using Moq; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.Responses; -using Ocelot.Values; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.LoadBalancer -{ - public class LoadBalancerHouseTests - { - private DownstreamReRoute _reRoute; - private ILoadBalancer _loadBalancer; - private readonly LoadBalancerHouse _loadBalancerHouse; - private Response _getResult; - private readonly Mock _factory; - private readonly ServiceProviderConfiguration _serviceProviderConfig; - - public LoadBalancerHouseTests() - { - _factory = new Mock(); - _loadBalancerHouse = new LoadBalancerHouse(_factory.Object); - _serviceProviderConfig = new ServiceProviderConfiguration("myType","myHost",123, string.Empty); - } - - [Fact] - public void should_store_load_balancer_on_first_request() - { - var reRoute = new DownstreamReRouteBuilder().WithReRouteKey("test").Build(); - - this.Given(x => x.GivenThereIsALoadBalancer(reRoute, new FakeLoadBalancer())) - .Then(x => x.ThenItIsAdded()) - .BDDfy(); - } - - [Fact] - public void should_not_store_load_balancer_on_second_request() - { - var reRoute = new DownstreamReRouteBuilder().WithLoadBalancer("FakeLoadBalancer").WithReRouteKey("test").Build(); - - this.Given(x => x.GivenThereIsALoadBalancer(reRoute, new FakeLoadBalancer())) - .When(x => x.WhenWeGetTheLoadBalancer(reRoute)) - .Then(x => x.ThenItIsReturned()) - .BDDfy(); - } - - [Fact] - public void should_store_load_balancers_by_key() - { - var reRoute = new DownstreamReRouteBuilder().WithLoadBalancer("FakeLoadBalancer").WithReRouteKey("test").Build(); - var reRouteTwo = new DownstreamReRouteBuilder().WithLoadBalancer("FakeRoundRobinLoadBalancer").WithReRouteKey("testtwo").Build(); - - this.Given(x => x.GivenThereIsALoadBalancer(reRoute, new FakeLoadBalancer())) - .And(x => x.GivenThereIsALoadBalancer(reRouteTwo, new FakeRoundRobinLoadBalancer())) - .When(x => x.WhenWeGetTheLoadBalancer(reRoute)) - .Then(x => x.ThenTheLoadBalancerIs()) - .When(x => x.WhenWeGetTheLoadBalancer(reRouteTwo)) - .Then(x => x.ThenTheLoadBalancerIs()) - .BDDfy(); - } - - [Fact] - public void should_return_error_if_exception() - { - var reRoute = new DownstreamReRouteBuilder().Build(); - - this.When(x => x.WhenWeGetTheLoadBalancer(reRoute)) - .Then(x => x.ThenAnErrorIsReturned()) - .BDDfy(); - } - - [Fact] - public void should_get_new_load_balancer_if_reroute_load_balancer_has_changed() - { - var reRoute = new DownstreamReRouteBuilder().WithLoadBalancer("FakeLoadBalancer").WithReRouteKey("test").Build(); - - var reRouteTwo = new DownstreamReRouteBuilder().WithLoadBalancer("LeastConnection").WithReRouteKey("test").Build(); - - this.Given(x => x.GivenThereIsALoadBalancer(reRoute, new FakeLoadBalancer())) - .When(x => x.WhenWeGetTheLoadBalancer(reRoute)) - .Then(x => x.ThenTheLoadBalancerIs()) - .When(x => x.WhenIGetTheReRouteWithTheSameKeyButDifferentLoadBalancer(reRouteTwo)) - .Then(x => x.ThenTheLoadBalancerIs()) - .BDDfy(); - } - - private void WhenIGetTheReRouteWithTheSameKeyButDifferentLoadBalancer(DownstreamReRoute reRoute) - { - _reRoute = reRoute; - _factory.Setup(x => x.Get(_reRoute, _serviceProviderConfig)).ReturnsAsync(new LeastConnection(null, null)); - _getResult = _loadBalancerHouse.Get(_reRoute, _serviceProviderConfig).Result; - } - - private void ThenAnErrorIsReturned() - { - _getResult.IsError.ShouldBeTrue(); - _getResult.Errors[0].ShouldBeOfType(); - } - - private void ThenTheLoadBalancerIs() - { - _getResult.Data.ShouldBeOfType(); - } - - private void ThenItIsAdded() - { - _getResult.IsError.ShouldBe(false); - _getResult.ShouldBeOfType>(); - _getResult.Data.ShouldBe(_loadBalancer); - _factory.Verify(x => x.Get(_reRoute, _serviceProviderConfig), Times.Once); - } - - private void GivenThereIsALoadBalancer(DownstreamReRoute reRoute, ILoadBalancer loadBalancer) - { - _reRoute = reRoute; - _loadBalancer = loadBalancer; - _factory.Setup(x => x.Get(_reRoute, _serviceProviderConfig)).ReturnsAsync(loadBalancer); - _getResult = _loadBalancerHouse.Get(reRoute, _serviceProviderConfig).Result; - } - - private void WhenWeGetTheLoadBalancer(DownstreamReRoute reRoute) - { - _getResult = _loadBalancerHouse.Get(reRoute, _serviceProviderConfig).Result; - } - - private void ThenItIsReturned() - { - _getResult.Data.ShouldBe(_loadBalancer); - _factory.Verify(x => x.Get(_reRoute, _serviceProviderConfig), Times.Once); - } - - class FakeLoadBalancer : ILoadBalancer - { - public Task> Lease() - { - throw new NotImplementedException(); - } - - public void Release(ServiceHostAndPort hostAndPort) - { - throw new NotImplementedException(); - } - } - - class FakeRoundRobinLoadBalancer : ILoadBalancer - { - public Task> Lease() - { - throw new NotImplementedException(); - } - - public void Release(ServiceHostAndPort hostAndPort) - { - throw new NotImplementedException(); - } - } - } -} +using System; +using System.Threading.Tasks; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Middleware; +using Ocelot.Responses; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class LoadBalancerHouseTests + { + private DownstreamReRoute _reRoute; + private ILoadBalancer _loadBalancer; + private readonly LoadBalancerHouse _loadBalancerHouse; + private Response _getResult; + private readonly Mock _factory; + private readonly ServiceProviderConfiguration _serviceProviderConfig; + + public LoadBalancerHouseTests() + { + _factory = new Mock(); + _loadBalancerHouse = new LoadBalancerHouse(_factory.Object); + _serviceProviderConfig = new ServiceProviderConfiguration("myType","myHost",123, string.Empty); + } + + [Fact] + public void should_store_load_balancer_on_first_request() + { + var reRoute = new DownstreamReRouteBuilder() + .WithReRouteKey("test") + .Build(); + + this.Given(x => x.GivenThereIsALoadBalancer(reRoute, new FakeLoadBalancer())) + .Then(x => x.ThenItIsAdded()) + .BDDfy(); + } + + [Fact] + public void should_not_store_load_balancer_on_second_request() + { + var reRoute = new DownstreamReRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("FakeLoadBalancer", "", 0)) + .WithReRouteKey("test") + .Build(); + + this.Given(x => x.GivenThereIsALoadBalancer(reRoute, new FakeLoadBalancer())) + .When(x => x.WhenWeGetTheLoadBalancer(reRoute)) + .Then(x => x.ThenItIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_store_load_balancers_by_key() + { + var reRoute = new DownstreamReRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("FakeLoadBalancer", "", 0)) + .WithReRouteKey("test") + .Build(); + + var reRouteTwo = new DownstreamReRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("FakeRoundRobinLoadBalancer", "", 0)) + .WithReRouteKey("testtwo") + .Build(); + + this.Given(x => x.GivenThereIsALoadBalancer(reRoute, new FakeLoadBalancer())) + .And(x => x.GivenThereIsALoadBalancer(reRouteTwo, new FakeRoundRobinLoadBalancer())) + .When(x => x.WhenWeGetTheLoadBalancer(reRoute)) + .Then(x => x.ThenTheLoadBalancerIs()) + .When(x => x.WhenWeGetTheLoadBalancer(reRouteTwo)) + .Then(x => x.ThenTheLoadBalancerIs()) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_exception() + { + var reRoute = new DownstreamReRouteBuilder().Build(); + + this.When(x => x.WhenWeGetTheLoadBalancer(reRoute)) + .Then(x => x.ThenAnErrorIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_get_new_load_balancer_if_reroute_load_balancer_has_changed() + { + var reRoute = new DownstreamReRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("FakeLoadBalancer", "", 0)) + .WithReRouteKey("test") + .Build(); + + var reRouteTwo = new DownstreamReRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("LeastConnection", "", 0)) + .WithReRouteKey("test") + .Build(); + + this.Given(x => x.GivenThereIsALoadBalancer(reRoute, new FakeLoadBalancer())) + .When(x => x.WhenWeGetTheLoadBalancer(reRoute)) + .Then(x => x.ThenTheLoadBalancerIs()) + .When(x => x.WhenIGetTheReRouteWithTheSameKeyButDifferentLoadBalancer(reRouteTwo)) + .Then(x => x.ThenTheLoadBalancerIs()) + .BDDfy(); + } + + private void WhenIGetTheReRouteWithTheSameKeyButDifferentLoadBalancer(DownstreamReRoute reRoute) + { + _reRoute = reRoute; + _factory.Setup(x => x.Get(_reRoute, _serviceProviderConfig)).ReturnsAsync(new LeastConnection(null, null)); + _getResult = _loadBalancerHouse.Get(_reRoute, _serviceProviderConfig).Result; + } + + private void ThenAnErrorIsReturned() + { + _getResult.IsError.ShouldBeTrue(); + _getResult.Errors[0].ShouldBeOfType(); + } + + private void ThenTheLoadBalancerIs() + { + _getResult.Data.ShouldBeOfType(); + } + + private void ThenItIsAdded() + { + _getResult.IsError.ShouldBe(false); + _getResult.ShouldBeOfType>(); + _getResult.Data.ShouldBe(_loadBalancer); + _factory.Verify(x => x.Get(_reRoute, _serviceProviderConfig), Times.Once); + } + + private void GivenThereIsALoadBalancer(DownstreamReRoute reRoute, ILoadBalancer loadBalancer) + { + _reRoute = reRoute; + _loadBalancer = loadBalancer; + _factory.Setup(x => x.Get(_reRoute, _serviceProviderConfig)).ReturnsAsync(loadBalancer); + _getResult = _loadBalancerHouse.Get(reRoute, _serviceProviderConfig).Result; + } + + private void WhenWeGetTheLoadBalancer(DownstreamReRoute reRoute) + { + _getResult = _loadBalancerHouse.Get(reRoute, _serviceProviderConfig).Result; + } + + private void ThenItIsReturned() + { + _getResult.Data.ShouldBe(_loadBalancer); + _factory.Verify(x => x.Get(_reRoute, _serviceProviderConfig), Times.Once); + } + + class FakeLoadBalancer : ILoadBalancer + { + public Task> Lease(DownstreamContext context) + { + throw new NotImplementedException(); + } + + public void Release(ServiceHostAndPort hostAndPort) + { + throw new NotImplementedException(); + } + } + + class FakeRoundRobinLoadBalancer : ILoadBalancer + { + public Task> Lease(DownstreamContext context) + { + throw new NotImplementedException(); + } + + public void Release(ServiceHostAndPort hostAndPort) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs index a51b5cff..9e919d9f 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs @@ -1,193 +1,193 @@ -using Ocelot.Middleware; - -namespace Ocelot.UnitTests.LoadBalancer -{ - using System.Collections.Generic; - using System.Net.Http; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Errors; - using Ocelot.LoadBalancer.LoadBalancers; - using Ocelot.LoadBalancer.Middleware; - using Ocelot.Logging; - using Ocelot.Request.Middleware; - using Ocelot.Responses; - using Ocelot.Values; - using Shouldly; - using TestStack.BDDfy; - using Xunit; - - public class LoadBalancerMiddlewareTests - { - private readonly Mock _loadBalancerHouse; - private readonly Mock _loadBalancer; - private ServiceHostAndPort _hostAndPort; - private ErrorResponse _getLoadBalancerHouseError; - private ErrorResponse _getHostAndPortError; - private HttpRequestMessage _downstreamRequest; - private ServiceProviderConfiguration _config; - private Mock _loggerFactory; - private Mock _logger; - private LoadBalancingMiddleware _middleware; - private DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; - - public LoadBalancerMiddlewareTests() - { - _loadBalancerHouse = new Mock(); - _loadBalancer = new Mock(); - _loadBalancerHouse = new Mock(); - _downstreamRequest = new HttpRequestMessage(HttpMethod.Get, "http://test.com/"); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _next = context => Task.CompletedTask; - _downstreamContext.DownstreamRequest = new DownstreamRequest(_downstreamRequest); - } - - [Fact] - public void should_call_scoped_data_repository_correctly() - { - var downstreamRoute = new DownstreamReRouteBuilder() - .WithUpstreamHttpMethod(new List { "Get" }) - .Build(); - - var serviceProviderConfig = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamUrlIs("http://my.url/abc?q=123")) - .And(x => GivenTheConfigurationIs(serviceProviderConfig)) - .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute, new List())) - .And(x => x.GivenTheLoadBalancerHouseReturns()) - .And(x => x.GivenTheLoadBalancerReturns()) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamUrlIsReplacedWith("http://127.0.0.1:80/abc?q=123")) - .BDDfy(); - } - - [Fact] - public void should_set_pipeline_error_if_cannot_get_load_balancer() - { - var downstreamRoute = new DownstreamReRouteBuilder() - .WithUpstreamHttpMethod(new List { "Get" }) - .Build(); - - var serviceProviderConfig = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamUrlIs("http://my.url/abc?q=123")) - .And(x => GivenTheConfigurationIs(serviceProviderConfig)) - .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute, new List())) - .And(x => x.GivenTheLoadBalancerHouseReturnsAnError()) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenAnErrorStatingLoadBalancerCouldNotBeFoundIsSetOnPipeline()) - .BDDfy(); - } - - [Fact] - public void should_set_pipeline_error_if_cannot_get_least() - { - var downstreamRoute = new DownstreamReRouteBuilder() - .WithUpstreamHttpMethod(new List { "Get" }) - .Build(); - - var serviceProviderConfig = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamUrlIs("http://my.url/abc?q=123")) - .And(x => GivenTheConfigurationIs(serviceProviderConfig)) - .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute, new List())) - .And(x => x.GivenTheLoadBalancerHouseReturns()) - .And(x => x.GivenTheLoadBalancerReturnsAnError()) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenAnErrorStatingHostAndPortCouldNotBeFoundIsSetOnPipeline()) - .BDDfy(); - } - - private void WhenICallTheMiddleware() - { - _middleware = new LoadBalancingMiddleware(_next, _loggerFactory.Object, _loadBalancerHouse.Object); - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); - } - - private void GivenTheConfigurationIs(ServiceProviderConfiguration config) - { - _config = config; - _downstreamContext.ServiceProviderConfiguration = config; - } - - private void GivenTheDownStreamUrlIs(string downstreamUrl) - { - _downstreamRequest.RequestUri = new System.Uri(downstreamUrl); - _downstreamContext.DownstreamRequest = new DownstreamRequest(_downstreamRequest); - } - - private void GivenTheLoadBalancerReturnsAnError() - { - _getHostAndPortError = new ErrorResponse(new List() { new ServicesAreNullError($"services were null for bah") }); - _loadBalancer - .Setup(x => x.Lease()) - .ReturnsAsync(_getHostAndPortError); - } - - private void GivenTheLoadBalancerReturns() - { - _hostAndPort = new ServiceHostAndPort("127.0.0.1", 80); - _loadBalancer - .Setup(x => x.Lease()) - .ReturnsAsync(new OkResponse(_hostAndPort)); - } - - private void GivenTheDownStreamRouteIs(DownstreamReRoute downstreamRoute, List placeholder) - { - _downstreamContext.TemplatePlaceholderNameAndValues = placeholder; - _downstreamContext.DownstreamReRoute = downstreamRoute; - } - - private void GivenTheLoadBalancerHouseReturns() - { - _loadBalancerHouse - .Setup(x => x.Get(It.IsAny(), It.IsAny())) - .ReturnsAsync(new OkResponse(_loadBalancer.Object)); - } - - private void GivenTheLoadBalancerHouseReturnsAnError() - { - _getLoadBalancerHouseError = new ErrorResponse(new List() - { - new UnableToFindLoadBalancerError($"unabe to find load balancer for bah") - }); - - _loadBalancerHouse - .Setup(x => x.Get(It.IsAny(), It.IsAny())) - .ReturnsAsync(_getLoadBalancerHouseError); - } - - private void ThenAnErrorStatingLoadBalancerCouldNotBeFoundIsSetOnPipeline() - { - _downstreamContext.IsError.ShouldBeTrue(); - _downstreamContext.Errors.ShouldBe(_getLoadBalancerHouseError.Errors); - } - - private void ThenAnErrorSayingReleaseFailedIsSetOnThePipeline() - { - _downstreamContext.IsError.ShouldBeTrue(); - _downstreamContext.Errors.ShouldBe(It.IsAny>()); - } - - private void ThenAnErrorStatingHostAndPortCouldNotBeFoundIsSetOnPipeline() - { - _downstreamContext.IsError.ShouldBeTrue(); - _downstreamContext.Errors.ShouldBe(_getHostAndPortError.Errors); - } - - private void ThenTheDownstreamUrlIsReplacedWith(string expectedUri) - { - _downstreamContext.DownstreamRequest.ToHttpRequestMessage().RequestUri.OriginalString.ShouldBe(expectedUri); - } - } -} +using Ocelot.Middleware; + +namespace Ocelot.UnitTests.LoadBalancer +{ + using System.Collections.Generic; + using System.Net.Http; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Http; + using Moq; + using Ocelot.Configuration; + using Ocelot.Configuration.Builder; + using Ocelot.Errors; + using Ocelot.LoadBalancer.LoadBalancers; + using Ocelot.LoadBalancer.Middleware; + using Ocelot.Logging; + using Ocelot.Request.Middleware; + using Ocelot.Responses; + using Ocelot.Values; + using Shouldly; + using TestStack.BDDfy; + using Xunit; + + public class LoadBalancerMiddlewareTests + { + private readonly Mock _loadBalancerHouse; + private readonly Mock _loadBalancer; + private ServiceHostAndPort _hostAndPort; + private ErrorResponse _getLoadBalancerHouseError; + private ErrorResponse _getHostAndPortError; + private HttpRequestMessage _downstreamRequest; + private ServiceProviderConfiguration _config; + private Mock _loggerFactory; + private Mock _logger; + private LoadBalancingMiddleware _middleware; + private DownstreamContext _downstreamContext; + private OcelotRequestDelegate _next; + + public LoadBalancerMiddlewareTests() + { + _loadBalancerHouse = new Mock(); + _loadBalancer = new Mock(); + _loadBalancerHouse = new Mock(); + _downstreamRequest = new HttpRequestMessage(HttpMethod.Get, "http://test.com/"); + _downstreamContext = new DownstreamContext(new DefaultHttpContext()); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _next = context => Task.CompletedTask; + _downstreamContext.DownstreamRequest = new DownstreamRequest(_downstreamRequest); + } + + [Fact] + public void should_call_scoped_data_repository_correctly() + { + var downstreamRoute = new DownstreamReRouteBuilder() + .WithUpstreamHttpMethod(new List { "Get" }) + .Build(); + + var serviceProviderConfig = new ServiceProviderConfigurationBuilder() + .Build(); + + this.Given(x => x.GivenTheDownStreamUrlIs("http://my.url/abc?q=123")) + .And(x => GivenTheConfigurationIs(serviceProviderConfig)) + .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute, new List())) + .And(x => x.GivenTheLoadBalancerHouseReturns()) + .And(x => x.GivenTheLoadBalancerReturns()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheDownstreamUrlIsReplacedWith("http://127.0.0.1:80/abc?q=123")) + .BDDfy(); + } + + [Fact] + public void should_set_pipeline_error_if_cannot_get_load_balancer() + { + var downstreamRoute = new DownstreamReRouteBuilder() + .WithUpstreamHttpMethod(new List { "Get" }) + .Build(); + + var serviceProviderConfig = new ServiceProviderConfigurationBuilder() + .Build(); + + this.Given(x => x.GivenTheDownStreamUrlIs("http://my.url/abc?q=123")) + .And(x => GivenTheConfigurationIs(serviceProviderConfig)) + .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute, new List())) + .And(x => x.GivenTheLoadBalancerHouseReturnsAnError()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenAnErrorStatingLoadBalancerCouldNotBeFoundIsSetOnPipeline()) + .BDDfy(); + } + + [Fact] + public void should_set_pipeline_error_if_cannot_get_least() + { + var downstreamRoute = new DownstreamReRouteBuilder() + .WithUpstreamHttpMethod(new List { "Get" }) + .Build(); + + var serviceProviderConfig = new ServiceProviderConfigurationBuilder() + .Build(); + + this.Given(x => x.GivenTheDownStreamUrlIs("http://my.url/abc?q=123")) + .And(x => GivenTheConfigurationIs(serviceProviderConfig)) + .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute, new List())) + .And(x => x.GivenTheLoadBalancerHouseReturns()) + .And(x => x.GivenTheLoadBalancerReturnsAnError()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenAnErrorStatingHostAndPortCouldNotBeFoundIsSetOnPipeline()) + .BDDfy(); + } + + private void WhenICallTheMiddleware() + { + _middleware = new LoadBalancingMiddleware(_next, _loggerFactory.Object, _loadBalancerHouse.Object); + _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); + } + + private void GivenTheConfigurationIs(ServiceProviderConfiguration config) + { + _config = config; + _downstreamContext.ServiceProviderConfiguration = config; + } + + private void GivenTheDownStreamUrlIs(string downstreamUrl) + { + _downstreamRequest.RequestUri = new System.Uri(downstreamUrl); + _downstreamContext.DownstreamRequest = new DownstreamRequest(_downstreamRequest); + } + + private void GivenTheLoadBalancerReturnsAnError() + { + _getHostAndPortError = new ErrorResponse(new List() { new ServicesAreNullError($"services were null for bah") }); + _loadBalancer + .Setup(x => x.Lease(It.IsAny())) + .ReturnsAsync(_getHostAndPortError); + } + + private void GivenTheLoadBalancerReturns() + { + _hostAndPort = new ServiceHostAndPort("127.0.0.1", 80); + _loadBalancer + .Setup(x => x.Lease(It.IsAny())) + .ReturnsAsync(new OkResponse(_hostAndPort)); + } + + private void GivenTheDownStreamRouteIs(DownstreamReRoute downstreamRoute, List placeholder) + { + _downstreamContext.TemplatePlaceholderNameAndValues = placeholder; + _downstreamContext.DownstreamReRoute = downstreamRoute; + } + + private void GivenTheLoadBalancerHouseReturns() + { + _loadBalancerHouse + .Setup(x => x.Get(It.IsAny(), It.IsAny())) + .ReturnsAsync(new OkResponse(_loadBalancer.Object)); + } + + private void GivenTheLoadBalancerHouseReturnsAnError() + { + _getLoadBalancerHouseError = new ErrorResponse(new List() + { + new UnableToFindLoadBalancerError($"unabe to find load balancer for bah") + }); + + _loadBalancerHouse + .Setup(x => x.Get(It.IsAny(), It.IsAny())) + .ReturnsAsync(_getLoadBalancerHouseError); + } + + private void ThenAnErrorStatingLoadBalancerCouldNotBeFoundIsSetOnPipeline() + { + _downstreamContext.IsError.ShouldBeTrue(); + _downstreamContext.Errors.ShouldBe(_getLoadBalancerHouseError.Errors); + } + + private void ThenAnErrorSayingReleaseFailedIsSetOnThePipeline() + { + _downstreamContext.IsError.ShouldBeTrue(); + _downstreamContext.Errors.ShouldBe(It.IsAny>()); + } + + private void ThenAnErrorStatingHostAndPortCouldNotBeFoundIsSetOnPipeline() + { + _downstreamContext.IsError.ShouldBeTrue(); + _downstreamContext.Errors.ShouldBe(_getHostAndPortError.Errors); + } + + private void ThenTheDownstreamUrlIsReplacedWith(string expectedUri) + { + _downstreamContext.DownstreamRequest.ToHttpRequestMessage().RequestUri.OriginalString.ShouldBe(expectedUri); + } + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs index 74b5c4b0..7b531d1e 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs @@ -1,75 +1,77 @@ -using System.Collections.Generic; -using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.Responses; -using Ocelot.Values; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.LoadBalancer -{ - public class NoLoadBalancerTests - { - private List _services; - private NoLoadBalancer _loadBalancer; - private Response _result; - - [Fact] - public void should_return_host_and_port() - { - var hostAndPort = new ServiceHostAndPort("127.0.0.1", 80); - - var services = new List - { - new Service("product", hostAndPort, string.Empty, string.Empty, new string[0]) - }; - this.Given(x => x.GivenServices(services)) - .When(x => x.WhenIGetTheNextHostAndPort()) - .Then(x => x.ThenTheHostAndPortIs(hostAndPort)) - .BDDfy(); - } - - [Fact] - public void should_return_error_if_no_services() - { - var services = new List(); - - this.Given(x => x.GivenServices(services)) - .When(x => x.WhenIGetTheNextHostAndPort()) - .Then(x => x.ThenThereIsAnError()) - .BDDfy(); - } - - [Fact] - public void should_return_error_if_null_services() - { - List services = null; - - this.Given(x => x.GivenServices(services)) - .When(x => x.WhenIGetTheNextHostAndPort()) - .Then(x => x.ThenThereIsAnError()) - .BDDfy(); - } - - private void ThenThereIsAnError() - { - _result.IsError.ShouldBeTrue(); - } - - private void GivenServices(List services) - { - _services = services; - } - - private void WhenIGetTheNextHostAndPort() - { - _loadBalancer = new NoLoadBalancer(_services); - _result = _loadBalancer.Lease().Result; - } - - private void ThenTheHostAndPortIs(ServiceHostAndPort expected) - { - _result.Data.ShouldBe(expected); - } - } -} +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Middleware; +using Ocelot.Responses; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class NoLoadBalancerTests + { + private List _services; + private NoLoadBalancer _loadBalancer; + private Response _result; + + [Fact] + public void should_return_host_and_port() + { + var hostAndPort = new ServiceHostAndPort("127.0.0.1", 80); + + var services = new List + { + new Service("product", hostAndPort, string.Empty, string.Empty, new string[0]) + }; + this.Given(x => x.GivenServices(services)) + .When(x => x.WhenIGetTheNextHostAndPort()) + .Then(x => x.ThenTheHostAndPortIs(hostAndPort)) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_no_services() + { + var services = new List(); + + this.Given(x => x.GivenServices(services)) + .When(x => x.WhenIGetTheNextHostAndPort()) + .Then(x => x.ThenThereIsAnError()) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_null_services() + { + List services = null; + + this.Given(x => x.GivenServices(services)) + .When(x => x.WhenIGetTheNextHostAndPort()) + .Then(x => x.ThenThereIsAnError()) + .BDDfy(); + } + + private void ThenThereIsAnError() + { + _result.IsError.ShouldBeTrue(); + } + + private void GivenServices(List services) + { + _services = services; + } + + private void WhenIGetTheNextHostAndPort() + { + _loadBalancer = new NoLoadBalancer(_services); + _result = _loadBalancer.Lease(new DownstreamContext(new DefaultHttpContext())).Result; + } + + private void ThenTheHostAndPortIs(ServiceHostAndPort expected) + { + _result.Data.ShouldBe(expected); + } + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs index 2ad1621f..9af14055 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs @@ -1,69 +1,74 @@ -using System.Collections.Generic; -using System.Diagnostics; -using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.Responses; -using Ocelot.Values; -using Shouldly; -using TestStack.BDDfy; -using Xunit; -using System.Threading.Tasks; - -namespace Ocelot.UnitTests.LoadBalancer -{ - public class RoundRobinTests - { - private readonly RoundRobin _roundRobin; - private readonly List _services; - private Response _hostAndPort; - - public RoundRobinTests() - { - _services = new List - { - new Service("product", new ServiceHostAndPort("127.0.0.1", 5000), string.Empty, string.Empty, new string[0]), - new Service("product", new ServiceHostAndPort("127.0.0.1", 5001), string.Empty, string.Empty, new string[0]), - new Service("product", new ServiceHostAndPort("127.0.0.1", 5001), string.Empty, string.Empty, new string[0]) - }; - - _roundRobin = new RoundRobin(() => Task.FromResult(_services)); - } - - [Fact] - public void should_get_next_address() - { - this.Given(x => x.GivenIGetTheNextAddress()) - .Then(x => x.ThenTheNextAddressIndexIs(0)) - .Given(x => x.GivenIGetTheNextAddress()) - .Then(x => x.ThenTheNextAddressIndexIs(1)) - .Given(x => x.GivenIGetTheNextAddress()) - .Then(x => x.ThenTheNextAddressIndexIs(2)) - .BDDfy(); - } - - [Fact] - public void should_go_back_to_first_address_after_finished_last() - { - var stopWatch = Stopwatch.StartNew(); - - while (stopWatch.ElapsedMilliseconds < 1000) - { - var address = _roundRobin.Lease().Result; - address.Data.ShouldBe(_services[0].HostAndPort); - address = _roundRobin.Lease().Result; - address.Data.ShouldBe(_services[1].HostAndPort); - address = _roundRobin.Lease().Result; - address.Data.ShouldBe(_services[2].HostAndPort); - } - } - - private void GivenIGetTheNextAddress() - { - _hostAndPort = _roundRobin.Lease().Result; - } - - private void ThenTheNextAddressIndexIs(int index) - { - _hostAndPort.Data.ShouldBe(_services[index].HostAndPort); - } - } -} +using System.Collections.Generic; +using System.Diagnostics; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Responses; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; +using System.Threading.Tasks; +using Ocelot.Middleware; +using Microsoft.AspNetCore.Http; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class RoundRobinTests + { + private readonly RoundRobin _roundRobin; + private readonly List _services; + private Response _hostAndPort; + private DownstreamContext _context; + + public RoundRobinTests() + { + _context = new DownstreamContext(new DefaultHttpContext()); + + _services = new List + { + new Service("product", new ServiceHostAndPort("127.0.0.1", 5000), string.Empty, string.Empty, new string[0]), + new Service("product", new ServiceHostAndPort("127.0.0.1", 5001), string.Empty, string.Empty, new string[0]), + new Service("product", new ServiceHostAndPort("127.0.0.1", 5001), string.Empty, string.Empty, new string[0]) + }; + + _roundRobin = new RoundRobin(() => Task.FromResult(_services)); + } + + [Fact] + public void should_get_next_address() + { + this.Given(x => x.GivenIGetTheNextAddress()) + .Then(x => x.ThenTheNextAddressIndexIs(0)) + .Given(x => x.GivenIGetTheNextAddress()) + .Then(x => x.ThenTheNextAddressIndexIs(1)) + .Given(x => x.GivenIGetTheNextAddress()) + .Then(x => x.ThenTheNextAddressIndexIs(2)) + .BDDfy(); + } + + [Fact] + public void should_go_back_to_first_address_after_finished_last() + { + var stopWatch = Stopwatch.StartNew(); + + while (stopWatch.ElapsedMilliseconds < 1000) + { + var address = _roundRobin.Lease(_context).Result; + address.Data.ShouldBe(_services[0].HostAndPort); + address = _roundRobin.Lease(_context).Result; + address.Data.ShouldBe(_services[1].HostAndPort); + address = _roundRobin.Lease(_context).Result; + address.Data.ShouldBe(_services[2].HostAndPort); + } + } + + private void GivenIGetTheNextAddress() + { + _hostAndPort = _roundRobin.Lease(_context).Result; + } + + private void ThenTheNextAddressIndexIs(int index) + { + _hostAndPort.Data.ShouldBe(_services[index].HostAndPort); + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs b/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs index ae027143..9c56a323 100644 --- a/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs +++ b/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs @@ -26,7 +26,7 @@ namespace Ocelot.UnitTests.Requester [Fact] public void should_store_qos_provider_on_first_request() { - var reRoute = new DownstreamReRouteBuilder().WithReRouteKey("test").Build(); + var reRoute = new DownstreamReRouteBuilder().WithQosKey("test").Build(); this.Given(x => x.GivenThereIsAQoSProvider(reRoute, new FakeQoSProvider())) .Then(x => x.ThenItIsAdded()) @@ -36,7 +36,7 @@ namespace Ocelot.UnitTests.Requester [Fact] public void should_not_store_qos_provider_on_first_request() { - var reRoute = new DownstreamReRouteBuilder().WithReRouteKey("test").Build(); + var reRoute = new DownstreamReRouteBuilder().WithQosKey("test").Build(); this.Given(x => x.GivenThereIsAQoSProvider(reRoute, new FakeQoSProvider())) .When(x => x.WhenWeGetTheQoSProvider(reRoute)) @@ -47,8 +47,8 @@ namespace Ocelot.UnitTests.Requester [Fact] public void should_store_qos_providers_by_key() { - var reRoute = new DownstreamReRouteBuilder().WithReRouteKey("test").Build(); - var reRouteTwo = new DownstreamReRouteBuilder().WithReRouteKey("testTwo").Build(); + var reRoute = new DownstreamReRouteBuilder().WithQosKey("test").Build(); + var reRouteTwo = new DownstreamReRouteBuilder().WithQosKey("testTwo").Build(); this.Given(x => x.GivenThereIsAQoSProvider(reRoute, new FakeQoSProvider())) .And(x => x.GivenThereIsAQoSProvider(reRouteTwo, new FakePollyQoSProvider())) @@ -72,9 +72,9 @@ namespace Ocelot.UnitTests.Requester [Fact] public void should_get_new_qos_provider_if_reroute_qos_provider_has_changed() { - var reRoute = new DownstreamReRouteBuilder().WithReRouteKey("test").Build(); + var reRoute = new DownstreamReRouteBuilder().WithQosKey("test").Build(); - var reRouteTwo = new DownstreamReRouteBuilder().WithReRouteKey("test").WithIsQos(true).Build(); + var reRouteTwo = new DownstreamReRouteBuilder().WithQosKey("test").WithIsQos(true).Build(); this.Given(x => x.GivenThereIsAQoSProvider(reRoute, new FakeQoSProvider())) .When(x => x.WhenWeGetTheQoSProvider(reRoute))