From 59b63eab9c2cd2ab33c5752bbc3bd00a079cd74c Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Fri, 12 Apr 2024 18:30:24 +0300 Subject: [PATCH 01/15] #2034 More rapid CircleCI builds (Part 1) (#2045) * Override `DefaultRequestTimeoutSeconds` by new property * Build using .NET 8 SDK only * Build all 3 SDKs if target is Release * Run "dotnet tool restore" to make the "dotnet-cake" command available * Update GitVersion.Tool package * GitVersion.Tool 5.12.0 * Newtonsoft.Json * Review packages * Prepare folder structure for samples * samples Administration * Move Docker files * samples solution folder * Move ServiceFabric folders * Rename ServiceFabric folders * Re-add OcelotApplication of ServiceFabric sample * New Samples view in Visual Studio * Add Ocelot.Samples.sln * Add Ocelot.Release.sln * Remove Samples projects from main solution * log settings of Compile task * Remove legacy build settings file * queue/block_workflow * resource_class * Update .editorconfig * CS8936: Feature 'primary constructors' is not available in C# 10, 11. Feature 'primary constructors' is available in C# 12.0 or greater. But we use `net6.0` and `net7.0` * CS8936 Feature 'collection expressions' is not available in C# 10, 11. CS8936: Feature 'primary constructors' is not available in C# 10, 11. * CS0618: 'member' is obsolete: 'text'. https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs0618 Switch off the warning CS0618 for testing projects. * Fix warnings. xUnit1013 Public method 'GivenThereIsAnIdentityServerOn' on test class 'AuthenticationTests' should be marked as a Theory. Reduce the visibility of the method, or add a Theory attribute to the method. CS0618 'FileAuthenticationOptions.AuthenticationProviderKey' is obsolete: 'Use the AuthenticationProviderKeys property!'. * Don't restore and don't build in RunUnitTests target * --verbosity:detailed * Disable BDDfy console report * enable * Inherit from `UnitTest` --- .circleci/config.yml | 15 +- .editorconfig | 249 ++++++++++++++- Directory.Build.props | 15 - Ocelot.Release.sln | 231 ++++++++++++++ Ocelot.sln | 155 ++------- build.cake | 66 ++-- .../Issue645.postman_collection.json | 298 +++++++++--------- .../Ocelot.Samples.AdministrationApi.csproj} | 0 .../Program.cs | 0 .../Properties/launchSettings.json | 0 .../README.md | 188 +++++------ .../appsettings.json | 0 .../ocelot.json | 34 +- .../tempkey.rsa | 0 .../Ocelot.Samples.Basic.ApiGateway.csproj} | 0 samples/{OcelotBasic => Basic}/Program.cs | 56 ++-- .../Properties/launchSettings.json | 0 samples/{OcelotBasic => Basic}/Startup.cs | 0 .../appsettings.Development.json | 0 .../ApiGateway => Basic}/appsettings.json | 0 samples/{OcelotBasic => Basic}/ocelot.json | 40 +-- samples/Docker/README.md | 0 .../Ocelot.Samples.Eureka.ApiGateway.csproj} | 0 .../ApiGateway/Program.cs | 0 .../ApiGateway/Properties/launchSettings.json | 0 .../ApiGateway/appsettings.json | 0 .../ApiGateway/ocelot.json | 44 +-- .../Controllers/CategoryController.cs | 0 ...t.Samples.Eureka.DownstreamService.csproj} | 0 .../DownstreamService/Program.cs | 0 .../Properties/launchSettings.json | 0 .../DownstreamService/Startup.cs | 0 .../appsettings.Development.json | 0 .../DownstreamService/appsettings.json | 0 .../{OcelotEureka => Eureka}/OcelotEureka.sln | 0 samples/{OcelotEureka => Eureka}/README.md | 0 .../Ocelot.Samples.GraphQL.csproj} | 0 .../OcelotGraphQL.sln | 0 samples/{OcelotGraphQL => GraphQL}/Program.cs | 268 ++++++++-------- .../Properties/launchSettings.json | 0 samples/{OcelotGraphQL => GraphQL}/README.md | 140 ++++---- .../{OcelotGraphQL => GraphQL}/ocelot.json | 36 +-- .../{OcelotKube => Kubernetes}/.dockerignore | 0 .../ApiGateway/Dockerfile | 0 ...elot.Samples.Kubernetes.ApiGateway.csproj} | 0 .../ApiGateway/Program.cs | 0 .../ApiGateway/Properties/launchSettings.json | 0 .../ApiGateway/Startup.cs | 0 .../ApiGateway/appsettings.Development.json | 0 .../ApiGateway}/appsettings.json | 0 .../ApiGateway/ocelot.json | 40 +-- samples/{OcelotKube => Kubernetes}/Dockerfile | 0 .../Controllers/ValuesController.cs | 0 .../Controllers/WeatherForecastController.cs | 0 .../DownstreamService/Dockerfile | 0 .../Models/WeatherForecast.cs | 0 ...mples.Kubernetes.DownstreamService.csproj} | 2 +- .../DownstreamService/Program.cs | 0 .../Properties/launchSettings.json | 0 .../appsettings.Development.json | 0 .../DownstreamService}/appsettings.json | 16 +- .../{OcelotKube => Kubernetes}/OcelotKube.sln | 0 samples/Ocelot.Samples.sln | 91 ++++++ .../DownstreamService/.dockerignore | 25 -- .../DownstreamService/Dockerfile | 22 -- ....ServiceDiscovery.DownstreamService.csproj | 17 - .../Ocelot.Samples.OpenTracing.csproj} | 0 .../Program.cs | 0 .../appsettings.Development.json | 0 .../appsettings.json | 0 .../ocelot.json | 0 .../.dockerignore | 0 ...Samples.ServiceDiscovery.ApiGateway.csproj | 4 +- .../ApiGateway/Program.cs | 0 .../ApiGateway/Properties/launchSettings.json | 0 .../MyServiceDiscoveryProvider.cs | 0 .../MyServiceDiscoveryProviderFactory.cs | 0 .../ApiGateway/appsettings.json | 0 .../ApiGateway/ocelot.json | 0 .../Controllers/CategoriesController.cs | 0 .../Controllers/HealthController.cs | 0 .../Controllers/WeatherForecastController.cs | 0 .../DownstreamService/Models/HealthResult.cs | 0 .../Models/MicroserviceResult.cs | 0 .../DownstreamService/Models/ReadyResult.cs | 0 .../Models/WeatherForecast.cs | 0 ....ServiceDiscovery.DownstreamService.csproj | 13 + .../DownstreamService/Program.cs | 0 .../Properties/launchSettings.json | 0 .../DownstreamService/Startup.cs | 0 .../appsettings.Development.json | 0 .../DownstreamService/appsettings.json | 0 .../Ocelot.Samples.ServiceDiscovery.sln | 0 .../README.md | 0 .../.gitignore | 0 ...t.Samples.ServiceFabric.ApiGateway.csproj} | 2 +- .../OcelotApplicationApiGateway.cs | 0 .../ApiGateway}/Program.cs | 0 .../Properties/launchSettings.json | 0 .../ApiGateway}/ServiceEventListener.cs | 0 .../ApiGateway}/ServiceEventSource.cs | 0 .../ApiGateway}/WebCommunicationListener.cs | 0 .../ApiGateway}/appsettings.json | 0 .../ApiGateway}/ocelot.json | 42 +-- .../CONTRIBUTING.md | 0 .../DownstreamService}/ApiGateway.cs | 0 .../Controllers/ValuesController.cs | 0 ...es.ServiceFabric.DownstreamService.csproj} | 0 .../DownstreamService}/Program.cs | 0 .../Properties/launchSettings.json | 0 .../DownstreamService}/ServiceEventSource.cs | 0 .../DownstreamService}/Startup.cs | 0 .../LICENSE.md | 0 .../OcelotApplication/ApplicationManifest.xml | 0 .../Code/entryPoint.cmd | 0 .../Code/entryPoint.sh | 0 .../Config/Settings.xml | 0 .../Config/_readme.txt | 0 .../Data/_readme.txt | 0 .../ServiceManifest-Linux.xml | 0 .../ServiceManifest-Windows.xml | 0 .../ServiceManifest.xml | 0 .../Code/entryPoint.cmd | 0 .../Code/entryPoint.sh | 0 .../Config/Settings.xml | 0 .../Config/_readme.txt | 0 .../Data/_readme.txt | 0 .../ServiceManifest-Linux.xml | 0 .../ServiceManifest-Windows.xml | 0 .../ServiceManifest.xml | 0 .../README.md | 0 .../build.bat | 0 .../build.sh | 0 .../dotnet-include.sh | 0 .../install.ps1 | 0 .../install.sh | 0 .../uninstall.ps1 | 0 .../uninstall.sh | 0 .../PollyQoSProviderBase.cs | 6 +- .../v7/PollyQoSProvider.cs | 2 +- .../Configuration/AuthenticationOptions.cs | 13 +- .../Builder/AuthenticationOptionsBuilder.cs | 2 +- .../File/FileAuthenticationOptions.cs | 6 +- .../LoadBalancers/LoadBalancerHouse.cs | 7 +- src/Ocelot/Middleware/HttpItemsExtensions.cs | 2 +- .../Multiplexer/MultiplexingMiddleware.cs | 2 +- .../DownstreamRequestInitialiserMiddleware.cs | 2 +- src/Ocelot/Requester/MessageInvokerPool.cs | 28 +- test/Ocelot.AcceptanceTests/AggregateTests.cs | 247 +++++++-------- .../Authentication/AuthenticationSteps.cs | 30 +- .../Authentication/AuthenticationTests.cs | 8 + .../MultipleAuthSchemesFeatureTests.cs | 6 +- .../Caching/CachingTests.cs | 14 +- test/Ocelot.AcceptanceTests/ContentTests.cs | 14 +- .../Ocelot.AcceptanceTests.csproj | 4 +- test/Ocelot.AcceptanceTests/PollyQoSTests.cs | 182 ++++++----- .../Request/RequestMapperTests.cs | 8 +- .../Request/StreamContentTests.cs | 27 +- .../Requester/PayloadTooLargeTests.cs | 8 +- test/Ocelot.AcceptanceTests/RoutingTests.cs | 2 +- .../ServiceDiscoveryTests.cs | 14 +- .../ServiceFabricTests.cs | 6 +- test/Ocelot.AcceptanceTests/Steps.cs | 31 +- .../Ocelot.Benchmarks.csproj | 4 +- test/Ocelot.Benchmarks/PayloadBenchmarks.cs | 20 +- test/Ocelot.Benchmarks/ResponseBenchmarks.cs | 22 +- .../Ocelot.IntegrationTests.csproj | 4 +- .../Ocelot.ManualTest}/Dockerfile | 94 +++--- .../Ocelot.ManualTest.csproj | 4 +- .../Ocelot.ManualTest}/docker-compose.yaml | 48 +-- .../OcelotAdministrationBuilderTests.cs | 4 +- .../AuthenticationMiddlewareTests.cs | 6 +- .../AuthorizationMiddlewareTests.cs | 2 +- .../Authorization/ClaimsAuthorizerTests.cs | 2 +- .../Cache/AspMemoryCacheTests.cs | 2 +- .../Cache/DefaultCacheKeyGeneratorTests.cs | 2 +- .../Cache/OutputCacheMiddlewareTests.cs | 2 +- .../Cache/RegionCreatorTests.cs | 2 +- .../OcelotBuilderExtensionsTests.cs | 2 +- .../CacheManager/OcelotCacheManagerCache.cs | 2 +- .../OutputCacheMiddlewareRealCacheTests.cs | 2 +- .../Claims/AddClaimsToRequestTests.cs | 2 +- .../Claims/ClaimsToClaimsMiddlewareTests.cs | 2 +- .../Configuration/AggregatesCreatorTests.cs | 2 +- .../AuthenticationOptionsCreatorTests.cs | 2 +- ...elotConfigurationChangeTokenSourceTests.cs | 2 +- .../OcelotConfigurationChangeTokenTests.cs | 2 +- .../ClaimToThingConfigurationParserTests.cs | 2 +- .../ClaimsToThingCreatorTests.cs | 2 +- .../ConfigurationCreatorTests.cs | 2 +- .../DiskFileConfigurationRepositoryTests.cs | 2 +- .../DownstreamAddressesCreatorTests.cs | 2 +- .../Configuration/DynamicsCreatorTests.cs | 2 +- .../FileConfigurationPollerTests.cs | 2 +- .../FileConfigurationSetterTests.cs | 2 +- .../FileInternalConfigurationCreatorTests.cs | 2 +- .../HeaderFindAndReplaceCreatorTests.cs | 2 +- .../HttpHandlerOptionsCreatorTests.cs | 2 +- .../InMemoryConfigurationRepositoryTests.cs | 2 +- .../LoadBalancerOptionsCreatorTests.cs | 2 +- .../Configuration/QoSOptionsCreatorTests.cs | 2 +- .../RateLimitOptionsCreatorTests.cs | 2 +- .../Configuration/RequestIdKeyCreatorTests.cs | 2 +- .../Configuration/RouteKeyCreatorTests.cs | 22 +- .../Configuration/RouteOptionsCreatorTests.cs | 4 +- .../Configuration/RoutesCreatorTests.cs | 2 +- .../SecurityOptionsCreatorTests.cs | 2 +- .../ServiceProviderCreatorTests.cs | 2 +- .../UpstreamTemplatePatternCreatorTests.cs | 2 +- .../FileConfigurationFluentValidatorTests.cs | 132 +++----- .../FileQoSOptionsFluentValidatorTests.cs | 2 +- .../Validation/HostAndPortValidatorTests.cs | 2 +- .../Validation/RouteFluentValidatorTests.cs | 2 +- .../Configuration/VersionCreatorTests.cs | 2 +- .../ConsulFileConfigurationRepositoryTests.cs | 2 +- .../ConsulServiceDiscoveryProviderTests.cs | 2 +- .../Consul/OcelotBuilderExtensionsTests.cs | 2 +- ...lingConsulServiceDiscoveryProviderTests.cs | 2 +- .../FileConfigurationControllerTests.cs | 2 +- .../Controllers/OutputCacheControllerTests.cs | 2 +- .../ConfigurationBuilderExtensionsTests.cs | 22 +- .../DependencyInjection/OcelotBuilderTests.cs | 2 +- .../ServiceCollectionExtensionsTests.cs | 2 +- .../ChangeDownstreamPathTemplateTests.cs | 2 +- .../ClaimsToDownstreamPathMiddlewareTests.cs | 2 +- .../DownstreamRouteCreatorTests.cs | 2 +- .../DownstreamRouteFinderMiddlewareTests.cs | 2 +- .../DownstreamRouteFinderTests.cs | 2 +- .../DownstreamRouteProviderFactoryTests.cs | 2 +- .../UrlMatcher/RegExUrlMatcherTests.cs | 2 +- ...lPathPlaceholderNameAndValueFinderTests.cs | 2 +- .../DownstreamPathPlaceholderReplacerTests.cs | 2 +- .../DownstreamUrlCreatorMiddlewareTests.cs | 2 +- .../Errors/ExceptionHandlerMiddlewareTests.cs | 2 +- .../EurekaServiceDiscoveryProviderTests.cs | 2 +- test/Ocelot.UnitTests/FileUnitTest.cs | 9 +- .../AddHeadersToRequestClaimToThingTests.cs | 2 +- .../Headers/AddHeadersToRequestPlainTests.cs | 2 +- .../Headers/AddHeadersToResponseTests.cs | 2 +- .../Headers/ClaimsToHeadersMiddlewareTests.cs | 2 +- .../HttpContextRequestHeaderReplacerTests.cs | 2 +- ...ttpHeadersTransformationMiddlewareTests.cs | 2 +- .../HttpResponseHeaderReplacerTests.cs | 2 +- .../Headers/RemoveHeadersTests.cs | 2 +- .../Infrastructure/ClaimParserTests.cs | 2 +- .../Infrastructure/HttpDataRepositoryTests.cs | 2 +- .../IScopedRequestDataRepository.cs | 1 - .../Infrastructure/ScopesAuthorizerTests.cs | 2 +- test/Ocelot.UnitTests/Kubernetes/KubeTests.cs | 2 +- .../OcelotBuilderExtensionsTests.cs | 2 +- .../Kubernetes/PollKubeTests.cs | 2 +- .../CookieStickySessionsCreatorTests.cs | 2 +- .../LoadBalancer/CookieStickySessionsTests.cs | 2 +- ...elegateInvokingLoadBalancerCreatorTests.cs | 2 +- .../LeastConnectionCreatorTests.cs | 2 +- .../LoadBalancer/LeastConnectionTests.cs | 2 +- .../LoadBalancer/LoadBalancerFactoryTests.cs | 2 +- .../LoadBalancer/LoadBalancerHouseTests.cs | 2 +- .../LoadBalancerMiddlewareTests.cs | 2 +- .../NoLoadBalancerCreatorTests.cs | 2 +- .../LoadBalancer/NoLoadBalancerTests.cs | 2 +- .../LoadBalancer/RoundRobinCreatorTests.cs | 2 +- .../LoadBalancer/RoundRobinTests.cs | 2 +- .../Logging/OcelotDiagnosticListenerTests.cs | 2 +- .../Middleware/BaseUrlFinderTests.cs | 2 +- .../OcelotPipelineExtensionsTests.cs | 2 +- .../Middleware/OcelotPiplineBuilderTests.cs | 2 +- .../DefinedAggregatorProviderTests.cs | 2 +- .../MultiplexingMiddlewareTests.cs | 16 +- .../ResponseAggregatorFactoryTests.cs | 2 +- .../SimpleJsonResponseAggregatorTests.cs | 2 +- .../UserDefinedResponseAggregatorTests.cs | 2 +- test/Ocelot.UnitTests/Ocelot.UnitTests.csproj | 3 +- ...esiliencePipelineDelegatingHandlerTests.cs | 2 +- .../QueryStrings/AddQueriesToRequestTests.cs | 2 +- .../ClaimsToQueryStringMiddlewareTests.cs | 2 +- .../ClientRateLimitMiddlewareTests.cs | 2 +- .../ScopedRequestDataRepositoryTests.cs | 2 +- .../Creator/DownstreamRequestCreatorTests.cs | 2 +- ...streamRequestInitialiserMiddlewareTests.cs | 2 +- .../Request/Mapper/RequestMapperTests.cs | 7 +- .../Request/Mapper/StreamHttpContentTests.cs | 15 +- .../RequestId/RequestIdMiddlewareTests.cs | 2 +- ...atingHandlerHandlerProviderFactoryTests.cs | 2 +- .../Requester/HttpRequesterMiddlewareTests.cs | 2 +- .../Requester/MessageInvokerPoolTests.cs | 6 +- .../ErrorsToHttpStatusCodeMapperTests.cs | 4 +- .../Responder/ResponderMiddlewareTests.cs | 2 +- .../Security/IPSecurityPolicyTests.cs | 2 +- .../Security/SecurityMiddlewareTests.cs | 2 +- .../ConfigurationServiceProviderTests.cs | 2 +- .../ServiceDiscoveryProviderFactoryTests.cs | 2 +- ...viceFabricServiceDiscoveryProviderTests.cs | 2 +- .../ServiceDiscovery/ServiceRegistryTests.cs | 2 +- test/Ocelot.UnitTests/UnitTest.cs | 9 +- .../WebSocketsProxyMiddlewareTests.cs | 2 +- 296 files changed, 1913 insertions(+), 1545 deletions(-) delete mode 100644 Directory.Build.props create mode 100644 Ocelot.Release.sln rename samples/{AdministrationApi => Administration}/Issue645.postman_collection.json (97%) rename samples/{AdministrationApi/AdministrationApi.csproj => Administration/Ocelot.Samples.AdministrationApi.csproj} (100%) rename samples/{AdministrationApi => Administration}/Program.cs (100%) rename samples/{AdministrationApi => Administration}/Properties/launchSettings.json (100%) rename samples/{AdministrationApi => Administration}/README.md (96%) rename samples/{AdministrationApi => Administration}/appsettings.json (100%) rename samples/{AdministrationApi => Administration}/ocelot.json (95%) rename samples/{AdministrationApi => Administration}/tempkey.rsa (100%) rename samples/{OcelotBasic/Ocelot.Samples.OcelotBasic.ApiGateway.csproj => Basic/Ocelot.Samples.Basic.ApiGateway.csproj} (100%) rename samples/{OcelotBasic => Basic}/Program.cs (97%) rename samples/{OcelotBasic => Basic}/Properties/launchSettings.json (100%) rename samples/{OcelotBasic => Basic}/Startup.cs (100%) rename samples/{OcelotBasic => Basic}/appsettings.Development.json (100%) rename samples/{OcelotKube/ApiGateway => Basic}/appsettings.json (100%) rename samples/{OcelotBasic => Basic}/ocelot.json (95%) delete mode 100644 samples/Docker/README.md rename samples/{OcelotEureka/ApiGateway/ApiGateway.csproj => Eureka/ApiGateway/Ocelot.Samples.Eureka.ApiGateway.csproj} (100%) rename samples/{OcelotEureka => Eureka}/ApiGateway/Program.cs (100%) rename samples/{OcelotEureka => Eureka}/ApiGateway/Properties/launchSettings.json (100%) rename samples/{OcelotEureka => Eureka}/ApiGateway/appsettings.json (100%) rename samples/{OcelotEureka => Eureka}/ApiGateway/ocelot.json (96%) rename samples/{OcelotEureka => Eureka}/DownstreamService/Controllers/CategoryController.cs (100%) rename samples/{OcelotEureka/DownstreamService/DownstreamService.csproj => Eureka/DownstreamService/Ocelot.Samples.Eureka.DownstreamService.csproj} (100%) rename samples/{OcelotEureka => Eureka}/DownstreamService/Program.cs (100%) rename samples/{OcelotEureka => Eureka}/DownstreamService/Properties/launchSettings.json (100%) rename samples/{OcelotEureka => Eureka}/DownstreamService/Startup.cs (100%) rename samples/{OcelotEureka => Eureka}/DownstreamService/appsettings.Development.json (100%) rename samples/{OcelotEureka => Eureka}/DownstreamService/appsettings.json (100%) rename samples/{OcelotEureka => Eureka}/OcelotEureka.sln (100%) rename samples/{OcelotEureka => Eureka}/README.md (100%) rename samples/{OcelotGraphQL/OcelotGraphQL.csproj => GraphQL/Ocelot.Samples.GraphQL.csproj} (100%) rename samples/{OcelotGraphQL => GraphQL}/OcelotGraphQL.sln (100%) rename samples/{OcelotGraphQL => GraphQL}/Program.cs (96%) rename samples/{OcelotGraphQL => GraphQL}/Properties/launchSettings.json (100%) rename samples/{OcelotGraphQL => GraphQL}/README.md (96%) rename samples/{OcelotGraphQL => GraphQL}/ocelot.json (96%) rename samples/{OcelotKube => Kubernetes}/.dockerignore (100%) rename samples/{OcelotKube => Kubernetes}/ApiGateway/Dockerfile (100%) rename samples/{OcelotKube/ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj => Kubernetes/ApiGateway/Ocelot.Samples.Kubernetes.ApiGateway.csproj} (100%) rename samples/{OcelotKube => Kubernetes}/ApiGateway/Program.cs (100%) rename samples/{OcelotKube => Kubernetes}/ApiGateway/Properties/launchSettings.json (100%) rename samples/{OcelotKube => Kubernetes}/ApiGateway/Startup.cs (100%) rename samples/{OcelotKube => Kubernetes}/ApiGateway/appsettings.Development.json (100%) rename samples/{OcelotKube/DownstreamService => Kubernetes/ApiGateway}/appsettings.json (100%) rename samples/{OcelotKube => Kubernetes}/ApiGateway/ocelot.json (95%) rename samples/{OcelotKube => Kubernetes}/Dockerfile (100%) rename samples/{OcelotKube => Kubernetes}/DownstreamService/Controllers/ValuesController.cs (100%) rename samples/{OcelotKube => Kubernetes}/DownstreamService/Controllers/WeatherForecastController.cs (100%) rename samples/{OcelotKube => Kubernetes}/DownstreamService/Dockerfile (100%) rename samples/{OcelotKube => Kubernetes}/DownstreamService/Models/WeatherForecast.cs (100%) rename samples/{OcelotKube/DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj => Kubernetes/DownstreamService/Ocelot.Samples.Kubernetes.DownstreamService.csproj} (86%) rename samples/{OcelotKube => Kubernetes}/DownstreamService/Program.cs (100%) rename samples/{OcelotKube => Kubernetes}/DownstreamService/Properties/launchSettings.json (100%) rename samples/{OcelotKube => Kubernetes}/DownstreamService/appsettings.Development.json (100%) rename samples/{OcelotBasic => Kubernetes/DownstreamService}/appsettings.json (92%) rename samples/{OcelotKube => Kubernetes}/OcelotKube.sln (100%) create mode 100644 samples/Ocelot.Samples.sln delete mode 100644 samples/OcelotServiceDiscovery/DownstreamService/.dockerignore delete mode 100644 samples/OcelotServiceDiscovery/DownstreamService/Dockerfile delete mode 100644 samples/OcelotServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj rename samples/{OcelotOpenTracing/OcelotOpenTracing.csproj => OpenTracing/Ocelot.Samples.OpenTracing.csproj} (100%) rename samples/{OcelotOpenTracing => OpenTracing}/Program.cs (100%) rename samples/{OcelotOpenTracing => OpenTracing}/appsettings.Development.json (100%) rename samples/{OcelotOpenTracing => OpenTracing}/appsettings.json (100%) rename samples/{OcelotOpenTracing => OpenTracing}/ocelot.json (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/.dockerignore (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj (59%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/ApiGateway/Program.cs (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/ApiGateway/Properties/launchSettings.json (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/ApiGateway/appsettings.json (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/ApiGateway/ocelot.json (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/DownstreamService/Controllers/CategoriesController.cs (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/DownstreamService/Controllers/HealthController.cs (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/DownstreamService/Controllers/WeatherForecastController.cs (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/DownstreamService/Models/HealthResult.cs (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/DownstreamService/Models/MicroserviceResult.cs (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/DownstreamService/Models/ReadyResult.cs (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/DownstreamService/Models/WeatherForecast.cs (100%) create mode 100644 samples/ServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/DownstreamService/Program.cs (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/DownstreamService/Properties/launchSettings.json (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/DownstreamService/Startup.cs (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/DownstreamService/appsettings.Development.json (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/DownstreamService/appsettings.json (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/Ocelot.Samples.ServiceDiscovery.sln (100%) rename samples/{OcelotServiceDiscovery => ServiceDiscovery}/README.md (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/.gitignore (100%) rename samples/{OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj => ServiceFabric/ApiGateway/Ocelot.Samples.ServiceFabric.ApiGateway.csproj} (92%) rename samples/{OcelotServiceFabric/src/OcelotApplicationApiGateway => ServiceFabric/ApiGateway}/OcelotApplicationApiGateway.cs (100%) rename samples/{OcelotServiceFabric/src/OcelotApplicationApiGateway => ServiceFabric/ApiGateway}/Program.cs (100%) rename samples/{OcelotServiceFabric/src/OcelotApplicationApiGateway => ServiceFabric/ApiGateway}/Properties/launchSettings.json (100%) rename samples/{OcelotServiceFabric/src/OcelotApplicationApiGateway => ServiceFabric/ApiGateway}/ServiceEventListener.cs (100%) rename samples/{OcelotServiceFabric/src/OcelotApplicationApiGateway => ServiceFabric/ApiGateway}/ServiceEventSource.cs (100%) rename samples/{OcelotServiceFabric/src/OcelotApplicationApiGateway => ServiceFabric/ApiGateway}/WebCommunicationListener.cs (100%) rename samples/{OcelotServiceFabric/src/OcelotApplicationApiGateway => ServiceFabric/ApiGateway}/appsettings.json (100%) rename samples/{OcelotServiceFabric/src/OcelotApplicationApiGateway => ServiceFabric/ApiGateway}/ocelot.json (95%) rename samples/{OcelotServiceFabric => ServiceFabric}/CONTRIBUTING.md (100%) rename samples/{OcelotServiceFabric/src/OcelotApplicationService => ServiceFabric/DownstreamService}/ApiGateway.cs (100%) rename samples/{OcelotServiceFabric/src/OcelotApplicationService => ServiceFabric/DownstreamService}/Controllers/ValuesController.cs (100%) rename samples/{OcelotServiceFabric/src/OcelotApplicationService/OcelotApplicationService.csproj => ServiceFabric/DownstreamService/Ocelot.Samples.ServiceFabric.DownstreamService.csproj} (100%) rename samples/{OcelotServiceFabric/src/OcelotApplicationService => ServiceFabric/DownstreamService}/Program.cs (100%) rename samples/{OcelotServiceFabric/src/OcelotApplicationService => ServiceFabric/DownstreamService}/Properties/launchSettings.json (100%) rename samples/{OcelotServiceFabric/src/OcelotApplicationService => ServiceFabric/DownstreamService}/ServiceEventSource.cs (100%) rename samples/{OcelotServiceFabric/src/OcelotApplicationService => ServiceFabric/DownstreamService}/Startup.cs (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/LICENSE.md (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/ApplicationManifest.xml (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.cmd (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.sh (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/Settings.xml (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/_readme.txt (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/OcelotApplicationApiGatewayPkg/Data/_readme.txt (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Linux.xml (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Windows.xml (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest.xml (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.cmd (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.sh (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/OcelotApplicationServicePkg/Config/Settings.xml (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/OcelotApplicationServicePkg/Config/_readme.txt (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/OcelotApplicationServicePkg/Data/_readme.txt (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Linux.xml (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Windows.xml (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest.xml (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/README.md (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/build.bat (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/build.sh (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/dotnet-include.sh (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/install.ps1 (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/install.sh (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/uninstall.ps1 (100%) rename samples/{OcelotServiceFabric => ServiceFabric}/uninstall.sh (100%) rename {samples/Docker => test/Ocelot.ManualTest}/Dockerfile (98%) rename {samples/Docker-Compose => test/Ocelot.ManualTest}/docker-compose.yaml (95%) delete mode 100644 test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs diff --git a/.circleci/config.yml b/.circleci/config.yml index cbdcf7c9d..876d46b5c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,10 +1,11 @@ version: 2.1 orbs: - queue: eddiewebb/queue@2.2.1 + queue: eddiewebb/queue@3.1.4 jobs: build: docker: - image: ocelot2/circleci-build:latest + resource_class: medium+ steps: - checkout - run: dotnet tool restore && dotnet cake @@ -18,12 +19,12 @@ workflows: version: 2 main: jobs: - - queue/block_workflow: - time: '20' - only-on-branch: main + # - queue/block_workflow: + # time: '20' + # only-on-branch: main - release: - requires: - - queue/block_workflow + # requires: + # - queue/block_workflow filters: branches: only: main @@ -33,7 +34,7 @@ workflows: filters: branches: only: develop - pr: + PR: jobs: - build: filters: diff --git a/.editorconfig b/.editorconfig index e4e769b52..e8766a5e0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,15 +1,246 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories root = true -[*] -end_of_line = lf -insert_final_newline = true +# XML files +[*.xml] +indent_style = space +indent_size = 2 +# C# files [*.cs] -end_of_line = lf -indent_style = space -indent_size = 4 -# XML files -[*.xml] +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 indent_style = space -indent_size = 2 +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = false:suggestion +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_primary_constructors = false:suggestion +csharp_style_prefer_top_level_statements = true:silent + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +insert_final_newline = true +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index 4fe1ec98f..000000000 --- a/Directory.Build.props +++ /dev/null @@ -1,15 +0,0 @@ - - - latest - git - https://github.com/ThreeMammals/Ocelot - - true - - true - snupkg - - - - - diff --git a/Ocelot.Release.sln b/Ocelot.Release.sln new file mode 100644 index 000000000..20a76ee83 --- /dev/null +++ b/Ocelot.Release.sln @@ -0,0 +1,231 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5CFB79B7-C9DC-45A4-9A75-625D92471702}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3FA7C349-DBE8-4904-A2CE-015B8869CE6C}" + ProjectSection(SolutionItems) = preProject + .dockerignore = .dockerignore + .editorconfig = .editorconfig + .gitignore = .gitignore + .readthedocs.yaml = .readthedocs.yaml + build.cake = build.cake + build.ps1 = build.ps1 + codeanalysis.ruleset = codeanalysis.ruleset + .circleci\config.yml = .circleci\config.yml + GitVersion.yml = GitVersion.yml + LICENSE.md = LICENSE.md + README.md = README.md + ReleaseNotes.md = ReleaseNotes.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5B401523-36DA-4491-B73A-7590A26E420B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.UnitTests", "test\Ocelot.UnitTests\Ocelot.UnitTests.csproj", "{54E84F1A-E525-4443-96EC-039CBD50C263}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.AcceptanceTests", "test\Ocelot.AcceptanceTests\Ocelot.AcceptanceTests.csproj", "{F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.ManualTest", "test\Ocelot.ManualTest\Ocelot.ManualTest.csproj", "{02BBF4C5-517E-4157-8D21-4B8B9E118B7A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Benchmarks", "test\Ocelot.Benchmarks\Ocelot.Benchmarks.csproj", "{106B49E6-95F6-4A7B-B81C-96BFA74AF035}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.IntegrationTests", "test\Ocelot.IntegrationTests\Ocelot.IntegrationTests.csproj", "{D4575572-99CA-4530-8737-C296EDA326F8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Testing", "test\Ocelot.Testing\Ocelot.Testing.csproj", "{AE6BCCBD-0687-4C58-B30F-4ABBC6422087}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot", "src\Ocelot\Ocelot.csproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Administration", "src\Ocelot.Administration\Ocelot.Administration.csproj", "{F69CEF43-27D2-4940-A47A-FCA879E371BC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Cache.CacheManager", "src\Ocelot.Cache.CacheManager\Ocelot.Cache.CacheManager.csproj", "{EB9F438F-062E-499F-B6EA-4412BEF6D74C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Consul", "src\Ocelot.Provider.Consul\Ocelot.Provider.Consul.csproj", "{02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Eureka", "src\Ocelot.Provider.Eureka\Ocelot.Provider.Eureka.csproj", "{9BBD3586-145C-4FA0-91C5-9ED58287D753}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Kubernetes", "src\Ocelot.Provider.Kubernetes\Ocelot.Provider.Kubernetes.csproj", "{72C8E528-B4F5-45CE-8A06-CD3787364856}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Polly", "src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj", "{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.Butterfly", "src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj", "{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.OpenTracing", "src\Ocelot.Tracing.OpenTracing\Ocelot.Tracing.OpenTracing.csproj", "{11C622AD-8C0A-4CF4-811B-3DBB76550797}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "samples", "{8FA0CBA0-0338-48EB-B37F-83CA5022237C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.AdministrationApi", "samples\Administration\Ocelot.Samples.AdministrationApi.csproj", "{A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Basic.ApiGateway", "samples\Basic\Ocelot.Samples.Basic.ApiGateway.csproj", "{F00C73F4-019D-490D-8194-CA1754D717FA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Eureka.ApiGateway", "samples\Eureka\ApiGateway\Ocelot.Samples.Eureka.ApiGateway.csproj", "{FECB0C8B-5778-4441-B10E-0C815F5106D5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Eureka.DownstreamService", "samples\Eureka\DownstreamService\Ocelot.Samples.Eureka.DownstreamService.csproj", "{28AD7065-8DB1-4711-83BF-9EA47D75F8F7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.GraphQL", "samples\GraphQL\Ocelot.Samples.GraphQL.csproj", "{869EE931-7E4A-40AA-ADDD-D20DF34C3BB3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Kubernetes.ApiGateway", "samples\Kubernetes\ApiGateway\Ocelot.Samples.Kubernetes.ApiGateway.csproj", "{681B6E08-114D-4B9B-8F82-E370CA29B8EC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Kubernetes.DownstreamService", "samples\Kubernetes\DownstreamService\Ocelot.Samples.Kubernetes.DownstreamService.csproj", "{161DD558-993D-491B-AD20-966127D71E49}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OpenTracing", "samples\OpenTracing\Ocelot.Samples.OpenTracing.csproj", "{DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.ApiGateway", "samples\ServiceDiscovery\ApiGateway\Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj", "{F25EA682-A763-431B-9D88-012A388D3618}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.DownstreamService", "samples\ServiceDiscovery\DownstreamService\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "{DCBD0AB5-85DD-4F28-9166-0A23969E19EC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceFabric.ApiGateway", "samples\ServiceFabric\ApiGateway\Ocelot.Samples.ServiceFabric.ApiGateway.csproj", "{D991C694-01F0-4F04-8135-5C133DC8E029}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceFabric.DownstreamService", "samples\ServiceFabric\DownstreamService\Ocelot.Samples.ServiceFabric.DownstreamService.csproj", "{AD09D124-7DD7-4C9E-9BCC-782B579B1786}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.Build.0 = Release|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Release|Any CPU.Build.0 = Release|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Release|Any CPU.Build.0 = Release|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Release|Any CPU.Build.0 = Release|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Debug|Any CPU.Build.0 = Debug|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Release|Any CPU.ActiveCfg = Release|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Release|Any CPU.Build.0 = Release|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.Build.0 = Release|Any CPU + {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Release|Any CPU.Build.0 = Release|Any CPU + {EB9F438F-062E-499F-B6EA-4412BEF6D74C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB9F438F-062E-499F-B6EA-4412BEF6D74C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB9F438F-062E-499F-B6EA-4412BEF6D74C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB9F438F-062E-499F-B6EA-4412BEF6D74C}.Release|Any CPU.Build.0 = Release|Any CPU + {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0}.Release|Any CPU.Build.0 = Release|Any CPU + {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Release|Any CPU.Build.0 = Release|Any CPU + {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Release|Any CPU.Build.0 = Release|Any CPU + {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.Build.0 = Release|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.Build.0 = Release|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Release|Any CPU.Build.0 = Release|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.Build.0 = Release|Any CPU + {A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22}.Release|Any CPU.Build.0 = Release|Any CPU + {F00C73F4-019D-490D-8194-CA1754D717FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F00C73F4-019D-490D-8194-CA1754D717FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F00C73F4-019D-490D-8194-CA1754D717FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F00C73F4-019D-490D-8194-CA1754D717FA}.Release|Any CPU.Build.0 = Release|Any CPU + {FECB0C8B-5778-4441-B10E-0C815F5106D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FECB0C8B-5778-4441-B10E-0C815F5106D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FECB0C8B-5778-4441-B10E-0C815F5106D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FECB0C8B-5778-4441-B10E-0C815F5106D5}.Release|Any CPU.Build.0 = Release|Any CPU + {28AD7065-8DB1-4711-83BF-9EA47D75F8F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28AD7065-8DB1-4711-83BF-9EA47D75F8F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28AD7065-8DB1-4711-83BF-9EA47D75F8F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28AD7065-8DB1-4711-83BF-9EA47D75F8F7}.Release|Any CPU.Build.0 = Release|Any CPU + {869EE931-7E4A-40AA-ADDD-D20DF34C3BB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {869EE931-7E4A-40AA-ADDD-D20DF34C3BB3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {869EE931-7E4A-40AA-ADDD-D20DF34C3BB3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {869EE931-7E4A-40AA-ADDD-D20DF34C3BB3}.Release|Any CPU.Build.0 = Release|Any CPU + {681B6E08-114D-4B9B-8F82-E370CA29B8EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {681B6E08-114D-4B9B-8F82-E370CA29B8EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {681B6E08-114D-4B9B-8F82-E370CA29B8EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {681B6E08-114D-4B9B-8F82-E370CA29B8EC}.Release|Any CPU.Build.0 = Release|Any CPU + {161DD558-993D-491B-AD20-966127D71E49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {161DD558-993D-491B-AD20-966127D71E49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {161DD558-993D-491B-AD20-966127D71E49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {161DD558-993D-491B-AD20-966127D71E49}.Release|Any CPU.Build.0 = Release|Any CPU + {DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8}.Release|Any CPU.Build.0 = Release|Any CPU + {F25EA682-A763-431B-9D88-012A388D3618}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F25EA682-A763-431B-9D88-012A388D3618}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F25EA682-A763-431B-9D88-012A388D3618}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F25EA682-A763-431B-9D88-012A388D3618}.Release|Any CPU.Build.0 = Release|Any CPU + {DCBD0AB5-85DD-4F28-9166-0A23969E19EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DCBD0AB5-85DD-4F28-9166-0A23969E19EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DCBD0AB5-85DD-4F28-9166-0A23969E19EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DCBD0AB5-85DD-4F28-9166-0A23969E19EC}.Release|Any CPU.Build.0 = Release|Any CPU + {D991C694-01F0-4F04-8135-5C133DC8E029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D991C694-01F0-4F04-8135-5C133DC8E029}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D991C694-01F0-4F04-8135-5C133DC8E029}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D991C694-01F0-4F04-8135-5C133DC8E029}.Release|Any CPU.Build.0 = Release|Any CPU + {AD09D124-7DD7-4C9E-9BCC-782B579B1786}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD09D124-7DD7-4C9E-9BCC-782B579B1786}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD09D124-7DD7-4C9E-9BCC-782B579B1786}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD09D124-7DD7-4C9E-9BCC-782B579B1786}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {54E84F1A-E525-4443-96EC-039CBD50C263} = {5B401523-36DA-4491-B73A-7590A26E420B} + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52} = {5B401523-36DA-4491-B73A-7590A26E420B} + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A} = {5B401523-36DA-4491-B73A-7590A26E420B} + {106B49E6-95F6-4A7B-B81C-96BFA74AF035} = {5B401523-36DA-4491-B73A-7590A26E420B} + {D4575572-99CA-4530-8737-C296EDA326F8} = {5B401523-36DA-4491-B73A-7590A26E420B} + {F69CEF43-27D2-4940-A47A-FCA879E371BC} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {EB9F438F-062E-499F-B6EA-4412BEF6D74C} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {9BBD3586-145C-4FA0-91C5-9ED58287D753} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {6045E23D-669C-4F27-AF8E-8EEE6DB3557F} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {72C8E528-B4F5-45CE-8A06-CD3787364856} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {11C622AD-8C0A-4CF4-811B-3DBB76550797} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087} = {5B401523-36DA-4491-B73A-7590A26E420B} + {A7F0CAFA-AECB-43CA-BE89-5F5B728E7C22} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {F00C73F4-019D-490D-8194-CA1754D717FA} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {FECB0C8B-5778-4441-B10E-0C815F5106D5} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {28AD7065-8DB1-4711-83BF-9EA47D75F8F7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {869EE931-7E4A-40AA-ADDD-D20DF34C3BB3} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {681B6E08-114D-4B9B-8F82-E370CA29B8EC} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {161DD558-993D-491B-AD20-966127D71E49} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {DF9EFF21-58D3-428D-8A33-ACFA24E9B6E8} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {F25EA682-A763-431B-9D88-012A388D3618} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {DCBD0AB5-85DD-4F28-9166-0A23969E19EC} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {D991C694-01F0-4F04-8135-5C133DC8E029} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {AD09D124-7DD7-4C9E-9BCC-782B579B1786} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48} + EndGlobalSection +EndGlobal diff --git a/Ocelot.sln b/Ocelot.sln index e40f83cfb..f09456c44 100644 --- a/Ocelot.sln +++ b/Ocelot.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.8.34309.116 +VisualStudioVersion = 17.9.34728.123 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5CFB79B7-C9DC-45A4-9A75-625D92471702}" EndProject @@ -23,8 +22,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5B401523-36DA-4491-B73A-7590A26E420B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot", "src\Ocelot\Ocelot.csproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.UnitTests", "test\Ocelot.UnitTests\Ocelot.UnitTests.csproj", "{54E84F1A-E525-4443-96EC-039CBD50C263}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.AcceptanceTests", "test\Ocelot.AcceptanceTests\Ocelot.AcceptanceTests.csproj", "{F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}" @@ -35,6 +32,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Benchmarks", "test\O EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.IntegrationTests", "test\Ocelot.IntegrationTests\Ocelot.IntegrationTests.csproj", "{D4575572-99CA-4530-8737-C296EDA326F8}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Testing", "test\Ocelot.Testing\Ocelot.Testing.csproj", "{AE6BCCBD-0687-4C58-B30F-4ABBC6422087}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot", "src\Ocelot\Ocelot.csproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Administration", "src\Ocelot.Administration\Ocelot.Administration.csproj", "{F69CEF43-27D2-4940-A47A-FCA879E371BC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Cache.CacheManager", "src\Ocelot.Cache.CacheManager\Ocelot.Cache.CacheManager.csproj", "{EB9F438F-062E-499F-B6EA-4412BEF6D74C}" @@ -43,68 +44,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Consul", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Eureka", "src\Ocelot.Provider.Eureka\Ocelot.Provider.Eureka.csproj", "{9BBD3586-145C-4FA0-91C5-9ED58287D753}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Polly", "src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj", "{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.Butterfly", "src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj", "{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Kubernetes", "src\Ocelot.Provider.Kubernetes\Ocelot.Provider.Kubernetes.csproj", "{72C8E528-B4F5-45CE-8A06-CD3787364856}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{8FA0CBA0-0338-48EB-B37F-83CA5022237C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OcelotBasic.ApiGateway", "samples\OcelotBasic\Ocelot.Samples.OcelotBasic.ApiGateway.csproj", "{ED0B3A09-112B-4BA4-82D6-11569BC7A99B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdministrationApi", "samples\AdministrationApi\AdministrationApi.csproj", "{B180F8AE-2F8F-44F9-9E5D-FE65B84B742E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotGraphQL", "samples\OcelotGraphQL\OcelotGraphQL.csproj", "{F43429C3-EC49-464F-9423-9118A36E8FE3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eureka", "eureka", "{F1CF6F06-5A34-4A6A-8C19-003A78AB0DCF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiGateway", "samples\OcelotEureka\ApiGateway\ApiGateway.csproj", "{48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DownstreamService", "samples\OcelotEureka\DownstreamService\DownstreamService.csproj", "{32ADF9B3-CBFA-4607-8A8E-1532D90A7197}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "k8s", "k8s", "{4B706988-4817-43A8-ABE1-32A67998C2C8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OcelotKube.ApiGateway", "samples\OcelotKube\ApiGateway\Ocelot.Samples.OcelotKube.ApiGateway.csproj", "{8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OcelotKube.DownstreamService", "samples\OcelotKube\DownstreamService\Ocelot.Samples.OcelotKube.DownstreamService.csproj", "{7B319B8C-8155-4779-BD93-5ABD05CA2AB6}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "service-fabric", "service-fabric", "{B412628F-C325-47E1-A8D9-873DE04C8AF5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotApplicationApiGateway", "samples\OcelotServiceFabric\src\OcelotApplicationApiGateway\OcelotApplicationApiGateway.csproj", "{8E6DAE6E-E9B1-433A-80C3-1E2640FBA590}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotApplicationService", "samples\OcelotServiceFabric\src\OcelotApplicationService\OcelotApplicationService.csproj", "{33BE6D88-F188-4E60-83AC-3C4B94D24675}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "administration", "administration", "{1F1F324D-6EA4-4E63-A6A7-C6053F412F1A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "basic", "basic", "{ED066001-BAF7-4117-9884-DF591A56347D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Polly", "src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj", "{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "graphql", "graphql", "{C15CD120-5F8D-41DE-9B21-00E3EA77D6C1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.Butterfly", "src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj", "{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.OpenTracing", "src\Ocelot.Tracing.OpenTracing\Ocelot.Tracing.OpenTracing.csproj", "{11C622AD-8C0A-4CF4-811B-3DBB76550797}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "open-tracing", "open-tracing", "{731C6A8A-69ED-445C-A132-C638AA93F9C7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotOpenTracing", "samples\OcelotOpenTracing\OcelotOpenTracing.csproj", "{C9427E78-4281-4F59-A66E-17C0B66550E5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "service-discovery", "service-discovery", "{25C30AAA-12DD-4BA5-A53F-9271E54EBAB7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.ApiGateway", "samples\OcelotServiceDiscovery\ApiGateway\Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj", "{D37209EA-C13E-42AE-B851-A8604F1FCD0E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.DownstreamService", "samples\OcelotServiceDiscovery\DownstreamService\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "{E2AC741A-4120-4D59-B5E4-16382ED45E8D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Testing", "test\Ocelot.Testing\Ocelot.Testing.csproj", "{AE6BCCBD-0687-4C58-B30F-4ABBC6422087}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.Build.0 = Release|Any CPU {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.Build.0 = Debug|Any CPU {54E84F1A-E525-4443-96EC-039CBD50C263}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -125,6 +78,14 @@ Global {D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.Build.0 = Debug|Any CPU {D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.Build.0 = Release|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.Build.0 = Release|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.Build.0 = Release|Any CPU {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Debug|Any CPU.Build.0 = Debug|Any CPU {F69CEF43-27D2-4940-A47A-FCA879E371BC}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -141,6 +102,10 @@ Global {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Debug|Any CPU.Build.0 = Debug|Any CPU {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Release|Any CPU.ActiveCfg = Release|Any CPU {9BBD3586-145C-4FA0-91C5-9ED58287D753}.Release|Any CPU.Build.0 = Release|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.Build.0 = Release|Any CPU {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Debug|Any CPU.Build.0 = Debug|Any CPU {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -149,106 +114,30 @@ Global {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Debug|Any CPU.Build.0 = Debug|Any CPU {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.ActiveCfg = Release|Any CPU {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.Build.0 = Release|Any CPU - {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {72C8E528-B4F5-45CE-8A06-CD3787364856}.Debug|Any CPU.Build.0 = Debug|Any CPU - {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.ActiveCfg = Release|Any CPU - {72C8E528-B4F5-45CE-8A06-CD3787364856}.Release|Any CPU.Build.0 = Release|Any CPU - {ED0B3A09-112B-4BA4-82D6-11569BC7A99B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ED0B3A09-112B-4BA4-82D6-11569BC7A99B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ED0B3A09-112B-4BA4-82D6-11569BC7A99B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ED0B3A09-112B-4BA4-82D6-11569BC7A99B}.Release|Any CPU.Build.0 = Release|Any CPU - {B180F8AE-2F8F-44F9-9E5D-FE65B84B742E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B180F8AE-2F8F-44F9-9E5D-FE65B84B742E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B180F8AE-2F8F-44F9-9E5D-FE65B84B742E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B180F8AE-2F8F-44F9-9E5D-FE65B84B742E}.Release|Any CPU.Build.0 = Release|Any CPU - {F43429C3-EC49-464F-9423-9118A36E8FE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F43429C3-EC49-464F-9423-9118A36E8FE3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F43429C3-EC49-464F-9423-9118A36E8FE3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F43429C3-EC49-464F-9423-9118A36E8FE3}.Release|Any CPU.Build.0 = Release|Any CPU - {48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29}.Debug|Any CPU.Build.0 = Debug|Any CPU - {48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29}.Release|Any CPU.ActiveCfg = Release|Any CPU - {48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29}.Release|Any CPU.Build.0 = Release|Any CPU - {32ADF9B3-CBFA-4607-8A8E-1532D90A7197}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {32ADF9B3-CBFA-4607-8A8E-1532D90A7197}.Debug|Any CPU.Build.0 = Debug|Any CPU - {32ADF9B3-CBFA-4607-8A8E-1532D90A7197}.Release|Any CPU.ActiveCfg = Release|Any CPU - {32ADF9B3-CBFA-4607-8A8E-1532D90A7197}.Release|Any CPU.Build.0 = Release|Any CPU - {8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}.Release|Any CPU.Build.0 = Release|Any CPU - {7B319B8C-8155-4779-BD93-5ABD05CA2AB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7B319B8C-8155-4779-BD93-5ABD05CA2AB6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7B319B8C-8155-4779-BD93-5ABD05CA2AB6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7B319B8C-8155-4779-BD93-5ABD05CA2AB6}.Release|Any CPU.Build.0 = Release|Any CPU - {8E6DAE6E-E9B1-433A-80C3-1E2640FBA590}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8E6DAE6E-E9B1-433A-80C3-1E2640FBA590}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8E6DAE6E-E9B1-433A-80C3-1E2640FBA590}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8E6DAE6E-E9B1-433A-80C3-1E2640FBA590}.Release|Any CPU.Build.0 = Release|Any CPU - {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Debug|Any CPU.Build.0 = Debug|Any CPU - {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Release|Any CPU.ActiveCfg = Release|Any CPU - {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Release|Any CPU.Build.0 = Release|Any CPU {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Debug|Any CPU.Build.0 = Debug|Any CPU {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Release|Any CPU.ActiveCfg = Release|Any CPU {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Release|Any CPU.Build.0 = Release|Any CPU - {C9427E78-4281-4F59-A66E-17C0B66550E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C9427E78-4281-4F59-A66E-17C0B66550E5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C9427E78-4281-4F59-A66E-17C0B66550E5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C9427E78-4281-4F59-A66E-17C0B66550E5}.Release|Any CPU.Build.0 = Release|Any CPU - {D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Release|Any CPU.Build.0 = Release|Any CPU - {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Release|Any CPU.Build.0 = Release|Any CPU - {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {D6DF4206-0DBA-41D8-884D-C3E08290FDBB} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {54E84F1A-E525-4443-96EC-039CBD50C263} = {5B401523-36DA-4491-B73A-7590A26E420B} {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52} = {5B401523-36DA-4491-B73A-7590A26E420B} {02BBF4C5-517E-4157-8D21-4B8B9E118B7A} = {5B401523-36DA-4491-B73A-7590A26E420B} {106B49E6-95F6-4A7B-B81C-96BFA74AF035} = {5B401523-36DA-4491-B73A-7590A26E420B} {D4575572-99CA-4530-8737-C296EDA326F8} = {5B401523-36DA-4491-B73A-7590A26E420B} + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087} = {5B401523-36DA-4491-B73A-7590A26E420B} + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {F69CEF43-27D2-4940-A47A-FCA879E371BC} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {EB9F438F-062E-499F-B6EA-4412BEF6D74C} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {9BBD3586-145C-4FA0-91C5-9ED58287D753} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {72C8E528-B4F5-45CE-8A06-CD3787364856} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {6045E23D-669C-4F27-AF8E-8EEE6DB3557F} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} - {72C8E528-B4F5-45CE-8A06-CD3787364856} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} - {ED0B3A09-112B-4BA4-82D6-11569BC7A99B} = {ED066001-BAF7-4117-9884-DF591A56347D} - {B180F8AE-2F8F-44F9-9E5D-FE65B84B742E} = {1F1F324D-6EA4-4E63-A6A7-C6053F412F1A} - {F43429C3-EC49-464F-9423-9118A36E8FE3} = {C15CD120-5F8D-41DE-9B21-00E3EA77D6C1} - {F1CF6F06-5A34-4A6A-8C19-003A78AB0DCF} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {48B3DD3C-7F4D-40C1-A104-3BF9EF4ACE29} = {F1CF6F06-5A34-4A6A-8C19-003A78AB0DCF} - {32ADF9B3-CBFA-4607-8A8E-1532D90A7197} = {F1CF6F06-5A34-4A6A-8C19-003A78AB0DCF} - {4B706988-4817-43A8-ABE1-32A67998C2C8} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {8500055B-2C51-4CF1-A6EE-F05BB3E9BF16} = {4B706988-4817-43A8-ABE1-32A67998C2C8} - {7B319B8C-8155-4779-BD93-5ABD05CA2AB6} = {4B706988-4817-43A8-ABE1-32A67998C2C8} - {B412628F-C325-47E1-A8D9-873DE04C8AF5} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {8E6DAE6E-E9B1-433A-80C3-1E2640FBA590} = {B412628F-C325-47E1-A8D9-873DE04C8AF5} - {33BE6D88-F188-4E60-83AC-3C4B94D24675} = {B412628F-C325-47E1-A8D9-873DE04C8AF5} - {1F1F324D-6EA4-4E63-A6A7-C6053F412F1A} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {ED066001-BAF7-4117-9884-DF591A56347D} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {C15CD120-5F8D-41DE-9B21-00E3EA77D6C1} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} {11C622AD-8C0A-4CF4-811B-3DBB76550797} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} - {731C6A8A-69ED-445C-A132-C638AA93F9C7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {C9427E78-4281-4F59-A66E-17C0B66550E5} = {731C6A8A-69ED-445C-A132-C638AA93F9C7} - {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} - {D37209EA-C13E-42AE-B851-A8604F1FCD0E} = {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} - {E2AC741A-4120-4D59-B5E4-16382ED45E8D} = {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} - {AE6BCCBD-0687-4C58-B30F-4ABBC6422087} = {5B401523-36DA-4491-B73A-7590A26E420B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48} diff --git a/build.cake b/build.cake index f7896932f..a8f7cc362 100644 --- a/build.cake +++ b/build.cake @@ -1,9 +1,9 @@ -#tool "dotnet:?package=GitVersion.Tool&version=5.8.1" -#tool "dotnet:?package=coveralls.net&version=4.0.1" -#addin nuget:?package=Newtonsoft.Json -#addin nuget:?package=System.Text.Encodings.Web&version=4.7.1 -#tool "nuget:?package=ReportGenerator&version=5.2.0" -#addin Cake.Coveralls&version=1.1.0 +#tool dotnet:?package=GitVersion.Tool&version=5.12.0 // 6.0.0-beta.7 supports .NET 8, 7, 6 +#tool dotnet:?package=coveralls.net&version=4.0.1 +#tool nuget:?package=ReportGenerator&version=5.2.4 +#addin nuget:?package=Newtonsoft.Json&version=13.0.3 +#addin nuget:?package=System.Text.Encodings.Web&version=8.0.0 +#addin nuget:?package=Cake.Coveralls&version=1.1.0 #r "Spectre.Console" using Spectre.Console @@ -13,10 +13,8 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; -// compile -var compileConfig = Argument("configuration", "Release"); - -var slnFile = "./Ocelot.sln"; +const string Release = "Release"; // task name, target, and Release config name +var compileConfig = Argument("configuration", Release); // compile // build artifacts var artifactsDir = Directory("artifacts"); @@ -61,9 +59,10 @@ string gitHubUsername = "TomPallister"; string gitHubPassword = Environment.GetEnvironmentVariable("OCELOT_GITHUB_API_KEY"); var target = Argument("target", "Default"); - -Information("target is " + target); -Information("Build configuration is " + compileConfig); +var slnFile = (target == Release) ? $"./Ocelot.{Release}.sln" : "./Ocelot.sln"; +Information("\nTarget: " + target); +Information("Build: " + compileConfig); +Information("Solution: " + slnFile); TaskTeardown(context => { AnsiConsole.Markup($"[green]DONE[/] {context.Task.Name}\n"); @@ -83,7 +82,7 @@ Task("RunTests") .IsDependentOn("RunAcceptanceTests") .IsDependentOn("RunIntegrationTests"); -Task("Release") +Task(Release) .IsDependentOn("Build") .IsDependentOn("CreateReleaseNotes") .IsDependentOn("CreateArtifacts") @@ -95,11 +94,18 @@ Task("Compile") .IsDependentOn("Version") .Does(() => { + Information("Build: " + compileConfig); + Information("Solution: " + slnFile); var settings = new DotNetBuildSettings { Configuration = compileConfig, }; - + if (target != Release) + { + settings.Framework = "net8.0"; // build using .NET 8 SDK only + } + Information($"Settings {nameof(DotNetBuildSettings.Framework)}: {settings.Framework}"); + Information($"Settings {nameof(DotNetBuildSettings.Configuration)}: {settings.Configuration}"); DotNetBuild(slnFile, settings); }); @@ -344,15 +350,23 @@ Task("RunUnitTests") { Configuration = compileConfig, ResultsDirectory = artifactsForUnitTestsDir, - ArgumentCustomization = args => args - // this create the code coverage report - .Append("--collect:\"XPlat Code Coverage\"") + ArgumentCustomization = args => args + .Append("--no-restore") + .Append("--no-build") + .Append("--collect:\"XPlat Code Coverage\"") // this create the code coverage report + .Append("--verbosity:detailed") + .Append("--consoleLoggerParameters:ErrorsOnly") }; - + if (target != Release) + { + testSettings.Framework = "net8.0"; // .NET 8 SDK only + } EnsureDirectoryExists(artifactsForUnitTestsDir); DotNetTest(unitTestAssemblies, testSettings); - var coverageSummaryFile = GetSubDirectories(artifactsForUnitTestsDir).First().CombineWithFilePath(File("coverage.cobertura.xml")); + var coverageSummaryFile = GetSubDirectories(artifactsForUnitTestsDir) + .First() + .CombineWithFilePath(File("coverage.cobertura.xml")); Information(coverageSummaryFile); Information(artifactsForUnitTestsDir); @@ -396,11 +410,15 @@ Task("RunAcceptanceTests") var settings = new DotNetTestSettings { Configuration = compileConfig, + Framework = "net8.0", // .NET 8 SDK only ArgumentCustomization = args => args .Append("--no-restore") .Append("--no-build") }; - + if (target != Release) + { + settings.Framework = "net8.0"; // .NET 8 SDK only + } EnsureDirectoryExists(artifactsForAcceptanceTestsDir); DotNetTest(acceptanceTestAssemblies, settings); }); @@ -412,11 +430,15 @@ Task("RunIntegrationTests") var settings = new DotNetTestSettings { Configuration = compileConfig, + Framework = "net8.0", // .NET 8 SDK only ArgumentCustomization = args => args .Append("--no-restore") .Append("--no-build") }; - + if (target != Release) + { + settings.Framework = "net8.0"; // .NET 8 SDK only + } EnsureDirectoryExists(artifactsForIntegrationTestsDir); DotNetTest(integrationTestAssemblies, settings); }); diff --git a/samples/AdministrationApi/Issue645.postman_collection.json b/samples/Administration/Issue645.postman_collection.json similarity index 97% rename from samples/AdministrationApi/Issue645.postman_collection.json rename to samples/Administration/Issue645.postman_collection.json index c9bac89f5..6fedd16b9 100644 --- a/samples/AdministrationApi/Issue645.postman_collection.json +++ b/samples/Administration/Issue645.postman_collection.json @@ -1,150 +1,150 @@ -{ - "info": { - "_postman_id": "6234b40a-e363-4c73-8577-1c9074abb951", - "name": "Issue645", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "1. GET http://localhost: 55580/administration/.well-known/openid-configuration", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{AccessToken}}" - } - ], - "body": {}, - "url": { - "raw": "http://localhost:5000/administration/.well-known/openid-configuration", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "5000", - "path": [ - "administration", - ".well-known", - "openid-configuration" - ] - } - }, - "response": [] - }, - { - "name": "3. GET http://localhost: 55580/administration/configuration", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{AccessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"routes\": [\r\n {\r\n \"downstreamPathTemplate\": \"/{everything}\",\r\n \"upstreamPathTemplate\": \"/templates/{everything}\",\r\n \"upstreamHttpMethod\": [\r\n \"GET\"\r\n ],\r\n \"addHeadersToRequest\": {},\r\n \"upstreamHeaderTransform\": {},\r\n \"downstreamHeaderTransform\": {},\r\n \"addClaimsToRequest\": {},\r\n \"routeClaimsRequirement\": {},\r\n \"addQueriesToRequest\": {},\r\n \"requestIdKey\": null,\r\n \"fileCacheOptions\": {\r\n \"ttlSeconds\": 0,\r\n \"region\": null\r\n },\r\n \"routeIsCaseSensitive\": false,\r\n \"downstreamScheme\": \"http\",\r\n \"qoSOptions\": {\r\n \"exceptionsAllowedBeforeBreaking\": 0,\r\n \"durationOfBreak\": 0,\r\n \"timeoutValue\": 0\r\n },\r\n \"loadBalancerOptions\": {\r\n \"type\": null,\r\n \"key\": null,\r\n \"expiry\": 0\r\n },\r\n \"rateLimitOptions\": {\r\n \"clientWhitelist\": [],\r\n \"enableRateLimiting\": false,\r\n \"period\": null,\r\n \"periodTimespan\": 0,\r\n \"limit\": 0\r\n },\r\n \"authenticationOptions\": {\r\n \"authenticationProviderKey\": null,\r\n \"allowedScopes\": []\r\n },\r\n \"httpHandlerOptions\": {\r\n \"allowAutoRedirect\": false,\r\n \"useCookieContainer\": false,\r\n \"useTracing\": false,\r\n \"useProxy\": true\r\n },\r\n \"downstreamHostAndPorts\": [\r\n {\r\n \"host\": \"localhost\",\r\n \"port\": 50689\r\n }\r\n ],\r\n \"upstreamHost\": null,\r\n \"key\": null,\r\n \"delegatingHandlers\": [],\r\n \"priority\": 1,\r\n \"timeout\": 0,\r\n \"dangerousAcceptAnyServerCertificateValidator\": false\r\n }\r\n ],\r\n \"aggregates\": [],\r\n \"globalConfiguration\": {\r\n \"requestIdKey\": \"Request-Id\",\r\n \"rateLimitOptions\": {\r\n \"clientIdHeader\": \"ClientId\",\r\n \"quotaExceededMessage\": null,\r\n \"rateLimitCounterPrefix\": \"ocelot\",\r\n \"disableRateLimitHeaders\": false,\r\n \"httpStatusCode\": 429\r\n },\r\n \"qoSOptions\": {\r\n \"exceptionsAllowedBeforeBreaking\": 0,\r\n \"durationOfBreak\": 0,\r\n \"timeoutValue\": 0\r\n },\r\n \"baseUrl\": \"http://localhost:55580\",\r\n \"loadBalancerOptions\": {\r\n \"type\": null,\r\n \"key\": null,\r\n \"expiry\": 0\r\n },\r\n \"downstreamScheme\": null,\r\n \"httpHandlerOptions\": {\r\n \"allowAutoRedirect\": false,\r\n \"useCookieContainer\": false,\r\n \"useTracing\": false,\r\n \"useProxy\": true\r\n }\r\n }\r\n}" - }, - "url": { - "raw": "http://localhost:5000/administration/configuration", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "5000", - "path": [ - "administration", - "configuration" - ] - } - }, - "response": [] - }, - { - "name": "2. POST http://localhost: 55580/administration/connect/token", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var jsonData = JSON.parse(responseBody);", - "postman.setGlobalVariable(\"AccessToken\", jsonData.access_token);", - "postman.setGlobalVariable(\"RefreshToken\", jsonData.refresh_token);" - ] - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "client_id", - "value": "admin", - "type": "text" - }, - { - "key": "client_secret", - "value": "secret", - "type": "text" - }, - { - "key": "scope", - "value": "admin", - "type": "text" - }, - { - "key": "grant_type", - "value": "client_credentials", - "type": "text" - } - ] - }, - "url": { - "raw": "http://localhost:5000/administration/connect/token", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "5000", - "path": [ - "administration", - "connect", - "token" - ] - } - }, - "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "id": "0f60e7b3-e4f1-4458-bbc4-fc4809e86b2d", - "type": "text/javascript", - "exec": [ - string.Empty - ] - } - }, - { - "listen": "test", - "script": { - "id": "1279a2cf-b771-4a86-9dfa-302b240fac62", - "type": "text/javascript", - "exec": [ - string.Empty - ] - } - } - ] +{ + "info": { + "_postman_id": "6234b40a-e363-4c73-8577-1c9074abb951", + "name": "Issue645", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "1. GET http://localhost: 55580/administration/.well-known/openid-configuration", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{AccessToken}}" + } + ], + "body": {}, + "url": { + "raw": "http://localhost:5000/administration/.well-known/openid-configuration", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "administration", + ".well-known", + "openid-configuration" + ] + } + }, + "response": [] + }, + { + "name": "3. GET http://localhost: 55580/administration/configuration", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{AccessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"routes\": [\r\n {\r\n \"downstreamPathTemplate\": \"/{everything}\",\r\n \"upstreamPathTemplate\": \"/templates/{everything}\",\r\n \"upstreamHttpMethod\": [\r\n \"GET\"\r\n ],\r\n \"addHeadersToRequest\": {},\r\n \"upstreamHeaderTransform\": {},\r\n \"downstreamHeaderTransform\": {},\r\n \"addClaimsToRequest\": {},\r\n \"routeClaimsRequirement\": {},\r\n \"addQueriesToRequest\": {},\r\n \"requestIdKey\": null,\r\n \"fileCacheOptions\": {\r\n \"ttlSeconds\": 0,\r\n \"region\": null\r\n },\r\n \"routeIsCaseSensitive\": false,\r\n \"downstreamScheme\": \"http\",\r\n \"qoSOptions\": {\r\n \"exceptionsAllowedBeforeBreaking\": 0,\r\n \"durationOfBreak\": 0,\r\n \"timeoutValue\": 0\r\n },\r\n \"loadBalancerOptions\": {\r\n \"type\": null,\r\n \"key\": null,\r\n \"expiry\": 0\r\n },\r\n \"rateLimitOptions\": {\r\n \"clientWhitelist\": [],\r\n \"enableRateLimiting\": false,\r\n \"period\": null,\r\n \"periodTimespan\": 0,\r\n \"limit\": 0\r\n },\r\n \"authenticationOptions\": {\r\n \"authenticationProviderKey\": null,\r\n \"allowedScopes\": []\r\n },\r\n \"httpHandlerOptions\": {\r\n \"allowAutoRedirect\": false,\r\n \"useCookieContainer\": false,\r\n \"useTracing\": false,\r\n \"useProxy\": true\r\n },\r\n \"downstreamHostAndPorts\": [\r\n {\r\n \"host\": \"localhost\",\r\n \"port\": 50689\r\n }\r\n ],\r\n \"upstreamHost\": null,\r\n \"key\": null,\r\n \"delegatingHandlers\": [],\r\n \"priority\": 1,\r\n \"timeout\": 0,\r\n \"dangerousAcceptAnyServerCertificateValidator\": false\r\n }\r\n ],\r\n \"aggregates\": [],\r\n \"globalConfiguration\": {\r\n \"requestIdKey\": \"Request-Id\",\r\n \"rateLimitOptions\": {\r\n \"clientIdHeader\": \"ClientId\",\r\n \"quotaExceededMessage\": null,\r\n \"rateLimitCounterPrefix\": \"ocelot\",\r\n \"disableRateLimitHeaders\": false,\r\n \"httpStatusCode\": 429\r\n },\r\n \"qoSOptions\": {\r\n \"exceptionsAllowedBeforeBreaking\": 0,\r\n \"durationOfBreak\": 0,\r\n \"timeoutValue\": 0\r\n },\r\n \"baseUrl\": \"http://localhost:55580\",\r\n \"loadBalancerOptions\": {\r\n \"type\": null,\r\n \"key\": null,\r\n \"expiry\": 0\r\n },\r\n \"downstreamScheme\": null,\r\n \"httpHandlerOptions\": {\r\n \"allowAutoRedirect\": false,\r\n \"useCookieContainer\": false,\r\n \"useTracing\": false,\r\n \"useProxy\": true\r\n }\r\n }\r\n}" + }, + "url": { + "raw": "http://localhost:5000/administration/configuration", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "administration", + "configuration" + ] + } + }, + "response": [] + }, + { + "name": "2. POST http://localhost: 55580/administration/connect/token", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var jsonData = JSON.parse(responseBody);", + "postman.setGlobalVariable(\"AccessToken\", jsonData.access_token);", + "postman.setGlobalVariable(\"RefreshToken\", jsonData.refresh_token);" + ] + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "client_id", + "value": "admin", + "type": "text" + }, + { + "key": "client_secret", + "value": "secret", + "type": "text" + }, + { + "key": "scope", + "value": "admin", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + } + ] + }, + "url": { + "raw": "http://localhost:5000/administration/connect/token", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "administration", + "connect", + "token" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "id": "0f60e7b3-e4f1-4458-bbc4-fc4809e86b2d", + "type": "text/javascript", + "exec": [ + string.Empty + ] + } + }, + { + "listen": "test", + "script": { + "id": "1279a2cf-b771-4a86-9dfa-302b240fac62", + "type": "text/javascript", + "exec": [ + string.Empty + ] + } + } + ] } \ No newline at end of file diff --git a/samples/AdministrationApi/AdministrationApi.csproj b/samples/Administration/Ocelot.Samples.AdministrationApi.csproj similarity index 100% rename from samples/AdministrationApi/AdministrationApi.csproj rename to samples/Administration/Ocelot.Samples.AdministrationApi.csproj diff --git a/samples/AdministrationApi/Program.cs b/samples/Administration/Program.cs similarity index 100% rename from samples/AdministrationApi/Program.cs rename to samples/Administration/Program.cs diff --git a/samples/AdministrationApi/Properties/launchSettings.json b/samples/Administration/Properties/launchSettings.json similarity index 100% rename from samples/AdministrationApi/Properties/launchSettings.json rename to samples/Administration/Properties/launchSettings.json diff --git a/samples/AdministrationApi/README.md b/samples/Administration/README.md similarity index 96% rename from samples/AdministrationApi/README.md rename to samples/Administration/README.md index 42a01b2f4..59d236aa7 100644 --- a/samples/AdministrationApi/README.md +++ b/samples/Administration/README.md @@ -1,94 +1,94 @@ -```json -{ - "routes": [ - { - "downstreamPathTemplate": "/{everything}", - "upstreamPathTemplate": "/templates/{everything}", - "upstreamHttpMethod": [ - "GET" - ], - "addHeadersToRequest": {}, - "upstreamHeaderTransform": {}, - "downstreamHeaderTransform": {}, - "addClaimsToRequest": {}, - "routeClaimsRequirement": {}, - "addQueriesToRequest": {}, - "requestIdKey": null, - "fileCacheOptions": { - "ttlSeconds": 0, - "region": null - }, - "routeIsCaseSensitive": false, - "downstreamScheme": "http", - "qoSOptions": { - "exceptionsAllowedBeforeBreaking": 0, - "durationOfBreak": 0, - "timeoutValue": 0 - }, - "loadBalancerOptions": { - "type": null, - "key": null, - "expiry": 0 - }, - "rateLimitOptions": { - "clientWhitelist": [], - "enableRateLimiting": false, - "period": null, - "periodTimespan": 0, - "limit": 0 - }, - "authenticationOptions": { - "authenticationProviderKey": null, - "allowedScopes": [] - }, - "httpHandlerOptions": { - "allowAutoRedirect": false, - "useCookieContainer": false, - "useTracing": false, - "useProxy": true - }, - "downstreamHostAndPorts": [ - { - "host": "localhost", - "port": 50689 - } - ], - "upstreamHost": null, - "key": null, - "delegatingHandlers": [], - "priority": 1, - "timeout": 0, - "dangerousAcceptAnyServerCertificateValidator": false - } - ], - "aggregates": [], - "globalConfiguration": { - "requestIdKey": "Request-Id", - "rateLimitOptions": { - "clientIdHeader": "ClientId", - "quotaExceededMessage": null, - "rateLimitCounterPrefix": "ocelot", - "disableRateLimitHeaders": false, - "httpStatusCode": 429 - }, - "qoSOptions": { - "exceptionsAllowedBeforeBreaking": 0, - "durationOfBreak": 0, - "timeoutValue": 0 - }, - "baseUrl": "http://localhost:55580", - "loadBalancerOptions": { - "type": null, - "key": null, - "expiry": 0 - }, - "downstreamScheme": null, - "httpHandlerOptions": { - "allowAutoRedirect": false, - "useCookieContainer": false, - "useTracing": false, - "useProxy": true - } - } -} -``` +```json +{ + "routes": [ + { + "downstreamPathTemplate": "/{everything}", + "upstreamPathTemplate": "/templates/{everything}", + "upstreamHttpMethod": [ + "GET" + ], + "addHeadersToRequest": {}, + "upstreamHeaderTransform": {}, + "downstreamHeaderTransform": {}, + "addClaimsToRequest": {}, + "routeClaimsRequirement": {}, + "addQueriesToRequest": {}, + "requestIdKey": null, + "fileCacheOptions": { + "ttlSeconds": 0, + "region": null + }, + "routeIsCaseSensitive": false, + "downstreamScheme": "http", + "qoSOptions": { + "exceptionsAllowedBeforeBreaking": 0, + "durationOfBreak": 0, + "timeoutValue": 0 + }, + "loadBalancerOptions": { + "type": null, + "key": null, + "expiry": 0 + }, + "rateLimitOptions": { + "clientWhitelist": [], + "enableRateLimiting": false, + "period": null, + "periodTimespan": 0, + "limit": 0 + }, + "authenticationOptions": { + "authenticationProviderKey": null, + "allowedScopes": [] + }, + "httpHandlerOptions": { + "allowAutoRedirect": false, + "useCookieContainer": false, + "useTracing": false, + "useProxy": true + }, + "downstreamHostAndPorts": [ + { + "host": "localhost", + "port": 50689 + } + ], + "upstreamHost": null, + "key": null, + "delegatingHandlers": [], + "priority": 1, + "timeout": 0, + "dangerousAcceptAnyServerCertificateValidator": false + } + ], + "aggregates": [], + "globalConfiguration": { + "requestIdKey": "Request-Id", + "rateLimitOptions": { + "clientIdHeader": "ClientId", + "quotaExceededMessage": null, + "rateLimitCounterPrefix": "ocelot", + "disableRateLimitHeaders": false, + "httpStatusCode": 429 + }, + "qoSOptions": { + "exceptionsAllowedBeforeBreaking": 0, + "durationOfBreak": 0, + "timeoutValue": 0 + }, + "baseUrl": "http://localhost:55580", + "loadBalancerOptions": { + "type": null, + "key": null, + "expiry": 0 + }, + "downstreamScheme": null, + "httpHandlerOptions": { + "allowAutoRedirect": false, + "useCookieContainer": false, + "useTracing": false, + "useProxy": true + } + } +} +``` diff --git a/samples/AdministrationApi/appsettings.json b/samples/Administration/appsettings.json similarity index 100% rename from samples/AdministrationApi/appsettings.json rename to samples/Administration/appsettings.json diff --git a/samples/AdministrationApi/ocelot.json b/samples/Administration/ocelot.json similarity index 95% rename from samples/AdministrationApi/ocelot.json rename to samples/Administration/ocelot.json index 0fa4143ff..02e7c5512 100644 --- a/samples/AdministrationApi/ocelot.json +++ b/samples/Administration/ocelot.json @@ -1,18 +1,18 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/service/stats/collected", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 5100 - } - ], - "UpstreamPathTemplate": "/api/stats/collected" - } - ], - "GlobalConfiguration": { - "BaseUrl": "http://localhost:5000" - } +{ + "Routes": [ + { + "DownstreamPathTemplate": "/service/stats/collected", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 5100 + } + ], + "UpstreamPathTemplate": "/api/stats/collected" + } + ], + "GlobalConfiguration": { + "BaseUrl": "http://localhost:5000" + } } \ No newline at end of file diff --git a/samples/AdministrationApi/tempkey.rsa b/samples/Administration/tempkey.rsa similarity index 100% rename from samples/AdministrationApi/tempkey.rsa rename to samples/Administration/tempkey.rsa diff --git a/samples/OcelotBasic/Ocelot.Samples.OcelotBasic.ApiGateway.csproj b/samples/Basic/Ocelot.Samples.Basic.ApiGateway.csproj similarity index 100% rename from samples/OcelotBasic/Ocelot.Samples.OcelotBasic.ApiGateway.csproj rename to samples/Basic/Ocelot.Samples.Basic.ApiGateway.csproj diff --git a/samples/OcelotBasic/Program.cs b/samples/Basic/Program.cs similarity index 97% rename from samples/OcelotBasic/Program.cs rename to samples/Basic/Program.cs index 34c555f19..3901407c2 100644 --- a/samples/OcelotBasic/Program.cs +++ b/samples/Basic/Program.cs @@ -1,29 +1,29 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using System.IO; +using System.IO; -namespace Ocelot.Samples.OcelotBasic.ApiGateway; - -public class Program -{ - public static void Main(string[] args) - { - new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("ocelot.json") - .AddEnvironmentVariables(); - }) - .ConfigureLogging((hostingContext, logging) => +namespace Ocelot.Samples.OcelotBasic.ApiGateway; + +public class Program +{ + public static void Main(string[] args) + { + new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json") + .AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, logging) => { if (hostingContext.HostingEnvironment.IsDevelopment()) { @@ -31,10 +31,10 @@ public static void Main(string[] args) logging.AddConsole(); } //add your logging - }) - .UseIISIntegration() + }) + .UseIISIntegration() .UseStartup() - .Build() - .Run(); - } -} + .Build() + .Run(); + } +} diff --git a/samples/OcelotBasic/Properties/launchSettings.json b/samples/Basic/Properties/launchSettings.json similarity index 100% rename from samples/OcelotBasic/Properties/launchSettings.json rename to samples/Basic/Properties/launchSettings.json diff --git a/samples/OcelotBasic/Startup.cs b/samples/Basic/Startup.cs similarity index 100% rename from samples/OcelotBasic/Startup.cs rename to samples/Basic/Startup.cs diff --git a/samples/OcelotBasic/appsettings.Development.json b/samples/Basic/appsettings.Development.json similarity index 100% rename from samples/OcelotBasic/appsettings.Development.json rename to samples/Basic/appsettings.Development.json diff --git a/samples/OcelotKube/ApiGateway/appsettings.json b/samples/Basic/appsettings.json similarity index 100% rename from samples/OcelotKube/ApiGateway/appsettings.json rename to samples/Basic/appsettings.json diff --git a/samples/OcelotBasic/ocelot.json b/samples/Basic/ocelot.json similarity index 95% rename from samples/OcelotBasic/ocelot.json rename to samples/Basic/ocelot.json index 2864550cd..7cab02430 100644 --- a/samples/OcelotBasic/ocelot.json +++ b/samples/Basic/ocelot.json @@ -1,21 +1,21 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/todos/{id}", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 443 - } - ], - "UpstreamPathTemplate": "/posts/{id}", - "UpstreamHttpMethod": [ - "Get" - ] - } - ], - "GlobalConfiguration": { - "BaseUrl": "https://localhost:5000" - } +{ + "Routes": [ + { + "DownstreamPathTemplate": "/todos/{id}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 443 + } + ], + "UpstreamPathTemplate": "/posts/{id}", + "UpstreamHttpMethod": [ + "Get" + ] + } + ], + "GlobalConfiguration": { + "BaseUrl": "https://localhost:5000" + } } diff --git a/samples/Docker/README.md b/samples/Docker/README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/samples/OcelotEureka/ApiGateway/ApiGateway.csproj b/samples/Eureka/ApiGateway/Ocelot.Samples.Eureka.ApiGateway.csproj similarity index 100% rename from samples/OcelotEureka/ApiGateway/ApiGateway.csproj rename to samples/Eureka/ApiGateway/Ocelot.Samples.Eureka.ApiGateway.csproj diff --git a/samples/OcelotEureka/ApiGateway/Program.cs b/samples/Eureka/ApiGateway/Program.cs similarity index 100% rename from samples/OcelotEureka/ApiGateway/Program.cs rename to samples/Eureka/ApiGateway/Program.cs diff --git a/samples/OcelotEureka/ApiGateway/Properties/launchSettings.json b/samples/Eureka/ApiGateway/Properties/launchSettings.json similarity index 100% rename from samples/OcelotEureka/ApiGateway/Properties/launchSettings.json rename to samples/Eureka/ApiGateway/Properties/launchSettings.json diff --git a/samples/OcelotEureka/ApiGateway/appsettings.json b/samples/Eureka/ApiGateway/appsettings.json similarity index 100% rename from samples/OcelotEureka/ApiGateway/appsettings.json rename to samples/Eureka/ApiGateway/appsettings.json diff --git a/samples/OcelotEureka/ApiGateway/ocelot.json b/samples/Eureka/ApiGateway/ocelot.json similarity index 96% rename from samples/OcelotEureka/ApiGateway/ocelot.json rename to samples/Eureka/ApiGateway/ocelot.json index 5a69973de..747cf23c8 100644 --- a/samples/OcelotEureka/ApiGateway/ocelot.json +++ b/samples/Eureka/ApiGateway/ocelot.json @@ -1,22 +1,22 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/api/Category", - "DownstreamScheme": "http", - "UpstreamPathTemplate": "/Category", - "ServiceName": "ncore-rat", - "UpstreamHttpMethod": [ "Get" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10000, - "TimeoutValue": 5000 - }, - "FileCacheOptions": { "TtlSeconds": 15 } - } - ], - "GlobalConfiguration": { - "RequestIdKey": "OcRequestId", - "AdministrationPath": "/administration", - "ServiceDiscoveryProvider": { "Type": "Eureka" } - } -} +{ + "Routes": [ + { + "DownstreamPathTemplate": "/api/Category", + "DownstreamScheme": "http", + "UpstreamPathTemplate": "/Category", + "ServiceName": "ncore-rat", + "UpstreamHttpMethod": [ "Get" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10000, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + } + ], + "GlobalConfiguration": { + "RequestIdKey": "OcRequestId", + "AdministrationPath": "/administration", + "ServiceDiscoveryProvider": { "Type": "Eureka" } + } +} diff --git a/samples/OcelotEureka/DownstreamService/Controllers/CategoryController.cs b/samples/Eureka/DownstreamService/Controllers/CategoryController.cs similarity index 100% rename from samples/OcelotEureka/DownstreamService/Controllers/CategoryController.cs rename to samples/Eureka/DownstreamService/Controllers/CategoryController.cs diff --git a/samples/OcelotEureka/DownstreamService/DownstreamService.csproj b/samples/Eureka/DownstreamService/Ocelot.Samples.Eureka.DownstreamService.csproj similarity index 100% rename from samples/OcelotEureka/DownstreamService/DownstreamService.csproj rename to samples/Eureka/DownstreamService/Ocelot.Samples.Eureka.DownstreamService.csproj diff --git a/samples/OcelotEureka/DownstreamService/Program.cs b/samples/Eureka/DownstreamService/Program.cs similarity index 100% rename from samples/OcelotEureka/DownstreamService/Program.cs rename to samples/Eureka/DownstreamService/Program.cs diff --git a/samples/OcelotEureka/DownstreamService/Properties/launchSettings.json b/samples/Eureka/DownstreamService/Properties/launchSettings.json similarity index 100% rename from samples/OcelotEureka/DownstreamService/Properties/launchSettings.json rename to samples/Eureka/DownstreamService/Properties/launchSettings.json diff --git a/samples/OcelotEureka/DownstreamService/Startup.cs b/samples/Eureka/DownstreamService/Startup.cs similarity index 100% rename from samples/OcelotEureka/DownstreamService/Startup.cs rename to samples/Eureka/DownstreamService/Startup.cs diff --git a/samples/OcelotEureka/DownstreamService/appsettings.Development.json b/samples/Eureka/DownstreamService/appsettings.Development.json similarity index 100% rename from samples/OcelotEureka/DownstreamService/appsettings.Development.json rename to samples/Eureka/DownstreamService/appsettings.Development.json diff --git a/samples/OcelotEureka/DownstreamService/appsettings.json b/samples/Eureka/DownstreamService/appsettings.json similarity index 100% rename from samples/OcelotEureka/DownstreamService/appsettings.json rename to samples/Eureka/DownstreamService/appsettings.json diff --git a/samples/OcelotEureka/OcelotEureka.sln b/samples/Eureka/OcelotEureka.sln similarity index 100% rename from samples/OcelotEureka/OcelotEureka.sln rename to samples/Eureka/OcelotEureka.sln diff --git a/samples/OcelotEureka/README.md b/samples/Eureka/README.md similarity index 100% rename from samples/OcelotEureka/README.md rename to samples/Eureka/README.md diff --git a/samples/OcelotGraphQL/OcelotGraphQL.csproj b/samples/GraphQL/Ocelot.Samples.GraphQL.csproj similarity index 100% rename from samples/OcelotGraphQL/OcelotGraphQL.csproj rename to samples/GraphQL/Ocelot.Samples.GraphQL.csproj diff --git a/samples/OcelotGraphQL/OcelotGraphQL.sln b/samples/GraphQL/OcelotGraphQL.sln similarity index 100% rename from samples/OcelotGraphQL/OcelotGraphQL.sln rename to samples/GraphQL/OcelotGraphQL.sln diff --git a/samples/OcelotGraphQL/Program.cs b/samples/GraphQL/Program.cs similarity index 96% rename from samples/OcelotGraphQL/Program.cs rename to samples/GraphQL/Program.cs index e519875f1..e2f19aaaf 100644 --- a/samples/OcelotGraphQL/Program.cs +++ b/samples/GraphQL/Program.cs @@ -1,134 +1,134 @@ -using GraphQL; -using GraphQL.Types; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace OcelotGraphQL -{ - public class Hero - { - public int Id { get; set; } - public string Name { get; set; } - } - - public class Query - { - private readonly List _heroes = new() - { - new Hero { Id = 1, Name = "R2-D2" }, - new Hero { Id = 2, Name = "Batman" }, - new Hero { Id = 3, Name = "Wonder Woman" }, - new Hero { Id = 4, Name = "Tom Pallister" } - }; - - [GraphQLMetadata("hero")] - public Hero GetHero(int id) - { - return _heroes.FirstOrDefault(x => x.Id == id); - } - } - - public class GraphQlDelegatingHandler : DelegatingHandler - { - //private readonly ISchema _schema; - private readonly IDocumentExecuter _executer; - private readonly IDocumentWriter _writer; - - public GraphQlDelegatingHandler(IDocumentExecuter executer, IDocumentWriter writer) - { - _executer = executer; - _writer = writer; - } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - //try get query from body, could check http method :) - var query = await request.Content.ReadAsStringAsync(cancellationToken); - - //if not body try query string, dont hack like this in real world.. - if (query.Length == 0) - { - var decoded = WebUtility.UrlDecode(request.RequestUri.Query); - query = decoded.Replace("?query=", string.Empty); - } - - var result = await _executer.ExecuteAsync(_ => - { - _.Query = query; - }); - - var responseBody = await _writer.WriteToStringAsync(result); - - //maybe check for errors and headers etc in real world? - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(responseBody) - }; - - //ocelot will treat this like any other http request... - return response; - } - } - - public class Program - { - public static void Main() - { - var schema = Schema.For(@" - type Hero { - id: Int - name: String - } - - type Query { - hero(id: Int): Hero - } - ", _ => - { - _.Types.Include(); - }); - - new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("ocelot.json", false, false) - .AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(schema); - s.AddOcelot() - .AddDelegatingHandler(); - }) - .ConfigureLogging((hostingContext, logging) => - { - logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); - logging.AddConsole(); - }) - .UseIISIntegration() - .Configure(app => - { - app.UseOcelot().Wait(); - }) - .Build() - .Run(); - } - } -} +using GraphQL; +using GraphQL.Types; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace OcelotGraphQL +{ + public class Hero + { + public int Id { get; set; } + public string Name { get; set; } + } + + public class Query + { + private readonly List _heroes = new() + { + new Hero { Id = 1, Name = "R2-D2" }, + new Hero { Id = 2, Name = "Batman" }, + new Hero { Id = 3, Name = "Wonder Woman" }, + new Hero { Id = 4, Name = "Tom Pallister" } + }; + + [GraphQLMetadata("hero")] + public Hero GetHero(int id) + { + return _heroes.FirstOrDefault(x => x.Id == id); + } + } + + public class GraphQlDelegatingHandler : DelegatingHandler + { + //private readonly ISchema _schema; + private readonly IDocumentExecuter _executer; + private readonly IDocumentWriter _writer; + + public GraphQlDelegatingHandler(IDocumentExecuter executer, IDocumentWriter writer) + { + _executer = executer; + _writer = writer; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + //try get query from body, could check http method :) + var query = await request.Content.ReadAsStringAsync(cancellationToken); + + //if not body try query string, dont hack like this in real world.. + if (query.Length == 0) + { + var decoded = WebUtility.UrlDecode(request.RequestUri.Query); + query = decoded.Replace("?query=", string.Empty); + } + + var result = await _executer.ExecuteAsync(_ => + { + _.Query = query; + }); + + var responseBody = await _writer.WriteToStringAsync(result); + + //maybe check for errors and headers etc in real world? + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responseBody) + }; + + //ocelot will treat this like any other http request... + return response; + } + } + + public class Program + { + public static void Main() + { + var schema = Schema.For(@" + type Hero { + id: Int + name: String + } + + type Query { + hero(id: Int): Hero + } + ", _ => + { + _.Types.Include(); + }); + + new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json", false, false) + .AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(schema); + s.AddOcelot() + .AddDelegatingHandler(); + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + logging.AddConsole(); + }) + .UseIISIntegration() + .Configure(app => + { + app.UseOcelot().Wait(); + }) + .Build() + .Run(); + } + } +} diff --git a/samples/OcelotGraphQL/Properties/launchSettings.json b/samples/GraphQL/Properties/launchSettings.json similarity index 100% rename from samples/OcelotGraphQL/Properties/launchSettings.json rename to samples/GraphQL/Properties/launchSettings.json diff --git a/samples/OcelotGraphQL/README.md b/samples/GraphQL/README.md similarity index 96% rename from samples/OcelotGraphQL/README.md rename to samples/GraphQL/README.md index 7f16ed985..1a470c344 100644 --- a/samples/OcelotGraphQL/README.md +++ b/samples/GraphQL/README.md @@ -1,71 +1,71 @@ -# Ocelot using GraphQL example - -Loads of people keep asking me if Ocelot will every support GraphQL, in my mind Ocelot and GraphQL are two different things that can work together. -I would not try and implement GraphQL in Ocelot instead I would either have Ocelot in front of GraphQL to handle things like authorization / authentication or I would -bring in the awesome [graphql-dotnet](https://github.com/graphql-dotnet/graphql-dotnet) library and use it in a [DelegatingHandler](http://ocelot.readthedocs.io/en/latest/features/delegatinghandlers.html). This way you could have Ocelot and GraphQL without the extra hop to GraphQL. This same is an example of how to do that. - -## Example - -If you run this project with - -$ dotnet run - -Use postman or something to make the following requests and you can see Ocelot and GraphQL in action together... - -GET http://localhost:5000/graphql?query={ hero(id: 4) { id name } } - -RESPONSE -```json - { - "data": { - "hero": { - "id": 4, - "name": "Tom Pallister" - } - } - } -``` - -POST http://localhost:5000/graphql - -BODY -```json - { hero(id: 4) { id name } } -``` - -RESPONSE -```json - { - "data": { - "hero": { - "id": 4, - "name": "Tom Pallister" - } - } - } -``` - -## Notes - -Please note this project never goes out to another service, it just gets the data for GraphQL in memory. You would need to add the details of your GraphQL server in ocelot.json e.g. - -```json -{ - "Routes": [ - { - "DownstreamPathTemplate": "/graphql", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "yourgraphqlhost.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/graphql", - "DelegatingHandlers": [ - "GraphQlDelegatingHandler" - ] - } - ] - } +# Ocelot using GraphQL example + +Loads of people keep asking me if Ocelot will every support GraphQL, in my mind Ocelot and GraphQL are two different things that can work together. +I would not try and implement GraphQL in Ocelot instead I would either have Ocelot in front of GraphQL to handle things like authorization / authentication or I would +bring in the awesome [graphql-dotnet](https://github.com/graphql-dotnet/graphql-dotnet) library and use it in a [DelegatingHandler](http://ocelot.readthedocs.io/en/latest/features/delegatinghandlers.html). This way you could have Ocelot and GraphQL without the extra hop to GraphQL. This same is an example of how to do that. + +## Example + +If you run this project with + +$ dotnet run + +Use postman or something to make the following requests and you can see Ocelot and GraphQL in action together... + +GET http://localhost:5000/graphql?query={ hero(id: 4) { id name } } + +RESPONSE +```json + { + "data": { + "hero": { + "id": 4, + "name": "Tom Pallister" + } + } + } +``` + +POST http://localhost:5000/graphql + +BODY +```json + { hero(id: 4) { id name } } +``` + +RESPONSE +```json + { + "data": { + "hero": { + "id": 4, + "name": "Tom Pallister" + } + } + } +``` + +## Notes + +Please note this project never goes out to another service, it just gets the data for GraphQL in memory. You would need to add the details of your GraphQL server in ocelot.json e.g. + +```json +{ + "Routes": [ + { + "DownstreamPathTemplate": "/graphql", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "yourgraphqlhost.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/graphql", + "DelegatingHandlers": [ + "GraphQlDelegatingHandler" + ] + } + ] + } ``` \ No newline at end of file diff --git a/samples/OcelotGraphQL/ocelot.json b/samples/GraphQL/ocelot.json similarity index 96% rename from samples/OcelotGraphQL/ocelot.json rename to samples/GraphQL/ocelot.json index c716bf258..3529e0ba8 100644 --- a/samples/OcelotGraphQL/ocelot.json +++ b/samples/GraphQL/ocelot.json @@ -1,19 +1,19 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/graphql", - "DelegatingHandlers": [ - "GraphQlDelegatingHandler" - ] - } - ] - } +{ + "Routes": [ + { + "DownstreamPathTemplate": "/", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/graphql", + "DelegatingHandlers": [ + "GraphQlDelegatingHandler" + ] + } + ] + } \ No newline at end of file diff --git a/samples/OcelotKube/.dockerignore b/samples/Kubernetes/.dockerignore similarity index 100% rename from samples/OcelotKube/.dockerignore rename to samples/Kubernetes/.dockerignore diff --git a/samples/OcelotKube/ApiGateway/Dockerfile b/samples/Kubernetes/ApiGateway/Dockerfile similarity index 100% rename from samples/OcelotKube/ApiGateway/Dockerfile rename to samples/Kubernetes/ApiGateway/Dockerfile diff --git a/samples/OcelotKube/ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj b/samples/Kubernetes/ApiGateway/Ocelot.Samples.Kubernetes.ApiGateway.csproj similarity index 100% rename from samples/OcelotKube/ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj rename to samples/Kubernetes/ApiGateway/Ocelot.Samples.Kubernetes.ApiGateway.csproj diff --git a/samples/OcelotKube/ApiGateway/Program.cs b/samples/Kubernetes/ApiGateway/Program.cs similarity index 100% rename from samples/OcelotKube/ApiGateway/Program.cs rename to samples/Kubernetes/ApiGateway/Program.cs diff --git a/samples/OcelotKube/ApiGateway/Properties/launchSettings.json b/samples/Kubernetes/ApiGateway/Properties/launchSettings.json similarity index 100% rename from samples/OcelotKube/ApiGateway/Properties/launchSettings.json rename to samples/Kubernetes/ApiGateway/Properties/launchSettings.json diff --git a/samples/OcelotKube/ApiGateway/Startup.cs b/samples/Kubernetes/ApiGateway/Startup.cs similarity index 100% rename from samples/OcelotKube/ApiGateway/Startup.cs rename to samples/Kubernetes/ApiGateway/Startup.cs diff --git a/samples/OcelotKube/ApiGateway/appsettings.Development.json b/samples/Kubernetes/ApiGateway/appsettings.Development.json similarity index 100% rename from samples/OcelotKube/ApiGateway/appsettings.Development.json rename to samples/Kubernetes/ApiGateway/appsettings.Development.json diff --git a/samples/OcelotKube/DownstreamService/appsettings.json b/samples/Kubernetes/ApiGateway/appsettings.json similarity index 100% rename from samples/OcelotKube/DownstreamService/appsettings.json rename to samples/Kubernetes/ApiGateway/appsettings.json diff --git a/samples/OcelotKube/ApiGateway/ocelot.json b/samples/Kubernetes/ApiGateway/ocelot.json similarity index 95% rename from samples/OcelotKube/ApiGateway/ocelot.json rename to samples/Kubernetes/ApiGateway/ocelot.json index 6a28b9eec..f4a5af0b8 100644 --- a/samples/OcelotKube/ApiGateway/ocelot.json +++ b/samples/Kubernetes/ApiGateway/ocelot.json @@ -1,20 +1,20 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/api/values", - "DownstreamScheme": "http", - "UpstreamPathTemplate": "/values", - "ServiceName": "downstreamservice", - "UpstreamHttpMethod": [ "Get" ] - } - ], - "GlobalConfiguration": { - "ServiceDiscoveryProvider": { - "Host": "192.168.0.13", - "Port": 443, - "Token": "txpc696iUhbVoudg164r93CxDTrKRVWG", - "Namespace": "dev", - "Type": "kube" - } - } -} +{ + "Routes": [ + { + "DownstreamPathTemplate": "/api/values", + "DownstreamScheme": "http", + "UpstreamPathTemplate": "/values", + "ServiceName": "downstreamservice", + "UpstreamHttpMethod": [ "Get" ] + } + ], + "GlobalConfiguration": { + "ServiceDiscoveryProvider": { + "Host": "192.168.0.13", + "Port": 443, + "Token": "txpc696iUhbVoudg164r93CxDTrKRVWG", + "Namespace": "dev", + "Type": "kube" + } + } +} diff --git a/samples/OcelotKube/Dockerfile b/samples/Kubernetes/Dockerfile similarity index 100% rename from samples/OcelotKube/Dockerfile rename to samples/Kubernetes/Dockerfile diff --git a/samples/OcelotKube/DownstreamService/Controllers/ValuesController.cs b/samples/Kubernetes/DownstreamService/Controllers/ValuesController.cs similarity index 100% rename from samples/OcelotKube/DownstreamService/Controllers/ValuesController.cs rename to samples/Kubernetes/DownstreamService/Controllers/ValuesController.cs diff --git a/samples/OcelotKube/DownstreamService/Controllers/WeatherForecastController.cs b/samples/Kubernetes/DownstreamService/Controllers/WeatherForecastController.cs similarity index 100% rename from samples/OcelotKube/DownstreamService/Controllers/WeatherForecastController.cs rename to samples/Kubernetes/DownstreamService/Controllers/WeatherForecastController.cs diff --git a/samples/OcelotKube/DownstreamService/Dockerfile b/samples/Kubernetes/DownstreamService/Dockerfile similarity index 100% rename from samples/OcelotKube/DownstreamService/Dockerfile rename to samples/Kubernetes/DownstreamService/Dockerfile diff --git a/samples/OcelotKube/DownstreamService/Models/WeatherForecast.cs b/samples/Kubernetes/DownstreamService/Models/WeatherForecast.cs similarity index 100% rename from samples/OcelotKube/DownstreamService/Models/WeatherForecast.cs rename to samples/Kubernetes/DownstreamService/Models/WeatherForecast.cs diff --git a/samples/OcelotKube/DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj b/samples/Kubernetes/DownstreamService/Ocelot.Samples.Kubernetes.DownstreamService.csproj similarity index 86% rename from samples/OcelotKube/DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj rename to samples/Kubernetes/DownstreamService/Ocelot.Samples.Kubernetes.DownstreamService.csproj index 3c1cfefb2..72b36b2fc 100644 --- a/samples/OcelotKube/DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj +++ b/samples/Kubernetes/DownstreamService/Ocelot.Samples.Kubernetes.DownstreamService.csproj @@ -1,6 +1,6 @@ - net7.0 + net6.0;net7.0;net8.0 disable disable InProcess diff --git a/samples/OcelotKube/DownstreamService/Program.cs b/samples/Kubernetes/DownstreamService/Program.cs similarity index 100% rename from samples/OcelotKube/DownstreamService/Program.cs rename to samples/Kubernetes/DownstreamService/Program.cs diff --git a/samples/OcelotKube/DownstreamService/Properties/launchSettings.json b/samples/Kubernetes/DownstreamService/Properties/launchSettings.json similarity index 100% rename from samples/OcelotKube/DownstreamService/Properties/launchSettings.json rename to samples/Kubernetes/DownstreamService/Properties/launchSettings.json diff --git a/samples/OcelotKube/DownstreamService/appsettings.Development.json b/samples/Kubernetes/DownstreamService/appsettings.Development.json similarity index 100% rename from samples/OcelotKube/DownstreamService/appsettings.Development.json rename to samples/Kubernetes/DownstreamService/appsettings.Development.json diff --git a/samples/OcelotBasic/appsettings.json b/samples/Kubernetes/DownstreamService/appsettings.json similarity index 92% rename from samples/OcelotBasic/appsettings.json rename to samples/Kubernetes/DownstreamService/appsettings.json index 7376aada1..def9159a7 100644 --- a/samples/OcelotBasic/appsettings.json +++ b/samples/Kubernetes/DownstreamService/appsettings.json @@ -1,8 +1,8 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning" - } - }, - "AllowedHosts": "*" -} +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/OcelotKube/OcelotKube.sln b/samples/Kubernetes/OcelotKube.sln similarity index 100% rename from samples/OcelotKube/OcelotKube.sln rename to samples/Kubernetes/OcelotKube.sln diff --git a/samples/Ocelot.Samples.sln b/samples/Ocelot.Samples.sln new file mode 100644 index 000000000..ca208f4a6 --- /dev/null +++ b/samples/Ocelot.Samples.sln @@ -0,0 +1,91 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.AdministrationApi", "Administration\Ocelot.Samples.AdministrationApi.csproj", "{238467FE-19EE-4102-9AF7-51EB2C6F0354}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Basic.ApiGateway", "Basic\Ocelot.Samples.Basic.ApiGateway.csproj", "{A7D2C43A-E35C-4A89-AEE5-5C87052ECD89}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Eureka.ApiGateway", "Eureka\ApiGateway\Ocelot.Samples.Eureka.ApiGateway.csproj", "{EA0E146F-2C2B-4176-B6EC-F62A587F5077}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Eureka.DownstreamService", "Eureka\DownstreamService\Ocelot.Samples.Eureka.DownstreamService.csproj", "{B7317B64-2208-472D-90AC-F42B61956B79}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.GraphQL", "GraphQL\Ocelot.Samples.GraphQL.csproj", "{6CCA3677-420A-4294-8D41-67CF3D818575}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Kubernetes.ApiGateway", "Kubernetes\ApiGateway\Ocelot.Samples.Kubernetes.ApiGateway.csproj", "{721C1737-70CB-4B11-A19B-C7AAC6856CC7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Kubernetes.DownstreamService", "Kubernetes\DownstreamService\Ocelot.Samples.Kubernetes.DownstreamService.csproj", "{CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OpenTracing", "OpenTracing\Ocelot.Samples.OpenTracing.csproj", "{707BD584-3CC0-4087-820C-049C3D68F6A3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.ApiGateway", "ServiceDiscovery\ApiGateway\Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj", "{96B9F16E-C95D-425A-A419-40CB3C90CB77}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.DownstreamService", "ServiceDiscovery\DownstreamService\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "{60E14B1A-C295-453B-910E-58E09F5A28AA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceFabric.ApiGateway", "ServiceFabric\ApiGateway\Ocelot.Samples.ServiceFabric.ApiGateway.csproj", "{115F7934-3326-492A-B131-64F0EAEBAD71}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceFabric.DownstreamService", "ServiceFabric\DownstreamService\Ocelot.Samples.ServiceFabric.DownstreamService.csproj", "{6C777A20-F557-45CF-B87B-11E3C6B29A36}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {238467FE-19EE-4102-9AF7-51EB2C6F0354}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {238467FE-19EE-4102-9AF7-51EB2C6F0354}.Debug|Any CPU.Build.0 = Debug|Any CPU + {238467FE-19EE-4102-9AF7-51EB2C6F0354}.Release|Any CPU.ActiveCfg = Release|Any CPU + {238467FE-19EE-4102-9AF7-51EB2C6F0354}.Release|Any CPU.Build.0 = Release|Any CPU + {A7D2C43A-E35C-4A89-AEE5-5C87052ECD89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7D2C43A-E35C-4A89-AEE5-5C87052ECD89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7D2C43A-E35C-4A89-AEE5-5C87052ECD89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7D2C43A-E35C-4A89-AEE5-5C87052ECD89}.Release|Any CPU.Build.0 = Release|Any CPU + {EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Release|Any CPU.Build.0 = Release|Any CPU + {B7317B64-2208-472D-90AC-F42B61956B79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7317B64-2208-472D-90AC-F42B61956B79}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7317B64-2208-472D-90AC-F42B61956B79}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7317B64-2208-472D-90AC-F42B61956B79}.Release|Any CPU.Build.0 = Release|Any CPU + {6CCA3677-420A-4294-8D41-67CF3D818575}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CCA3677-420A-4294-8D41-67CF3D818575}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CCA3677-420A-4294-8D41-67CF3D818575}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CCA3677-420A-4294-8D41-67CF3D818575}.Release|Any CPU.Build.0 = Release|Any CPU + {721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Release|Any CPU.Build.0 = Release|Any CPU + {CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Release|Any CPU.Build.0 = Release|Any CPU + {707BD584-3CC0-4087-820C-049C3D68F6A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {707BD584-3CC0-4087-820C-049C3D68F6A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {707BD584-3CC0-4087-820C-049C3D68F6A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {707BD584-3CC0-4087-820C-049C3D68F6A3}.Release|Any CPU.Build.0 = Release|Any CPU + {96B9F16E-C95D-425A-A419-40CB3C90CB77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {96B9F16E-C95D-425A-A419-40CB3C90CB77}.Debug|Any CPU.Build.0 = Debug|Any CPU + {96B9F16E-C95D-425A-A419-40CB3C90CB77}.Release|Any CPU.ActiveCfg = Release|Any CPU + {96B9F16E-C95D-425A-A419-40CB3C90CB77}.Release|Any CPU.Build.0 = Release|Any CPU + {60E14B1A-C295-453B-910E-58E09F5A28AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60E14B1A-C295-453B-910E-58E09F5A28AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60E14B1A-C295-453B-910E-58E09F5A28AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60E14B1A-C295-453B-910E-58E09F5A28AA}.Release|Any CPU.Build.0 = Release|Any CPU + {115F7934-3326-492A-B131-64F0EAEBAD71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {115F7934-3326-492A-B131-64F0EAEBAD71}.Debug|Any CPU.Build.0 = Debug|Any CPU + {115F7934-3326-492A-B131-64F0EAEBAD71}.Release|Any CPU.ActiveCfg = Release|Any CPU + {115F7934-3326-492A-B131-64F0EAEBAD71}.Release|Any CPU.Build.0 = Release|Any CPU + {6C777A20-F557-45CF-B87B-11E3C6B29A36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C777A20-F557-45CF-B87B-11E3C6B29A36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C777A20-F557-45CF-B87B-11E3C6B29A36}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C777A20-F557-45CF-B87B-11E3C6B29A36}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2C1620D4-EB38-4C3E-9FC5-029FB6B2F426} + EndGlobalSection +EndGlobal diff --git a/samples/OcelotServiceDiscovery/DownstreamService/.dockerignore b/samples/OcelotServiceDiscovery/DownstreamService/.dockerignore deleted file mode 100644 index e7b690f11..000000000 --- a/samples/OcelotServiceDiscovery/DownstreamService/.dockerignore +++ /dev/null @@ -1,25 +0,0 @@ -**/.classpath -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Dockerfile b/samples/OcelotServiceDiscovery/DownstreamService/Dockerfile deleted file mode 100644 index b7535cfcd..000000000 --- a/samples/OcelotServiceDiscovery/DownstreamService/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. - -FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base -WORKDIR /app -EXPOSE 80 -EXPOSE 443 - -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build -WORKDIR /src -COPY ["Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "."] -RUN dotnet restore "./Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj" -COPY . . -WORKDIR "/src/." -RUN dotnet build "Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj" -c Release -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "Ocelot.Samples.ServiceDiscovery.DownstreamService.dll"] diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj b/samples/OcelotServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj deleted file mode 100644 index c0669cb7d..000000000 --- a/samples/OcelotServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - net7.0 - disable - enable - Linux - . - d5492aa8-b50c-41ae-a044-9954846db9ac - - - - - - - - diff --git a/samples/OcelotOpenTracing/OcelotOpenTracing.csproj b/samples/OpenTracing/Ocelot.Samples.OpenTracing.csproj similarity index 100% rename from samples/OcelotOpenTracing/OcelotOpenTracing.csproj rename to samples/OpenTracing/Ocelot.Samples.OpenTracing.csproj diff --git a/samples/OcelotOpenTracing/Program.cs b/samples/OpenTracing/Program.cs similarity index 100% rename from samples/OcelotOpenTracing/Program.cs rename to samples/OpenTracing/Program.cs diff --git a/samples/OcelotOpenTracing/appsettings.Development.json b/samples/OpenTracing/appsettings.Development.json similarity index 100% rename from samples/OcelotOpenTracing/appsettings.Development.json rename to samples/OpenTracing/appsettings.Development.json diff --git a/samples/OcelotOpenTracing/appsettings.json b/samples/OpenTracing/appsettings.json similarity index 100% rename from samples/OcelotOpenTracing/appsettings.json rename to samples/OpenTracing/appsettings.json diff --git a/samples/OcelotOpenTracing/ocelot.json b/samples/OpenTracing/ocelot.json similarity index 100% rename from samples/OcelotOpenTracing/ocelot.json rename to samples/OpenTracing/ocelot.json diff --git a/samples/OcelotServiceDiscovery/.dockerignore b/samples/ServiceDiscovery/.dockerignore similarity index 100% rename from samples/OcelotServiceDiscovery/.dockerignore rename to samples/ServiceDiscovery/.dockerignore diff --git a/samples/OcelotServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj b/samples/ServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj similarity index 59% rename from samples/OcelotServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj rename to samples/ServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj index aac1dd20a..815c793e9 100644 --- a/samples/OcelotServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj +++ b/samples/ServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj @@ -1,6 +1,8 @@ - net6.0;net7.0;net8.0 + net8.0 + disable + disable diff --git a/samples/OcelotServiceDiscovery/ApiGateway/Program.cs b/samples/ServiceDiscovery/ApiGateway/Program.cs similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/Program.cs rename to samples/ServiceDiscovery/ApiGateway/Program.cs diff --git a/samples/OcelotServiceDiscovery/ApiGateway/Properties/launchSettings.json b/samples/ServiceDiscovery/ApiGateway/Properties/launchSettings.json similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/Properties/launchSettings.json rename to samples/ServiceDiscovery/ApiGateway/Properties/launchSettings.json diff --git a/samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs b/samples/ServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs rename to samples/ServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs diff --git a/samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs b/samples/ServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs rename to samples/ServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs diff --git a/samples/OcelotServiceDiscovery/ApiGateway/appsettings.json b/samples/ServiceDiscovery/ApiGateway/appsettings.json similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/appsettings.json rename to samples/ServiceDiscovery/ApiGateway/appsettings.json diff --git a/samples/OcelotServiceDiscovery/ApiGateway/ocelot.json b/samples/ServiceDiscovery/ApiGateway/ocelot.json similarity index 100% rename from samples/OcelotServiceDiscovery/ApiGateway/ocelot.json rename to samples/ServiceDiscovery/ApiGateway/ocelot.json diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs b/samples/ServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs rename to samples/ServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Controllers/HealthController.cs b/samples/ServiceDiscovery/DownstreamService/Controllers/HealthController.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Controllers/HealthController.cs rename to samples/ServiceDiscovery/DownstreamService/Controllers/HealthController.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs b/samples/ServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs rename to samples/ServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Models/HealthResult.cs b/samples/ServiceDiscovery/DownstreamService/Models/HealthResult.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Models/HealthResult.cs rename to samples/ServiceDiscovery/DownstreamService/Models/HealthResult.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs b/samples/ServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs rename to samples/ServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Models/ReadyResult.cs b/samples/ServiceDiscovery/DownstreamService/Models/ReadyResult.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Models/ReadyResult.cs rename to samples/ServiceDiscovery/DownstreamService/Models/ReadyResult.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Models/WeatherForecast.cs b/samples/ServiceDiscovery/DownstreamService/Models/WeatherForecast.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Models/WeatherForecast.cs rename to samples/ServiceDiscovery/DownstreamService/Models/WeatherForecast.cs diff --git a/samples/ServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj b/samples/ServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj new file mode 100644 index 000000000..163a956bb --- /dev/null +++ b/samples/ServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + disable + enable + + + + + + + diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Program.cs b/samples/ServiceDiscovery/DownstreamService/Program.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Program.cs rename to samples/ServiceDiscovery/DownstreamService/Program.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Properties/launchSettings.json b/samples/ServiceDiscovery/DownstreamService/Properties/launchSettings.json similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Properties/launchSettings.json rename to samples/ServiceDiscovery/DownstreamService/Properties/launchSettings.json diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Startup.cs b/samples/ServiceDiscovery/DownstreamService/Startup.cs similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/Startup.cs rename to samples/ServiceDiscovery/DownstreamService/Startup.cs diff --git a/samples/OcelotServiceDiscovery/DownstreamService/appsettings.Development.json b/samples/ServiceDiscovery/DownstreamService/appsettings.Development.json similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/appsettings.Development.json rename to samples/ServiceDiscovery/DownstreamService/appsettings.Development.json diff --git a/samples/OcelotServiceDiscovery/DownstreamService/appsettings.json b/samples/ServiceDiscovery/DownstreamService/appsettings.json similarity index 100% rename from samples/OcelotServiceDiscovery/DownstreamService/appsettings.json rename to samples/ServiceDiscovery/DownstreamService/appsettings.json diff --git a/samples/OcelotServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln b/samples/ServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln similarity index 100% rename from samples/OcelotServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln rename to samples/ServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln diff --git a/samples/OcelotServiceDiscovery/README.md b/samples/ServiceDiscovery/README.md similarity index 100% rename from samples/OcelotServiceDiscovery/README.md rename to samples/ServiceDiscovery/README.md diff --git a/samples/OcelotServiceFabric/.gitignore b/samples/ServiceFabric/.gitignore similarity index 100% rename from samples/OcelotServiceFabric/.gitignore rename to samples/ServiceFabric/.gitignore diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj b/samples/ServiceFabric/ApiGateway/Ocelot.Samples.ServiceFabric.ApiGateway.csproj similarity index 92% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj rename to samples/ServiceFabric/ApiGateway/Ocelot.Samples.ServiceFabric.ApiGateway.csproj index c97238c53..c9f886f75 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj +++ b/samples/ServiceFabric/ApiGateway/Ocelot.Samples.ServiceFabric.ApiGateway.csproj @@ -18,6 +18,6 @@ - + diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.cs b/samples/ServiceFabric/ApiGateway/OcelotApplicationApiGateway.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.cs rename to samples/ServiceFabric/ApiGateway/OcelotApplicationApiGateway.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/Program.cs b/samples/ServiceFabric/ApiGateway/Program.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/Program.cs rename to samples/ServiceFabric/ApiGateway/Program.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/Properties/launchSettings.json b/samples/ServiceFabric/ApiGateway/Properties/launchSettings.json similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/Properties/launchSettings.json rename to samples/ServiceFabric/ApiGateway/Properties/launchSettings.json diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventListener.cs b/samples/ServiceFabric/ApiGateway/ServiceEventListener.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventListener.cs rename to samples/ServiceFabric/ApiGateway/ServiceEventListener.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventSource.cs b/samples/ServiceFabric/ApiGateway/ServiceEventSource.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventSource.cs rename to samples/ServiceFabric/ApiGateway/ServiceEventSource.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/WebCommunicationListener.cs b/samples/ServiceFabric/ApiGateway/WebCommunicationListener.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/WebCommunicationListener.cs rename to samples/ServiceFabric/ApiGateway/WebCommunicationListener.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/appsettings.json b/samples/ServiceFabric/ApiGateway/appsettings.json similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/appsettings.json rename to samples/ServiceFabric/ApiGateway/appsettings.json diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ocelot.json b/samples/ServiceFabric/ApiGateway/ocelot.json similarity index 95% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ocelot.json rename to samples/ServiceFabric/ApiGateway/ocelot.json index b541e95c4..1b174cd62 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ocelot.json +++ b/samples/ServiceFabric/ApiGateway/ocelot.json @@ -1,21 +1,21 @@ -{ - "Routes": [ - { - "DownstreamPathTemplate": "/api/values", - "UpstreamPathTemplate": "/EquipmentInterfaces", - "UpstreamHttpMethod": [ - "Get" - ], - "DownstreamScheme": "http", - "ServiceName": "OcelotServiceApplication/OcelotApplicationService" - } - ], - "GlobalConfiguration": { - "RequestIdKey": "OcRequestId", - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 19081, - "Type": "ServiceFabric" - } - } -} +{ + "Routes": [ + { + "DownstreamPathTemplate": "/api/values", + "UpstreamPathTemplate": "/EquipmentInterfaces", + "UpstreamHttpMethod": [ + "Get" + ], + "DownstreamScheme": "http", + "ServiceName": "OcelotServiceApplication/OcelotApplicationService" + } + ], + "GlobalConfiguration": { + "RequestIdKey": "OcRequestId", + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 19081, + "Type": "ServiceFabric" + } + } +} diff --git a/samples/OcelotServiceFabric/CONTRIBUTING.md b/samples/ServiceFabric/CONTRIBUTING.md similarity index 100% rename from samples/OcelotServiceFabric/CONTRIBUTING.md rename to samples/ServiceFabric/CONTRIBUTING.md diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/ApiGateway.cs b/samples/ServiceFabric/DownstreamService/ApiGateway.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/ApiGateway.cs rename to samples/ServiceFabric/DownstreamService/ApiGateway.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/Controllers/ValuesController.cs b/samples/ServiceFabric/DownstreamService/Controllers/ValuesController.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/Controllers/ValuesController.cs rename to samples/ServiceFabric/DownstreamService/Controllers/ValuesController.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/OcelotApplicationService.csproj b/samples/ServiceFabric/DownstreamService/Ocelot.Samples.ServiceFabric.DownstreamService.csproj similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/OcelotApplicationService.csproj rename to samples/ServiceFabric/DownstreamService/Ocelot.Samples.ServiceFabric.DownstreamService.csproj diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/Program.cs b/samples/ServiceFabric/DownstreamService/Program.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/Program.cs rename to samples/ServiceFabric/DownstreamService/Program.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/Properties/launchSettings.json b/samples/ServiceFabric/DownstreamService/Properties/launchSettings.json similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/Properties/launchSettings.json rename to samples/ServiceFabric/DownstreamService/Properties/launchSettings.json diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/ServiceEventSource.cs b/samples/ServiceFabric/DownstreamService/ServiceEventSource.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/ServiceEventSource.cs rename to samples/ServiceFabric/DownstreamService/ServiceEventSource.cs diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/Startup.cs b/samples/ServiceFabric/DownstreamService/Startup.cs similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationService/Startup.cs rename to samples/ServiceFabric/DownstreamService/Startup.cs diff --git a/samples/OcelotServiceFabric/LICENSE.md b/samples/ServiceFabric/LICENSE.md similarity index 100% rename from samples/OcelotServiceFabric/LICENSE.md rename to samples/ServiceFabric/LICENSE.md diff --git a/samples/OcelotServiceFabric/OcelotApplication/ApplicationManifest.xml b/samples/ServiceFabric/OcelotApplication/ApplicationManifest.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/ApplicationManifest.xml rename to samples/ServiceFabric/OcelotApplication/ApplicationManifest.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.cmd b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.cmd similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.cmd rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.cmd diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.sh b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.sh similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.sh rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.sh diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/Settings.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/Settings.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/Settings.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/Settings.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/_readme.txt b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/_readme.txt similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/_readme.txt rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/_readme.txt diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Data/_readme.txt b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Data/_readme.txt similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Data/_readme.txt rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Data/_readme.txt diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Linux.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Linux.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Linux.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Linux.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Windows.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Windows.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Windows.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Windows.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.cmd b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.cmd similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.cmd rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.cmd diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.sh b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.sh similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.sh rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.sh diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/Settings.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/Settings.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/Settings.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/Settings.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/_readme.txt b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/_readme.txt similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/_readme.txt rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/_readme.txt diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Data/_readme.txt b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Data/_readme.txt similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Data/_readme.txt rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Data/_readme.txt diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Linux.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Linux.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Linux.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Linux.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Windows.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Windows.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Windows.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Windows.xml diff --git a/samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest.xml b/samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest.xml similarity index 100% rename from samples/OcelotServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest.xml rename to samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest.xml diff --git a/samples/OcelotServiceFabric/README.md b/samples/ServiceFabric/README.md similarity index 100% rename from samples/OcelotServiceFabric/README.md rename to samples/ServiceFabric/README.md diff --git a/samples/OcelotServiceFabric/build.bat b/samples/ServiceFabric/build.bat similarity index 100% rename from samples/OcelotServiceFabric/build.bat rename to samples/ServiceFabric/build.bat diff --git a/samples/OcelotServiceFabric/build.sh b/samples/ServiceFabric/build.sh similarity index 100% rename from samples/OcelotServiceFabric/build.sh rename to samples/ServiceFabric/build.sh diff --git a/samples/OcelotServiceFabric/dotnet-include.sh b/samples/ServiceFabric/dotnet-include.sh similarity index 100% rename from samples/OcelotServiceFabric/dotnet-include.sh rename to samples/ServiceFabric/dotnet-include.sh diff --git a/samples/OcelotServiceFabric/install.ps1 b/samples/ServiceFabric/install.ps1 similarity index 100% rename from samples/OcelotServiceFabric/install.ps1 rename to samples/ServiceFabric/install.ps1 diff --git a/samples/OcelotServiceFabric/install.sh b/samples/ServiceFabric/install.sh similarity index 100% rename from samples/OcelotServiceFabric/install.sh rename to samples/ServiceFabric/install.sh diff --git a/samples/OcelotServiceFabric/uninstall.ps1 b/samples/ServiceFabric/uninstall.ps1 similarity index 100% rename from samples/OcelotServiceFabric/uninstall.ps1 rename to samples/ServiceFabric/uninstall.ps1 diff --git a/samples/OcelotServiceFabric/uninstall.sh b/samples/ServiceFabric/uninstall.sh similarity index 100% rename from samples/OcelotServiceFabric/uninstall.sh rename to samples/ServiceFabric/uninstall.sh diff --git a/src/Ocelot.Provider.Polly/PollyQoSProviderBase.cs b/src/Ocelot.Provider.Polly/PollyQoSProviderBase.cs index 31ca23451..24760c800 100644 --- a/src/Ocelot.Provider.Polly/PollyQoSProviderBase.cs +++ b/src/Ocelot.Provider.Polly/PollyQoSProviderBase.cs @@ -5,8 +5,8 @@ namespace Ocelot.Provider.Polly; public abstract class PollyQoSProviderBase { - protected static readonly HashSet ServerErrorCodes = - [ + protected static readonly HashSet ServerErrorCodes = new() + { HttpStatusCode.InternalServerError, HttpStatusCode.NotImplemented, HttpStatusCode.BadGateway, @@ -16,7 +16,7 @@ public abstract class PollyQoSProviderBase HttpStatusCode.VariantAlsoNegotiates, HttpStatusCode.InsufficientStorage, HttpStatusCode.LoopDetected, - ]; + }; protected static string GetRouteName(DownstreamRoute route) => string.IsNullOrWhiteSpace(route.ServiceName) diff --git a/src/Ocelot.Provider.Polly/v7/PollyQoSProvider.cs b/src/Ocelot.Provider.Polly/v7/PollyQoSProvider.cs index 34f76aacb..51c727e80 100644 --- a/src/Ocelot.Provider.Polly/v7/PollyQoSProvider.cs +++ b/src/Ocelot.Provider.Polly/v7/PollyQoSProvider.cs @@ -10,7 +10,7 @@ namespace Ocelot.Provider.Polly.v7; [Obsolete("Due to new v8 policy definition in Polly 8 (use PollyQoSResiliencePipelineProvider)")] public class PollyQoSProvider : PollyQoSProviderBase, IPollyQoSProvider { - private readonly Dictionary> _policyWrappers = []; + private readonly Dictionary> _policyWrappers = new(); private readonly object _lockObject = new(); private readonly IOcelotLogger _logger; diff --git a/src/Ocelot/Configuration/AuthenticationOptions.cs b/src/Ocelot/Configuration/AuthenticationOptions.cs index af5cf7273..978a940a8 100644 --- a/src/Ocelot/Configuration/AuthenticationOptions.cs +++ b/src/Ocelot/Configuration/AuthenticationOptions.cs @@ -8,22 +8,21 @@ public AuthenticationOptions(List allowedScopes, string authenticationPr { AllowedScopes = allowedScopes; AuthenticationProviderKey = authenticationProviderKey; - AuthenticationProviderKeys = []; + AuthenticationProviderKeys = Array.Empty(); } public AuthenticationOptions(FileAuthenticationOptions from) { - AllowedScopes = from.AllowedScopes ?? []; + AllowedScopes = from.AllowedScopes ?? new(); AuthenticationProviderKey = from.AuthenticationProviderKey ?? string.Empty; - AuthenticationProviderKeys = from.AuthenticationProviderKeys ?? []; + AuthenticationProviderKeys = from.AuthenticationProviderKeys ?? Array.Empty(); } - public AuthenticationOptions(List allowedScopes, string authenticationProviderKey, - string[] authenticationProviderKeys) + public AuthenticationOptions(List allowedScopes, string authenticationProviderKey, string[] authenticationProviderKeys) { - AllowedScopes = allowedScopes ?? []; + AllowedScopes = allowedScopes ?? new(); AuthenticationProviderKey = authenticationProviderKey ?? string.Empty; - AuthenticationProviderKeys = authenticationProviderKeys ?? []; + AuthenticationProviderKeys = authenticationProviderKeys ?? Array.Empty(); } public List AllowedScopes { get; } diff --git a/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs index e911908c7..0b85ee809 100644 --- a/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs +++ b/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs @@ -4,7 +4,7 @@ public class AuthenticationOptionsBuilder { private List _allowedScopes = new(); private string _authenticationProviderKey; - private string[] _authenticationProviderKeys =[]; + private string[] _authenticationProviderKeys = Array.Empty(); public AuthenticationOptionsBuilder WithAllowedScopes(List allowedScopes) { diff --git a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs index 24d9b787d..0dd93a9c6 100644 --- a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs +++ b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs @@ -4,13 +4,13 @@ public sealed class FileAuthenticationOptions { public FileAuthenticationOptions() { - AllowedScopes = []; - AuthenticationProviderKeys = []; + AllowedScopes = new(); + AuthenticationProviderKeys = Array.Empty(); } public FileAuthenticationOptions(FileAuthenticationOptions from) { - AllowedScopes = [..from.AllowedScopes]; + AllowedScopes = new(from.AllowedScopes); AuthenticationProviderKey = from.AuthenticationProviderKey; AuthenticationProviderKeys = from.AuthenticationProviderKeys; } diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs index 4a3dc798f..dfa6279e6 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs @@ -1,4 +1,5 @@ using Ocelot.Configuration; +using Ocelot.Errors; using Ocelot.Responses; namespace Ocelot.LoadBalancer.LoadBalancers @@ -33,10 +34,10 @@ public Response Get(DownstreamRoute route, ServiceProviderConfigu } catch (Exception ex) { - return new ErrorResponse( - [ + return new ErrorResponse(new List() + { new UnableToFindLoadBalancerError($"Unable to find load balancer for '{route.LoadBalancerKey}'. Exception: {ex};"), - ]); + }); } } diff --git a/src/Ocelot/Middleware/HttpItemsExtensions.cs b/src/Ocelot/Middleware/HttpItemsExtensions.cs index d8d82ef9d..df97e0a19 100644 --- a/src/Ocelot/Middleware/HttpItemsExtensions.cs +++ b/src/Ocelot/Middleware/HttpItemsExtensions.cs @@ -56,7 +56,7 @@ public static IInternalConfiguration IInternalConfiguration(this IDictionary Errors(this IDictionary input) { var errors = input.Get>("Errors"); - return errors ?? []; + return errors ?? new(); } public static DownstreamRouteFinder.DownstreamRouteHolder diff --git a/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs b/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs index 731909006..17609dc18 100644 --- a/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs +++ b/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs @@ -104,7 +104,7 @@ private async Task ProcessRoutesAsync(HttpContext context, Route route) .Select(downstreamRoute => ProcessRouteAsync(context, downstreamRoute)) .ToArray(); var contexts = await Task.WhenAll(tasks); - await MapAsync(context, route, [.. contexts]); + await MapAsync(context, route, new(contexts)); } /// diff --git a/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs b/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs index 6440e3d46..f7da43960 100644 --- a/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs +++ b/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs @@ -35,7 +35,7 @@ public async Task Invoke(HttpContext httpContext) catch (Exception ex) { // TODO Review the error handling, we should throw an exception here and use the global error handler middleware to catch it - httpContext.Items.UpsertErrors([new UnmappableRequestError(ex)]); + httpContext.Items.SetError(new UnmappableRequestError(ex)); return; } diff --git a/src/Ocelot/Requester/MessageInvokerPool.cs b/src/Ocelot/Requester/MessageInvokerPool.cs index 32d9b5235..130b4bccb 100644 --- a/src/Ocelot/Requester/MessageInvokerPool.cs +++ b/src/Ocelot/Requester/MessageInvokerPool.cs @@ -6,11 +6,6 @@ namespace Ocelot.Requester; public class MessageInvokerPool : IMessageInvokerPool { - /// - /// TODO This should be configurable and available as global config parameter in ocelot.json. - /// - public const int DefaultRequestTimeoutSeconds = 90; - private readonly ConcurrentDictionary> _handlersPool; private readonly IDelegatingHandlerHandlerFactory _handlerFactory; private readonly IOcelotLogger _logger; @@ -37,6 +32,18 @@ public HttpMessageInvoker Get(DownstreamRoute downstreamRoute) public void Clear() => _handlersPool.Clear(); + /// + /// TODO This should be configurable and available as global config parameter in ocelot.json. + /// + public const int DefaultRequestTimeoutSeconds = 90; + private int _requestTimeoutSeconds; + + public int RequestTimeoutSeconds + { + get => _requestTimeoutSeconds > 0 ? _requestTimeoutSeconds : DefaultRequestTimeoutSeconds; + set => _requestTimeoutSeconds = value > 0 ? value : DefaultRequestTimeoutSeconds; + } + private HttpMessageInvoker CreateMessageInvoker(DownstreamRoute downstreamRoute) { var baseHandler = CreateHandler(downstreamRoute); @@ -52,7 +59,7 @@ private HttpMessageInvoker CreateMessageInvoker(DownstreamRoute downstreamRoute) // Adding timeout handler to the top of the chain. // It's standard behavior to throw TimeoutException after the defined timeout (90 seconds by default) var timeoutHandler = new TimeoutDelegatingHandler(downstreamRoute.QosOptions.TimeoutValue == 0 - ? TimeSpan.FromSeconds(DefaultRequestTimeoutSeconds) + ? TimeSpan.FromSeconds(RequestTimeoutSeconds) : TimeSpan.FromMilliseconds(downstreamRoute.QosOptions.TimeoutValue)) { InnerHandler = baseHandler, @@ -93,9 +100,14 @@ private HttpMessageHandler CreateHandler(DownstreamRoute downstreamRoute) return handler; } - private readonly struct MessageInvokerCacheKey(DownstreamRoute downstreamRoute) : IEquatable + private readonly struct MessageInvokerCacheKey : IEquatable { - public DownstreamRoute DownstreamRoute { get; } = downstreamRoute; + public MessageInvokerCacheKey(DownstreamRoute downstreamRoute) + { + DownstreamRoute = downstreamRoute; + } + + public DownstreamRoute DownstreamRoute { get; } public override bool Equals(object obj) => obj is MessageInvokerCacheKey key && Equals(key); diff --git a/test/Ocelot.AcceptanceTests/AggregateTests.cs b/test/Ocelot.AcceptanceTests/AggregateTests.cs index cd90b5f5a..daf3bdbfa 100644 --- a/test/Ocelot.AcceptanceTests/AggregateTests.cs +++ b/test/Ocelot.AcceptanceTests/AggregateTests.cs @@ -42,96 +42,86 @@ public void Should_fix_issue_597() var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key1data/{userid}", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port, }, - ], + }, Key = "key1", }, new FileRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key2data/{userid}", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port, }, - ], + }, Key = "key2", }, new FileRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key3data/{userid}", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port, }, - ], + }, Key = "key3", }, new FileRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key4data/{userid}", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port, }, - ], + }, Key = "key4", }, - ], - Aggregates = - [ + }, + Aggregates = new() + { new FileAggregateRoute { - RouteKeys = - [ - "key1", - "key2", - "key3", - "key4" - ], + RouteKeys = new() { "key1", "key2", "key3", "key4" }, UpstreamPathTemplate = "/EmpDetail/IN/{userid}", }, new FileAggregateRoute { - RouteKeys = - [ - "key1", - "key2" - ], + RouteKeys = new() { "key1", "key2" }, UpstreamPathTemplate = "/EmpDetail/US/{userid}", }, - ], + }, GlobalConfiguration = new FileGlobalConfiguration { RequestIdKey = "CorrelationID", @@ -157,78 +147,73 @@ public void Should_return_response_200_with_advanced_aggregate_configs() var port3 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port1, }, - ], + }, UpstreamPathTemplate = "/Comments", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Comments", }, new FileRoute { DownstreamPathTemplate = "/users/{userId}", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port2, }, - ], + }, UpstreamPathTemplate = "/UserDetails/{userId}", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "UserDetails", }, new FileRoute { DownstreamPathTemplate = "/posts/{postId}", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port3, }, - ], + }, UpstreamPathTemplate = "/PostDetails/{postId}", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "PostDetails", }, - ], - Aggregates = - [ + }, + Aggregates = new() + { new FileAggregateRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Comments", - "UserDetails", - "PostDetails" - ], - RouteKeysConfig = - [ + RouteKeys = new() { "Comments", "UserDetails", "PostDetails" }, + RouteKeysConfig = new() + { new AggregateRouteConfig { RouteKey = "UserDetails", JsonPath = "$[*].writerId", Parameter = "userId" }, new AggregateRouteConfig { RouteKey = "PostDetails", JsonPath = "$[*].postId", Parameter = "postId" }, - ], + }, }, - ], + }, }; var userDetailsResponseContent = @"{""id"":1,""firstName"":""abolfazl"",""lastName"":""rajabpour""}"; @@ -255,22 +240,22 @@ public void Should_return_response_200_with_simple_url_user_defined_aggregate() var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port1, }, - ], + }, UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Laura", }, @@ -278,33 +263,29 @@ public void Should_return_response_200_with_simple_url_user_defined_aggregate() { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port2, }, - ], + }, UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Tom", }, - ], - Aggregates = - [ + }, + Aggregates = new() + { new FileAggregateRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Laura", - "Tom" - ], + RouteKeys = new() { "Laura", "Tom" }, Aggregator = "FakeDefinedAggregator", }, - ], + }, }; var expected = "Bye from Laura, Bye from Tom"; @@ -347,54 +328,50 @@ public void Should_return_response_200_with_simple_url_one_service_404() var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port1, }, - ], + }, UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Laura", }, new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port2, }, - ], + }, UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Tom", }, - ], - Aggregates = - [ + }, + Aggregates = new() + { new FileAggregateRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Laura", - "Tom" - ], + RouteKeys = new() { "Laura", "Tom" }, }, - ], + }, }; var expected = "{\"Laura\":,\"Tom\":{Hello from Tom}}"; @@ -417,54 +394,50 @@ public void Should_return_response_200_with_simple_url_both_service_404() var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port1, }, - ], + }, UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Laura", }, new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port2, }, - ], + }, UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Tom", }, - ], - Aggregates = - [ + }, + Aggregates = new() + { new FileAggregateRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Laura", - "Tom" - ], + RouteKeys = new() { "Laura", "Tom" }, }, - ], + }, }; var expected = "{\"Laura\":,\"Tom\":}"; @@ -487,54 +460,50 @@ public void Should_be_thread_safe() var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port1, }, - ], + }, UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Laura", }, new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort { Host = "localhost", Port = port2, }, - ], + }, UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, Key = "Tom", }, - ], - Aggregates = - [ + }, + Aggregates = new() + { new FileAggregateRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Laura", - "Tom" - ], + RouteKeys = new() { "Laura", "Tom" }, }, - ], + }, }; this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 200, "{Hello from Laura}")) @@ -697,9 +666,9 @@ private void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPathOne, { DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, - DownstreamHostAndPorts = [new FileHostAndPort("localhost", port)], + DownstreamHostAndPorts = new() { new FileHostAndPort("localhost", port) }, UpstreamPathTemplate = upstream, - UpstreamHttpMethod = [HttpMethods.Get], + UpstreamHttpMethod = new() { HttpMethods.Get }, Key = key, }; diff --git a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationSteps.cs b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationSteps.cs index 0c2fb65fc..b9e626f00 100644 --- a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationSteps.cs +++ b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationSteps.cs @@ -57,7 +57,7 @@ protected static Client CreateClientWithSecret(string clientId, Secret secret, A protected static Client DefaultClient(AccessTokenType tokenType = AccessTokenType.Jwt, string[] apiScopes = null) { - apiScopes ??= ["api"]; + apiScopes ??= new string[] { "api" }; return new() { ClientId = "client", @@ -65,7 +65,7 @@ protected static Client DefaultClient(AccessTokenType tokenType = AccessTokenTyp ClientSecrets = new List { new("secret".Sha256()) }, AllowedScopes = apiScopes .Union(apiScopes.Select(x => $"{x}.readOnly")) - .Union(["openid", "offline_access"]) + .Union(new string[] { "openid", "offline_access" }) .ToList(), AccessTokenType = tokenType, Enabled = true, @@ -76,8 +76,8 @@ protected static Client DefaultClient(AccessTokenType tokenType = AccessTokenTyp public static IWebHostBuilder CreateIdentityServer(string url, AccessTokenType tokenType, string[] apiScopes, Client[] clients) { - apiScopes ??= ["api"]; - clients ??= [DefaultClient(tokenType, apiScopes)]; + apiScopes ??= new string[] { "api" }; + clients ??= new Client[] { DefaultClient(tokenType, apiScopes) }; var builder = new WebHostBuilder() .UseUrls(url) .UseKestrel() @@ -93,10 +93,10 @@ public static IWebHostBuilder CreateIdentityServer(string url, AccessTokenType t .Select(apiname => new ApiScope(apiname, apiname.ToUpper()))) .AddInMemoryApiResources(apiScopes .Select(x => new { i = Array.IndexOf(apiScopes, x), scope = x }) - .Select(x => CreateApiResource(x.scope, ["openid", "offline_access"]))) + .Select(x => CreateApiResource(x.scope, new string[] { "openid", "offline_access" }))) .AddInMemoryClients(clients) - .AddTestUsers( - [ + .AddTestUsers(new() + { new() { Username = "test", @@ -108,7 +108,7 @@ public static IWebHostBuilder CreateIdentityServer(string url, AccessTokenType t new("LocationId", "321"), }, }, - ]); + }); }) .Configure(app => { @@ -141,16 +141,16 @@ internal Task GivenAuthToken(string url, string apiScope, string cl public static FileRoute GivenDefaultAuthRoute(int port, string upstreamHttpMethod = null, string authProviderKey = null) => new() { DownstreamPathTemplate = "/", - DownstreamHostAndPorts = - [ - new("localhost", port), - ], + DownstreamHostAndPorts = new() + { + new("localhost", port), + }, DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = "/", - UpstreamHttpMethod = [upstreamHttpMethod ?? HttpMethods.Get], - AuthenticationOptions = new FileAuthenticationOptions + UpstreamHttpMethod = new() { upstreamHttpMethod ?? HttpMethods.Get }, + AuthenticationOptions = new() { - AuthenticationProviderKey = authProviderKey ?? "Test", + AuthenticationProviderKeys = new string[] { authProviderKey ?? "Test" }, }, }; diff --git a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs index ea3e9ee3a..05a60e7e2 100644 --- a/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs +++ b/test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs @@ -112,6 +112,7 @@ public void Should_return_201_using_identity_server_reference_token() .BDDfy(); } + [IgnorePublicMethod] public void GivenThereIsAnIdentityServerOn(string url, AccessTokenType tokenType) { var scopes = new string[] { "api", "api2" }; @@ -127,4 +128,11 @@ public override void Dispose() base.Dispose(); } } + + [AttributeUsage(AttributeTargets.Class)] + public sealed class IgnoreXunitAnalyzersRule1013Attribute : Attribute { } + + [IgnoreXunitAnalyzersRule1013] + [AttributeUsage(AttributeTargets.Method)] + public class IgnorePublicMethodAttribute : Attribute { } } diff --git a/test/Ocelot.AcceptanceTests/Authentication/MultipleAuthSchemesFeatureTests.cs b/test/Ocelot.AcceptanceTests/Authentication/MultipleAuthSchemesFeatureTests.cs index 204441497..f797431f9 100644 --- a/test/Ocelot.AcceptanceTests/Authentication/MultipleAuthSchemesFeatureTests.cs +++ b/test/Ocelot.AcceptanceTests/Authentication/MultipleAuthSchemesFeatureTests.cs @@ -19,9 +19,9 @@ public sealed class MultipleAuthSchemesFeatureTests : AuthenticationSteps, IDisp public MultipleAuthSchemesFeatureTests() : base() { - _identityServers = []; - _identityServerUrls = []; - _tokens = []; + _identityServers = Array.Empty(); + _identityServerUrls = Array.Empty(); + _tokens = Array.Empty(); } public override void Dispose() diff --git a/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs b/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs index 4eb8a5bf3..c61ce3864 100644 --- a/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs +++ b/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs @@ -154,21 +154,21 @@ public void Should_clean_cached_response_by_cache_header_via_new_caching_key() private static FileConfiguration GivenFileConfiguration(int port, FileCacheOptions cacheOptions) => new() { - Routes = - [ + Routes = new() + { new FileRoute() { DownstreamPathTemplate = "/", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort("localhost", port), - ], + }, DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = "/", - UpstreamHttpMethod =["Get"], + UpstreamHttpMethod = new() { HttpMethods.Get }, FileCacheOptions = cacheOptions, }, - ], + }, }; private static void GivenTheCacheExpires() diff --git a/test/Ocelot.AcceptanceTests/ContentTests.cs b/test/Ocelot.AcceptanceTests/ContentTests.cs index 5c64da21f..272dc5210 100644 --- a/test/Ocelot.AcceptanceTests/ContentTests.cs +++ b/test/Ocelot.AcceptanceTests/ContentTests.cs @@ -170,20 +170,20 @@ private static string GenerateDummyDatFile(int sizeInMb) private static FileConfiguration GivenConfiguration(int port, string method = null) => new() { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort("localhost", port), - ], + }, UpstreamPathTemplate = "/", - UpstreamHttpMethod = [method ?? HttpMethods.Get], + UpstreamHttpMethod = new() {method ?? HttpMethods.Get }, }, - ], + }, }; } } diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index bb1399943..701a23d67 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -14,9 +14,9 @@ false false false - ..\..\codeanalysis.ruleset True - 1591 + ..\..\codeanalysis.ruleset + $(NoWarn);CS0618;CS1591 diff --git a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs index 9697d2032..58481afa3 100644 --- a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs +++ b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs @@ -1,39 +1,44 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.File; +using Ocelot.Requester; +using System.Reflection; namespace Ocelot.AcceptanceTests { - public class PollyQoSTests : IDisposable + public sealed class PollyQoSTests : Steps, IDisposable { - private readonly Steps _steps; private readonly ServiceHandler _serviceHandler; public PollyQoSTests() { _serviceHandler = new ServiceHandler(); - _steps = new Steps(); + } + + public override void Dispose() + { + _serviceHandler.Dispose(); + base.Dispose(); } - private static FileConfiguration FileConfigurationFactory(int port, QoSOptions options, string httpMethod = nameof(HttpMethods.Get)) - => new() + private static FileConfiguration FileConfigurationFactory(int port, QoSOptions options, string httpMethod = nameof(HttpMethods.Get)) => new() + { + Routes = new() { - Routes = new List + new() { - new() + DownstreamPathTemplate = "/", + DownstreamScheme = Uri.UriSchemeHttp, + DownstreamHostAndPorts = new() { - DownstreamPathTemplate = "/", - DownstreamScheme = Uri.UriSchemeHttp, - DownstreamHostAndPorts = new() - { - new("localhost", port), - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new() {httpMethod}, - QoSOptions = new FileQoSOptions(options), + new("localhost", port), }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() {httpMethod}, + QoSOptions = new FileQoSOptions(options), }, - }; + }, + }; [Fact] public void Should_not_timeout() @@ -42,11 +47,11 @@ public void Should_not_timeout() var configuration = FileConfigurationFactory(port, new QoSOptions(10, 500, 1000, null), HttpMethods.Post); this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, string.Empty, 10)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => GivenThePostHasContent("postContent")) + .When(x => WhenIPostUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } @@ -57,11 +62,11 @@ public void Should_timeout() var configuration = FileConfigurationFactory(port, new QoSOptions(0, 0, 1000, null), HttpMethods.Post); this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 201, string.Empty, 2100)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => GivenThePostHasContent("postContent")) + .When(x => WhenIPostUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) .BDDfy(); } @@ -72,12 +77,12 @@ public void Should_open_circuit_breaker_after_two_exceptions() var configuration = FileConfigurationFactory(port, new QoSOptions(2, 5000, 100000, null)); this.Given(x => x.GivenThereIsABrokenServiceRunningOn($"http://localhost:{port}")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) .BDDfy(); } @@ -88,24 +93,24 @@ public void Should_open_circuit_breaker_then_close() var configuration = FileConfigurationFactory(port, new QoSOptions(2, 500, 1000, null)); this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port}", "Hello from Laura")) - .Given(x => _steps.GivenThereIsAConfiguration(configuration)) - .Given(x => _steps.GivenOcelotIsRunningWithPolly()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) // repeat same request because min ExceptionsAllowedBeforeBreaking is 2 - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => GivenThereIsAConfiguration(configuration)) + .Given(x => GivenOcelotIsRunningWithPolly()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(x => WhenIGetUrlOnTheApiGateway("/")) // repeat same request because min ExceptionsAllowedBeforeBreaking is 2 + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => WhenIGetUrlOnTheApiGateway("/")) + .Given(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => WhenIGetUrlOnTheApiGateway("/")) + .Given(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => WhenIGetUrlOnTheApiGateway("/")) + .Given(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) .Given(x => GivenIWaitMilliseconds(3000)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } @@ -125,43 +130,55 @@ public void Open_circuit_should_not_effect_different_route() this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port1}", "Hello from Laura")) .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port2}", 200, "Hello from Tom", 0)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) // repeat same request because min ExceptionsAllowedBeforeBreaking is 2 - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/working")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => WhenIGetUrlOnTheApiGateway("/")) // repeat same request because min ExceptionsAllowedBeforeBreaking is 2 + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => WhenIGetUrlOnTheApiGateway("/working")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Tom")) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) .And(x => GivenIWaitMilliseconds(3000)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] [Trait("Bug", "1833")] public void Should_timeout_per_default_after_90_seconds() - { + { + // Arrange var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(new FileQoSOptions()), HttpMethods.Get); - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 201, string.Empty, 95000)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .BDDfy(); - } + var configuration = FileConfigurationFactory(port, new QoSOptions(new FileQoSOptions()), HttpMethods.Get); + GivenThereIsAServiceRunningOn(DownstreamUrl(port), (int)HttpStatusCode.Created, string.Empty, 3500); // 3.5s > 3s -> ServiceUnavailable + GivenThereIsAConfiguration(configuration); + GivenOcelotIsRunningWithPolly(); + GivenIHackDefaultTimeoutValue(3); // after 3 secs -> Timeout exception aka request cancellation + + // Act + WhenIGetUrlOnTheApiGateway("/"); + + // Assert + ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); + } + + private void GivenIHackDefaultTimeoutValue(int defaultTimeoutSeconds) + { + var field = typeof(MessageInvokerPool).GetField("_requestTimeoutSeconds", BindingFlags.NonPublic | BindingFlags.Instance); + var service = _ocelotServer.Services.GetService(typeof(IMessageInvokerPool)); + field.SetValue(service, defaultTimeoutSeconds); // hack the value of default 90 seconds + } private static void GivenIWaitMilliseconds(int ms) => Thread.Sleep(ms); @@ -206,12 +223,5 @@ private void GivenThereIsAServiceRunningOn(string url, int statusCode, string re await context.Response.WriteAsync(responseBody); }); } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - GC.SuppressFinalize(this); - } } } diff --git a/test/Ocelot.AcceptanceTests/Request/RequestMapperTests.cs b/test/Ocelot.AcceptanceTests/Request/RequestMapperTests.cs index b5c69b006..ea93d6d8c 100644 --- a/test/Ocelot.AcceptanceTests/Request/RequestMapperTests.cs +++ b/test/Ocelot.AcceptanceTests/Request/RequestMapperTests.cs @@ -117,12 +117,12 @@ private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, Http { DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new("localhost", port), - ], + }, UpstreamPathTemplate = "/", - UpstreamHttpMethod = [method ?? HttpMethods.Get], + UpstreamHttpMethod = new() { method ?? HttpMethods.Get }, }; } diff --git a/test/Ocelot.AcceptanceTests/Request/StreamContentTests.cs b/test/Ocelot.AcceptanceTests/Request/StreamContentTests.cs index 5c20c53e1..1e97a36d1 100644 --- a/test/Ocelot.AcceptanceTests/Request/StreamContentTests.cs +++ b/test/Ocelot.AcceptanceTests/Request/StreamContentTests.cs @@ -86,22 +86,31 @@ static void options(KestrelServerOptions o) { DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new("localhost", port), - ], + }, UpstreamPathTemplate = "/", - UpstreamHttpMethod = [method ?? HttpMethods.Get], + UpstreamHttpMethod = new() { method ?? HttpMethods.Get }, }; } -internal class StreamTestContent(long size, bool sendChunked) : HttpContent +internal class StreamTestContent : HttpContent { - private readonly byte[] _dataBuffer = RandomNumberGenerator.GetBytes(8192); + private readonly long _size; + private readonly bool _sendChunked; + private readonly byte[] _dataBuffer; + + public StreamTestContent(long size, bool sendChunked) + { + _size = size; + _sendChunked = sendChunked; + _dataBuffer = RandomNumberGenerator.GetBytes(8192); + } protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { - var remaining = size; + var remaining = _size; while (remaining > 0) { var count = (int)Math.Min(remaining, _dataBuffer.Length); @@ -112,14 +121,14 @@ protected override async Task SerializeToStreamAsync(Stream stream, TransportCon protected override bool TryComputeLength(out long length) { - if (sendChunked) + if (_sendChunked) { length = -1; return false; } else { - length = size; + length = _size; return true; } } diff --git a/test/Ocelot.AcceptanceTests/Requester/PayloadTooLargeTests.cs b/test/Ocelot.AcceptanceTests/Requester/PayloadTooLargeTests.cs index 35c789143..fdb5741ed 100644 --- a/test/Ocelot.AcceptanceTests/Requester/PayloadTooLargeTests.cs +++ b/test/Ocelot.AcceptanceTests/Requester/PayloadTooLargeTests.cs @@ -71,13 +71,13 @@ public void Should_throw_payload_too_large_exception_using_http_sys() private static FileRoute GivenRoute(int port, string method = null) => new() { DownstreamPathTemplate = "/", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new("localhost", port), - ], + }, DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = "/", - UpstreamHttpMethod = [method ?? HttpMethods.Get], + UpstreamHttpMethod = new() {method ?? HttpMethods.Get }, }; private void GivenThereIsAServiceRunningOn(string baseUrl) diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/RoutingTests.cs index 6470d365c..b881a831f 100644 --- a/test/Ocelot.AcceptanceTests/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingTests.cs @@ -1210,7 +1210,7 @@ internal void ThenTheDownstreamUrlQueryStringShouldBe(string expectedQueryString new("localhost", port), }, UpstreamPathTemplate = upstream, - UpstreamHttpMethod = [HttpMethods.Get], + UpstreamHttpMethod = new() { HttpMethods.Get }, }, }, }; diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs index c37333cf2..7ba087e55 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs @@ -497,7 +497,7 @@ public void Should_use_consul_service_discovery_based_on_upstream_host(string lo Address = "localhost", Port = servicePortUS, ID = Guid.NewGuid().ToString(), - Tags = ["US"], + Tags = new string[] { "US" }, }, }; var serviceEntryEU = new ServiceEntry @@ -508,20 +508,20 @@ public void Should_use_consul_service_discovery_based_on_upstream_host(string lo Address = "localhost", Port = servicePortEU, ID = Guid.NewGuid().ToString(), - Tags = ["EU"], + Tags = new string[] { "EU" }, }, }; var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new() { DownstreamPathTemplate = "/products", DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { "Get" }, UpstreamHost = upstreamHostUS, ServiceName = serviceNameUS, LoadBalancerOptions = new() { Type = loadBalancerType }, @@ -531,12 +531,12 @@ public void Should_use_consul_service_discovery_based_on_upstream_host(string lo DownstreamPathTemplate = "/products", DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() {"Get" }, UpstreamHost = upstreamHostEU, ServiceName = serviceNameEU, LoadBalancerOptions = new() { Type = loadBalancerType }, }, - ], + }, GlobalConfiguration = new() { ServiceDiscoveryProvider = new() diff --git a/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs b/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs index ec952bd42..20db1ed4a 100644 --- a/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs @@ -142,14 +142,14 @@ public void should_support_placeholder_in_service_fabric_service_name(string ups var configuration = new FileConfiguration { - Routes = new List + Routes = new() { new() { DownstreamPathTemplate = downstream, - DownstreamScheme = "http", + DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = upstream, - UpstreamHttpMethod = ["Get"], + UpstreamHttpMethod = new() { HttpMethods.Get }, ServiceName = serviceName, }, }, diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index fe4b2bc7f..7f95a8a9d 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -804,35 +804,8 @@ public void GivenOcelotIsRunningWithEureka() _ocelotClient = _ocelotServer.CreateClient(); } - public void GivenOcelotIsRunningWithPolly() - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", true, false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddPolly(); - }) - .Configure(app => - { - app.UseOcelot() - .Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } + public void GivenOcelotIsRunningWithPolly() => GivenOcelotIsRunningWithServices(WithPolly); + public static void WithPolly(IServiceCollection services) => services.AddOcelot().AddPolly(); public void WhenIGetUrlOnTheApiGateway(string url) { diff --git a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj index bcaf49e12..288d79019 100644 --- a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj +++ b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj @@ -13,9 +13,9 @@ false false false - ..\..\codeanalysis.ruleset True - 1591 + ..\..\codeanalysis.ruleset + $(NoWarn);CS0618;CS1591 diff --git a/test/Ocelot.Benchmarks/PayloadBenchmarks.cs b/test/Ocelot.Benchmarks/PayloadBenchmarks.cs index 7547ad921..fe776f0ea 100644 --- a/test/Ocelot.Benchmarks/PayloadBenchmarks.cs +++ b/test/Ocelot.Benchmarks/PayloadBenchmarks.cs @@ -38,20 +38,20 @@ public void SetUp() { var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort("localhost", 51879), - ], + }, DownstreamScheme = "http", UpstreamPathTemplate = "/", - UpstreamHttpMethod =["Post"], + UpstreamHttpMethod = new() { "Post" }, }, - ], + }, }; GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201); @@ -113,12 +113,12 @@ private static object[] GeneratePayload(int size, string directory, string fileN { var filePath = Path.Combine(directory, fileName); var generateDummy = isJson ? (Func) GenerateDummyJsonFile : GenerateDummyDatFile; - return - [ + return new object[] + { generateDummy(size, filePath), fileName, isJson, - ]; + }; } /// diff --git a/test/Ocelot.Benchmarks/ResponseBenchmarks.cs b/test/Ocelot.Benchmarks/ResponseBenchmarks.cs index f68ecb254..a7c1d85b7 100644 --- a/test/Ocelot.Benchmarks/ResponseBenchmarks.cs +++ b/test/Ocelot.Benchmarks/ResponseBenchmarks.cs @@ -41,20 +41,20 @@ public void SetUp() { var configuration = new FileConfiguration { - Routes = - [ + Routes = new() + { new FileRoute { DownstreamPathTemplate = "/", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new FileHostAndPort("localhost", 51879), - ], - DownstreamScheme = "http", + }, + DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = "/", - UpstreamHttpMethod =["GET"], + UpstreamHttpMethod = new() { HttpMethods.Get }, }, - ], + }, }; GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201); @@ -115,12 +115,12 @@ private static object[] GeneratePayload(int size, string directory, string fileN { var filePath = Path.Combine(directory, fileName); var generateDummy = isJson ? (Func)GenerateDummyJsonFile : GenerateDummyDatFile; - return - [ + return new object[] + { generateDummy(size, filePath), fileName, isJson, - ]; + }; } /// diff --git a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj index 996603c55..857f2c0b5 100644 --- a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj +++ b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj @@ -14,9 +14,9 @@ false false false - ..\..\codeanalysis.ruleset True - 1591 + ..\..\codeanalysis.ruleset + $(NoWarn);CS0618;CS1591 diff --git a/samples/Docker/Dockerfile b/test/Ocelot.ManualTest/Dockerfile similarity index 98% rename from samples/Docker/Dockerfile rename to test/Ocelot.ManualTest/Dockerfile index dad54b20f..31b4580b3 100644 --- a/samples/Docker/Dockerfile +++ b/test/Ocelot.ManualTest/Dockerfile @@ -1,47 +1,47 @@ -#This is the base image used for any ran images -FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-stretch-slim AS base -WORKDIR /app -EXPOSE 80 - -#This image is used to build the source for the runnable app -#It can also be used to run other CLI commands on the project, such as packing/deploying nuget packages. Some examples: -#Run tests: docker build --target builder -t ocelot-build . && docker run ocelot-build test --logger:trx;LogFileName=results.trx -#Run benchmarks: docker build --target builder --build-arg build_configuration=Release -t ocelot-build . && docker run ocelot-build run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj -FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS builder -WORKDIR /build -#First we add only the project files so that we can cache nuget packages with dotnet restore -COPY Ocelot.sln Ocelot.sln -COPY src/Ocelot/Ocelot.csproj src/Ocelot/Ocelot.csproj -COPY src/Ocelot.Administration/Ocelot.Administration.csproj src/Ocelot.Administration/Ocelot.Administration.csproj -COPY src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj -COPY src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj -COPY src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj -COPY src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj -COPY src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj -COPY src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj -COPY test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj -COPY test/Ocelot.ManualTest/Ocelot.ManualTest.csproj test/Ocelot.ManualTest/Ocelot.ManualTest.csproj -COPY test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj -COPY test/Ocelot.UnitTests/Ocelot.UnitTests.csproj test/Ocelot.UnitTests/Ocelot.UnitTests.csproj -COPY test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj - -RUN dotnet restore -#Now we add the rest of the source and run a complete build... --no-restore is used because nuget should be resolved at this point -COPY codeanalysis.ruleset codeanalysis.ruleset -COPY src src -COPY test test -ARG build_configuration=Debug -RUN dotnet build --no-restore -c ${build_configuration} -ENTRYPOINT ["dotnet"] - -#This is just for holding the published manual tests... -FROM builder AS manual-test-publish -ARG build_configuration=Debug -RUN dotnet publish --no-build -c ${build_configuration} -o /app test/Ocelot.ManualTest - -#Run manual tests! This is the default run option. -#docker build -t ocelot-manual-test . && docker run --net host ocelot-manual-test -FROM base AS manual-test -ENV ASPNETCORE_ENVIRONMENT=Development -COPY --from=manual-test-publish /app . -ENTRYPOINT ["dotnet", "Ocelot.ManualTest.dll"] +#This is the base image used for any ran images +FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-stretch-slim AS base +WORKDIR /app +EXPOSE 80 + +#This image is used to build the source for the runnable app +#It can also be used to run other CLI commands on the project, such as packing/deploying nuget packages. Some examples: +#Run tests: docker build --target builder -t ocelot-build . && docker run ocelot-build test --logger:trx;LogFileName=results.trx +#Run benchmarks: docker build --target builder --build-arg build_configuration=Release -t ocelot-build . && docker run ocelot-build run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj +FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS builder +WORKDIR /build +#First we add only the project files so that we can cache nuget packages with dotnet restore +COPY Ocelot.sln Ocelot.sln +COPY src/Ocelot/Ocelot.csproj src/Ocelot/Ocelot.csproj +COPY src/Ocelot.Administration/Ocelot.Administration.csproj src/Ocelot.Administration/Ocelot.Administration.csproj +COPY src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj +COPY src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj +COPY src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj +COPY src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj +COPY src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj +COPY src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj +COPY test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +COPY test/Ocelot.ManualTest/Ocelot.ManualTest.csproj test/Ocelot.ManualTest/Ocelot.ManualTest.csproj +COPY test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj +COPY test/Ocelot.UnitTests/Ocelot.UnitTests.csproj test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +COPY test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj + +RUN dotnet restore +#Now we add the rest of the source and run a complete build... --no-restore is used because nuget should be resolved at this point +COPY codeanalysis.ruleset codeanalysis.ruleset +COPY src src +COPY test test +ARG build_configuration=Debug +RUN dotnet build --no-restore -c ${build_configuration} +ENTRYPOINT ["dotnet"] + +#This is just for holding the published manual tests... +FROM builder AS manual-test-publish +ARG build_configuration=Debug +RUN dotnet publish --no-build -c ${build_configuration} -o /app test/Ocelot.ManualTest + +#Run manual tests! This is the default run option. +#docker build -t ocelot-manual-test . && docker run --net host ocelot-manual-test +FROM base AS manual-test +ENV ASPNETCORE_ENVIRONMENT=Development +COPY --from=manual-test-publish /app . +ENTRYPOINT ["dotnet", "Ocelot.ManualTest.dll"] diff --git a/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj b/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj index 81aabb2f1..919f6b913 100644 --- a/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj +++ b/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj @@ -9,9 +9,9 @@ Exe Ocelot.ManualTest win-x64;osx-x64 - ..\..\codeanalysis.ruleset True - 1591 + ..\..\codeanalysis.ruleset + $(NoWarn);CS0618;CS1591 diff --git a/samples/Docker-Compose/docker-compose.yaml b/test/Ocelot.ManualTest/docker-compose.yaml similarity index 95% rename from samples/Docker-Compose/docker-compose.yaml rename to test/Ocelot.ManualTest/docker-compose.yaml index 5236202fd..831368441 100644 --- a/samples/Docker-Compose/docker-compose.yaml +++ b/test/Ocelot.ManualTest/docker-compose.yaml @@ -1,24 +1,24 @@ -version: "3.4" -services: - - tests: - build: - context: . - target: builder - volumes: - - type: bind - source: . - target: /results - command: test --logger:trx -r /results - - benchmarks: - build: - context: . - target: builder - args: - build_configuration: Release - command: run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj 0 1 2 3 4 - - manual-test: - build: . - ports: [ "5000:80" ] +version: "3.4" +services: + + tests: + build: + context: . + target: builder + volumes: + - type: bind + source: . + target: /results + command: test --logger:trx -r /results + + benchmarks: + build: + context: . + target: builder + args: + build_configuration: Release + command: run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj 0 1 2 3 4 + + manual-test: + build: . + ports: [ "5000:80" ] diff --git a/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs b/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs index 838c6f239..15c827662 100644 --- a/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs +++ b/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Administration { - public class OcelotAdministrationBuilderTests + public class OcelotAdministrationBuilderTests : UnitTest { private readonly IServiceCollection _services; private IServiceProvider _serviceProvider; @@ -22,7 +22,7 @@ public OcelotAdministrationBuilderTests() _services = new ServiceCollection(); _services.AddSingleton(GetHostingEnvironment()); _services.AddSingleton(_configRoot); - } + } private static IWebHostEnvironment GetHostingEnvironment() { diff --git a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs index 2201722eb..0bd9eb603 100644 --- a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs @@ -12,7 +12,7 @@ namespace Ocelot.UnitTests.Authentication { - public class AuthenticationMiddlewareTests + public class AuthenticationMiddlewareTests : UnitTest { private readonly Mock _authentication; private readonly Mock _factory; @@ -121,7 +121,7 @@ public void Should_provide_backward_compatibility_if_route_has_several_options_a { var options = new AuthenticationOptions(null, "Test", - [string.Empty, "Fail", "Test"] + new string[] { string.Empty, "Fail", "Test" } ); var methods = new List { "Get" }; this.Given(x => GivenTheDownStreamRouteIs(new DownstreamRouteBuilder() @@ -143,7 +143,7 @@ public void Should_provide_backward_compatibility_if_route_has_several_options_a public void Should_not_call_next_middleware_and_return_no_result_if_all_multiple_keys_were_failed() { var options = new AuthenticationOptions(null, null, - [string.Empty, "Fail", "Fail", "UnknownScheme"] + new string[] { string.Empty, "Fail", "Fail", "UnknownScheme" } ); var methods = new List { "Get" }; diff --git a/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs index 045257e32..717de3daf 100644 --- a/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.Authorization { - public class AuthorizationMiddlewareTests + public class AuthorizationMiddlewareTests : UnitTest { private readonly Mock _authService; private readonly Mock _authScopesService; diff --git a/test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs b/test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs index 45865a456..6033d0a7a 100644 --- a/test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs +++ b/test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Authorization { - public class ClaimsAuthorizerTests + public class ClaimsAuthorizerTests : UnitTest { private readonly ClaimsAuthorizer _claimsAuthorizer; private ClaimsPrincipal _claimsPrincipal; diff --git a/test/Ocelot.UnitTests/Cache/AspMemoryCacheTests.cs b/test/Ocelot.UnitTests/Cache/AspMemoryCacheTests.cs index 573c076ce..69cda7c8d 100644 --- a/test/Ocelot.UnitTests/Cache/AspMemoryCacheTests.cs +++ b/test/Ocelot.UnitTests/Cache/AspMemoryCacheTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.Cache { - public class AspMemoryCacheTests + public class AspMemoryCacheTests : UnitTest { private readonly AspMemoryCache _cache; diff --git a/test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs b/test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs index a81ec7d07..be9de81e8 100644 --- a/test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs +++ b/test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Cache; -public sealed class DefaultCacheKeyGeneratorTests : IDisposable +public sealed class DefaultCacheKeyGeneratorTests : UnitTest, IDisposable { private readonly ICacheKeyGenerator _cacheKeyGenerator; private readonly HttpRequestMessage _request; diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs index bb7e829a9..aaaf263f0 100644 --- a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Cache { - public class OutputCacheMiddlewareTests + public class OutputCacheMiddlewareTests : UnitTest { private readonly Mock> _cache; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs b/test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs index 841ff5166..89d552009 100644 --- a/test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs +++ b/test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.Cache { - public class RegionCreatorTests + public class RegionCreatorTests : UnitTest { private string _result; private FileRoute _route; diff --git a/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs index 9d980aa23..410c73b64 100644 --- a/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.CacheManager { - public class OcelotBuilderExtensionsTests + public class OcelotBuilderExtensionsTests : UnitTest { private readonly IServiceCollection _services; private readonly IConfiguration _configRoot; diff --git a/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs b/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs index 82ffc5bf9..a2a662029 100644 --- a/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs +++ b/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.CacheManager { - public class OcelotCacheManagerCache + public class OcelotCacheManagerCache : UnitTest { private readonly OcelotCacheManagerCache _ocelotOcelotCacheManager; private readonly Mock> _mockCacheManager; diff --git a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs index ee52d3785..fc002eaab 100644 --- a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs +++ b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.CacheManager { - public class OutputCacheMiddlewareRealCacheTests + public class OutputCacheMiddlewareRealCacheTests : UnitTest { private readonly IOcelotCache _cacheManager; private readonly ICacheKeyGenerator _cacheKeyGenerator; diff --git a/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs b/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs index c3d2c212e..362df7f4a 100644 --- a/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs +++ b/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Claims { - public class AddClaimsToRequestTests + public class AddClaimsToRequestTests : UnitTest { private readonly AddClaimsToRequest _addClaimsToRequest; private readonly Mock _parser; diff --git a/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs b/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs index e996968af..2fcface83 100644 --- a/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Claims { - public class ClaimsToClaimsMiddlewareTests + public class ClaimsToClaimsMiddlewareTests : UnitTest { private readonly Mock _addHeaders; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs index d4605f038..469e3d7f2 100644 --- a/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Configuration { - public class AggregatesCreatorTests + public class AggregatesCreatorTests : UnitTest { private readonly AggregatesCreator _creator; private readonly Mock _utpCreator; diff --git a/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs index b1830ba62..d0e8d599e 100644 --- a/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs @@ -46,7 +46,7 @@ public void Create_OptionsObjIsNotNull_CreatedSuccessfully(bool isAuthentication // Arrange string authenticationProviderKey = !isAuthenticationProviderKeys ? "Test" : null; string[] authenticationProviderKeys = isAuthenticationProviderKeys ? - ["Test #1", "Test #2"] : null; + new string[] { "Test #1", "Test #2" } : null; var fileRoute = new FileRoute() { AuthenticationOptions = new FileAuthenticationOptions diff --git a/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs b/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs index 299eb30ff..dd0b22bd3 100644 --- a/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs @@ -2,7 +2,7 @@ namespace Ocelot.UnitTests.Configuration.ChangeTracking { - public class OcelotConfigurationChangeTokenSourceTests + public class OcelotConfigurationChangeTokenSourceTests : UnitTest { private readonly IOcelotConfigurationChangeTokenSource _source; diff --git a/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs b/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs index 58b20d7ea..20602553e 100644 --- a/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs @@ -2,7 +2,7 @@ namespace Ocelot.UnitTests.Configuration.ChangeTracking { - public class OcelotConfigurationChangeTokenTests + public class OcelotConfigurationChangeTokenTests : UnitTest { [Fact] public void should_call_callback_with_state() diff --git a/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs b/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs index 6b46b0fff..553a32e89 100644 --- a/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Configuration { - public class ClaimToThingConfigurationParserTests + public class ClaimToThingConfigurationParserTests : UnitTest { private Dictionary _dictionary; private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser; diff --git a/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs index 70438aed8..878d1da77 100644 --- a/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Configuration { - public class ClaimsToThingCreatorTests + public class ClaimsToThingCreatorTests : UnitTest { private readonly Mock _configParser; private Dictionary _claimsToThings; diff --git a/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs index 2af3224df..8f6c7f803 100644 --- a/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Configuration { - public class ConfigurationCreatorTests + public class ConfigurationCreatorTests : UnitTest { private ConfigurationCreator _creator; private InternalConfiguration _result; diff --git a/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs index 30a6354a1..eea4ee955 100644 --- a/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs @@ -229,7 +229,7 @@ private static FileConfiguration FakeFileConfigurationForGet() private static FileRoute GivenRoute(string host, string downstream) => new() { - DownstreamHostAndPorts = [new(host, 80)], + DownstreamHostAndPorts = new() { new(host, 80) }, DownstreamScheme = Uri.UriSchemeHttps, DownstreamPathTemplate = downstream, }; diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs index 6cb6ece1c..0fe22959f 100644 --- a/test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration { - public class DownstreamAddressesCreatorTests + public class DownstreamAddressesCreatorTests : UnitTest { public DownstreamAddressesCreator _creator; private FileRoute _route; diff --git a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs index 1a8b31566..aec6ffb75 100644 --- a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Configuration { - public class DynamicsCreatorTests + public class DynamicsCreatorTests : UnitTest { private readonly DynamicsCreator _creator; private readonly Mock _rloCreator; diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationPollerTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationPollerTests.cs index 524a66dba..366f40ae8 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationPollerTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationPollerTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Configuration { - public class FileConfigurationPollerTests : IDisposable + public class FileConfigurationPollerTests : UnitTest, IDisposable { private readonly FileConfigurationPoller _poller; private readonly Mock _factory; diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs index ba4df0d0f..2c2201a33 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Configuration { - public class FileConfigurationSetterTests + public class FileConfigurationSetterTests : UnitTest { private FileConfiguration _fileConfiguration; private readonly FileAndInternalConfigurationSetter _configSetter; diff --git a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs index 865604071..b8fb5b7f6 100644 --- a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Configuration { - public class FileInternalConfigurationCreatorTests + public class FileInternalConfigurationCreatorTests : UnitTest { private readonly Mock _validator; private readonly Mock _routesCreator; diff --git a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs index 854f2abea..b5f42d6e3 100644 --- a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Configuration { - public class HeaderFindAndReplaceCreatorTests + public class HeaderFindAndReplaceCreatorTests : UnitTest { private readonly HeaderFindAndReplaceCreator _creator; private FileRoute _route; diff --git a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs index 8e41eec9e..33f6d629a 100644 --- a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Configuration { - public class HttpHandlerOptionsCreatorTests + public class HttpHandlerOptionsCreatorTests : UnitTest { private IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; private FileRoute _fileRoute; diff --git a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs index a9af24523..85d6bf210 100644 --- a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Configuration { - public class InMemoryConfigurationRepositoryTests + public class InMemoryConfigurationRepositoryTests : UnitTest { private readonly InMemoryInternalConfigurationRepository _repo; private IInternalConfiguration _config; diff --git a/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs index d7d2e1bac..7e1464ecd 100644 --- a/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration { - public class LoadBalancerOptionsCreatorTests + public class LoadBalancerOptionsCreatorTests : UnitTest { private readonly ILoadBalancerOptionsCreator _creator; private FileLoadBalancerOptions _fileLoadBalancerOptions; diff --git a/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs index 893c8630b..f394932df 100644 --- a/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Configuration { - public class QoSOptionsCreatorTests + public class QoSOptionsCreatorTests : UnitTest { private readonly QoSOptionsCreator _creator; private FileRoute _fileRoute; diff --git a/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs index 63e779312..e514877d2 100644 --- a/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Configuration { - public class RateLimitOptionsCreatorTests + public class RateLimitOptionsCreatorTests : UnitTest { private FileRoute _fileRoute; private FileGlobalConfiguration _fileGlobalConfig; diff --git a/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs index 4511c04fa..d2be4fcc5 100644 --- a/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.Configuration { - public class RequestIdKeyCreatorTests + public class RequestIdKeyCreatorTests : UnitTest { private FileRoute _fileRoute; private FileGlobalConfiguration _fileGlobalConfig; diff --git a/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs index 8326fb5a0..ff26b406f 100644 --- a/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration { - public class RouteKeyCreatorTests + public class RouteKeyCreatorTests : UnitTest { private readonly RouteKeyCreator _creator; private FileRoute _route; @@ -39,12 +39,12 @@ public void Should_return_route_key() var route = new FileRoute { UpstreamPathTemplate = "/api/product", - UpstreamHttpMethod = ["GET", "POST", "PUT"], - DownstreamHostAndPorts = - [ + UpstreamHttpMethod = new() { "GET", "POST", "PUT" }, + DownstreamHostAndPorts = new() + { new("localhost", 8080), new("localhost", 4430), - ], + }, }; this.Given(_ => GivenThe(route)) @@ -60,12 +60,12 @@ public void Should_return_route_key_with_upstream_host() { UpstreamHost = "my-host", UpstreamPathTemplate = "/api/product", - UpstreamHttpMethod = ["GET", "POST", "PUT"], - DownstreamHostAndPorts = - [ + UpstreamHttpMethod = new() { "GET", "POST", "PUT" }, + DownstreamHostAndPorts = new() + { new("localhost", 8080), new("localhost", 4430), - ], + }, }; this.Given(_ => GivenThe(route)) @@ -80,7 +80,7 @@ public void Should_return_route_key_with_svc_name() var route = new FileRoute { UpstreamPathTemplate = "/api/product", - UpstreamHttpMethod = ["GET", "POST", "PUT"], + UpstreamHttpMethod = new() { "GET", "POST", "PUT" }, ServiceName = "products-service", }; @@ -96,7 +96,7 @@ public void Should_return_route_key_with_load_balancer_options() var route = new FileRoute { UpstreamPathTemplate = "/api/product", - UpstreamHttpMethod = ["GET", "POST", "PUT"], + UpstreamHttpMethod = new() { "GET", "POST", "PUT" }, ServiceName = "products-service", LoadBalancerOptions = new FileLoadBalancerOptions { diff --git a/test/Ocelot.UnitTests/Configuration/RouteOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RouteOptionsCreatorTests.cs index bfb2927e5..21f9058ae 100644 --- a/test/Ocelot.UnitTests/Configuration/RouteOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RouteOptionsCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration; -public class RouteOptionsCreatorTests +public class RouteOptionsCreatorTests : UnitTest { private readonly RouteOptionsCreator _creator; @@ -126,7 +126,7 @@ public void Create_RouteOptions_HappyPath(bool isAuthenticationProviderKeys) { AuthenticationProviderKey = !isAuthenticationProviderKeys ? "Test" : null, AuthenticationProviderKeys = isAuthenticationProviderKeys ? - [string.Empty, "Test #1"] : null, + new string[] { string.Empty, "Test #1" } : null, }, RouteClaimsRequirement = new Dictionary { diff --git a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs index cf5f38e7b..382eb3a44 100644 --- a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Configuration { - public class RoutesCreatorTests + public class RoutesCreatorTests : UnitTest { private readonly RoutesCreator _creator; private readonly Mock _cthCreator; diff --git a/test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs index 6c2968020..81d9fd36d 100644 --- a/test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration { - public class SecurityOptionsCreatorTests + public class SecurityOptionsCreatorTests : UnitTest { private FileRoute _fileRoute; private SecurityOptions _result; diff --git a/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs index ac92db206..aaa238cca 100644 --- a/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Configuration { - public class ServiceProviderCreatorTests + public class ServiceProviderCreatorTests : UnitTest { private readonly ServiceProviderConfigurationCreator _creator; private FileGlobalConfiguration _globalConfig; diff --git a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs index 4028d456b..76cb789d3 100644 --- a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration { - public class UpstreamTemplatePatternCreatorTests + public class UpstreamTemplatePatternCreatorTests : UnitTest { private FileRoute _fileRoute; private readonly UpstreamTemplatePatternCreator _creator; diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs index c45cb9d54..3a87dec2f 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs @@ -18,7 +18,7 @@ namespace Ocelot.UnitTests.Configuration.Validation { - public class FileConfigurationFluentValidatorTests + public class FileConfigurationFluentValidatorTests : UnitTest { private IConfigurationValidator _configurationValidator; private FileConfiguration _fileConfiguration; @@ -201,19 +201,15 @@ public void Configuration_is_valid_if_aggregates_are_valid() var route2 = GivenDefaultRoute("/tom", "/"); route2.Key = "Tom"; var configuration = GivenAConfiguration(route, route2); - configuration.Aggregates = - [ + configuration.Aggregates = new() + { new() { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - ]; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsValid()) @@ -229,19 +225,15 @@ public void Configuration_is_invalid_if_aggregates_are_duplicate_of_routes() route2.Key = "Tom"; route2.UpstreamHost = "localhost"; var configuration = GivenAConfiguration(route, route2); - configuration.Aggregates = - [ + configuration.Aggregates = new() + { new() { UpstreamPathTemplate = "/tom", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - ]; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -256,21 +248,17 @@ public void Configuration_is_valid_if_aggregates_are_not_duplicate_of_routes() route.Key = "Laura"; var route2 = GivenDefaultRoute("/tom", "/"); route2.Key = "Tom"; - route2.UpstreamHttpMethod = ["Post"]; + route2.UpstreamHttpMethod = new() { "Post" }; var configuration = GivenAConfiguration(route, route2); - configuration.Aggregates = - [ + configuration.Aggregates = new() + { new() { UpstreamPathTemplate = "/tom", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - ]; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsValid()) @@ -285,29 +273,21 @@ public void Configuration_is_invalid_if_aggregates_are_duplicate_of_aggregates() var route2 = GivenDefaultRoute("/lol", "/"); route2.Key = "Tom"; var configuration = GivenAConfiguration(route, route2); - configuration.Aggregates = - [ + configuration.Aggregates = new() + { new() { UpstreamPathTemplate = "/tom", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, new() { UpstreamPathTemplate = "/tom", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - ]; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -321,19 +301,15 @@ public void Configuration_is_invalid_if_routes_dont_exist_for_aggregate() var route = GivenDefaultRoute("/laura", "/"); route.Key = "Laura"; var configuration = GivenAConfiguration(route); - configuration.Aggregates = - [ + configuration.Aggregates = new() + { new() { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - ]; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -350,19 +326,15 @@ public void Configuration_is_invalid_if_aggregate_has_routes_with_specific_reque route2.Key = "Tom"; route2.RequestIdKey = "should_fail"; var configuration = GivenAConfiguration(route, route2); - configuration.Aggregates = - [ + configuration.Aggregates = new() + { new() { UpstreamPathTemplate = "/", UpstreamHost = "localhost", - RouteKeys = - [ - "Tom", - "Laura", - ], + RouteKeys = new() { "Tom", "Laura" }, }, - ]; + }; this.Given(x => x.GivenAConfiguration(configuration)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -455,7 +427,7 @@ public void Configuration_is_invalid_with_invalid_authentication_provider() route.AuthenticationOptions = new FileAuthenticationOptions() { AuthenticationProviderKey = "Test", - AuthenticationProviderKeys = ["Test #1", "Test #2"], + AuthenticationProviderKeys = new string[] { "Test #1", "Test #2" }, }; this.Given(x => x.GivenAConfiguration(route)) .When(x => x.WhenIValidateTheConfiguration()) @@ -495,7 +467,7 @@ public void Configuration_is_not_valid_with_duplicate_routes_specific_verbs() { var route = GivenDefaultRoute(); var duplicate = GivenDefaultRoute(null, "/www/test/"); - duplicate.UpstreamHttpMethod = ["Get"]; + duplicate.UpstreamHttpMethod = new() { "Get" }; this.Given(x => x.GivenAConfiguration(route, duplicate)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -508,7 +480,7 @@ public void Configuration_is_valid_with_duplicate_routes_different_verbs() { var route = GivenDefaultRoute(); // "Get" verb is inside var duplicate = GivenDefaultRoute(null, "/www/test/"); - duplicate.UpstreamHttpMethod = ["Post"]; + duplicate.UpstreamHttpMethod = new() { "Post" }; this.Given(x => x.GivenAConfiguration(route, duplicate)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsValid()) @@ -519,11 +491,11 @@ public void Configuration_is_valid_with_duplicate_routes_different_verbs() public void Configuration_is_not_valid_with_duplicate_routes_with_duplicated_upstreamhosts() { var route = GivenDefaultRoute(); - route.UpstreamHttpMethod = []; + route.UpstreamHttpMethod = new(); route.UpstreamHost = "upstreamhost"; var duplicate = GivenDefaultRoute(null, "/www/test/"); - duplicate.UpstreamHttpMethod = []; + duplicate.UpstreamHttpMethod = new(); duplicate.UpstreamHost = "upstreamhost"; this.Given(x => x.GivenAConfiguration(route, duplicate)) @@ -537,11 +509,11 @@ public void Configuration_is_not_valid_with_duplicate_routes_with_duplicated_ups public void Configuration_is_valid_with_duplicate_routes_but_different_upstreamhosts() { var route = GivenDefaultRoute(); - route.UpstreamHttpMethod = []; + route.UpstreamHttpMethod = new(); route.UpstreamHost = "upstreamhost111"; var duplicate = GivenDefaultRoute(null, "/www/test/"); - duplicate.UpstreamHttpMethod = []; + duplicate.UpstreamHttpMethod = new(); duplicate.UpstreamHost = "upstreamhost222"; this.Given(x => x.GivenAConfiguration(route, duplicate)) @@ -554,11 +526,11 @@ public void Configuration_is_valid_with_duplicate_routes_but_different_upstreamh public void Configuration_is_valid_with_duplicate_routes_but_one_upstreamhost_is_not_set() { var route = GivenDefaultRoute(); - route.UpstreamHttpMethod = []; + route.UpstreamHttpMethod = new(); route.UpstreamHost = "upstreamhost"; var duplicate = GivenDefaultRoute(null, "/www/test/"); - duplicate.UpstreamHttpMethod = []; + duplicate.UpstreamHttpMethod = new(); this.Given(x => x.GivenAConfiguration(route, duplicate)) .When(x => x.WhenIValidateTheConfiguration()) @@ -697,10 +669,10 @@ public void HaveServiceDiscoveryProviderRegistered_ServiceDiscoveryFinderDelegat public void Configuration_is_valid_when_not_using_service_discovery_and_host_is_set() { var route = GivenDefaultRoute(); - route.DownstreamHostAndPorts = - [ + route.DownstreamHostAndPorts = new() + { new("bbc.co.uk", 123), - ]; + }; this.Given(x => x.GivenAConfiguration(route)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsValid()) @@ -711,10 +683,10 @@ public void Configuration_is_valid_when_not_using_service_discovery_and_host_is_ public void Configuration_is_valid_when_no_downstream_but_has_host_and_port() { var route = GivenDefaultRoute(); - route.DownstreamHostAndPorts = - [ + route.DownstreamHostAndPorts = new() + { new("test", 123), - ]; + }; this.Given(x => x.GivenAConfiguration(route)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsValid()) @@ -725,7 +697,7 @@ public void Configuration_is_valid_when_no_downstream_but_has_host_and_port() public void Configuration_is_not_valid_when_no_host_and_port() { var route = GivenDefaultRoute(); - route.DownstreamHostAndPorts = []; + route.DownstreamHostAndPorts = new(); this.Given(x => x.GivenAConfiguration(route)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -737,10 +709,10 @@ public void Configuration_is_not_valid_when_no_host_and_port() public void Configuration_is_not_valid_when_host_and_port_is_empty() { var route = GivenDefaultRoute(); - route.DownstreamHostAndPorts = - [ + route.DownstreamHostAndPorts = new() + { new(), - ]; + }; this.Given(x => x.GivenAConfiguration(route)) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) @@ -815,19 +787,19 @@ public void Configuration_is_invalid_when_placeholder_is_used_twice_in_downstrea private static FileRoute GivenDefaultRoute(string upstream, string downstream, string host) => new() { - UpstreamHttpMethod = [HttpMethods.Get], + UpstreamHttpMethod = new() { HttpMethods.Get }, UpstreamPathTemplate = upstream ?? "/asdf/", DownstreamPathTemplate = downstream ?? "/api/products/", - DownstreamHostAndPorts = - [ + DownstreamHostAndPorts = new() + { new(host ?? "bbc.co.uk", 12345), - ], + }, DownstreamScheme = Uri.UriSchemeHttp, }; private static FileRoute GivenServiceDiscoveryRoute() => new() { - UpstreamHttpMethod = [HttpMethods.Get], + UpstreamHttpMethod = new() { HttpMethods.Get }, UpstreamPathTemplate = "/laura", DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, @@ -918,7 +890,7 @@ private void GivenAServiceDiscoveryHandler() private class FakeServiceDiscoveryProvider : IServiceDiscoveryProvider { - public Task> GetAsync() => Task.FromResult>([]); + public Task> GetAsync() => Task.FromResult>(new()); } private class TestOptions : AuthenticationSchemeOptions { } diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs index 69a7401e9..84d041e23 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Configuration.Validation { - public class FileQoSOptionsFluentValidatorTests + public class FileQoSOptionsFluentValidatorTests : UnitTest { private FileQoSOptionsFluentValidator _validator; private readonly ServiceCollection _services; diff --git a/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs index 9ea2bb364..af975654f 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Configuration.Validation { - public class HostAndPortValidatorTests + public class HostAndPortValidatorTests : UnitTest { private HostAndPortValidator _validator; private ValidationResult _result; diff --git a/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs index 2e696f41d..1b2bfadb3 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Configuration.Validation { - public class RouteFluentValidatorTests + public class RouteFluentValidatorTests : UnitTest { private readonly RouteFluentValidator _validator; private readonly Mock _authProvider; diff --git a/test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs index dde40682a..129ff791a 100644 --- a/test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs @@ -2,7 +2,7 @@ namespace Ocelot.UnitTests.Configuration { - public class VersionCreatorTests + public class VersionCreatorTests : UnitTest { private readonly HttpVersionCreator _creator; private string _input; diff --git a/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs index e7f49f3d2..2b2ee6557 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Consul { - public class ConsulFileConfigurationRepositoryTests + public class ConsulFileConfigurationRepositoryTests : UnitTest { private ConsulFileConfigurationRepository _repo; private readonly Mock> _options; diff --git a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs index bf570142c..7df0a2c60 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Consul { - public class ConsulServiceDiscoveryProviderTests : IDisposable + public class ConsulServiceDiscoveryProviderTests : UnitTest, IDisposable { private IWebHost _fakeConsulBuilder; private readonly List _serviceEntries; diff --git a/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs index f324f4b4d..b9c7532c0 100644 --- a/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Consul { - public class OcelotBuilderExtensionsTests + public class OcelotBuilderExtensionsTests : UnitTest { private readonly IServiceCollection _services; private readonly IConfiguration _configRoot; diff --git a/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs index a5dc99585..5b8eda527 100644 --- a/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Consul { - public class PollingConsulServiceDiscoveryProviderTests + public class PollingConsulServiceDiscoveryProviderTests : UnitTest { private readonly int _delay; private readonly List _services; diff --git a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs index 1b143273f..aa21ff625 100644 --- a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs +++ b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Controllers { - public class FileConfigurationControllerTests + public class FileConfigurationControllerTests : UnitTest { private readonly FileConfigurationController _controller; private readonly Mock _repo; diff --git a/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs b/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs index 067d06c68..71c2b4fa6 100644 --- a/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs +++ b/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.Controllers { - public class OutputCacheControllerTests + public class OutputCacheControllerTests : UnitTest { private readonly OutputCacheController _controller; private readonly Mock> _cache; diff --git a/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs index 3514ca97d..f0e322fa8 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs @@ -199,14 +199,14 @@ private void GivenMultipleConfigurationFiles(string folder, bool withEnvironment RequestIdKey = "RequestIdKey", }; - private static List GetFileAggregatesRouteData() => - [ + private static List GetFileAggregatesRouteData() => new() + { new() { - RouteKeys = [ "KeyB", "KeyBB" ], + RouteKeys = new() { "KeyB", "KeyBB" }, UpstreamPathTemplate = "UpstreamPathTemplate", }, - ]; + }; private static FileRoute GetRoute(string suffix) => new() { @@ -214,16 +214,16 @@ private static List GetFileAggregatesRouteData() => DownstreamPathTemplate = "DownstreamPathTemplate" + suffix, Key = "Key" + suffix, UpstreamHost = "UpstreamHost" + suffix, - UpstreamHttpMethod = ["UpstreamHttpMethod" + suffix], - DownstreamHostAndPorts = - [ + UpstreamHttpMethod = new() { "UpstreamHttpMethod" + suffix }, + DownstreamHostAndPorts = new() + { new("Host"+suffix, 80), - ], + }, }; - private static List GetServiceARoutes() => [GetRoute("A")]; - private static List GetServiceBRoutes() => [GetRoute("B"), GetRoute("BB")]; - private static List GetEnvironmentSpecificRoutes() => [GetRoute("Spec")]; + private static List GetServiceARoutes() => new() { GetRoute("A") }; + private static List GetServiceBRoutes() => new() { GetRoute("B"), GetRoute("BB") }; + private static List GetEnvironmentSpecificRoutes() => new() { GetRoute("Spec") }; private void GivenTheEnvironmentIs(string folder, [CallerMemberName] string testName = null) { diff --git a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs index 3879d4c0a..262014927 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs @@ -27,7 +27,7 @@ namespace Ocelot.UnitTests.DependencyInjection { - public class OcelotBuilderTests + public class OcelotBuilderTests : UnitTest { private readonly IConfiguration _configRoot; private readonly IServiceCollection _services; diff --git a/test/Ocelot.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs b/test/Ocelot.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs index 2ff94d48f..87a7c97d9 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -41,7 +41,7 @@ public void FindConfiguration_HasDescriptor_HappyPath(bool hasConfig) // Act var method = typeof(Extensions).GetMethod("FindConfiguration", BindingFlags.NonPublic | BindingFlags.Static); - var actual = (IConfiguration)method.Invoke(null, [services, env]); + var actual = (IConfiguration)method.Invoke(null, new object[] { services, env }); // Assert actual.ShouldNotBeNull(); diff --git a/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs b/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs index 3281aaa1a..efe212166 100644 --- a/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs +++ b/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.DownstreamPathManipulation { - public class ChangeDownstreamPathTemplateTests + public class ChangeDownstreamPathTemplateTests : UnitTest { private readonly ChangeDownstreamPathTemplate _changeDownstreamPath; private DownstreamPathTemplate _downstreamPathTemplate; diff --git a/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs index 9be0a8f02..4a1fcd7f3 100644 --- a/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs @@ -13,7 +13,7 @@ namespace Ocelot.UnitTests.DownstreamPathManipulation { - public class ClaimsToDownstreamPathMiddlewareTests + public class ClaimsToDownstreamPathMiddlewareTests : UnitTest { private readonly Mock _changePath; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs index 0ead908aa..86ba4078a 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder { - public class DownstreamRouteCreatorTests + public class DownstreamRouteCreatorTests : UnitTest { private readonly DownstreamRouteCreator _creator; private readonly QoSOptions _qoSOptions; diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs index f743f6914..6d331a36e 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder { - public class DownstreamRouteFinderMiddlewareTests + public class DownstreamRouteFinderMiddlewareTests : UnitTest { private readonly Mock _finder; private readonly Mock _factory; diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index 46654d8f4..e9b2bf8ed 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder { - public class DownstreamRouteFinderTests + public class DownstreamRouteFinderTests : UnitTest { private readonly IDownstreamRouteProvider _downstreamRouteFinder; private readonly Mock _mockMatcher; diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs index 209d67b72..db89a5eeb 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder { using Ocelot.DownstreamRouteFinder.Finder; - public class DownstreamRouteProviderFactoryTests + public class DownstreamRouteProviderFactoryTests : UnitTest { private readonly DownstreamRouteProviderFactory _factory; private IInternalConfiguration _config; diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs index 6cddb2f53..4117fc562 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher { - public class RegExUrlMatcherTests + public class RegExUrlMatcherTests : UnitTest { private readonly IUrlPathToUrlTemplateMatcher _urlMatcher; private string _path; diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs index 606b63714..4dc610a50 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher { - public class UrlPathPlaceholderNameAndValueFinderTests + public class UrlPathPlaceholderNameAndValueFinderTests : UnitTest { private readonly IPlaceholderNameAndValueFinder _finder; private string _downstreamUrlPath; diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs index b7ff203dd..0f25294ee 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator { - public class DownstreamPathPlaceholderReplacerTests + public class DownstreamPathPlaceholderReplacerTests : UnitTest { private DownstreamRouteHolder _downstreamRoute; private Response _result; diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs index c6529e33d..615eb704e 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs @@ -14,7 +14,7 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator { - public class DownstreamUrlCreatorMiddlewareTests + public class DownstreamUrlCreatorMiddlewareTests : UnitTest { private readonly Mock _downstreamUrlTemplateVariableReplacer; private OkResponse _downstreamPath; diff --git a/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs b/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs index b59c0bb68..ab72f90e2 100644 --- a/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Errors { - public class ExceptionHandlerMiddlewareTests + public class ExceptionHandlerMiddlewareTests : UnitTest { private bool _shouldThrowAnException; private readonly Mock _repo; diff --git a/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs index b14534afc..fe92101c3 100644 --- a/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Eureka { - public class EurekaServiceDiscoveryProviderTests + public class EurekaServiceDiscoveryProviderTests : UnitTest { private readonly _Eureka_ _provider; private readonly Mock _client; diff --git a/test/Ocelot.UnitTests/FileUnitTest.cs b/test/Ocelot.UnitTests/FileUnitTest.cs index 1b81b7e8b..34c9de0a7 100644 --- a/test/Ocelot.UnitTests/FileUnitTest.cs +++ b/test/Ocelot.UnitTests/FileUnitTest.cs @@ -16,12 +16,17 @@ protected FileUnitTest(string folder) { folder ??= TestID; Directory.CreateDirectory(folder); - _folders = [folder]; + _folders = new() { folder }; _primaryConfigFileName = Path.Combine(folder, ConfigurationBuilderExtensions.PrimaryConfigFile); _globalConfigFileName = Path.Combine(folder, ConfigurationBuilderExtensions.GlobalConfigFile); _environmentConfigFileName = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, EnvironmentName())); - _files = [_primaryConfigFileName, _globalConfigFileName, _environmentConfigFileName]; + _files = new() + { + _primaryConfigFileName, + _globalConfigFileName, + _environmentConfigFileName, + }; } protected virtual string EnvironmentName() => TestID; diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs index 8cfcca4fd..d2a1cda2d 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Headers { - public class AddHeadersToRequestClaimToThingTests + public class AddHeadersToRequestClaimToThingTests : UnitTest { private readonly AddHeadersToRequest _addHeadersToRequest; private readonly Mock _parser; diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs index d4c75ed7e..0b30141cf 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Headers { - public class AddHeadersToRequestPlainTests + public class AddHeadersToRequestPlainTests : UnitTest { private readonly AddHeadersToRequest _addHeadersToRequest; private HttpContext _context; diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs index 4563965bb..b0a6e0514 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Headers { - public class AddHeadersToResponseTests + public class AddHeadersToResponseTests : UnitTest { private readonly IAddHeadersToResponse _adder; private readonly Mock _placeholders; diff --git a/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs index 7188500fd..9e2eec2b1 100644 --- a/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.Headers { - public class ClaimsToHeadersMiddlewareTests + public class ClaimsToHeadersMiddlewareTests : UnitTest { private readonly Mock _addHeaders; private Response _downstreamRoute; diff --git a/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs b/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs index 51fe342b1..18021084d 100644 --- a/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Headers { - public class HttpContextRequestHeaderReplacerTests + public class HttpContextRequestHeaderReplacerTests : UnitTest { private HttpContext _context; private List _fAndRs; diff --git a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs index d9441462b..f61c72896 100644 --- a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Headers { - public class HttpHeadersTransformationMiddlewareTests + public class HttpHeadersTransformationMiddlewareTests : UnitTest { private readonly Mock _preReplacer; private readonly Mock _postReplacer; diff --git a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs index 82e98c0f2..8ccef9447 100644 --- a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Headers { - public class HttpResponseHeaderReplacerTests + public class HttpResponseHeaderReplacerTests : UnitTest { private DownstreamResponse _response; private readonly Placeholders _placeholders; diff --git a/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs b/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs index 12df11575..f39192295 100644 --- a/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs +++ b/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.Headers { - public class RemoveHeadersTests + public class RemoveHeadersTests : UnitTest { private List
_headers; private readonly Ocelot.Headers.RemoveOutputHeaders _removeOutputHeaders; diff --git a/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs b/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs index ddcf726ac..9c5f7bf10 100644 --- a/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.Infrastructure { - public class ClaimParserTests + public class ClaimParserTests : UnitTest { private readonly IClaimsParser _claimsParser; private readonly List _claims; diff --git a/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs b/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs index 191cc0499..17e21a622 100644 --- a/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Infrastructure { - public class HttpDataRepositoryTests + public class HttpDataRepositoryTests : UnitTest { private readonly HttpContext _httpContext; private readonly IHttpContextAccessor _httpContextAccessor; diff --git a/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs b/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs deleted file mode 100644 index 8b1378917..000000000 --- a/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs b/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs index f580a746a..b94774bbe 100644 --- a/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Infrastructure { - public class ScopesAuthorizerTests + public class ScopesAuthorizerTests : UnitTest { private readonly ScopesAuthorizer _authorizer; public Mock _parser; diff --git a/test/Ocelot.UnitTests/Kubernetes/KubeTests.cs b/test/Ocelot.UnitTests/Kubernetes/KubeTests.cs index 49e91fe6e..10207d6f4 100644 --- a/test/Ocelot.UnitTests/Kubernetes/KubeTests.cs +++ b/test/Ocelot.UnitTests/Kubernetes/KubeTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Kubernetes { - public class KubeTests : IDisposable + public class KubeTests : UnitTest, IDisposable { private IWebHost _fakeKubeBuilder; private readonly Kube _provider; diff --git a/test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs index 2f32b562d..d3c0b9967 100644 --- a/test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Kubernetes { - public class OcelotBuilderExtensionsTests + public class OcelotBuilderExtensionsTests : UnitTest { private readonly IServiceCollection _services; private readonly IConfiguration _configRoot; diff --git a/test/Ocelot.UnitTests/Kubernetes/PollKubeTests.cs b/test/Ocelot.UnitTests/Kubernetes/PollKubeTests.cs index d041288f6..794589bc3 100644 --- a/test/Ocelot.UnitTests/Kubernetes/PollKubeTests.cs +++ b/test/Ocelot.UnitTests/Kubernetes/PollKubeTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.Kubernetes { - public class PollKubeTests + public class PollKubeTests : UnitTest { private readonly int _delay; private PollKube _provider; diff --git a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs index 601e837e9..682ea9d41 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class CookieStickySessionsCreatorTests + public class CookieStickySessionsCreatorTests : UnitTest { private readonly CookieStickySessionsCreator _creator; private readonly Mock _serviceProvider; diff --git a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs index 615218deb..71c4d0517 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class CookieStickySessionsTests + public class CookieStickySessionsTests : UnitTest { private readonly CookieStickySessions _stickySessions; private readonly Mock _loadBalancer; diff --git a/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs index 6631051fc..deb25a12e 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class DelegateInvokingLoadBalancerCreatorTests + public class DelegateInvokingLoadBalancerCreatorTests : UnitTest { private DelegateInvokingLoadBalancerCreator _creator; private Func _creatorFunc; diff --git a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs index e94cfc5b6..e56fa8f7b 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class LeastConnectionCreatorTests + public class LeastConnectionCreatorTests : UnitTest { private readonly LeastConnectionCreator _creator; private readonly Mock _serviceProvider; diff --git a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs index 2766f58ab..368e866e6 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class LeastConnectionTests + public class LeastConnectionTests : UnitTest { private ServiceHostAndPort _hostAndPort; private Response _result; diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs index 5eb21c6d7..616168548 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class LoadBalancerFactoryTests + public class LoadBalancerFactoryTests : UnitTest { private DownstreamRoute _route; private readonly LoadBalancerFactory _factory; diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs index 216bda84e..fa0b835ff 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class LoadBalancerHouseTests + public class LoadBalancerHouseTests : UnitTest { private DownstreamRoute _route; private ILoadBalancer _loadBalancer; diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs index 411c43fd4..f5b5dd925 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs @@ -13,7 +13,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class LoadBalancerMiddlewareTests + public class LoadBalancerMiddlewareTests : UnitTest { private readonly Mock _loadBalancerHouse; private readonly Mock _loadBalancer; diff --git a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs index df478ec24..dadf453e1 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class NoLoadBalancerCreatorTests + public class NoLoadBalancerCreatorTests : UnitTest { private readonly NoLoadBalancerCreator _creator; private readonly Mock _serviceProvider; diff --git a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs index 5e562352a..e1490e898 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs @@ -5,7 +5,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class NoLoadBalancerTests + public class NoLoadBalancerTests : UnitTest { private readonly List _services; private NoLoadBalancer _loadBalancer; diff --git a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs index 2b3d00d8a..13f5b6622 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class RoundRobinCreatorTests + public class RoundRobinCreatorTests : UnitTest { private readonly RoundRobinCreator _creator; private readonly Mock _serviceProvider; diff --git a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs index 78196c13e..af55d65aa 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs @@ -6,7 +6,7 @@ namespace Ocelot.UnitTests.LoadBalancer { - public class RoundRobinTests + public class RoundRobinTests : UnitTest { private readonly RoundRobin _roundRobin; private readonly List _services; diff --git a/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs b/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs index d7ac54b64..94d3da1c7 100644 --- a/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs +++ b/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Logging { - public class OcelotDiagnosticListenerTests + public class OcelotDiagnosticListenerTests : UnitTest { private readonly OcelotDiagnosticListener _listener; private readonly Mock _factory; diff --git a/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs b/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs index 4049e08aa..e6db74c33 100644 --- a/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs +++ b/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Middleware { - public class BaseUrlFinderTests + public class BaseUrlFinderTests : UnitTest { private BaseUrlFinder _baseUrlFinder; private IConfiguration _config; diff --git a/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs index f7f41ae6a..7d2e4b75a 100644 --- a/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs @@ -12,7 +12,7 @@ namespace Ocelot.UnitTests.Middleware { - public class OcelotPipelineExtensionsTests + public class OcelotPipelineExtensionsTests : UnitTest { private ApplicationBuilder _builder; private RequestDelegate _handlers; diff --git a/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs index 0cd2147c3..2948b70a6 100644 --- a/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs +++ b/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.Middleware { - public class OcelotPiplineBuilderTests + public class OcelotPiplineBuilderTests : UnitTest { private readonly IServiceCollection _services; private readonly IConfiguration _configRoot; diff --git a/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs b/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs index b9a6c91b3..7a1c8e914 100644 --- a/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs @@ -7,7 +7,7 @@ namespace Ocelot.UnitTests.Multiplexing { - public class DefinedAggregatorProviderTests + public class DefinedAggregatorProviderTests : UnitTest { private ServiceLocatorDefinedAggregatorProvider _provider; private Response _aggregator; diff --git a/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs b/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs index 633fae43b..95f41e863 100644 --- a/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs @@ -13,7 +13,7 @@ namespace Ocelot.UnitTests.Multiplexing { - public class MultiplexingMiddlewareTests + public class MultiplexingMiddlewareTests : UnitTest { private MultiplexingMiddleware _middleware; private Ocelot.DownstreamRouteFinder.DownstreamRouteHolder _downstreamRoute; @@ -68,7 +68,7 @@ public void CreateThreadContext_CopyUser_ToTarget() // Act var method = _middleware.GetType().GetMethod("CreateThreadContext", BindingFlags.NonPublic | BindingFlags.Static); - var actual = (HttpContext)method.Invoke(_middleware, [_httpContext]); + var actual = (HttpContext)method.Invoke(_middleware, new object[] { _httpContext }); // Assert AssertUsers(actual); @@ -318,13 +318,11 @@ private static Route GivenRoutesWithAggregator() b.WithDownstreamRoute(route2); b.WithDownstreamRoute(route3); - b.WithAggregateRouteConfig( - [ - new AggregateRouteConfig - { RouteKey = "UserDetails", JsonPath = "$[*].writerId", Parameter = "userId" }, - new AggregateRouteConfig - { RouteKey = "PostDetails", JsonPath = "$[*].postId", Parameter = "postId" } - ]); + b.WithAggregateRouteConfig(new() + { + new AggregateRouteConfig { RouteKey = "UserDetails", JsonPath = "$[*].writerId", Parameter = "userId" }, + new AggregateRouteConfig { RouteKey = "PostDetails", JsonPath = "$[*].postId", Parameter = "postId" }, + }); b.WithAggregator("TestAggregator"); diff --git a/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs b/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs index 7db1d8faf..15a229573 100644 --- a/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Multiplexing { - public class ResponseAggregatorFactoryTests + public class ResponseAggregatorFactoryTests : UnitTest { private readonly InMemoryResponseAggregatorFactory _factory; private readonly Mock _provider; diff --git a/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs b/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs index 45f7f2d92..fafda9bfd 100644 --- a/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.Multiplexing { - public class SimpleJsonResponseAggregatorTests + public class SimpleJsonResponseAggregatorTests : UnitTest { private readonly SimpleJsonResponseAggregator _aggregator; private List _downstreamContexts; diff --git a/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs b/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs index a960dc119..4a0349795 100644 --- a/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Multiplexing { - public class UserDefinedResponseAggregatorTests + public class UserDefinedResponseAggregatorTests : UnitTest { private readonly UserDefinedResponseAggregator _aggregator; private readonly Mock _provider; diff --git a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj index d9af1c858..30c2ca794 100644 --- a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +++ b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj @@ -16,7 +16,8 @@ false ..\..\codeanalysis.ruleset True - 1591;CS0618 + ..\..\codeanalysis.ruleset + $(NoWarn);CS0618;CS1591 full diff --git a/test/Ocelot.UnitTests/Polly/PollyResiliencePipelineDelegatingHandlerTests.cs b/test/Ocelot.UnitTests/Polly/PollyResiliencePipelineDelegatingHandlerTests.cs index b1f448c6a..f144fe305 100644 --- a/test/Ocelot.UnitTests/Polly/PollyResiliencePipelineDelegatingHandlerTests.cs +++ b/test/Ocelot.UnitTests/Polly/PollyResiliencePipelineDelegatingHandlerTests.cs @@ -88,7 +88,7 @@ public async void SendAsync_OnePolicy() private async Task InvokeAsync(string methodName) { var m = typeof(PollyResiliencePipelineDelegatingHandler).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); - var task = (Task)m.Invoke(_sut, [new HttpRequestMessage(), CancellationToken.None]); + var task = (Task)m.Invoke(_sut, new object[] { new HttpRequestMessage(), CancellationToken.None }); var actual = await task!; return actual; } diff --git a/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs b/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs index 83c5b320a..5c2c3b794 100644 --- a/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs +++ b/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.QueryStrings { - public class AddQueriesToRequestTests + public class AddQueriesToRequestTests : UnitTest { private readonly AddQueriesToRequest _addQueriesToRequest; private DownstreamRequest _downstreamRequest; diff --git a/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs b/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs index 731f3fa0e..bfb0af8aa 100644 --- a/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs @@ -13,7 +13,7 @@ namespace Ocelot.UnitTests.QueryStrings { - public class ClaimsToQueryStringMiddlewareTests + public class ClaimsToQueryStringMiddlewareTests : UnitTest { private readonly Mock _addQueries; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs index 0e6c66068..1c2267ae2 100644 --- a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.RateLimit { - public class ClientRateLimitMiddlewareTests + public class ClientRateLimitMiddlewareTests : UnitTest { private readonly IRateLimitCounterHandler _rateLimitCounterHandler; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs b/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs index 33b797025..ba6e2c9a3 100644 --- a/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Repository { - public class ScopedRequestDataRepositoryTests + public class ScopedRequestDataRepositoryTests : UnitTest { private readonly IRequestScopedDataRepository _requestScopedDataRepository; private readonly IHttpContextAccessor _httpContextAccesor; diff --git a/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs b/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs index e61811314..7426b77aa 100644 --- a/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs +++ b/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.Request.Creator { - public class DownstreamRequestCreatorTests + public class DownstreamRequestCreatorTests : UnitTest { private readonly Mock _framework; private readonly DownstreamRequestCreator _downstreamRequestCreator; diff --git a/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs b/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs index 854b70e6d..ed19d1aae 100644 --- a/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Request; -public class DownstreamRequestInitialiserMiddlewareTests +public class DownstreamRequestInitialiserMiddlewareTests : UnitTest { private readonly DownstreamRequestInitialiserMiddleware _middleware; private readonly HttpContext _httpContext; diff --git a/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs b/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs index 624dc779e..6f06d0295 100644 --- a/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs +++ b/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Request.Mapper; -public class RequestMapperTests +public class RequestMapperTests : UnitTest { private readonly HttpRequest _inputRequest; private readonly RequestMapper _requestMapper; @@ -206,8 +206,9 @@ public void Should_handle_no_content_length() [Fact] public void Should_map_content_headers() - { - var md5Bytes = MD5.HashData("some md5"u8.ToArray()); + { + var bytes = Encoding.UTF8.GetBytes("some md5"); + var md5Bytes = MD5.HashData(bytes); this.Given(_ => GivenTheInputRequestHasContent("This is my content")) .And(_ => GivenTheContentTypeIs("application/json")) diff --git a/test/Ocelot.UnitTests/Request/Mapper/StreamHttpContentTests.cs b/test/Ocelot.UnitTests/Request/Mapper/StreamHttpContentTests.cs index b8ae8a50f..105bb85de 100644 --- a/test/Ocelot.UnitTests/Request/Mapper/StreamHttpContentTests.cs +++ b/test/Ocelot.UnitTests/Request/Mapper/StreamHttpContentTests.cs @@ -35,8 +35,9 @@ public async Task Copy_body_to_stream_with_unknown_length_and_stream_content_sho var bytes = Encoding.UTF8.GetBytes(PayLoad); using var inputStream = new MemoryStream(bytes); using var outputStream = new MemoryStream(); - await CopyAsyncTest(new StreamHttpContent(_httpContext), - [inputStream, outputStream, StreamHttpContent.UnknownLength, false, CancellationToken.None]); + await CopyAsyncTest( + new StreamHttpContent(_httpContext), + new object[] { inputStream, outputStream, StreamHttpContent.UnknownLength, false, CancellationToken.None }); inputStream.Position = 0; outputStream.Position = 0; var result = Encoding.UTF8.GetString(outputStream.ToArray()); @@ -49,8 +50,9 @@ public async Task Copy_body_to_stream_with_body_length_and_stream_content_should var bytes = Encoding.UTF8.GetBytes(PayLoad); using var inputStream = new MemoryStream(bytes); using var outputStream = new MemoryStream(); - await CopyAsyncTest(new StreamHttpContent(_httpContext), - [inputStream, outputStream, bytes.Length, false, CancellationToken.None]); + await CopyAsyncTest( + new StreamHttpContent(_httpContext), + new object[] { inputStream, outputStream, bytes.Length, false, CancellationToken.None }); inputStream.Position = 0; outputStream.Position = 0; var result = Encoding.UTF8.GetString(outputStream.ToArray()); @@ -64,8 +66,9 @@ public async Task Should_throw_if_passed_body_length_does_not_match_real_body_le using var inputStream = new MemoryStream(bytes); using var outputStream = new MemoryStream(); await Assert.ThrowsAsync(async () => - await CopyAsyncTest(new StreamHttpContent(_httpContext), - [inputStream, outputStream, 10, false, CancellationToken.None])); + await CopyAsyncTest( + new StreamHttpContent(_httpContext), + new object[] { inputStream, outputStream, 10, false, CancellationToken.None })); } private StreamHttpContent StreamHttpContentFactory() diff --git a/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs b/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs index 641f9c9cc..bac14d857 100644 --- a/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.RequestId { - public class RequestIdMiddlewareTests + public class RequestIdMiddlewareTests : UnitTest { private readonly HttpRequestMessage _downstreamRequest; private string _value; diff --git a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs index a6c74c115..dde971d69 100644 --- a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Requester { - public class DelegatingHandlerHandlerProviderFactoryTests + public class DelegatingHandlerHandlerProviderFactoryTests : UnitTest { private DelegatingHandlerHandlerFactory _factory; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs index 717ca04ad..9060494b6 100644 --- a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Requester { - public class HttpRequesterMiddlewareTests + public class HttpRequesterMiddlewareTests : UnitTest { private readonly Mock _requester; private Response _response; diff --git a/test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs b/test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs index fd4eb7d4d..57e3b803a 100644 --- a/test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs +++ b/test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs @@ -14,7 +14,7 @@ namespace Ocelot.UnitTests.Requester; [Trait("PR", "1824")] -public class MessageInvokerPoolTests +public class MessageInvokerPoolTests : UnitTest { private DownstreamRoute _downstreamRoute1; private DownstreamRoute _downstreamRoute2; @@ -321,7 +321,7 @@ private Mock GetHandlerFactory() { var handlerFactory = new Mock(); handlerFactory.Setup(x => x.Get(It.IsAny())) - .Returns(new OkResponse>>([])); + .Returns(new OkResponse>>(new())); return handlerFactory; } @@ -333,7 +333,7 @@ private DownstreamRoute DownstreamRouteFactory(string path) .WithLoadBalancerKey(string.Empty) .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build()) .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, false, 10, TimeSpan.FromSeconds(120))) - .WithUpstreamHttpMethod(["Get"]) + .WithUpstreamHttpMethod(new() { "Get" }) .Build(); return downstreamRoute; diff --git a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs index e80551117..da55e1752 100644 --- a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs +++ b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.Responder { - public class ErrorsToHttpStatusCodeMapperTests + public class ErrorsToHttpStatusCodeMapperTests : UnitTest { private readonly IErrorsToHttpStatusCodeMapper _codeMapper; private int _result; @@ -86,7 +86,7 @@ public void should_return_not_found(OcelotErrorCode errorCode) [Fact] public void should_return_request_entity_too_large() { - ShouldMapErrorsToStatusCode([OcelotErrorCode.PayloadTooLargeError], HttpStatusCode.RequestEntityTooLarge); + ShouldMapErrorsToStatusCode(new() { OcelotErrorCode.PayloadTooLargeError }, HttpStatusCode.RequestEntityTooLarge); } [Fact] diff --git a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs index bde95def8..5a8bc5644 100644 --- a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs @@ -8,7 +8,7 @@ namespace Ocelot.UnitTests.Responder { - public class ResponderMiddlewareTests + public class ResponderMiddlewareTests : UnitTest { private readonly Mock _responder; private readonly Mock _codeMapper; diff --git a/test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs b/test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs index bdc86842d..f32d46bb1 100644 --- a/test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs +++ b/test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs @@ -10,7 +10,7 @@ namespace Ocelot.UnitTests.Security { - public class IPSecurityPolicyTests + public class IPSecurityPolicyTests : UnitTest { private readonly DownstreamRouteBuilder _downstreamRouteBuilder; private readonly IPSecurityPolicy _ipSecurityPolicy; diff --git a/test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs b/test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs index e8310bb92..c24ccffc2 100644 --- a/test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Security { - public class SecurityMiddlewareTests + public class SecurityMiddlewareTests : UnitTest { private readonly List> _securityPolicyList; private readonly Mock _loggerFactory; diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs index 1de3f5cea..ad1f7bb72 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.ServiceDiscovery { - public class ConfigurationServiceProviderTests + public class ConfigurationServiceProviderTests : UnitTest { private ConfigurationServiceProvider _serviceProvider; private List _result; diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs index 88d33f89c..8f416b2d4 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs @@ -11,7 +11,7 @@ namespace Ocelot.UnitTests.ServiceDiscovery { - public class ServiceDiscoveryProviderFactoryTests + public class ServiceDiscoveryProviderFactoryTests : UnitTest { private ServiceProviderConfiguration _serviceConfig; private Response _result; diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs index 0836afffc..1032cbac5 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs @@ -4,7 +4,7 @@ namespace Ocelot.UnitTests.ServiceDiscovery { - public class ServiceFabricServiceDiscoveryProviderTests + public class ServiceFabricServiceDiscoveryProviderTests : UnitTest { private ServiceFabricServiceDiscoveryProvider _provider; private ServiceFabricConfiguration _config; diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs index 22e6dc316..33670a0dd 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs @@ -3,7 +3,7 @@ // nothing in use namespace Ocelot.UnitTests.ServiceDiscovery { - public class ServiceRegistryTests + public class ServiceRegistryTests : UnitTest { private Service _service; private List _services; diff --git a/test/Ocelot.UnitTests/UnitTest.cs b/test/Ocelot.UnitTests/UnitTest.cs index aa7b716b3..a50782f5a 100644 --- a/test/Ocelot.UnitTests/UnitTest.cs +++ b/test/Ocelot.UnitTests/UnitTest.cs @@ -1,7 +1,14 @@ -namespace Ocelot.UnitTests; +using TestStack.BDDfy.Configuration; + +namespace Ocelot.UnitTests; public class UnitTest { + public UnitTest() + { + Configurator.Processors.ConsoleReport.Disable(); + } + protected readonly Guid _testId = Guid.NewGuid(); protected string TestID { get => _testId.ToString("N"); } diff --git a/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs b/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs index d5190ce01..42af9716d 100644 --- a/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.WebSockets; -public class WebSocketsProxyMiddlewareTests +public class WebSocketsProxyMiddlewareTests : UnitTest { private readonly WebSocketsProxyMiddleware _middleware; From ab9fb65e1f8ca93274c3244577164880cb3fe769 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Wed, 17 Apr 2024 12:49:08 +0300 Subject: [PATCH 02/15] Notes about Merging Configuration Files feature in `configuration.rst` (#2048) * Update configuration.rst * Static CSS overrides * Notes are now smaller --- .gitignore | 1 - docs/_static/overrides.css | 6 ++++++ docs/conf.py | 1 + docs/features/configuration.rst | 15 ++++++++++++--- 4 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 docs/_static/overrides.css diff --git a/.gitignore b/.gitignore index 1c30928ce..f01a1095e 100644 --- a/.gitignore +++ b/.gitignore @@ -417,5 +417,4 @@ test/Ocelot.AcceptanceTests/ocelot.json # Read the Docs # https://ocelot.readthedocs.io _build/ -_static/ _templates/ diff --git a/docs/_static/overrides.css b/docs/_static/overrides.css new file mode 100644 index 000000000..a5f45454c --- /dev/null +++ b/docs/_static/overrides.css @@ -0,0 +1,6 @@ +blockquote { + font-size: 0.9em; +} +aside.footnote-list { + font-size: 0.9em; +} diff --git a/docs/conf.py b/docs/conf.py index 41b0fd36d..6d08568ce 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,3 +27,4 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_static_path html_static_path = ['_static'] +html_css_files = ['overrides.css'] diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 552767f49..931e4f58d 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -123,9 +123,16 @@ If you want to set the **GlobalConfiguration** property, you must have a file ca The way Ocelot merges the files is basically load them, loop over them, add any **Routes**, add any **AggregateRoutes** and if the file is called ``ocelot.global.json`` add the **GlobalConfiguration** aswell as any **Routes** or **AggregateRoutes**. Ocelot will then save the merged configuration to a file called `ocelot.json`_ and this will be used as the source of truth while Ocelot is running. -At the moment there is no validation at this stage it only happens when Ocelot validates the final merged configuration. -This is something to be aware of when you are investigating problems. -We would advise always checking what is in `ocelot.json`_ file if you have any problems. + **Note 1**: Currently, validation occurs only during the final merging of configurations in Ocelot. + It's essential to be aware of this when troubleshooting issues. + We recommend thoroughly inspecting the contents of the ``ocelot.json`` file if you encounter any problems. + + **Note 2**: The Merging feature is operational only during the application's startup. + Consequently, the merged configuration in ``ocelot.json`` remains static post-merging and startup. + It's important to be aware that the ``ConfigureAppConfiguration`` method is invoked solely during the startup of an ASP.NET web application. + Once the Ocelot application has started, you cannot call the ``AddOcelot`` method, nor can you employ the merging feature within ``AddOcelot``. + If you still require on-the-fly updating of the primary configuration file, ``ocelot.json``, please refer to the :ref:`config-react-to-changes` section. + Additionally, note that merging partial configuration files (such as ``ocelot.*.json``) on the fly using :doc:`../features/administration` API is not currently implemented. Keep files in a folder ^^^^^^^^^^^^^^^^^^^^^^ @@ -336,6 +343,8 @@ As a team, we highly recommend following these instructions when developing your System administrators or DevOps engineers must create real valid certificates being signed by hosting or cloud providers. **Switch off the feature for all routes!** Remove the **DangerousAcceptAnyServerCertificateValidator** property for all routes in production version of `ocelot.json`_ file! +.. _config-react-to-changes: + React to Configuration Changes ------------------------------ From 233f87a43f5e2fbb1857f17bb6181fbd05de03bc Mon Sep 17 00:00:00 2001 From: Paul Roy Date: Thu, 25 Apr 2024 13:37:02 +0200 Subject: [PATCH 03/15] #2039 Buffer request body and copy the body to downstreams during multiplexing (#2050) * feat: buffer the request body during multiplexing multiple routes * style: rename clone request body method to be more explicit * Code review by @raman-m * feat: refactor clone request method, add acceptance test for form-based requests * fix: add content-length log, refactor tests from @raman-m commit * Update requestaggregation.rst * style: reverse return condition * Register `Stream` objects for disposing by downstream `HttpResponse` --------- Co-authored-by: Paul Roy Co-authored-by: Raman Maksimchuk --- docs/features/requestaggregation.rst | 7 +- .../Multiplexer/MultiplexingMiddleware.cs | 45 ++- test/Ocelot.AcceptanceTests/AggregateTests.cs | 274 ++++++++++++------ test/Ocelot.AcceptanceTests/Steps.cs | 27 +- .../MultiplexingMiddlewareTests.cs | 37 ++- 5 files changed, 280 insertions(+), 110 deletions(-) diff --git a/docs/features/requestaggregation.rst b/docs/features/requestaggregation.rst index f1123cf19..c9a642388 100644 --- a/docs/features/requestaggregation.rst +++ b/docs/features/requestaggregation.rst @@ -217,9 +217,12 @@ Below is an example of an aggregator that you could implement for your solution: Gotchas ------- -You cannot use Routes with specific **RequestIdKeys** as this would be crazy complicated to track. +* You cannot use Routes with specific **RequestIdKeys** as this would be crazy complicated to track. +* Aggregation only supports the ``GET`` HTTP verb. +* Aggregation allows for the forwarding of ``HttpRequest.Body`` to downstream services by duplicating the body data. + Form data and attached files should also be forwarded. + It is essential to always specify the ``Content-Length`` header in requests to upstream; otherwise, Ocelot will log warnings like *"Aggregation does not support body copy without Content-Length header!"*. -Aggregation only supports the ``GET`` HTTP verb. """" diff --git a/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs b/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs index 17609dc18..43a98fcd3 100644 --- a/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs +++ b/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs @@ -19,8 +19,7 @@ public class MultiplexingMiddleware : OcelotMiddleware public MultiplexingMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, - IResponseAggregatorFactory factory - ) + IResponseAggregatorFactory factory) : base(loggerFactory.CreateLogger()) { _factory = factory; @@ -184,7 +183,7 @@ private IEnumerable> ProcessRouteWithComplexAggregation(Aggreg /// The cloned Http context. private async Task ProcessRouteAsync(HttpContext sourceContext, DownstreamRoute route, List placeholders = null) { - var newHttpContext = CreateThreadContext(sourceContext); + var newHttpContext = await CreateThreadContextAsync(sourceContext); CopyItemsToNewContext(newHttpContext, sourceContext, placeholders); newHttpContext.Items.UpsertDownstreamRoute(route); @@ -208,14 +207,15 @@ private static void CopyItemsToNewContext(HttpContext target, HttpContext source ///
/// The base http context. /// The cloned context. - private static HttpContext CreateThreadContext(HttpContext source) + protected virtual async Task CreateThreadContextAsync(HttpContext source) { - var from = source.Request; + var from = source.Request; + var bodyStream = await CloneRequestBodyAsync(from, source.RequestAborted); var target = new DefaultHttpContext { Request = { - Body = from.Body, // TODO Consider stream cloning for multiple reads + Body = bodyStream, ContentLength = from.ContentLength, ContentType = from.ContentType, Host = from.Host, @@ -237,12 +237,13 @@ private static HttpContext CreateThreadContext(HttpContext source) RequestAborted = source.RequestAborted, User = source.User, }; - foreach (var header in from.Headers) { target.Request.Headers[header.Key] = header.Value.ToArray(); - } - + } + + // Once the downstream request is completed and the downstream response has been read, the downstream response object can dispose of the body's Stream object + target.Response.RegisterForDisposeAsync(bodyStream); // manage Stream lifetime by HttpResponse object return target; } @@ -255,5 +256,29 @@ protected virtual Task MapAsync(HttpContext httpContext, Route route, List CloneRequestBodyAsync(HttpRequest request, CancellationToken aborted) + { + request.EnableBuffering(); + if (request.Body.Position != 0) + { + Logger.LogWarning("Ocelot does not support body copy without stream in initial position 0"); + return request.Body; + } + + var targetBuffer = new MemoryStream(); + if (request.ContentLength is not null) + { + await request.Body.CopyToAsync(targetBuffer, (int)request.ContentLength, aborted); + targetBuffer.Position = 0; + request.Body.Position = 0; + } + else + { + Logger.LogWarning("Aggregation does not support body copy without Content-Length header!"); + } + + return targetBuffer; + } } diff --git a/test/Ocelot.AcceptanceTests/AggregateTests.cs b/test/Ocelot.AcceptanceTests/AggregateTests.cs index daf3bdbfa..6257e27e6 100644 --- a/test/Ocelot.AcceptanceTests/AggregateTests.cs +++ b/test/Ocelot.AcceptanceTests/AggregateTests.cs @@ -15,6 +15,7 @@ using Ocelot.DependencyInjection; using Ocelot.Middleware; using Ocelot.Multiplexer; +using System.Text; namespace Ocelot.AcceptanceTests { @@ -598,114 +599,209 @@ public void Should_return_response_200_with_user_forwarding() } } - private void GivenServiceIsRunning(string baseUrl, int statusCode, string responseBody) + [Fact] + [Trait("Bug", "2039")] + public void Should_return_response_200_with_copied_body_sent_on_multiple_services() { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, async context => - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var route1 = GivenRoute(port1, "/Service1", "Service1", "/Sub1"); + var route2 = GivenRoute(port2, "/Service2", "Service2", "/Sub2"); + var configuration = GivenConfiguration(route1, route2); + var requestBody = @"{""id"":1,""response"":""fromBody-#REPLACESTRING#""}"; + var sub1ResponseContent = @"{""id"":1,""response"":""fromBody-s1""}"; + var sub2ResponseContent = @"{""id"":1,""response"":""fromBody-s2""}"; + var expected = $"{{\"Service1\":{sub1ResponseContent},\"Service2\":{sub2ResponseContent}}}"; + + this.Given(x => x.GivenServiceIsRunning(0, port1, "/Sub1", 200, reqBody => reqBody.Replace("#REPLACESTRING#", "s1"))) + .Given(x => x.GivenServiceIsRunning(1, port2, "/Sub2", 200, reqBody => reqBody.Replace("#REPLACESTRING#", "s2"))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlWithBodyOnTheApiGateway("/", requestBody)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(expected)) + .BDDfy(); + } + + [Fact] + [Trait("Bug", "2039")] + public void Should_return_response_200_with_copied_form_sent_on_multiple_services() + { + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var route1 = GivenRoute(port1, "/Service1", "Service1", "/Sub1"); + var route2 = GivenRoute(port2, "/Service2", "Service2", "/Sub2"); + var configuration = GivenConfiguration(route1, route2); + + var formValues = new[] + { + new KeyValuePair("param1", "value1"), + new KeyValuePair("param2", "from-form-REPLACESTRING"), + }; + + var sub1ResponseContent = "\"[key:param1=value1¶m2=from-form-s1]\""; + var sub2ResponseContent = "\"[key:param1=value1¶m2=from-form-s2]\""; + var expected = $"{{\"Service1\":{sub1ResponseContent},\"Service2\":{sub2ResponseContent}}}"; + + this.Given(x => x.GivenServiceIsRunning(0, port1, "/Sub1", 200, (IFormCollection reqForm) => FormatFormCollection(reqForm).Replace("REPLACESTRING", "s1"))) + .Given(x => x.GivenServiceIsRunning(1, port2, "/Sub2", 200, (IFormCollection reqForm) => FormatFormCollection(reqForm).Replace("REPLACESTRING", "s2"))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlWithFormOnTheApiGateway("/", "key", formValues)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(expected)) + .BDDfy(); + } + + private static string FormatFormCollection(IFormCollection reqForm) + { + var sb = new StringBuilder() + .Append('"'); + + foreach (var kvp in reqForm) + { + sb.Append($"[{kvp.Key}:{kvp.Value}]"); + } + + return sb + .Append('"') + .ToString(); + } + + private void GivenServiceIsRunning(string baseUrl, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); } private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, string responseBody) - { - var baseUrl = $"{Uri.UriSchemeHttp}://localhost:{port}"; - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPaths[index] = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPaths[index] != basePath) + => GivenServiceIsRunning(index, port, basePath, statusCode, + async context => { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didn't match base path"); - } - else + await context.Response.WriteAsync(responseBody); + }); + + private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Func responseFromBody) + => GivenServiceIsRunning(index, port, basePath, statusCode, + async context => + { + var requestBody = await new StreamReader(context.Request.Body).ReadToEndAsync(); + var responseBody = responseFromBody(requestBody); + await context.Response.WriteAsync(responseBody); + }); + + private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Func responseFromForm) + => GivenServiceIsRunning(index, port, basePath, statusCode, + async context => { - context.Response.StatusCode = statusCode; + var responseBody = responseFromForm(context.Request.Form); await context.Response.WriteAsync(responseBody); - } - }); + }); + + private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Action processContext) + { + var baseUrl = DownstreamUrl(port); + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPaths[index] = !string.IsNullOrEmpty(context.Request.PathBase.Value) + ? context.Request.PathBase.Value + : context.Request.Path.Value; + + if (_downstreamPaths[index] != basePath) + { + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync("downstream path doesn't match base path"); + } + else + { + context.Response.StatusCode = statusCode; + processContext?.Invoke(context); + } + }); } private void GivenOcelotIsRunningWithSpecificAggregatorsRegisteredInDi() where TAggregator : class, IDefinedAggregator - where TDependency : class - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", true, false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); - config.AddJsonFile(_ocelotConfigFileName, true, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddSingleton(); - s.AddOcelot() - .AddSingletonDefinedAggregator(); - }) - .Configure(a => { a.UseOcelot().Wait(); }); - - _ocelotServer = new TestServer(_webHostBuilder); - _ocelotClient = _ocelotServer.CreateClient(); + where TDependency : class + { + _webHostBuilder = new WebHostBuilder(); + + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddSingleton(); + s.AddOcelot() + .AddSingletonDefinedAggregator(); + }) + .Configure(a => { a.UseOcelot().Wait(); }); + + _ocelotServer = new TestServer(_webHostBuilder); + _ocelotClient = _ocelotServer.CreateClient(); } - private void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPathOne, string expectedDownstreamPath) - { - _downstreamPaths[0].ShouldBe(expectedDownstreamPathOne); - _downstreamPaths[1].ShouldBe(expectedDownstreamPath); + private void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPathOne, string expectedDownstreamPath) + { + _downstreamPaths[0].ShouldBe(expectedDownstreamPathOne); + _downstreamPaths[1].ShouldBe(expectedDownstreamPath); } - private static FileRoute GivenRoute(int port, string upstream, string key) => new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = Uri.UriSchemeHttp, - DownstreamHostAndPorts = new() { new FileHostAndPort("localhost", port) }, - UpstreamPathTemplate = upstream, - UpstreamHttpMethod = new() { HttpMethods.Get }, - Key = key, + private static FileRoute GivenRoute(int port, string upstream, string key, string downstream = null) => new() + { + DownstreamPathTemplate = downstream ?? "/", + DownstreamScheme = Uri.UriSchemeHttp, + DownstreamHostAndPorts = new() { new("localhost", port) }, + UpstreamPathTemplate = upstream, + UpstreamHttpMethod = new() { HttpMethods.Get }, + Key = key, }; - private static new FileConfiguration GivenConfiguration(params FileRoute[] routes) - { - var obj = Steps.GivenConfiguration(routes); - obj.Aggregates.Add( - new() - { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - RouteKeys = routes.Select(r => r.Key).ToList(), // [ "Laura", "Tom" ], - } - ); - return obj; - } + private static new FileConfiguration GivenConfiguration(params FileRoute[] routes) + { + var obj = Steps.GivenConfiguration(routes); + obj.Aggregates.Add( + new() + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = routes.Select(r => r.Key).ToList(), // [ "Laura", "Tom" ], + } + ); + return obj; + } } - public class FakeDep - { + public class FakeDep + { } - public class FakeDefinedAggregator : IDefinedAggregator - { - public FakeDefinedAggregator(FakeDep dep) - { - } - - public async Task Aggregate(List responses) - { - var one = await responses[0].Items.DownstreamResponse().Content.ReadAsStringAsync(); - var two = await responses[1].Items.DownstreamResponse().Content.ReadAsStringAsync(); - - var merge = $"{one}, {two}"; - merge = merge.Replace("Hello", "Bye").Replace("{", "").Replace("}", ""); - var headers = responses.SelectMany(x => x.Items.DownstreamResponse().Headers).ToList(); - return new DownstreamResponse(new StringContent(merge), HttpStatusCode.OK, headers, "some reason"); - } + public class FakeDefinedAggregator : IDefinedAggregator + { + public FakeDefinedAggregator(FakeDep dep) + { + } + + public async Task Aggregate(List responses) + { + var one = await responses[0].Items.DownstreamResponse().Content.ReadAsStringAsync(); + var two = await responses[1].Items.DownstreamResponse().Content.ReadAsStringAsync(); + + var merge = $"{one}, {two}"; + merge = merge.Replace("Hello", "Bye").Replace("{", "").Replace("}", ""); + var headers = responses.SelectMany(x => x.Items.DownstreamResponse().Headers).ToList(); + return new DownstreamResponse(new StringContent(merge), HttpStatusCode.OK, headers, "some reason"); + } } -} +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 7f95a8a9d..5acb57cbc 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -190,8 +190,8 @@ protected virtual void DeleteOcelotConfig(params string[] files) { Console.WriteLine(e); } - } - } + } + } public void ThenTheResponseBodyHeaderIs(string key, string value) { @@ -817,6 +817,29 @@ public void WhenIGetUrlOnTheApiGatewayAndDontWait(string url) _ocelotClient.GetAsync(url); } + public void WhenIGetUrlWithBodyOnTheApiGateway(string url, string body) + { + var request = new HttpRequestMessage(HttpMethod.Get, url) + { + Content = new StringContent(body), + }; + _response = _ocelotClient.SendAsync(request).Result; + } + + public void WhenIGetUrlWithFormOnTheApiGateway(string url, string name, IEnumerable> values) + { + var content = new MultipartFormDataContent(); + var dataContent = new FormUrlEncodedContent(values); + content.Add(dataContent, name); + content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data"); + + var request = new HttpRequestMessage(HttpMethod.Get, url) + { + Content = content, + }; + _response = _ocelotClient.SendAsync(request).Result; + } + public void WhenICancelTheRequest() { _ocelotClient.CancelPendingRequests(); diff --git a/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs b/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs index 95f41e863..b94c247ff 100644 --- a/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs @@ -61,14 +61,14 @@ public void should_not_multiplex() [Fact] [Trait("Bug", "1396")] - public void CreateThreadContext_CopyUser_ToTarget() + public async Task CreateThreadContextAsync_CopyUser_ToTarget() { // Arrange - GivenUser("test", "Copy", nameof(CreateThreadContext_CopyUser_ToTarget)); + GivenUser("test", "Copy", nameof(CreateThreadContextAsync_CopyUser_ToTarget)); // Act - var method = _middleware.GetType().GetMethod("CreateThreadContext", BindingFlags.NonPublic | BindingFlags.Static); - var actual = (HttpContext)method.Invoke(_middleware, new object[] { _httpContext }); + var method = _middleware.GetType().GetMethod("CreateThreadContextAsync", BindingFlags.NonPublic | BindingFlags.Instance); + var actual = await (Task)method.Invoke(_middleware, new object[] { _httpContext }); // Assert AssertUsers(actual); @@ -188,8 +188,8 @@ public async Task Should_Create_As_Many_Contexts_As_Routes_And_Map_Is_Called_Onc ItExpr.IsAny(), ItExpr.Is>(list => list.Count == routesCount) ); - } - + } + [Fact] [Trait("PR", "1826")] public async Task Should_Not_Call_ProcessSingleRoute_Or_Map_If_No_Route() @@ -212,7 +212,30 @@ public async Task Should_Not_Call_ProcessSingleRoute_Or_Map_If_No_Route() ItExpr.IsAny(), ItExpr.IsAny(), ItExpr.IsAny>()); - } + } + + [Theory] + [Trait("Bug", "2039")] + [InlineData(1)] // Times.Never() + [InlineData(2)] // Times.Exactly(2) + [InlineData(3)] // Times.Exactly(3) + [InlineData(4)] // Times.Exactly(4) + public async Task Should_Call_CloneRequestBodyAsync_Each_Time_Per_Requests(int numberOfRoutes) + { + // Arrange + var mock = MockMiddlewareFactory(null, null); + GivenUser("test", "Invoke", nameof(Should_Call_CloneRequestBodyAsync_Each_Time_Per_Requests)); + GivenTheFollowing(GivenDefaultRoute(numberOfRoutes)); + + // Act + await WhenIMultiplex(); + + // Assert + mock.Protected().Verify>("CloneRequestBodyAsync", + numberOfRoutes > 1 ? Times.Exactly(numberOfRoutes) : Times.Never(), + ItExpr.IsAny(), + ItExpr.IsAny()); + } [Fact] [Trait("PR", "1826")] From 0b247afdf0c9cba7fd37f603102d151dc7e74487 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Fri, 26 Apr 2024 18:38:54 +0300 Subject: [PATCH 04/15] #1967 Customize K8s services creation in `Kube` service discovery provider (#2052) * Initial refactoring * Interfaces namespace * `IKubeServiceBuilder` interface vs `KubeServiceBuilder` class * `IKubeServiceCreator` interface vs `KubeServiceCreator` class * Customize K8s services creation * Add logger * namespace Ocelot.AcceptanceTests.ServiceDiscovery * Add `KubernetesServiceDiscoveryTests` * Unit tests * AAA pattern * Acceptance tests * Update kubernetes.rst * Check docs --- docs/features/kubernetes.rst | 50 +- .../EndPointClientV1.cs | 1 + .../{ => Interfaces}/IEndPointClient.cs | 2 +- .../Interfaces/IKubeServiceBuilder.cs | 9 + .../Interfaces/IKubeServiceCreator.cs | 10 + src/Ocelot.Provider.Kubernetes/Kube.cs | 46 +- .../KubeRegistryConfiguration.cs | 12 +- .../KubeServiceBuilder.cs | 36 + .../KubeServiceCreator.cs | 59 + .../KubernetesProviderFactory.cs | 7 +- .../OcelotBuilderExtensions.cs | 20 +- .../ServiceDiscoveryFinderDelegate.cs | 7 +- .../Ocelot.AcceptanceTests.csproj | 5 +- .../ConsulServiceDiscoveryTests.cs} | 1488 ++++++++--------- .../EurekaServiceDiscoveryTests.cs | 547 +++--- .../KubernetesServiceDiscoveryTests.cs | 212 +++ .../ServiceFabricTests.cs | 410 ++--- test/Ocelot.AcceptanceTests/Steps.cs | 25 +- .../Kubernetes/KubeServiceBuilderTests.cs | 162 ++ .../Kubernetes/KubeServiceCreatorTests.cs | 150 ++ test/Ocelot.UnitTests/Kubernetes/KubeTests.cs | 33 +- test/Ocelot.UnitTests/Ocelot.UnitTests.csproj | 2 +- 22 files changed, 1982 insertions(+), 1311 deletions(-) rename src/Ocelot.Provider.Kubernetes/{ => Interfaces}/IEndPointClient.cs (83%) create mode 100644 src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceBuilder.cs create mode 100644 src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceCreator.cs create mode 100644 src/Ocelot.Provider.Kubernetes/KubeServiceBuilder.cs create mode 100644 src/Ocelot.Provider.Kubernetes/KubeServiceCreator.cs rename test/Ocelot.AcceptanceTests/{ServiceDiscoveryTests.cs => ServiceDiscovery/ConsulServiceDiscoveryTests.cs} (97%) rename test/Ocelot.AcceptanceTests/{ => ServiceDiscovery}/EurekaServiceDiscoveryTests.cs (96%) create mode 100644 test/Ocelot.AcceptanceTests/ServiceDiscovery/KubernetesServiceDiscoveryTests.cs rename test/Ocelot.AcceptanceTests/{ => ServiceDiscovery}/ServiceFabricTests.cs (94%) create mode 100644 test/Ocelot.UnitTests/Kubernetes/KubeServiceBuilderTests.cs create mode 100644 test/Ocelot.UnitTests/Kubernetes/KubeServiceCreatorTests.cs diff --git a/docs/features/kubernetes.rst b/docs/features/kubernetes.rst index 44411e589..8c6967c87 100644 --- a/docs/features/kubernetes.rst +++ b/docs/features/kubernetes.rst @@ -73,7 +73,11 @@ The example here shows a typical configuration: } Service deployment in **Namespace** ``Dev``, **ServiceDiscoveryProvider** type is ``Kube``, you also can set :ref:`k8s-pollkube-provider` type. -Note: **Host**, **Port** and **Token** are no longer in use. + + **Note 1**: ``Host``, ``Port`` and ``Token`` are no longer in use. + + **Note 2**: The ``Kube`` provider searches for the service entry using ``ServiceName`` and then retrieves the first available port from the ``EndpointSubsetV1.Ports`` collection. + Therefore, if the port name is not specified, the default downstream scheme will be ``http``; .. _k8s-pollkube-provider: @@ -99,10 +103,10 @@ This really depends on how volatile your services are. We doubt it will matter for most people and polling may give a tiny performance improvement over calling Kubernetes per request. There is no way for Ocelot to work these out for you. -Global vs Route levels -^^^^^^^^^^^^^^^^^^^^^^ +Global vs Route Levels +---------------------- -If your downstream service resides in a different namespace, you can override the global setting at the Route-level by specifying a **ServiceNamespace**: +If your downstream service resides in a different namespace, you can override the global setting at the Route-level by specifying a ``ServiceNamespace``: .. code-block:: json @@ -113,7 +117,45 @@ If your downstream service resides in a different namespace, you can override th } ] +Downstream Scheme vs Port Names [#f3]_ +-------------------------------------- + +Kubernetes configuration permits the definition of multiple ports with names for each address of an endpoint subset. +When binding multiple ports, you assign a name to each subset port. +To allow the ``Kube`` provider to recognize the desired port by its name, you need to specify the ``DownstreamScheme`` with the port's name; +if not, the collection's first port entry will be chosen by default. + +For instance, consider a service on Kubernetes that exposes two ports: ``https`` for **443** and ``http`` for **80**, as follows: + +.. code-block:: text + + Name: my-service + Namespace: default + Subsets: + Addresses: 10.1.161.59 + Ports: + Name Port Protocol + ---- ---- -------- + https 443 TCP + http 80 TCP + +**When** you need to use the ``http`` port while intentionally bypassing the default ``https`` port (first one), +you must define ``DownstreamScheme`` to enable the provider to recognize the desired ``http`` port by comparing ``DownstreamScheme`` with the port name as follows: + +.. code-block:: json + + "Routes": [ + { + "ServiceName": "my-service", + "DownstreamScheme": "http", // port name -> http -> port is 80 + } + ] + +**Note**: In the absence of a specified ``DownstreamScheme`` (which is the default behavior), the ``Kube`` provider will select **the first available port** from the ``EndpointSubsetV1.Ports`` collection. +Consequently, if the port name is not designated, the default downstream scheme utilized will be ``http``. + """" .. [#f1] `Wikipedia `_ | `K8s Website `_ | `K8s Documentation `_ | `K8s GitHub `_ .. [#f2] This feature was requested as part of `issue 345 `_ to add support for `Kubernetes `_ :doc:`../features/servicediscovery` provider. +.. [#f3] *"Downstream Scheme vs Port Names"* feature was requested as part of `issue 1967 `_ and released in version `23.3 `_ diff --git a/src/Ocelot.Provider.Kubernetes/EndPointClientV1.cs b/src/Ocelot.Provider.Kubernetes/EndPointClientV1.cs index 22f58e538..83418957b 100644 --- a/src/Ocelot.Provider.Kubernetes/EndPointClientV1.cs +++ b/src/Ocelot.Provider.Kubernetes/EndPointClientV1.cs @@ -1,6 +1,7 @@ using HTTPlease; using KubeClient.Models; using KubeClient.ResourceClients; +using Ocelot.Provider.Kubernetes.Interfaces; namespace Ocelot.Provider.Kubernetes { diff --git a/src/Ocelot.Provider.Kubernetes/IEndPointClient.cs b/src/Ocelot.Provider.Kubernetes/Interfaces/IEndPointClient.cs similarity index 83% rename from src/Ocelot.Provider.Kubernetes/IEndPointClient.cs rename to src/Ocelot.Provider.Kubernetes/Interfaces/IEndPointClient.cs index 6dfca972d..10f79f8af 100644 --- a/src/Ocelot.Provider.Kubernetes/IEndPointClient.cs +++ b/src/Ocelot.Provider.Kubernetes/Interfaces/IEndPointClient.cs @@ -1,7 +1,7 @@ using KubeClient.Models; using KubeClient.ResourceClients; -namespace Ocelot.Provider.Kubernetes; +namespace Ocelot.Provider.Kubernetes.Interfaces; public interface IEndPointClient : IKubeResourceClient { diff --git a/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceBuilder.cs b/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceBuilder.cs new file mode 100644 index 000000000..d5c6bcc30 --- /dev/null +++ b/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceBuilder.cs @@ -0,0 +1,9 @@ +using KubeClient.Models; +using Ocelot.Values; + +namespace Ocelot.Provider.Kubernetes.Interfaces; + +public interface IKubeServiceBuilder +{ + IEnumerable BuildServices(KubeRegistryConfiguration configuration, EndpointsV1 endpoint); +} diff --git a/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceCreator.cs b/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceCreator.cs new file mode 100644 index 000000000..a6ace7b2d --- /dev/null +++ b/src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceCreator.cs @@ -0,0 +1,10 @@ +using KubeClient.Models; +using Ocelot.Values; + +namespace Ocelot.Provider.Kubernetes.Interfaces; + +public interface IKubeServiceCreator +{ + IEnumerable Create(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset); + IEnumerable CreateInstance(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address); +} diff --git a/src/Ocelot.Provider.Kubernetes/Kube.cs b/src/Ocelot.Provider.Kubernetes/Kube.cs index 15b5cf6cc..5350f43b9 100644 --- a/src/Ocelot.Provider.Kubernetes/Kube.cs +++ b/src/Ocelot.Provider.Kubernetes/Kube.cs @@ -1,5 +1,6 @@ using KubeClient.Models; using Ocelot.Logging; +using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.Values; namespace Ocelot.Provider.Kubernetes; @@ -9,47 +10,44 @@ namespace Ocelot.Provider.Kubernetes; ///
public class Kube : IServiceDiscoveryProvider { - private readonly KubeRegistryConfiguration _kubeRegistryConfiguration; + private readonly KubeRegistryConfiguration _configuration; private readonly IOcelotLogger _logger; private readonly IKubeApiClient _kubeApi; - - public Kube(KubeRegistryConfiguration kubeRegistryConfiguration, IOcelotLoggerFactory factory, IKubeApiClient kubeApi) + private readonly IKubeServiceBuilder _serviceBuilder; + private readonly List _services; + + public Kube( + KubeRegistryConfiguration configuration, + IOcelotLoggerFactory factory, + IKubeApiClient kubeApi, + IKubeServiceBuilder serviceBuilder) { - _kubeRegistryConfiguration = kubeRegistryConfiguration; + _configuration = configuration; _logger = factory.CreateLogger(); _kubeApi = kubeApi; + _serviceBuilder = serviceBuilder; + _services = new(); } - public async Task> GetAsync() + public virtual async Task> GetAsync() { var endpoint = await _kubeApi .ResourceClient(client => new EndPointClientV1(client)) - .GetAsync(_kubeRegistryConfiguration.KeyOfServiceInK8s, _kubeRegistryConfiguration.KubeNamespace); + .GetAsync(_configuration.KeyOfServiceInK8s, _configuration.KubeNamespace); - var services = new List(); - if (endpoint != null && endpoint.Subsets.Any()) + _services.Clear(); + if (endpoint?.Subsets.Count != 0) { - services.AddRange(BuildServices(endpoint)); + _services.AddRange(BuildServices(_configuration, endpoint)); } else { - _logger.LogWarning(() => $"namespace:{_kubeRegistryConfiguration.KubeNamespace}service:{_kubeRegistryConfiguration.KeyOfServiceInK8s} Unable to use ,it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"); + _logger.LogWarning(() => $"K8s Namespace:{_configuration.KubeNamespace}, Service:{_configuration.KeyOfServiceInK8s}; Unable to use: it is invalid. Address must contain host only e.g. localhost and port must be greater than 0!"); } - return services; + return _services; } - private static List BuildServices(EndpointsV1 endpoint) - { - var services = new List(); - - foreach (var subset in endpoint.Subsets) - { - services.AddRange(subset.Addresses.Select(address => new Service(endpoint.Metadata.Name, - new ServiceHostAndPort(address.Ip, subset.Ports.First().Port), - endpoint.Metadata.Uid, string.Empty, Enumerable.Empty()))); - } - - return services; - } + protected virtual IEnumerable BuildServices(KubeRegistryConfiguration configuration, EndpointsV1 endpoint) + => _serviceBuilder.BuildServices(configuration, endpoint); } diff --git a/src/Ocelot.Provider.Kubernetes/KubeRegistryConfiguration.cs b/src/Ocelot.Provider.Kubernetes/KubeRegistryConfiguration.cs index 2a3d7e815..b264e1b67 100644 --- a/src/Ocelot.Provider.Kubernetes/KubeRegistryConfiguration.cs +++ b/src/Ocelot.Provider.Kubernetes/KubeRegistryConfiguration.cs @@ -1,8 +1,8 @@ -namespace Ocelot.Provider.Kubernetes +namespace Ocelot.Provider.Kubernetes; + +public class KubeRegistryConfiguration { - public class KubeRegistryConfiguration - { - public string KubeNamespace { get; set; } - public string KeyOfServiceInK8s { get; set; } - } + public string KubeNamespace { get; set; } + public string KeyOfServiceInK8s { get; set; } + public string Scheme { get; set; } } diff --git a/src/Ocelot.Provider.Kubernetes/KubeServiceBuilder.cs b/src/Ocelot.Provider.Kubernetes/KubeServiceBuilder.cs new file mode 100644 index 000000000..589cfe5ba --- /dev/null +++ b/src/Ocelot.Provider.Kubernetes/KubeServiceBuilder.cs @@ -0,0 +1,36 @@ +using KubeClient.Models; +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes.Interfaces; +using Ocelot.Values; + +namespace Ocelot.Provider.Kubernetes; + +public class KubeServiceBuilder : IKubeServiceBuilder +{ + private readonly IOcelotLogger _logger; + private readonly IKubeServiceCreator _serviceCreator; + + public KubeServiceBuilder(IOcelotLoggerFactory factory, IKubeServiceCreator serviceCreator) + { + ArgumentNullException.ThrowIfNull(factory); + _logger = factory.CreateLogger(); + + ArgumentNullException.ThrowIfNull(serviceCreator); + _serviceCreator = serviceCreator; + } + + public virtual IEnumerable BuildServices(KubeRegistryConfiguration configuration, EndpointsV1 endpoint) + { + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(endpoint); + + var services = endpoint.Subsets + .SelectMany(subset => _serviceCreator.Create(configuration, endpoint, subset)) + .ToArray(); + + _logger.LogDebug(() => $"K8s '{Check(endpoint.Kind)}:{Check(endpoint.ApiVersion)}:{Check(endpoint.Metadata?.Name)}' endpoint: Total built {services.Length} services."); + return services; + } + + private static string Check(string str) => string.IsNullOrEmpty(str) ? "?" : str; +} diff --git a/src/Ocelot.Provider.Kubernetes/KubeServiceCreator.cs b/src/Ocelot.Provider.Kubernetes/KubeServiceCreator.cs new file mode 100644 index 000000000..3d51159c3 --- /dev/null +++ b/src/Ocelot.Provider.Kubernetes/KubeServiceCreator.cs @@ -0,0 +1,59 @@ +using KubeClient.Models; +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes.Interfaces; +using Ocelot.Values; + +namespace Ocelot.Provider.Kubernetes; + +public class KubeServiceCreator : IKubeServiceCreator +{ + private readonly IOcelotLogger _logger; + + public KubeServiceCreator(IOcelotLoggerFactory factory) + { + ArgumentNullException.ThrowIfNull(factory); + _logger = factory.CreateLogger(); + } + + public virtual IEnumerable Create(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset) + => (configuration == null || endpoint == null || subset == null) + ? Array.Empty() + : subset.Addresses + .SelectMany(address => CreateInstance(configuration, endpoint, subset, address)) + .ToArray(); + + public virtual IEnumerable CreateInstance(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + { + var instance = new Service( + GetServiceName(configuration, endpoint, subset, address), + GetServiceHostAndPort(configuration, endpoint, subset, address), + GetServiceId(configuration, endpoint, subset, address), + GetServiceVersion(configuration, endpoint, subset, address), + GetServiceTags(configuration, endpoint, subset, address) + ); + return new Service[] { instance }; + } + + protected virtual string GetServiceName(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + => endpoint.Metadata?.Name; + + protected virtual ServiceHostAndPort GetServiceHostAndPort(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + { + var ports = subset.Ports; + bool portNameToScheme(EndpointPortV1 p) => string.Equals(p.Name, configuration.Scheme, StringComparison.InvariantCultureIgnoreCase); + var portV1 = string.IsNullOrEmpty(configuration.Scheme) || !ports.Any(portNameToScheme) + ? ports.FirstOrDefault() + : ports.FirstOrDefault(portNameToScheme); + portV1 ??= new(); + portV1.Name ??= configuration.Scheme ?? string.Empty; + _logger.LogDebug(() => $"K8s service with key '{configuration.KeyOfServiceInK8s}' and address {address.Ip}; Detected port is {portV1.Name}:{portV1.Port}. Total {ports.Count} ports of [{string.Join(',', ports.Select(p => p.Name))}]."); + return new ServiceHostAndPort(address.Ip, portV1.Port, portV1.Name); + } + + protected virtual string GetServiceId(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + => endpoint.Metadata?.Uid; + protected virtual string GetServiceVersion(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + => endpoint.ApiVersion; + protected virtual IEnumerable GetServiceTags(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) + => Enumerable.Empty(); +} diff --git a/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs b/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs index 4507c03e6..a3a1d48c0 100644 --- a/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs +++ b/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; -using Ocelot.Logging; +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes.Interfaces; namespace Ocelot.Provider.Kubernetes { @@ -17,14 +18,16 @@ private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provide { var factory = provider.GetService(); var kubeClient = provider.GetService(); + var serviceBuilder = provider.GetService(); var configuration = new KubeRegistryConfiguration { KeyOfServiceInK8s = route.ServiceName, KubeNamespace = string.IsNullOrEmpty(route.ServiceNamespace) ? config.Namespace : route.ServiceNamespace, + Scheme = route.DownstreamScheme, }; - var defaultK8sProvider = new Kube(configuration, factory, kubeClient); + var defaultK8sProvider = new Kube(configuration, factory, kubeClient, serviceBuilder); return PollKube.Equals(config.Type, StringComparison.OrdinalIgnoreCase) ? new PollKube(config.PollingInterval, factory, defaultK8sProvider) diff --git a/src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs index 0110bddbe..fadedc356 100644 --- a/src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs @@ -1,16 +1,18 @@ using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; +using Ocelot.Provider.Kubernetes.Interfaces; -namespace Ocelot.Provider.Kubernetes +namespace Ocelot.Provider.Kubernetes; + +public static class OcelotBuilderExtensions { - public static class OcelotBuilderExtensions + public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder, bool usePodServiceAccount = true) { - public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder, bool usePodServiceAccount = true) - { - builder.Services - .AddSingleton(KubernetesProviderFactory.Get) - .AddKubeClient(usePodServiceAccount); - return builder; - } + builder.Services + .AddKubeClient(usePodServiceAccount) + .AddSingleton(KubernetesProviderFactory.Get) + .AddSingleton() + .AddSingleton(); + return builder; } } diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs index c6e500925..e6c49a7d6 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs @@ -1,7 +1,6 @@ using Ocelot.Configuration; using Ocelot.ServiceDiscovery.Providers; -namespace Ocelot.ServiceDiscovery -{ - public delegate IServiceDiscoveryProvider ServiceDiscoveryFinderDelegate(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route); -} +namespace Ocelot.ServiceDiscovery; + +public delegate IServiceDiscoveryProvider ServiceDiscoveryFinderDelegate(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route); diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index 701a23d67..c997d07c5 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -30,13 +30,14 @@
- - + + + diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs similarity index 97% rename from test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs rename to test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs index 7ba087e55..d25c2075b 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs @@ -1,744 +1,744 @@ -using Consul; -using Microsoft.AspNetCore.Http; -using Newtonsoft.Json; -using Ocelot.Configuration.File; -using System.Text.RegularExpressions; - -namespace Ocelot.AcceptanceTests -{ - public class ServiceDiscoveryTests : IDisposable - { - private readonly Steps _steps; - private readonly List _consulServices; - private int _counterOne; - private int _counterTwo; - private int _counterConsul; - private static readonly object SyncLock = new(); - private string _downstreamPath; - private string _receivedToken; - private readonly ServiceHandler _serviceHandler; - private readonly ServiceHandler _serviceHandler2; - private readonly ServiceHandler _consulHandler; - - public ServiceDiscoveryTests() - { - _serviceHandler = new ServiceHandler(); - _serviceHandler2 = new ServiceHandler(); - _consulHandler = new ServiceHandler(); - _steps = new Steps(); - _consulServices = new List(); - } - - [Fact] - public void should_use_consul_service_discovery_and_load_balance_request() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort1 = PortFinder.GetRandomPort(); - var servicePort2 = PortFinder.GetRandomPort(); - var serviceName = "product"; - var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; - var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort1, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort2, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/home")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes() - { - var consulPort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = downstreamServicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - DownstreamScheme = "http", - HttpHandlerOptions = new FileHttpHandlerOptions - { - AllowAutoRedirect = true, - UseCookieContainer = true, - UseTracing = false, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/web/something")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_use_consul_service_discovery_and_load_balance_request_no_re_routes() - { - var consulPort = PortFinder.GetRandomPort(); - var serviceName = "product"; - var serviceOnePort = PortFinder.GetRandomPort(); - var serviceTwoPort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{serviceOnePort}"; - var downstreamServiceTwoUrl = $"http://localhost:{serviceTwoPort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = serviceOnePort, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = serviceTwoPort, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - DownstreamScheme = "http", - }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) - .BDDfy(); - } - - [Fact] - public void should_use_token_to_make_request_to_consul() - { - var token = "abctoken"; - var consulPort = PortFinder.GetRandomPort(); - var serviceName = "web"; - var servicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - Token = token, - }, - }, - }; - - this.Given(_ => GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(_ => _steps.GivenThereIsAConfiguration(configuration)) - .And(_ => _steps.GivenOcelotIsRunningWithConsul()) - .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/home")) - .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(_ => ThenTheTokenIs(token)) - .BDDfy(); - } - - [Fact] - public void should_send_request_to_service_after_it_becomes_available_in_consul() - { - var consulPort = PortFinder.GetRandomPort(); - var serviceName = "product"; - var servicePort1 = PortFinder.GetRandomPort(); - var servicePort2 = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; - var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort1, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort2, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .And(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) - .And(x => WhenIRemoveAService(serviceEntryTwo)) - .And(x => GivenIResetCounters()) - .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .And(x => ThenOnlyOneServiceHasBeenCalled()) - .And(x => WhenIAddAServiceBackIn(serviceEntryTwo)) - .And(x => GivenIResetCounters()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request() - { - var consulPort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = downstreamServicePort, - ID = $"web_90_0_2_224_{downstreamServicePort}", - Tags = new[] { "version-v1" }, - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - Type = "PollConsul", - PollingInterval = 0, - Namespace = string.Empty, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Theory] - [Trait("PR", "1944")] - [Trait("Issues", "849 1496")] - [InlineData("LeastConnection")] - [InlineData("RoundRobin")] - [InlineData("NoLoadBalancer")] - [InlineData("CookieStickySessions")] - public void Should_use_consul_service_discovery_based_on_upstream_host(string loadBalancerType) - { - // Simulate two DIFFERENT downstream services (e.g. product services for US and EU markets) - // with different ServiceNames (e.g. product-us and product-eu), - // UpstreamHost is used to determine which ServiceName to use when making a request to Consul (e.g. Host: us-shop goes to product-us) - var consulPort = PortFinder.GetRandomPort(); - var servicePortUS = PortFinder.GetRandomPort(); - var servicePortEU = PortFinder.GetRandomPort(); - var serviceNameUS = "product-us"; - var serviceNameEU = "product-eu"; - var downstreamServiceUrlUS = $"http://localhost:{servicePortUS}"; - var downstreamServiceUrlEU = $"http://localhost:{servicePortEU}"; - var upstreamHostUS = "us-shop"; - var upstreamHostEU = "eu-shop"; - var publicUrlUS = $"http://{upstreamHostUS}"; - var publicUrlEU = $"http://{upstreamHostEU}"; - var responseBodyUS = "Phone chargers with US plug"; - var responseBodyEU = "Phone chargers with EU plug"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryUS = new ServiceEntry - { - Service = new AgentService - { - Service = serviceNameUS, - Address = "localhost", - Port = servicePortUS, - ID = Guid.NewGuid().ToString(), - Tags = new string[] { "US" }, - }, - }; - var serviceEntryEU = new ServiceEntry - { - Service = new AgentService - { - Service = serviceNameEU, - Address = "localhost", - Port = servicePortEU, - ID = Guid.NewGuid().ToString(), - Tags = new string[] { "EU" }, - }, - }; - - var configuration = new FileConfiguration - { - Routes = new() - { - new() - { - DownstreamPathTemplate = "/products", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new() { "Get" }, - UpstreamHost = upstreamHostUS, - ServiceName = serviceNameUS, - LoadBalancerOptions = new() { Type = loadBalancerType }, - }, - new() - { - DownstreamPathTemplate = "/products", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new() {"Get" }, - UpstreamHost = upstreamHostEU, - ServiceName = serviceNameEU, - LoadBalancerOptions = new() { Type = loadBalancerType }, - }, - }, - GlobalConfiguration = new() - { - ServiceDiscoveryProvider = new() - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - // Ocelot request for http://us-shop/ should find 'product-us' in Consul, call /products and return "Phone chargers with US plug" - // Ocelot request for http://eu-shop/ should find 'product-eu' in Consul, call /products and return "Phone chargers with EU plug" - this.Given(x => x._serviceHandler.GivenThereIsAServiceRunningOn(downstreamServiceUrlUS, "/products", MapGet("/products", responseBodyUS))) - .And(x => x._serviceHandler2.GivenThereIsAServiceRunningOn(downstreamServiceUrlEU, "/products", MapGet("/products", responseBodyEU))) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryUS, serviceEntryEU)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul(publicUrlUS, publicUrlEU)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop for the first time") - .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(1)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyUS)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop for the first time") - .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(2)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyEU)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop again") - .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(3)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyUS)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop again") - .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(4)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyEU)) - .BDDfy(); - } - - private void ThenTheTokenIs(string token) - { - _receivedToken.ShouldBe(token); - } - - private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo) - { - _consulServices.Add(serviceEntryTwo); - } - - private void ThenOnlyOneServiceHasBeenCalled() - { - _counterOne.ShouldBe(10); - _counterTwo.ShouldBe(0); - } - - private void WhenIRemoveAService(ServiceEntry serviceEntryTwo) - { - _consulServices.Remove(serviceEntryTwo); - } - - private void GivenIResetCounters() - { - _counterOne = 0; - _counterTwo = 0; - _counterConsul = 0; - } - - private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) - { - _counterOne.ShouldBeInRange(bottom, top); - _counterOne.ShouldBeInRange(bottom, top); - } - - private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) - { - var total = _counterOne + _counterTwo; - total.ShouldBe(expected); - } - - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) - { - foreach (var serviceEntry in serviceEntries) - { - _consulServices.Add(serviceEntry); - } - } - - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) - { - _consulHandler.GivenThereIsAServiceRunningOn(url, async context => - { - if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) - { - _receivedToken = values.First(); - } - - // Parse the request path to get the service name - var pathMatch = Regex.Match(context.Request.Path.Value, "/v1/health/service/(?[^/]+)"); - if (pathMatch.Success) - { - _counterConsul++; - - // Use the parsed service name to filter the registered Consul services - var serviceName = pathMatch.Groups["serviceName"].Value; - var services = _consulServices.Where(x => x.Service.Service == serviceName).ToList(); - var json = JsonConvert.SerializeObject(services); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - } - - private void ThenConsulShouldHaveBeenCalledTimes(int expected) - { - _counterConsul.ShouldBe(expected); - } - - private void GivenProductServiceOneIsRunning(string url, int statusCode) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - try - { - string response; - lock (SyncLock) - { - _counterOne++; - response = _counterOne.ToString(); - } - - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(response); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } - - private void GivenProductServiceTwoIsRunning(string url, int statusCode) - { - _serviceHandler2.GivenThereIsAServiceRunningOn(url, async context => - { - try - { - string response; - lock (SyncLock) - { - _counterTwo++; - response = _counterTwo.ToString(); - } - - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(response); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, 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); - } - }); - } - - private RequestDelegate MapGet(string path, string responseBody) => async context => - { - var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - if (downstreamPath == path) - { - context.Response.StatusCode = 200; - await context.Response.WriteAsync(responseBody); - } - else - { - context.Response.StatusCode = 404; - await context.Response.WriteAsync("Not Found"); - } - }; - - public void Dispose() - { - _serviceHandler?.Dispose(); - _serviceHandler2?.Dispose(); - _consulHandler?.Dispose(); - _steps.Dispose(); - } - } -} +using Consul; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using System.Text.RegularExpressions; + +namespace Ocelot.AcceptanceTests.ServiceDiscovery +{ + public class ConsulServiceDiscoveryTests : IDisposable + { + private readonly Steps _steps; + private readonly List _consulServices; + private int _counterOne; + private int _counterTwo; + private int _counterConsul; + private static readonly object SyncLock = new(); + private string _downstreamPath; + private string _receivedToken; + private readonly ServiceHandler _serviceHandler; + private readonly ServiceHandler _serviceHandler2; + private readonly ServiceHandler _consulHandler; + + public ConsulServiceDiscoveryTests() + { + _serviceHandler = new ServiceHandler(); + _serviceHandler2 = new ServiceHandler(); + _consulHandler = new ServiceHandler(); + _steps = new Steps(); + _consulServices = new List(); + } + + [Fact] + public void should_use_consul_service_discovery_and_load_balance_request() + { + var consulPort = PortFinder.GetRandomPort(); + var servicePort1 = PortFinder.GetRandomPort(); + var servicePort2 = PortFinder.GetRandomPort(); + var serviceName = "product"; + var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; + var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort1, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + var serviceEntryTwo = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort2, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) + .BDDfy(); + } + + [Fact] + public void should_handle_request_to_consul_for_downstream_service_and_make_request() + { + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + const string serviceName = "web"; + var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort, + ID = "web_90_0_2_224_8080", + Tags = new[] { "version-v1" }, + }, + }; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/home", + DownstreamScheme = "http", + UpstreamPathTemplate = "/home", + UpstreamHttpMethod = new List { "Get", "Options" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/home")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes() + { + var consulPort = PortFinder.GetRandomPort(); + const string serviceName = "web"; + var downstreamServicePort = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = downstreamServicePort, + ID = "web_90_0_2_224_8080", + Tags = new[] { "version-v1" }, + }, + }; + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + DownstreamScheme = "http", + HttpHandlerOptions = new FileHttpHandlerOptions + { + AllowAutoRedirect = true, + UseCookieContainer = true, + UseTracing = false, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/web/something")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_use_consul_service_discovery_and_load_balance_request_no_re_routes() + { + var consulPort = PortFinder.GetRandomPort(); + var serviceName = "product"; + var serviceOnePort = PortFinder.GetRandomPort(); + var serviceTwoPort = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{serviceOnePort}"; + var downstreamServiceTwoUrl = $"http://localhost:{serviceTwoPort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = serviceOnePort, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + var serviceEntryTwo = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = serviceTwoPort, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + DownstreamScheme = "http", + }, + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) + .BDDfy(); + } + + [Fact] + public void should_use_token_to_make_request_to_consul() + { + var token = "abctoken"; + var consulPort = PortFinder.GetRandomPort(); + var serviceName = "web"; + var servicePort = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort, + ID = "web_90_0_2_224_8080", + Tags = new[] { "version-v1" }, + }, + }; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/home", + DownstreamScheme = "http", + UpstreamPathTemplate = "/home", + UpstreamHttpMethod = new List { "Get", "Options" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + Token = token, + }, + }, + }; + + this.Given(_ => GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(_ => _steps.GivenThereIsAConfiguration(configuration)) + .And(_ => _steps.GivenOcelotIsRunningWithConsul()) + .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/home")) + .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(_ => ThenTheTokenIs(token)) + .BDDfy(); + } + + [Fact] + public void should_send_request_to_service_after_it_becomes_available_in_consul() + { + var consulPort = PortFinder.GetRandomPort(); + var serviceName = "product"; + var servicePort1 = PortFinder.GetRandomPort(); + var servicePort2 = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; + var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort1, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + var serviceEntryTwo = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort2, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .And(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) + .And(x => WhenIRemoveAService(serviceEntryTwo)) + .And(x => GivenIResetCounters()) + .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .And(x => ThenOnlyOneServiceHasBeenCalled()) + .And(x => WhenIAddAServiceBackIn(serviceEntryTwo)) + .And(x => GivenIResetCounters()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) + .BDDfy(); + } + + [Fact] + public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request() + { + var consulPort = PortFinder.GetRandomPort(); + const string serviceName = "web"; + var downstreamServicePort = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = downstreamServicePort, + ID = $"web_90_0_2_224_{downstreamServicePort}", + Tags = new[] { "version-v1" }, + }, + }; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/home", + DownstreamScheme = "http", + UpstreamPathTemplate = "/home", + UpstreamHttpMethod = new List { "Get", "Options" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + Type = "PollConsul", + PollingInterval = 0, + Namespace = string.Empty, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Theory] + [Trait("PR", "1944")] + [Trait("Issues", "849 1496")] + [InlineData("LeastConnection")] + [InlineData("RoundRobin")] + [InlineData("NoLoadBalancer")] + [InlineData("CookieStickySessions")] + public void Should_use_consul_service_discovery_based_on_upstream_host(string loadBalancerType) + { + // Simulate two DIFFERENT downstream services (e.g. product services for US and EU markets) + // with different ServiceNames (e.g. product-us and product-eu), + // UpstreamHost is used to determine which ServiceName to use when making a request to Consul (e.g. Host: us-shop goes to product-us) + var consulPort = PortFinder.GetRandomPort(); + var servicePortUS = PortFinder.GetRandomPort(); + var servicePortEU = PortFinder.GetRandomPort(); + var serviceNameUS = "product-us"; + var serviceNameEU = "product-eu"; + var downstreamServiceUrlUS = $"http://localhost:{servicePortUS}"; + var downstreamServiceUrlEU = $"http://localhost:{servicePortEU}"; + var upstreamHostUS = "us-shop"; + var upstreamHostEU = "eu-shop"; + var publicUrlUS = $"http://{upstreamHostUS}"; + var publicUrlEU = $"http://{upstreamHostEU}"; + var responseBodyUS = "Phone chargers with US plug"; + var responseBodyEU = "Phone chargers with EU plug"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryUS = new ServiceEntry + { + Service = new AgentService + { + Service = serviceNameUS, + Address = "localhost", + Port = servicePortUS, + ID = Guid.NewGuid().ToString(), + Tags = new string[] { "US" }, + }, + }; + var serviceEntryEU = new ServiceEntry + { + Service = new AgentService + { + Service = serviceNameEU, + Address = "localhost", + Port = servicePortEU, + ID = Guid.NewGuid().ToString(), + Tags = new string[] { "EU" }, + }, + }; + + var configuration = new FileConfiguration + { + Routes = new() + { + new() + { + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() { "Get" }, + UpstreamHost = upstreamHostUS, + ServiceName = serviceNameUS, + LoadBalancerOptions = new() { Type = loadBalancerType }, + }, + new() + { + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() {"Get" }, + UpstreamHost = upstreamHostEU, + ServiceName = serviceNameEU, + LoadBalancerOptions = new() { Type = loadBalancerType }, + }, + }, + GlobalConfiguration = new() + { + ServiceDiscoveryProvider = new() + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + // Ocelot request for http://us-shop/ should find 'product-us' in Consul, call /products and return "Phone chargers with US plug" + // Ocelot request for http://eu-shop/ should find 'product-eu' in Consul, call /products and return "Phone chargers with EU plug" + this.Given(x => x._serviceHandler.GivenThereIsAServiceRunningOn(downstreamServiceUrlUS, "/products", MapGet("/products", responseBodyUS))) + .And(x => x._serviceHandler2.GivenThereIsAServiceRunningOn(downstreamServiceUrlEU, "/products", MapGet("/products", responseBodyEU))) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryUS, serviceEntryEU)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul(publicUrlUS, publicUrlEU)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop for the first time") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(1)) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyUS)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop for the first time") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(2)) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyEU)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop again") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(3)) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyUS)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop again") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(4)) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyEU)) + .BDDfy(); + } + + private void ThenTheTokenIs(string token) + { + _receivedToken.ShouldBe(token); + } + + private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo) + { + _consulServices.Add(serviceEntryTwo); + } + + private void ThenOnlyOneServiceHasBeenCalled() + { + _counterOne.ShouldBe(10); + _counterTwo.ShouldBe(0); + } + + private void WhenIRemoveAService(ServiceEntry serviceEntryTwo) + { + _consulServices.Remove(serviceEntryTwo); + } + + private void GivenIResetCounters() + { + _counterOne = 0; + _counterTwo = 0; + _counterConsul = 0; + } + + private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) + { + _counterOne.ShouldBeInRange(bottom, top); + _counterOne.ShouldBeInRange(bottom, top); + } + + private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) + { + var total = _counterOne + _counterTwo; + total.ShouldBe(expected); + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach (var serviceEntry in serviceEntries) + { + _consulServices.Add(serviceEntry); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) + { + _consulHandler.GivenThereIsAServiceRunningOn(url, async context => + { + if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) + { + _receivedToken = values.First(); + } + + // Parse the request path to get the service name + var pathMatch = Regex.Match(context.Request.Path.Value, "/v1/health/service/(?[^/]+)"); + if (pathMatch.Success) + { + _counterConsul++; + + // Use the parsed service name to filter the registered Consul services + var serviceName = pathMatch.Groups["serviceName"].Value; + var services = _consulServices.Where(x => x.Service.Service == serviceName).ToList(); + var json = JsonConvert.SerializeObject(services); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + } + + private void ThenConsulShouldHaveBeenCalledTimes(int expected) + { + _counterConsul.ShouldBe(expected); + } + + private void GivenProductServiceOneIsRunning(string url, int statusCode) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + string response; + lock (SyncLock) + { + _counterOne++; + response = _counterOne.ToString(); + } + + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } + + private void GivenProductServiceTwoIsRunning(string url, int statusCode) + { + _serviceHandler2.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + string response; + lock (SyncLock) + { + _counterTwo++; + response = _counterTwo.ToString(); + } + + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, 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); + } + }); + } + + private RequestDelegate MapGet(string path, string responseBody) => async context => + { + var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + if (downstreamPath == path) + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync(responseBody); + } + else + { + context.Response.StatusCode = 404; + await context.Response.WriteAsync("Not Found"); + } + }; + + public void Dispose() + { + _serviceHandler?.Dispose(); + _serviceHandler2?.Dispose(); + _consulHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs similarity index 96% rename from test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs rename to test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs index 1b7c1e99f..a12c20641 100644 --- a/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs @@ -1,281 +1,282 @@ using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Ocelot.Configuration.File; +using Ocelot.LoadBalancer.LoadBalancers; using Steeltoe.Common.Discovery; - -namespace Ocelot.AcceptanceTests -{ - public class EurekaServiceDiscoveryTests : IDisposable - { - private readonly Steps _steps; - private readonly List _eurekaInstances; - private readonly ServiceHandler _serviceHandler; - - public EurekaServiceDiscoveryTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - _eurekaInstances = new List(); - } - + +namespace Ocelot.AcceptanceTests.ServiceDiscovery +{ + public class EurekaServiceDiscoveryTests : IDisposable + { + private readonly Steps _steps; + private readonly List _eurekaInstances; + private readonly ServiceHandler _serviceHandler; + + public EurekaServiceDiscoveryTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + _eurekaInstances = new List(); + } + [Theory] [InlineData(true)] - [InlineData(false)] - public void should_use_eureka_service_discovery_and_make_request(bool dotnetRunningInContainer) - { - Environment.SetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER", dotnetRunningInContainer.ToString()); - var eurekaPort = 8761; - var serviceName = "product"; + [InlineData(false)] + public void should_use_eureka_service_discovery_and_make_request(bool dotnetRunningInContainer) + { + Environment.SetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER", dotnetRunningInContainer.ToString()); + var eurekaPort = 8761; + var serviceName = "product"; var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeEurekaServiceDiscoveryUrl = $"http://localhost:{eurekaPort}"; - - var instanceOne = new FakeEurekaService(serviceName, "localhost", downstreamServicePort, false, - new Uri($"http://localhost:{downstreamServicePort}"), new Dictionary()); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Type = "Eureka", - }, - }, - }; - - this.Given(x => x.GivenEurekaProductServiceOneIsRunning(downstreamServiceOneUrl)) - .And(x => x.GivenThereIsAFakeEurekaServiceDiscoveryProvider(fakeEurekaServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithEureka(instanceOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithEureka()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(_ => _steps.ThenTheResponseBodyShouldBe(nameof(EurekaServiceDiscoveryTests))) - .BDDfy(); - } - - private void GivenTheServicesAreRegisteredWithEureka(params IServiceInstance[] serviceInstances) - { - foreach (var instance in serviceInstances) - { - _eurekaInstances.Add(instance); - } - } - - private void GivenThereIsAFakeEurekaServiceDiscoveryProvider(string url, string serviceName) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - if (context.Request.Path.Value == "/eureka/apps/") - { - var apps = new List(); - - foreach (var serviceInstance in _eurekaInstances) - { - var a = new Application - { - name = serviceName, - instance = new List - { - new() - { - instanceId = $"{serviceInstance.Host}:{serviceInstance}", - hostName = serviceInstance.Host, - app = serviceName, - ipAddr = "127.0.0.1", - status = "UP", - overriddenstatus = "UNKNOWN", - port = new Port {value = serviceInstance.Port, enabled = "true"}, - securePort = new SecurePort {value = serviceInstance.Port, enabled = "true"}, - countryId = 1, - dataCenterInfo = new DataCenterInfo {value = "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", name = "MyOwn"}, - leaseInfo = new LeaseInfo - { - renewalIntervalInSecs = 30, - durationInSecs = 90, - registrationTimestamp = 1457714988223, - lastRenewalTimestamp= 1457716158319, - evictionTimestamp = 0, - serviceUpTimestamp = 1457714988223, - }, - metadata = new Metadata - { - value = "java.util.Collections$EmptyMap", - }, - homePageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", - statusPageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", - healthCheckUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", - vipAddress = serviceName, - isCoordinatingDiscoveryServer = "false", - lastUpdatedTimestamp = "1457714988223", - lastDirtyTimestamp = "1457714988172", - actionType = "ADDED", - }, - }, - }; - - apps.Add(a); - } - - var applications = new EurekaApplications - { - applications = new Applications - { - application = apps, - apps__hashcode = "UP_1_", - versions__delta = "1", - }, - }; - - var json = JsonConvert.SerializeObject(applications); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - } - - private void GivenEurekaProductServiceOneIsRunning(string url) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - try - { - context.Response.StatusCode = 200; - await context.Response.WriteAsync(nameof(EurekaServiceDiscoveryTests)); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } - - public void Dispose() + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeEurekaServiceDiscoveryUrl = $"http://localhost:{eurekaPort}"; + + var instanceOne = new FakeEurekaService(serviceName, "localhost", downstreamServicePort, false, + new Uri($"http://localhost:{downstreamServicePort}"), new Dictionary()); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = nameof(LeastConnection) }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Type = "Eureka", + }, + }, + }; + + this.Given(x => x.GivenEurekaProductServiceOneIsRunning(downstreamServiceOneUrl)) + .And(x => x.GivenThereIsAFakeEurekaServiceDiscoveryProvider(fakeEurekaServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithEureka(instanceOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithEureka()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe(nameof(EurekaServiceDiscoveryTests))) + .BDDfy(); + } + + private void GivenTheServicesAreRegisteredWithEureka(params IServiceInstance[] serviceInstances) + { + foreach (var instance in serviceInstances) + { + _eurekaInstances.Add(instance); + } + } + + private void GivenThereIsAFakeEurekaServiceDiscoveryProvider(string url, string serviceName) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + if (context.Request.Path.Value == "/eureka/apps/") + { + var apps = new List(); + + foreach (var serviceInstance in _eurekaInstances) + { + var a = new Application + { + name = serviceName, + instance = new List + { + new() + { + instanceId = $"{serviceInstance.Host}:{serviceInstance}", + hostName = serviceInstance.Host, + app = serviceName, + ipAddr = "127.0.0.1", + status = "UP", + overriddenstatus = "UNKNOWN", + port = new Port {value = serviceInstance.Port, enabled = "true"}, + securePort = new SecurePort {value = serviceInstance.Port, enabled = "true"}, + countryId = 1, + dataCenterInfo = new DataCenterInfo {value = "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", name = "MyOwn"}, + leaseInfo = new LeaseInfo + { + renewalIntervalInSecs = 30, + durationInSecs = 90, + registrationTimestamp = 1457714988223, + lastRenewalTimestamp= 1457716158319, + evictionTimestamp = 0, + serviceUpTimestamp = 1457714988223, + }, + metadata = new Metadata + { + value = "java.util.Collections$EmptyMap", + }, + homePageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + statusPageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + healthCheckUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + vipAddress = serviceName, + isCoordinatingDiscoveryServer = "false", + lastUpdatedTimestamp = "1457714988223", + lastDirtyTimestamp = "1457714988172", + actionType = "ADDED", + }, + }, + }; + + apps.Add(a); + } + + var applications = new EurekaApplications + { + applications = new Applications + { + application = apps, + apps__hashcode = "UP_1_", + versions__delta = "1", + }, + }; + + var json = JsonConvert.SerializeObject(applications); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + } + + private void GivenEurekaProductServiceOneIsRunning(string url) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync(nameof(EurekaServiceDiscoveryTests)); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } + + public class FakeEurekaService : IServiceInstance + { + public FakeEurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary metadata) { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } - - public class FakeEurekaService : IServiceInstance - { - public FakeEurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary metadata) - { - ServiceId = serviceId; - Host = host; - Port = port; - IsSecure = isSecure; - Uri = uri; - Metadata = metadata; - } - - public string ServiceId { get; } - public string Host { get; } - public int Port { get; } - public bool IsSecure { get; } - public Uri Uri { get; } - public IDictionary Metadata { get; } - } - - public class Port - { - [JsonProperty("$")] - public int value { get; set; } - - [JsonProperty("@enabled")] - public string enabled { get; set; } - } - - public class SecurePort - { - [JsonProperty("$")] - public int value { get; set; } - - [JsonProperty("@enabled")] - public string enabled { get; set; } - } - - public class DataCenterInfo - { - [JsonProperty("@class")] - public string value { get; set; } - - public string name { get; set; } - } - - public class LeaseInfo - { - public int renewalIntervalInSecs { get; set; } - - public int durationInSecs { get; set; } - - public long registrationTimestamp { get; set; } - - public long lastRenewalTimestamp { get; set; } - - public int evictionTimestamp { get; set; } - - public long serviceUpTimestamp { get; set; } - } - - public class Metadata - { - [JsonProperty("@class")] - public string value { get; set; } - } - - public class Instance - { - public string instanceId { get; set; } - public string hostName { get; set; } - public string app { get; set; } - public string ipAddr { get; set; } - public string status { get; set; } - public string overriddenstatus { get; set; } - public Port port { get; set; } - public SecurePort securePort { get; set; } - public int countryId { get; set; } - public DataCenterInfo dataCenterInfo { get; set; } - public LeaseInfo leaseInfo { get; set; } - public Metadata metadata { get; set; } - public string homePageUrl { get; set; } - public string statusPageUrl { get; set; } - public string healthCheckUrl { get; set; } - public string vipAddress { get; set; } - public string isCoordinatingDiscoveryServer { get; set; } - public string lastUpdatedTimestamp { get; set; } - public string lastDirtyTimestamp { get; set; } - public string actionType { get; set; } - } - - public class Application - { - public string name { get; set; } - public List instance { get; set; } - } - - public class Applications - { - public string versions__delta { get; set; } - public string apps__hashcode { get; set; } - public List application { get; set; } - } - - public class EurekaApplications - { - public Applications applications { get; set; } - } -} + ServiceId = serviceId; + Host = host; + Port = port; + IsSecure = isSecure; + Uri = uri; + Metadata = metadata; + } + + public string ServiceId { get; } + public string Host { get; } + public int Port { get; } + public bool IsSecure { get; } + public Uri Uri { get; } + public IDictionary Metadata { get; } + } + + public class Port + { + [JsonProperty("$")] + public int value { get; set; } + + [JsonProperty("@enabled")] + public string enabled { get; set; } + } + + public class SecurePort + { + [JsonProperty("$")] + public int value { get; set; } + + [JsonProperty("@enabled")] + public string enabled { get; set; } + } + + public class DataCenterInfo + { + [JsonProperty("@class")] + public string value { get; set; } + + public string name { get; set; } + } + + public class LeaseInfo + { + public int renewalIntervalInSecs { get; set; } + + public int durationInSecs { get; set; } + + public long registrationTimestamp { get; set; } + + public long lastRenewalTimestamp { get; set; } + + public int evictionTimestamp { get; set; } + + public long serviceUpTimestamp { get; set; } + } + + public class Metadata + { + [JsonProperty("@class")] + public string value { get; set; } + } + + public class Instance + { + public string instanceId { get; set; } + public string hostName { get; set; } + public string app { get; set; } + public string ipAddr { get; set; } + public string status { get; set; } + public string overriddenstatus { get; set; } + public Port port { get; set; } + public SecurePort securePort { get; set; } + public int countryId { get; set; } + public DataCenterInfo dataCenterInfo { get; set; } + public LeaseInfo leaseInfo { get; set; } + public Metadata metadata { get; set; } + public string homePageUrl { get; set; } + public string statusPageUrl { get; set; } + public string healthCheckUrl { get; set; } + public string vipAddress { get; set; } + public string isCoordinatingDiscoveryServer { get; set; } + public string lastUpdatedTimestamp { get; set; } + public string lastDirtyTimestamp { get; set; } + public string actionType { get; set; } + } + + public class Application + { + public string name { get; set; } + public List instance { get; set; } + } + + public class Applications + { + public string versions__delta { get; set; } + public string apps__hashcode { get; set; } + public List application { get; set; } + } + + public class EurekaApplications + { + public Applications applications { get; set; } + } +} diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/KubernetesServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/KubernetesServiceDiscoveryTests.cs new file mode 100644 index 000000000..5ca22da6e --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/KubernetesServiceDiscoveryTests.cs @@ -0,0 +1,212 @@ +using KubeClient; +using KubeClient.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Provider.Kubernetes; +using System.Runtime.CompilerServices; + +namespace Ocelot.AcceptanceTests.ServiceDiscovery; + +public sealed class KubernetesServiceDiscoveryTests : Steps, IDisposable +{ + private readonly string _kubernetesUrl; + private readonly IKubeApiClient _clientFactory; + private readonly ServiceHandler _serviceHandler; + private readonly ServiceHandler _kubernetesHandler; + private string _receivedToken; + + public KubernetesServiceDiscoveryTests() + { + _kubernetesUrl = DownstreamUrl(PortFinder.GetRandomPort()); //5567 + var option = new KubeClientOptions + { + ApiEndPoint = new Uri(_kubernetesUrl), + AccessToken = "txpc696iUhbVoudg164r93CxDTrKRVWG", + AuthStrategy = KubeAuthStrategy.BearerToken, + AllowInsecure = true, + }; + _clientFactory = KubeApiClient.Create(option); + _serviceHandler = new ServiceHandler(); + _kubernetesHandler = new ServiceHandler(); + } + + public override void Dispose() + { + _serviceHandler.Dispose(); + _kubernetesHandler.Dispose(); + base.Dispose(); + } + + [Fact] + public void ShouldReturnServicesFromK8s() + { + const string namespaces = nameof(KubernetesServiceDiscoveryTests); + const string serviceName = nameof(ShouldReturnServicesFromK8s); + var servicePort = PortFinder.GetRandomPort(); + var downstreamUrl = DownstreamUrl(servicePort); + var downstream = new Uri(downstreamUrl); + var subsetV1 = new EndpointSubsetV1(); + subsetV1.Addresses.Add(new() + { + Ip = Dns.GetHostAddresses(downstream.Host).Select(x => x.ToString()).First(a => a.Contains('.')), + Hostname = downstream.Host, + }); + subsetV1.Ports.Add(new() + { + Name = downstream.Scheme, + Port = servicePort, + }); + var endpoints = GivenEndpoints(subsetV1); + var route = GivenRouteWithServiceName(namespaces); + var configuration = GivenKubeConfiguration(namespaces, route); + var downstreamResponse = serviceName; + this.Given(x => GivenK8sProductServiceOneIsRunning(downstreamUrl, downstreamResponse)) + .And(x => GivenThereIsAFakeKubernetesProvider(serviceName, namespaces, endpoints)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => x.GivenOcelotIsRunningWithKubernetes()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => ThenTheResponseBodyShouldBe(downstreamResponse)) + .And(_ => ThenTheTokenIs("Bearer txpc696iUhbVoudg164r93CxDTrKRVWG")) + .BDDfy(); + } + + [Theory] + [Trait("Feat", "1967")] + [InlineData("", HttpStatusCode.BadGateway)] + [InlineData("http", HttpStatusCode.OK)] + public void ShouldReturnServicesByPortNameAsDownstreamScheme(string downstreamScheme, HttpStatusCode statusCode) + { + const string serviceName = "example-web"; + const string namespaces = "default"; + var servicePort = PortFinder.GetRandomPort(); + var downstreamUrl = DownstreamUrl(servicePort); + var downstream = new Uri(downstreamUrl); + + var subsetV1 = new EndpointSubsetV1(); + subsetV1.Addresses.Add(new() + { + Ip = Dns.GetHostAddresses(downstream.Host).Select(x => x.ToString()).First(a => a.Contains('.')), + Hostname = downstream.Host, + }); + subsetV1.Ports.Add(new() + { + Name = "https", // This service instance is offline -> BadGateway + Port = 443, + }); + subsetV1.Ports.Add(new() + { + Name = downstream.Scheme, // http, should be real scheme + Port = downstream.Port, // not 80, should be real port + }); + var endpoints = GivenEndpoints(subsetV1); + + var route = GivenRouteWithServiceName(namespaces); + route.DownstreamPathTemplate = "/{url}"; + route.DownstreamScheme = downstreamScheme; // !!! Warning !!! Select port by name as scheme + route.UpstreamPathTemplate = "/api/example/{url}"; + route.ServiceName = serviceName; // "example-web" + var configuration = GivenKubeConfiguration(namespaces, route); + + this.Given(x => GivenK8sProductServiceOneIsRunning(downstreamUrl, nameof(ShouldReturnServicesByPortNameAsDownstreamScheme))) + .And(x => GivenThereIsAFakeKubernetesProvider(serviceName, namespaces, endpoints)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => x.GivenOcelotIsRunningWithKubernetes()) + .When(x => WhenIGetUrlOnTheApiGateway("/api/example/1")) + .Then(x => ThenTheStatusCodeShouldBe(statusCode)) + .And(_ => ThenTheResponseBodyShouldBe(downstreamScheme == "http" + ? nameof(ShouldReturnServicesByPortNameAsDownstreamScheme) : string.Empty)) + .And(_ => ThenTheTokenIs("Bearer txpc696iUhbVoudg164r93CxDTrKRVWG")) + .BDDfy(); + } + + private void ThenTheTokenIs(string token) + { + _receivedToken.ShouldBe(token); + } + + private EndpointsV1 GivenEndpoints(EndpointSubsetV1 subset, [CallerMemberName] string serviceName = "") + { + var e = new EndpointsV1() + { + Kind = "endpoint", + ApiVersion = "1.0", + Metadata = new() + { + Name = serviceName, + Namespace = nameof(KubernetesServiceDiscoveryTests), + }, + }; + e.Subsets.Add(subset); + return e; + } + + private FileRoute GivenRouteWithServiceName(string serviceNamespace, [CallerMemberName] string serviceName = null) => new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = Uri.UriSchemeHttp, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() { HttpMethods.Get }, + ServiceName = serviceName, + ServiceNamespace = serviceNamespace, + LoadBalancerOptions = new() { Type = nameof(LeastConnection) }, + }; + + private FileConfiguration GivenKubeConfiguration(string serviceNamespace, params FileRoute[] routes) + { + var u = new Uri(_kubernetesUrl); + var configuration = GivenConfiguration(routes); + configuration.GlobalConfiguration.ServiceDiscoveryProvider = new() + { + Scheme = u.Scheme, + Host = u.Host, + Port = u.Port, + Type = nameof(Kube), + PollingInterval = 0, + Namespace = serviceNamespace, + }; + return configuration; + } + + private void GivenThereIsAFakeKubernetesProvider(string serviceName, string namespaces, EndpointsV1 endpoints) + => _kubernetesHandler.GivenThereIsAServiceRunningOn(_kubernetesUrl, async context => + { + if (context.Request.Path.Value == $"/api/v1/namespaces/{namespaces}/endpoints/{serviceName}") + { + if (context.Request.Headers.TryGetValue("Authorization", out var values)) + { + _receivedToken = values.First(); + } + + var json = JsonConvert.SerializeObject(endpoints); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + + private void GivenOcelotIsRunningWithKubernetes() + => GivenOcelotIsRunningWithServices(s => + { + s.AddOcelot().AddKubernetes(); + s.RemoveAll().AddSingleton(_clientFactory); + }); + + private void GivenK8sProductServiceOneIsRunning(string url, string response) + => _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + context.Response.StatusCode = (int)HttpStatusCode.OK; + await context.Response.WriteAsync(response ?? nameof(HttpStatusCode.OK)); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); +} diff --git a/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ServiceFabricTests.cs similarity index 94% rename from test/Ocelot.AcceptanceTests/ServiceFabricTests.cs rename to test/Ocelot.AcceptanceTests/ServiceDiscovery/ServiceFabricTests.cs index 20db1ed4a..739b19a7d 100644 --- a/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ServiceFabricTests.cs @@ -1,209 +1,209 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; - -namespace Ocelot.AcceptanceTests -{ - public class ServiceFabricTests : IDisposable - { - private readonly Steps _steps; - private string _downstreamPath; - private readonly ServiceHandler _serviceHandler; - - public ServiceFabricTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - + +namespace Ocelot.AcceptanceTests.ServiceDiscovery +{ + public class ServiceFabricTests : IDisposable + { + private readonly Steps _steps; + private string _downstreamPath; + private readonly ServiceHandler _serviceHandler; + + public ServiceFabricTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + [Fact] - [Trait("PR", "570")] - [Trait("Bug", "555")] - public void should_fix_issue_555() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/{everything}", - DownstreamScheme = "http", - UpstreamPathTemplate = "/{everything}", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "OcelotServiceApplication/OcelotApplicationService", - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = port, - Type = "ServiceFabric", - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/a", 200, "Hello from Laura", "b=c")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/a?b=c")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_support_service_fabric_naming_and_dns_service_stateless_and_guest() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/values", - DownstreamScheme = "http", - UpstreamPathTemplate = "/EquipmentInterfaces", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "OcelotServiceApplication/OcelotApplicationService", - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = port, - Type = "ServiceFabric", - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "test=best")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?test=best")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_support_service_fabric_naming_and_dns_service_statefull_and_actors() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/values", - DownstreamScheme = "http", - UpstreamPathTemplate = "/EquipmentInterfaces", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "OcelotServiceApplication/OcelotApplicationService", - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = port, - Type = "ServiceFabric", - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "PartitionKind=test&PartitionKey=1")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?PartitionKind=test&PartitionKey=1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Theory] - [Trait("PR", "722")] + [Trait("PR", "570")] + [Trait("Bug", "555")] + public void should_fix_issue_555() + { + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{everything}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/{everything}", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "OcelotServiceApplication/OcelotApplicationService", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = port, + Type = "ServiceFabric", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/a", 200, "Hello from Laura", "b=c")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/a?b=c")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_support_service_fabric_naming_and_dns_service_stateless_and_guest() + { + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + UpstreamPathTemplate = "/EquipmentInterfaces", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "OcelotServiceApplication/OcelotApplicationService", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = port, + Type = "ServiceFabric", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "test=best")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?test=best")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_support_service_fabric_naming_and_dns_service_statefull_and_actors() + { + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + UpstreamPathTemplate = "/EquipmentInterfaces", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "OcelotServiceApplication/OcelotApplicationService", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = port, + Type = "ServiceFabric", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "PartitionKind=test&PartitionKey=1")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?PartitionKind=test&PartitionKey=1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Theory] + [Trait("PR", "722")] [Trait("Feat", "721")] - [InlineData("/api/{version}/values", "/values", "Service_{version}/Api", "/Service_1.0/Api/values", "/api/1.0/values?test=best", "test=best")] - [InlineData("/api/{version}/{all}", "/{all}", "Service_{version}/Api", "/Service_1.0/Api/products", "/api/1.0/products?test=the-best-from-Aly", "test=the-best-from-Aly")] - public void should_support_placeholder_in_service_fabric_service_name(string upstream, string downstream, string serviceName, string downstreamUrl, string url, string query) - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new() - { - new() - { - DownstreamPathTemplate = downstream, - DownstreamScheme = Uri.UriSchemeHttp, - UpstreamPathTemplate = upstream, - UpstreamHttpMethod = new() { HttpMethods.Get }, - ServiceName = serviceName, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = port, - Type = "ServiceFabric", - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", downstreamUrl, 200, "Hello from Felix Boers", query)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(url)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Felix Boers")) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody, string expectedQueryString) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPath != basePath) - { - context.Response.StatusCode = (int)HttpStatusCode.NotFound; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - if (context.Request.QueryString.Value.Contains(expectedQueryString)) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - else - { - context.Response.StatusCode = (int)HttpStatusCode.NotFound; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - } - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } -} + [InlineData("/api/{version}/values", "/values", "Service_{version}/Api", "/Service_1.0/Api/values", "/api/1.0/values?test=best", "test=best")] + [InlineData("/api/{version}/{all}", "/{all}", "Service_{version}/Api", "/Service_1.0/Api/products", "/api/1.0/products?test=the-best-from-Aly", "test=the-best-from-Aly")] + public void should_support_placeholder_in_service_fabric_service_name(string upstream, string downstream, string serviceName, string downstreamUrl, string url, string query) + { + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new() + { + new() + { + DownstreamPathTemplate = downstream, + DownstreamScheme = Uri.UriSchemeHttp, + UpstreamPathTemplate = upstream, + UpstreamHttpMethod = new() { HttpMethods.Get }, + ServiceName = serviceName, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = port, + Type = "ServiceFabric", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", downstreamUrl, 200, "Hello from Felix Boers", query)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway(url)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Felix Boers")) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody, string expectedQueryString) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) + { + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + if (context.Request.QueryString.Value.Contains(expectedQueryString)) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + else + { + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + } + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 5acb57cbc..0d5505fcd 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -779,30 +779,7 @@ public void GivenOcelotIsRunningWithMinimumLogLevel(Logger logger, string appset } public void GivenOcelotIsRunningWithEureka() - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", true, false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddEureka(); - }) - .Configure(app => { app.UseOcelot().Wait(); }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } + => GivenOcelotIsRunningWithServices(s => s.AddOcelot().AddEureka()); public void GivenOcelotIsRunningWithPolly() => GivenOcelotIsRunningWithServices(WithPolly); public static void WithPolly(IServiceCollection services) => services.AddOcelot().AddPolly(); diff --git a/test/Ocelot.UnitTests/Kubernetes/KubeServiceBuilderTests.cs b/test/Ocelot.UnitTests/Kubernetes/KubeServiceBuilderTests.cs new file mode 100644 index 000000000..31f76c4e3 --- /dev/null +++ b/test/Ocelot.UnitTests/Kubernetes/KubeServiceBuilderTests.cs @@ -0,0 +1,162 @@ +using KubeClient.Models; +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes; +using Ocelot.Provider.Kubernetes.Interfaces; +using Ocelot.Values; + +namespace Ocelot.UnitTests.Kubernetes; + +[Trait("Feat", "1967")] +public sealed class KubeServiceBuilderTests +{ + private readonly Mock factory; + private readonly Mock serviceCreator; + private readonly Mock logger; + private KubeServiceBuilder sut; + + public KubeServiceBuilderTests() + { + factory = new(); + serviceCreator = new(); + logger = new(); + } + + private void Arrange() + { + factory.Setup(x => x.CreateLogger()) + .Returns(logger.Object) + .Verifiable(); + logger.Setup(x => x.LogDebug(It.IsAny>())) + .Verifiable(); + sut = new KubeServiceBuilder(factory.Object, serviceCreator.Object); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + public void Cstor_NullArgs_ThrownException(bool isFactory, bool isServiceCreator) + { + // Arrange + var arg1 = isFactory ? factory.Object : null; + var arg2 = isServiceCreator ? serviceCreator.Object : null; + + // Act, Assert + Assert.Throws( + arg1 is null ? "factory" : arg2 is null ? "serviceCreator" : string.Empty, + () => sut = new KubeServiceBuilder(arg1, arg2)); + } + + [Fact] + public void Cstor_NotNullArgs_ObjCreated() + { + // Arrange + factory.Setup(x => x.CreateLogger()).Verifiable(); + + // Act + sut = new KubeServiceBuilder(factory.Object, serviceCreator.Object); + + // Assert + Assert.NotNull(sut); + factory.Verify(x => x.CreateLogger(), Times.Once()); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + public void BuildServices_NullArgs_ThrownException(bool isConfiguration, bool isEndpoint) + { + // Arrange + var arg1 = isConfiguration ? new KubeRegistryConfiguration() : null; + var arg2 = isEndpoint ? new EndpointsV1() : null; + Arrange(); + + // Act, Assert + Assert.Throws( + arg1 is null ? "configuration" : arg2 is null ? "endpoint" : string.Empty, + () => _ = sut.BuildServices(arg1, arg2)); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void BuildServices_WithSubsets_SelectedManyServicesPerSubset(int subsetCount) + { + // Arrange + var configuration = new KubeRegistryConfiguration(); + var endpoint = new EndpointsV1(); + for (int i = 1; i <= subsetCount; i++) + { + var subset = new EndpointSubsetV1(); + subset.Addresses.Add(new() { NodeName = "subset" + i, Hostname = i.ToString() }); + endpoint.Subsets.Add(subset); + } + + serviceCreator.Setup(x => x.Create(configuration, endpoint, It.IsAny())) + .Returns((c, e, s) => + { + var item = s.Addresses[0]; + int count = int.Parse(item.Hostname); + var list = new List(count); + while (count > 0) + { + var id = count--.ToString(); + list.Add(new Service($"{item.NodeName}-service{id}", null, id, id, null)); + } + + return list; + }); + var many = endpoint.Subsets.Sum(s => int.Parse(s.Addresses[0].Hostname)); + Arrange(); + + // Act + var actual = sut.BuildServices(configuration, endpoint); + + // Assert + Assert.NotNull(actual); + var l = actual.ToList(); + Assert.Equal(many, l.Count); + serviceCreator.Verify(x => x.Create(configuration, endpoint, It.IsAny()), + Times.Exactly(endpoint.Subsets.Count)); + logger.Verify(x => x.LogDebug(It.IsAny>()), + Times.Once()); + } + + [Theory] + [InlineData(false, false, false, false, "K8s '?:?:?' endpoint: Total built 0 services.")] + [InlineData(false, false, false, true, "K8s '?:?:?' endpoint: Total built 0 services.")] + [InlineData(false, false, true, false, "K8s '?:?:?' endpoint: Total built 0 services.")] + [InlineData(false, false, true, true, "K8s '?:?:Name' endpoint: Total built 0 services.")] + [InlineData(false, true, true, true, "K8s '?:ApiVersion:Name' endpoint: Total built 0 services.")] + [InlineData(true, true, true, true, "K8s 'Kind:ApiVersion:Name' endpoint: Total built 0 services.")] + public void BuildServices_WithEndpoint_LogDebug(bool hasKind, bool hasApiVersion, bool hasMetadata, bool hasMetadataName, string message) + { + // Arrange + var configuration = new KubeRegistryConfiguration(); + var endpoint = new EndpointsV1() + { + Kind = hasKind ? nameof(EndpointsV1.Kind) : null, + ApiVersion = hasApiVersion ? nameof(EndpointsV1.ApiVersion) : null, + Metadata = hasMetadata ? new() + { + Name = hasMetadataName ? nameof(ObjectMetaV1.Name) : null, + } : null, + }; + Arrange(); + string actualMesssage = null; + logger.Setup(x => x.LogDebug(It.IsAny>())) + .Callback>(f => actualMesssage = f.Invoke()); + + // Act + var actual = sut.BuildServices(configuration, endpoint); + + // Assert + Assert.NotNull(actual); + logger.Verify(x => x.LogDebug(It.IsAny>()), + Times.Once()); + Assert.NotNull(actualMesssage); + Assert.Equal(message, actualMesssage); + } +} diff --git a/test/Ocelot.UnitTests/Kubernetes/KubeServiceCreatorTests.cs b/test/Ocelot.UnitTests/Kubernetes/KubeServiceCreatorTests.cs new file mode 100644 index 000000000..a8794ff87 --- /dev/null +++ b/test/Ocelot.UnitTests/Kubernetes/KubeServiceCreatorTests.cs @@ -0,0 +1,150 @@ +using KubeClient.Models; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes; + +namespace Ocelot.UnitTests.Kubernetes; + +[Trait("Feat", "1967")] +public sealed class KubeServiceCreatorTests +{ + private readonly Mock factory; + private readonly Mock logger; + private KubeServiceCreator sut; + + public KubeServiceCreatorTests() + { + factory = new(); + logger = new(); + } + + private void Arrange() + { + factory.Setup(x => x.CreateLogger()) + .Returns(logger.Object) + .Verifiable(); + logger.Setup(x => x.LogDebug(It.IsAny>())) + .Verifiable(); + sut = new KubeServiceCreator(factory.Object); + } + + [Fact] + public void Cstor_NullArg_ThrownException() + { + // Arrange, Act, Assert + Assert.Throws("factory", + () => sut = new KubeServiceCreator(null)); + } + + [Fact] + public void Cstor_NotNullArg_ObjCreated() + { + // Arrange + factory.Setup(x => x.CreateLogger()).Verifiable(); + + // Act + sut = new KubeServiceCreator(factory.Object); + + // Assert + Assert.NotNull(sut); + factory.Verify(x => x.CreateLogger(), Times.Once()); + } + + [Theory] + [InlineData(false, true, true)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + public void Create_NullArgs_ReturnedEmpty(bool isConfiguration, bool isEndpoint, bool isSubset) + { + // Arrange + var arg1 = isConfiguration ? new KubeRegistryConfiguration() : null; + var arg2 = isEndpoint ? new EndpointsV1() : null; + var arg3 = isSubset ? new EndpointSubsetV1() : null; + Arrange(); + + // Act + var actual = sut.Create(arg1, arg2, arg3); + + // Assert + Assert.NotNull(actual); + Assert.Empty(actual); + } + + [Fact(DisplayName = "Create: With empty args -> No exceptions during creation")] + public void Create_NotNullButEmptyArgs_CreatedEmptyService() + { + // Arrange + var arg1 = new KubeRegistryConfiguration() + { + KubeNamespace = nameof(KubeServiceCreatorTests), + KeyOfServiceInK8s = nameof(Create_NotNullButEmptyArgs_CreatedEmptyService), + }; + var arg2 = new EndpointsV1(); + var arg3 = new EndpointSubsetV1(); + arg3.Addresses.Add(new()); + arg2.Subsets.Add(arg3); + Arrange(); + + // Act + var actual = sut.Create(arg1, arg2, arg3); + + // Assert + Assert.NotNull(actual); + Assert.NotEmpty(actual); + var actualService = actual.SingleOrDefault(); + Assert.NotNull(actualService); + Assert.Null(actualService.Name); + } + + [Fact] + public void Create_ValidArgs_HappyPath() + { + // Arrange + var arg1 = new KubeRegistryConfiguration() + { + KubeNamespace = nameof(KubeServiceCreatorTests), + KeyOfServiceInK8s = nameof(Create_ValidArgs_HappyPath), + Scheme = "happy", //nameof(HttpScheme.Http), + }; + var arg2 = new EndpointsV1() + { + ApiVersion = "v1", + Metadata = new() + { + Namespace = nameof(KubeServiceCreatorTests), + Name = nameof(Create_ValidArgs_HappyPath), + Uid = Guid.NewGuid().ToString(), + }, + }; + var arg3 = new EndpointSubsetV1(); + arg3.Addresses.Add(new() + { + Ip = "8.8.8.8", + NodeName = "google", + Hostname = "dns.google", + }); + var ports = new List + { + new() { Name = nameof(HttpScheme.Http), Port = 80 }, + new() { Name = "happy", Port = 888 }, + }; + arg3.Ports.AddRange(ports); + arg2.Subsets.Add(arg3); + Arrange(); + + // Act + var actual = sut.Create(arg1, arg2, arg3); + + // Assert + Assert.NotNull(actual); + Assert.NotEmpty(actual); + var service = actual.SingleOrDefault(); + Assert.NotNull(service); + Assert.Equal(nameof(Create_ValidArgs_HappyPath), service.Name); + Assert.Equal("happy", service.HostAndPort.Scheme); + Assert.Equal(888, service.HostAndPort.DownstreamPort); + Assert.Equal("8.8.8.8", service.HostAndPort.DownstreamHost); + logger.Verify(x => x.LogDebug(It.IsAny>()), + Times.Once()); + } +} diff --git a/test/Ocelot.UnitTests/Kubernetes/KubeTests.cs b/test/Ocelot.UnitTests/Kubernetes/KubeTests.cs index 10207d6f4..213a25f65 100644 --- a/test/Ocelot.UnitTests/Kubernetes/KubeTests.cs +++ b/test/Ocelot.UnitTests/Kubernetes/KubeTests.cs @@ -6,11 +6,12 @@ using Newtonsoft.Json; using Ocelot.Logging; using Ocelot.Provider.Kubernetes; +using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.Values; namespace Ocelot.UnitTests.Kubernetes { - public class KubeTests : UnitTest, IDisposable + public class KubeTests : IDisposable { private IWebHost _fakeKubeBuilder; private readonly Kube _provider; @@ -21,10 +22,11 @@ public class KubeTests : UnitTest, IDisposable private readonly string _kubeHost; private readonly string _fakekubeServiceDiscoveryUrl; private List _services; + private string _receivedToken; private readonly Mock _factory; private readonly Mock _logger; - private string _receivedToken; private readonly IKubeApiClient _clientFactory; + private readonly Mock _serviceBuilder; public KubeTests() { @@ -33,8 +35,8 @@ public KubeTests() _port = 5567; _kubeHost = "localhost"; _fakekubeServiceDiscoveryUrl = $"{Uri.UriSchemeHttp}://{_kubeHost}:{_port}"; - _endpointEntries = new EndpointsV1(); - _factory = new Mock(); + _endpointEntries = new(); + _factory = new(); var option = new KubeClientOptions { @@ -45,19 +47,21 @@ public KubeTests() }; _clientFactory = KubeApiClient.Create(option); - _logger = new Mock(); + _logger = new(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); var config = new KubeRegistryConfiguration { KeyOfServiceInK8s = _serviceName, KubeNamespace = _namespaces, }; - _provider = new Kube(config, _factory.Object, _clientFactory); + _serviceBuilder = new(); + _provider = new Kube(config, _factory.Object, _clientFactory, _serviceBuilder.Object); } [Fact] public void Should_return_service_from_k8s() { + // Arrange var token = "Bearer txpc696iUhbVoudg164r93CxDTrKRVWG"; var endPointEntryOne = new EndpointsV1 { @@ -65,6 +69,7 @@ public void Should_return_service_from_k8s() ApiVersion = "1.0", Metadata = new ObjectMetaV1 { + Name = nameof(Should_return_service_from_k8s), Namespace = "dev", }, }; @@ -79,13 +84,17 @@ public void Should_return_service_from_k8s() Port = 80, }); endPointEntryOne.Subsets.Add(endpointSubsetV1); + _serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) + .Returns(new Service[] { new(nameof(Should_return_service_from_k8s), new("localhost", 80), string.Empty, string.Empty, new string[0]) }); + GivenThereIsAFakeKubeServiceDiscoveryProvider(_fakekubeServiceDiscoveryUrl, _serviceName, _namespaces); + GivenTheServicesAreRegisteredWithKube(endPointEntryOne); + + // Act + WhenIGetTheServices(); - this.Given(x => GivenThereIsAFakeKubeServiceDiscoveryProvider(_fakekubeServiceDiscoveryUrl, _serviceName, _namespaces)) - .And(x => GivenTheServicesAreRegisteredWithKube(endPointEntryOne)) - .When(x => WhenIGetTheServices()) - .Then(x => ThenTheCountIs(1)) - .And(_ => ThenTheTokenIs(token)) - .BDDfy(); + // Assert + ThenTheCountIs(1); + ThenTheTokenIs(token); } private void ThenTheTokenIs(string token) diff --git a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj index 30c2ca794..b9f107cfc 100644 --- a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +++ b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj @@ -27,12 +27,12 @@ - + From aef3e6b9f53ad8c8aa8c9ade92a572f60d0672a1 Mon Sep 17 00:00:00 2001 From: Sergii <109989060+sergio-str@users.noreply.github.com> Date: Tue, 7 May 2024 14:09:19 +0300 Subject: [PATCH 05/15] #1590 Use correct interval for request counting (#1592) * Use correct interval for request counting * Minor fixes, return correct counter value when ban period elapsed * Revert "Use correct interval for request counting" This reverts commit 7d232c7042f789d5ea32834f8c33d1adfb92ec24. * Revert "Artificial commit, initiate CI" This reverts commit e723dfac839f78961eb4ce0068c36eb0ab52e30c. * CA1822 Member 'XYZ' does not access instance data and can be marked as static * Quick code review by @raman-m * Rate Limiting feature name should match folder name * namespace `Ocelot.RateLimiting` * Extract `IRateLimitCore` interface * Remove useless `ClientRateLimitProcessor` class * Rename to `IRateLimitStorage` and dev docs * Wrap services as a feature * Review `IRateLimitCore` interface and dev docs * The middleware class prefix should match the feature name * Add some basic `RateLimitCoreTests` * Rename to `IRateLimiting` * Refactor rate limiting core * Remove redundant `SaveCounter` from the interface * Thread safe storage operations * Coalesce in return statement * Convert to file-scoped namespace * Use expression body * Unit tests for #1590 user scenario * Move test class to separate feature folder * Inherit from `Steps` * Refactoring: Follow the DRY principle * Acceptance test for #1590 user scenario * Update feature docs --------- Co-authored-by: raman-m --- docs/features/ratelimiting.rst | 92 ++++-- src/Ocelot/DependencyInjection/Features.cs | 16 ++ .../DependencyInjection/OcelotBuilder.cs | 4 +- .../Middleware/OcelotPipelineExtensions.cs | 2 +- .../RateLimit/ClientRateLimitProcessor.cs | 35 --- src/Ocelot/RateLimit/ClientRequestIdentity.cs | 18 -- ...DistributedCacheRateLimitCounterHandler.cs | 42 --- .../RateLimit/IRateLimitCounterHandler.cs | 13 - .../MemoryCacheRateLimitCounterHandler.cs | 28 -- .../RateLimitMiddlewareExtensions.cs | 12 - src/Ocelot/RateLimit/RateLimitCore.cs | 147 ---------- src/Ocelot/RateLimit/RateLimitCounter.cs | 21 -- src/Ocelot/RateLimit/RateLimitHeaders.cs | 23 -- .../RateLimiting/ClientRequestIdentity.cs | 15 + .../DistributedCacheRateLimitStorage.cs | 32 +++ src/Ocelot/RateLimiting/IRateLimitStorage.cs | 16 ++ src/Ocelot/RateLimiting/IRateLimiting.cs | 59 ++++ .../MemoryCacheRateLimitStorage.cs | 25 ++ .../Middleware/RateLimitingMiddleware.cs} | 36 ++- .../RateLimitingMiddlewareExtensions.cs | 11 + .../QuotaExceededError.cs | 2 +- src/Ocelot/RateLimiting/RateLimitCounter.cs | 29 ++ src/Ocelot/RateLimiting/RateLimitHeaders.cs | 19 ++ src/Ocelot/RateLimiting/RateLimiting.cs | 194 +++++++++++++ .../ClientRateLimitTests.cs | 217 -------------- .../RateLimiting/ClientRateLimitingTests.cs | 182 ++++++++++++ .../ClientRateLimitMiddlewareTests.cs | 183 ------------ .../RateLimitingMiddlewareTests.cs | 218 ++++++++++++++ .../RateLimiting/RateLimitingTests.cs | 268 ++++++++++++++++++ 29 files changed, 1165 insertions(+), 794 deletions(-) create mode 100644 src/Ocelot/DependencyInjection/Features.cs delete mode 100644 src/Ocelot/RateLimit/ClientRateLimitProcessor.cs delete mode 100644 src/Ocelot/RateLimit/ClientRequestIdentity.cs delete mode 100644 src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHandler.cs delete mode 100644 src/Ocelot/RateLimit/IRateLimitCounterHandler.cs delete mode 100644 src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs delete mode 100644 src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs delete mode 100644 src/Ocelot/RateLimit/RateLimitCore.cs delete mode 100644 src/Ocelot/RateLimit/RateLimitCounter.cs delete mode 100644 src/Ocelot/RateLimit/RateLimitHeaders.cs create mode 100644 src/Ocelot/RateLimiting/ClientRequestIdentity.cs create mode 100644 src/Ocelot/RateLimiting/DistributedCacheRateLimitStorage.cs create mode 100644 src/Ocelot/RateLimiting/IRateLimitStorage.cs create mode 100644 src/Ocelot/RateLimiting/IRateLimiting.cs create mode 100644 src/Ocelot/RateLimiting/MemoryCacheRateLimitStorage.cs rename src/Ocelot/{RateLimit/Middleware/ClientRateLimitMiddleware.cs => RateLimiting/Middleware/RateLimitingMiddleware.cs} (83%) create mode 100644 src/Ocelot/RateLimiting/Middleware/RateLimitingMiddlewareExtensions.cs rename src/Ocelot/{RateLimit => RateLimiting}/QuotaExceededError.cs (89%) create mode 100644 src/Ocelot/RateLimiting/RateLimitCounter.cs create mode 100644 src/Ocelot/RateLimiting/RateLimitHeaders.cs create mode 100644 src/Ocelot/RateLimiting/RateLimiting.cs delete mode 100644 test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs create mode 100644 test/Ocelot.AcceptanceTests/RateLimiting/ClientRateLimitingTests.cs delete mode 100644 test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs create mode 100644 test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs create mode 100644 test/Ocelot.UnitTests/RateLimiting/RateLimitingTests.cs diff --git a/docs/features/ratelimiting.rst b/docs/features/ratelimiting.rst index 9a69f4ded..94db1db5d 100644 --- a/docs/features/ratelimiting.rst +++ b/docs/features/ratelimiting.rst @@ -1,35 +1,50 @@ Rate Limiting ============= +`What's rate limiting? `_ + +* `Rate limiting | Wikipedia `_ +* `Rate Limiting pattern | Azure Architecture Center | Microsoft Learn `_ +* `Rate Limiting | Ask Google `_ + Ocelot Own Implementation ------------------------- -Ocelot supports rate limiting of upstream requests so that your downstream services do not become overloaded. +Ocelot provides *rate limiting* for upstream requests to prevent downstream services from becoming overwhelmed. [#f1]_ -The authors of this feature were inspired by `@catcherwong article `_ to finally write this documentation. -This feature was added by `@geffzhang `_ on GitHub! Thanks very much! +Rate Limit by Client's Header +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -To get rate limiting working for a Route you need to add the following JSON to it: +To implement *rate limiting* for a Route, you need to incorporate the following JSON configuration: .. code-block:: json "RateLimitOptions": { - "ClientWhitelist": [], + "ClientWhitelist": [], // array of strings "EnableRateLimiting": true, - "Period": "1s", - "PeriodTimespan": 1, + "Period": "1s", // seconds, minutes, hours, days + "PeriodTimespan": 1, // only seconds "Limit": 1 } -* **ClientWhitelist** - This is an array that contains the whitelist of the client. - It means that the client in this array will not be affected by the rate limiting. -* **EnableRateLimiting** - This value specifies enable endpoint rate limiting. -* **Period** - This value specifies the period that the limit applies to, such as ``1s``, ``5m``, ``1h``, ``1d`` and so on. - If you make more requests in the period than the limit allows then you need to wait for **PeriodTimespan** to elapse before you make another request. -* **PeriodTimespan** - This value specifies that we can retry after a certain number of seconds. -* **Limit** - This value specifies the maximum number of requests that a client can make in a defined period. +* **ClientWhitelist** - An array containing the whitelisted clients. Clients listed here will be exempt from rate limiting. + For more information on the **ClientIdHeader** option, refer to the :ref:`rl-global-configuration` section. +* **EnableRateLimiting** - This setting enables rate limiting on endpoints. +* **Period** - This parameter defines the duration for which the limit is applicable, such as ``1s`` (seconds), ``5m`` (minutes), ``1h`` (hours), and ``1d`` (days). + If you reach the exact **Limit** of requests, the excess occurs immediately, and the **PeriodTimespan** begins. + You must wait for the **PeriodTimespan** duration to pass before making another request. + Should you exceed the number of requests within the period more than the **Limit** permits, the **QuotaExceededMessage** will appear in the response, accompanied by the **HttpStatusCode**. +* **PeriodTimespan** - This parameter indicates the time in **seconds** after which a retry is permissible. + During this interval, the **QuotaExceededMessage** will appear in the response, accompanied by an **HttpStatusCode**. + Clients are advised to consult the ``Retry-After`` header to determine the timing of subsequent requests. +* **Limit** - This parameter defines the upper limit of requests a client is allowed to make within a specified **Period**. + +.. _rl-global-configuration: + +Global Configuration +^^^^^^^^^^^^^^^^^^^^ -You can also set the following in the **GlobalConfiguration** part of **ocelot.json**: +You can set the following in the ``GlobalConfiguration`` section of `ocelot.json`_: .. code-block:: json @@ -38,33 +53,48 @@ You can also set the following in the **GlobalConfiguration** part of **ocelot.j "RateLimitOptions": { "DisableRateLimitHeaders": false, "QuotaExceededMessage": "Customize Tips!", - "HttpStatusCode": 123, - "ClientIdHeader": "Test" + "HttpStatusCode": 418, // I'm a teapot + "ClientIdHeader": "MyRateLimiting" } } -* **DisableRateLimitHeaders** - This value specifies whether ``X-Rate-Limit`` and ``Retry-After`` headers are disabled. -* **QuotaExceededMessage** - This value specifies the exceeded message. -* **HttpStatusCode** - This value specifies the returned HTTP status code when rate limiting occurs. -* **ClientIdHeader** - Allows you to specifiy the header that should be used to identify clients. By default it is ``ClientId`` +* **DisableRateLimitHeaders** - Determines if the ``X-Rate-Limit`` and ``Retry-After`` headers are disabled. +* **QuotaExceededMessage** - Defines the message displayed when the quota is exceeded. It is optional and the default message is informative. +* **HttpStatusCode** - Indicates the HTTP status code returned during *rate limiting*. The default value is **429** (`Too Many Requests`_). +* **ClientIdHeader** - Specifies the header used to identify clients, with ``ClientId`` as the default. Future and ASP.NET Core Implementation -------------------------------------- -The Ocelot team considers to redesign *Rate Limiting* feature, -because of `Announcing Rate Limiting for .NET `_ by Brennan Conroy on July 13th, 2022. -There is no decision at the moment, and the old version of the feature is included as a part of release `20.0 `_ for .NET 7. +The Ocelot team is contemplating a redesign of the *Rate Limiting* feature following the `Announcing Rate Limiting for .NET`_ by Brennan Conroy on July 13th, 2022. +Currently, no decision has been made, and the previous version of the feature remains part of the `20.0`_ release for .NET 7. [#f2]_ -See more about new feature being added into ASP.NET Core 7.0 release: +Discover the new features being introduced in the ASP.NET Core 7.0 release: -* `RateLimiter Class `_, since ASP.NET Core **7.0** -* `System.Threading.RateLimiting `_ NuGet package -* `Rate limiting middleware in ASP.NET Core `_ article by Arvin Kahbazi, Maarten Balliauw, and Rick Anderson +* The `RateLimiter Class `_, available since ASP.NET Core 7.0 +* The `System.Threading.RateLimiting `_ NuGet package +* The `Rate limiting middleware in ASP.NET Core `_ article by Arvin Kahbazi, Maarten Balliauw, and Rick Anderson -However, it makes sense to keep the old implementation as a Ocelot built-in native feature, but we are going to migrate to the new Rate Limiter from ``Microsoft.AspNetCore.RateLimiting`` namespace. +While retaining the old implementation as an Ocelot built-in feature makes sense, we plan to transition to the new Rate Limiter from the ``Microsoft.AspNetCore.RateLimiting`` namespace. +Please share your thoughts with us in the `Discussions `_ space of the repository. |octocat| + +"""" + +.. [#f1] Historically, the *"Ocelot Own Rate Limiting"* feature is one of the oldest and first features of Ocelot. This feature was delivered in PR `37`_ by `@geffzhang`_ on GitHub. Many thanks! It was initially released in version `1.3.2`_. The authors were inspired by `@catcherwong article`_ to write this documentation. +.. [#f2] Since PR `37`_ and version `1.3.2`_, the Ocelot team has reviewed and redesigned the feature to provide stable behavior. The fix for bug `1590`_ (PR `1592`_) was released as part of version `23.3`_. + +.. _Announcing Rate Limiting for .NET: https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/ +.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/ocelot.json +.. _@geffzhang: https://github.com/ThreeMammals/Ocelot/commits?author=geffzhang +.. _@catcherwong article: http://www.c-sharpcorner.com/article/building-api-gateway-using-ocelot-in-asp-net-core-rate-limiting-part-four/ +.. _Too Many Requests: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429 +.. _37: https://github.com/ThreeMammals/Ocelot/pull/37 +.. _1590: https://github.com/ThreeMammals/Ocelot/issues/1590 +.. _1592: https://github.com/ThreeMammals/Ocelot/pull/1592 +.. _1.3.2: https://github.com/ThreeMammals/Ocelot/releases/tag/1.3.2 +.. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0 +.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 .. |octocat| image:: https://github.githubassets.com/images/icons/emoji/octocat.png :alt: octocat :width: 23 - -Please, share your opinion to us in the `Discussions `_ space of the repository. |octocat| diff --git a/src/Ocelot/DependencyInjection/Features.cs b/src/Ocelot/DependencyInjection/Features.cs new file mode 100644 index 000000000..51f836ea9 --- /dev/null +++ b/src/Ocelot/DependencyInjection/Features.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using Ocelot.RateLimiting; + +namespace Ocelot.DependencyInjection; + +public static class Features +{ + /// + /// Ocelot feature: Rate Limiting. + /// + /// The services collection to add the feature to. + /// The same object. + public static IServiceCollection AddRateLimiting(this IServiceCollection services) => services + .AddSingleton() + .AddSingleton(); +} diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index a72ec3cbf..a501014cc 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -27,7 +27,7 @@ using Ocelot.Multiplexer; using Ocelot.PathManipulation; using Ocelot.QueryStrings; -using Ocelot.RateLimit; +using Ocelot.RateLimiting; using Ocelot.Request.Creator; using Ocelot.Request.Mapper; using Ocelot.Requester; @@ -109,7 +109,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); + Services.AddRateLimiting(); // Feature: Rate Limiting Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); diff --git a/src/Ocelot/Middleware/OcelotPipelineExtensions.cs b/src/Ocelot/Middleware/OcelotPipelineExtensions.cs index 01ec573fb..16f4a5cff 100644 --- a/src/Ocelot/Middleware/OcelotPipelineExtensions.cs +++ b/src/Ocelot/Middleware/OcelotPipelineExtensions.cs @@ -12,7 +12,7 @@ using Ocelot.LoadBalancer.Middleware; using Ocelot.Multiplexer; using Ocelot.QueryStrings.Middleware; -using Ocelot.RateLimit.Middleware; +using Ocelot.RateLimiting.Middleware; using Ocelot.Request.Middleware; using Ocelot.Requester.Middleware; using Ocelot.RequestId.Middleware; diff --git a/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs b/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs deleted file mode 100644 index 14ebc0594..000000000 --- a/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration; - -namespace Ocelot.RateLimit -{ - public class ClientRateLimitProcessor - { - private readonly RateLimitCore _core; - - public ClientRateLimitProcessor(IRateLimitCounterHandler counterHandler) - { - _core = new RateLimitCore(counterHandler); - } - - public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - return _core.ProcessRequest(requestIdentity, option); - } - - public int RetryAfterFrom(DateTime timestamp, RateLimitRule rule) - { - return _core.RetryAfterFrom(timestamp, rule); - } - - public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - return _core.GetRateLimitHeaders(context, requestIdentity, option); - } - - public TimeSpan ConvertToTimeSpan(string timeSpan) - { - return _core.ConvertToTimeSpan(timeSpan); - } - } -} diff --git a/src/Ocelot/RateLimit/ClientRequestIdentity.cs b/src/Ocelot/RateLimit/ClientRequestIdentity.cs deleted file mode 100644 index b67b7c5a9..000000000 --- a/src/Ocelot/RateLimit/ClientRequestIdentity.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Ocelot.RateLimit -{ - public class ClientRequestIdentity - { - public ClientRequestIdentity(string clientId, string path, string httpverb) - { - ClientId = clientId; - Path = path; - HttpVerb = httpverb; - } - - public string ClientId { get; } - - public string Path { get; } - - public string HttpVerb { get; } - } -} diff --git a/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHandler.cs b/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHandler.cs deleted file mode 100644 index c98e256ad..000000000 --- a/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHandler.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.Extensions.Caching.Distributed; -using Newtonsoft.Json; - -namespace Ocelot.RateLimit -{ - public class DistributedCacheRateLimitCounterHandler : IRateLimitCounterHandler - { - private readonly IDistributedCache _memoryCache; - - public DistributedCacheRateLimitCounterHandler(IDistributedCache memoryCache) - { - _memoryCache = memoryCache; - } - - public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) - { - _memoryCache.SetString(id, JsonConvert.SerializeObject(counter), new DistributedCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); - } - - public bool Exists(string id) - { - var stored = _memoryCache.GetString(id); - return !string.IsNullOrEmpty(stored); - } - - public RateLimitCounter? Get(string id) - { - var stored = _memoryCache.GetString(id); - if (!string.IsNullOrEmpty(stored)) - { - return JsonConvert.DeserializeObject(stored); - } - - return null; - } - - public void Remove(string id) - { - _memoryCache.Remove(id); - } - } -} diff --git a/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs b/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs deleted file mode 100644 index c17d04f7c..000000000 --- a/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Ocelot.RateLimit -{ - public interface IRateLimitCounterHandler - { - bool Exists(string id); - - RateLimitCounter? Get(string id); - - void Remove(string id); - - void Set(string id, RateLimitCounter counter, TimeSpan expirationTime); - } -} diff --git a/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs b/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs deleted file mode 100644 index 1a030d511..000000000 --- a/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.Extensions.Caching.Memory; - -namespace Ocelot.RateLimit -{ - public class MemoryCacheRateLimitCounterHandler : IRateLimitCounterHandler - { - private readonly IMemoryCache _memoryCache; - - public MemoryCacheRateLimitCounterHandler(IMemoryCache memoryCache) - { - _memoryCache = memoryCache; - } - - public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) - { - _memoryCache.Set(id, counter, new MemoryCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); - } - - public bool Exists(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter); - - public RateLimitCounter? Get(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter) ? counter : null; - - public void Remove(string id) - { - _memoryCache.Remove(id); - } - } -} diff --git a/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs b/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs deleted file mode 100644 index 91609c67f..000000000 --- a/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.AspNetCore.Builder; - -namespace Ocelot.RateLimit.Middleware -{ - public static class RateLimitMiddlewareExtensions - { - public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } - } -} diff --git a/src/Ocelot/RateLimit/RateLimitCore.cs b/src/Ocelot/RateLimit/RateLimitCore.cs deleted file mode 100644 index dddf8a772..000000000 --- a/src/Ocelot/RateLimit/RateLimitCore.cs +++ /dev/null @@ -1,147 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration; -using System.Globalization; -using System.Security.Cryptography; - -namespace Ocelot.RateLimit -{ - public class RateLimitCore - { - private readonly IRateLimitCounterHandler _counterHandler; - private static readonly object ProcessLocker = new(); - - public RateLimitCore(IRateLimitCounterHandler counterStore) - { - _counterHandler = counterStore; - } - - public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - var counter = new RateLimitCounter(DateTime.UtcNow, 1); - var rule = option.RateLimitRule; - - var counterId = ComputeCounterKey(requestIdentity, option); - - // serial reads and writes - lock (ProcessLocker) - { - var entry = _counterHandler.Get(counterId); - if (entry.HasValue) - { - // entry has not expired - if (entry.Value.Timestamp + TimeSpan.FromSeconds(rule.PeriodTimespan) >= DateTime.UtcNow) - { - // increment request count - var totalRequests = entry.Value.TotalRequests + 1; - - // deep copy - counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests); - } - } - } - - if (counter.TotalRequests > rule.Limit) - { - var retryAfter = RetryAfterFrom(counter.Timestamp, rule); - if (retryAfter > 0) - { - var expirationTime = TimeSpan.FromSeconds(rule.PeriodTimespan); - _counterHandler.Set(counterId, counter, expirationTime); - } - else - { - _counterHandler.Remove(counterId); - } - } - else - { - var expirationTime = ConvertToTimeSpan(rule.Period); - _counterHandler.Set(counterId, counter, expirationTime); - } - - return counter; - } - - public void SaveRateLimitCounter(ClientRequestIdentity requestIdentity, RateLimitOptions option, RateLimitCounter counter, TimeSpan expirationTime) - { - var counterId = ComputeCounterKey(requestIdentity, option); - var rule = option.RateLimitRule; - - // stores: id (string) - timestamp (datetime) - total_requests (long) - _counterHandler.Set(counterId, counter, expirationTime); - } - - public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - var rule = option.RateLimitRule; - RateLimitHeaders headers; - var counterId = ComputeCounterKey(requestIdentity, option); - var entry = _counterHandler.Get(counterId); - if (entry.HasValue) - { - headers = new RateLimitHeaders(context, rule.Period, - (rule.Limit - entry.Value.TotalRequests).ToString(), - (entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo) - ); - } - else - { - headers = new RateLimitHeaders(context, - rule.Period, - rule.Limit.ToString(), - (DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)); - } - - return headers; - } - - public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - var key = $"{option.RateLimitCounterPrefix}_{requestIdentity.ClientId}_{option.RateLimitRule.Period}_{requestIdentity.HttpVerb}_{requestIdentity.Path}"; - - var idBytes = Encoding.UTF8.GetBytes(key); - - byte[] hashBytes; - - using (var algorithm = SHA1.Create()) - { - hashBytes = algorithm.ComputeHash(idBytes); - } - - return BitConverter.ToString(hashBytes).Replace("-", string.Empty); - } - - public int RetryAfterFrom(DateTime timestamp, RateLimitRule rule) - { - var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds); - var retryAfter = Convert.ToInt32(TimeSpan.FromSeconds(rule.PeriodTimespan).TotalSeconds); - retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1; - return retryAfter; - } - - public TimeSpan ConvertToTimeSpan(string timeSpan) - { - var l = timeSpan.Length - 1; - var value = timeSpan.Substring(0, l); - var type = timeSpan.Substring(l, 1); - - switch (type) - { - case "d": - return TimeSpan.FromDays(double.Parse(value)); - - case "h": - return TimeSpan.FromHours(double.Parse(value)); - - case "m": - return TimeSpan.FromMinutes(double.Parse(value)); - - case "s": - return TimeSpan.FromSeconds(double.Parse(value)); - - default: - throw new FormatException($"{timeSpan} can't be converted to TimeSpan, unknown type {type}"); - } - } - } -} diff --git a/src/Ocelot/RateLimit/RateLimitCounter.cs b/src/Ocelot/RateLimit/RateLimitCounter.cs deleted file mode 100644 index 4e869d440..000000000 --- a/src/Ocelot/RateLimit/RateLimitCounter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Newtonsoft.Json; - -namespace Ocelot.RateLimit -{ - /// - /// Stores the initial access time and the numbers of calls made from that point. - /// - public struct RateLimitCounter - { - [JsonConstructor] - public RateLimitCounter(DateTime timestamp, long totalRequests) - { - Timestamp = timestamp; - TotalRequests = totalRequests; - } - - public DateTime Timestamp { get; } - - public long TotalRequests { get; } - } -} diff --git a/src/Ocelot/RateLimit/RateLimitHeaders.cs b/src/Ocelot/RateLimit/RateLimitHeaders.cs deleted file mode 100644 index 67d7596ce..000000000 --- a/src/Ocelot/RateLimit/RateLimitHeaders.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace Ocelot.RateLimit -{ - public class RateLimitHeaders - { - public RateLimitHeaders(HttpContext context, string limit, string remaining, string reset) - { - Context = context; - Limit = limit; - Remaining = remaining; - Reset = reset; - } - - public HttpContext Context { get; } - - public string Limit { get; } - - public string Remaining { get; } - - public string Reset { get; } - } -} diff --git a/src/Ocelot/RateLimiting/ClientRequestIdentity.cs b/src/Ocelot/RateLimiting/ClientRequestIdentity.cs new file mode 100644 index 000000000..b73fcbbbb --- /dev/null +++ b/src/Ocelot/RateLimiting/ClientRequestIdentity.cs @@ -0,0 +1,15 @@ +namespace Ocelot.RateLimiting; + +public class ClientRequestIdentity +{ + public ClientRequestIdentity(string clientId, string path, string httpverb) + { + ClientId = clientId; + Path = path; + HttpVerb = httpverb; + } + + public string ClientId { get; } + public string Path { get; } + public string HttpVerb { get; } +} diff --git a/src/Ocelot/RateLimiting/DistributedCacheRateLimitStorage.cs b/src/Ocelot/RateLimiting/DistributedCacheRateLimitStorage.cs new file mode 100644 index 000000000..b7fb79de3 --- /dev/null +++ b/src/Ocelot/RateLimiting/DistributedCacheRateLimitStorage.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Caching.Distributed; +using Newtonsoft.Json; + +namespace Ocelot.RateLimiting; + +/// +/// Custom storage based on a distributed cache of a remote/local services. +/// +/// +/// See the interface docs for more details. +/// +public class DistributedCacheRateLimitStorage : IRateLimitStorage +{ + private readonly IDistributedCache _memoryCache; + + public DistributedCacheRateLimitStorage(IDistributedCache memoryCache) => _memoryCache = memoryCache; + + public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) + => _memoryCache.SetString(id, JsonConvert.SerializeObject(counter), new DistributedCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); + + public bool Exists(string id) => !string.IsNullOrEmpty(_memoryCache.GetString(id)); + + public RateLimitCounter? Get(string id) + { + var stored = _memoryCache.GetString(id); + return !string.IsNullOrEmpty(stored) + ? JsonConvert.DeserializeObject(stored) + : null; + } + + public void Remove(string id) => _memoryCache.Remove(id); +} diff --git a/src/Ocelot/RateLimiting/IRateLimitStorage.cs b/src/Ocelot/RateLimiting/IRateLimitStorage.cs new file mode 100644 index 000000000..1044998b1 --- /dev/null +++ b/src/Ocelot/RateLimiting/IRateLimitStorage.cs @@ -0,0 +1,16 @@ +namespace Ocelot.RateLimiting; + +/// +/// Defines a storage for keeping of rate limiting data. +/// +/// Concrete classes should be based on solutions with excellent performance, such as in-memory solutions. +public interface IRateLimitStorage +{ + bool Exists(string id); + + RateLimitCounter? Get(string id); + + void Remove(string id); + + void Set(string id, RateLimitCounter counter, TimeSpan expirationTime); +} diff --git a/src/Ocelot/RateLimiting/IRateLimiting.cs b/src/Ocelot/RateLimiting/IRateLimiting.cs new file mode 100644 index 000000000..684d2f70e --- /dev/null +++ b/src/Ocelot/RateLimiting/IRateLimiting.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; + +namespace Ocelot.RateLimiting; + +/// +/// Defines basic Rate Limiting functionality. +/// +public interface IRateLimiting +{ + /// Retrieves the key for the attached storage. + /// See the interface. + /// The current representation of the request. + /// The options of rate limiting. + /// A value of the key. + string GetStorageKey(ClientRequestIdentity identity, RateLimitOptions options); + + /// + /// Gets required information to create wanted headers in upper contexts (middleware, etc). + /// + /// The current context. + /// The current representation of the request. + /// The options of rate limiting. + /// A value. + RateLimitHeaders GetHeaders(HttpContext context, ClientRequestIdentity identity, RateLimitOptions options); + + /// + /// Main entry point to process the current request and apply the limiting rule. + /// + /// Warning! The method performs the storage operations which should be thread safe. + /// The representation of current request. + /// The current rate limiting options. + /// A value. + RateLimitCounter ProcessRequest(ClientRequestIdentity identity, RateLimitOptions options); + + /// + /// Counts requests based on the current counter state and taking into account the limiting rule. + /// + /// Old counter with starting moment inside. + /// The limiting rule. + /// A value. + RateLimitCounter Count(RateLimitCounter? entry, RateLimitRule rule); + + /// + /// Gets the seconds to wait for the next retry by starting moment and the rule. + /// + /// The method must be called after the counting by the method is completed; otherwise it doesn't make sense. + /// The counter with starting moment inside. + /// The limiting rule. + /// A value in seconds. + double RetryAfter(RateLimitCounter counter, RateLimitRule rule); + + /// + /// Converts to time span from a string, such as "1s", "1m", "1h", "1d". + /// + /// The string value with dimentions: '1s', '1m', '1h', '1d'. + /// A value. + TimeSpan ToTimespan(string timespan); +} diff --git a/src/Ocelot/RateLimiting/MemoryCacheRateLimitStorage.cs b/src/Ocelot/RateLimiting/MemoryCacheRateLimitStorage.cs new file mode 100644 index 000000000..7451dac97 --- /dev/null +++ b/src/Ocelot/RateLimiting/MemoryCacheRateLimitStorage.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Caching.Memory; + +namespace Ocelot.RateLimiting; + +/// +/// Default storage based on the memory cache of the local web server instance. +/// +/// +/// See the interface docs for more details. +/// +public class MemoryCacheRateLimitStorage : IRateLimitStorage +{ + private readonly IMemoryCache _memoryCache; + + public MemoryCacheRateLimitStorage(IMemoryCache memoryCache) => _memoryCache = memoryCache; + + public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) + => _memoryCache.Set(id, counter, new MemoryCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); + + public bool Exists(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter); + + public RateLimitCounter? Get(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter) ? counter : null; + + public void Remove(string id) => _memoryCache.Remove(id); +} diff --git a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs b/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddleware.cs similarity index 83% rename from src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs rename to src/Ocelot/RateLimiting/Middleware/RateLimitingMiddleware.cs index 47571046f..b407733ae 100644 --- a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs +++ b/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddleware.cs @@ -2,21 +2,23 @@ using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Middleware; +using System.Globalization; -namespace Ocelot.RateLimit.Middleware +namespace Ocelot.RateLimiting.Middleware { - public class ClientRateLimitMiddleware : OcelotMiddleware + public class RateLimitingMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; - private readonly ClientRateLimitProcessor _processor; + private readonly IRateLimiting _limiter; - public ClientRateLimitMiddleware(RequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IRateLimitCounterHandler counterHandler) - : base(loggerFactory.CreateLogger()) + public RateLimitingMiddleware( + RequestDelegate next, + IOcelotLoggerFactory factory, + IRateLimiting limiter) + : base(factory.CreateLogger()) { _next = next; - _processor = new ClientRateLimitProcessor(counterHandler); + _limiter = limiter; } public async Task Invoke(HttpContext httpContext) @@ -48,26 +50,20 @@ public async Task Invoke(HttpContext httpContext) if (rule.Limit > 0) { // increment counter - var counter = _processor.ProcessRequest(identity, options); + var counter = _limiter.ProcessRequest(identity, options); // check if limit is reached if (counter.TotalRequests > rule.Limit) { - //compute retry after value - var retryAfter = _processor.RetryAfterFrom(counter.Timestamp, rule); - - // log blocked request - LogBlockedRequest(httpContext, identity, counter, rule, downstreamRoute); - - var retrystring = retryAfter.ToString(System.Globalization.CultureInfo.InvariantCulture); + var retryAfter = _limiter.RetryAfter(counter, rule); // compute retry after value based on counter state + LogBlockedRequest(httpContext, identity, counter, rule, downstreamRoute); // log blocked request virtually // break execution - var ds = ReturnQuotaExceededResponse(httpContext, options, retrystring); + var ds = ReturnQuotaExceededResponse(httpContext, options, retryAfter.ToString(CultureInfo.InvariantCulture)); httpContext.Items.UpsertDownstreamResponse(ds); // Set Error httpContext.Items.SetError(new QuotaExceededError(GetResponseMessage(options), options.HttpStatusCode)); - return; } } @@ -75,7 +71,7 @@ public async Task Invoke(HttpContext httpContext) //set X-Rate-Limit headers for the longest period if (!options.DisableRateLimitHeaders) { - var headers = _processor.GetRateLimitHeaders(httpContext, identity, options); + var headers = _limiter.GetHeaders(httpContext, identity, options); httpContext.Response.OnStarting(SetRateLimitHeaders, state: headers); } @@ -123,7 +119,7 @@ public virtual DownstreamResponse ReturnQuotaExceededResponse(HttpContext httpCo if (!option.DisableRateLimitHeaders) { - http.Headers.TryAddWithoutValidation("Retry-After", retryAfter); + http.Headers.TryAddWithoutValidation("Retry-After", retryAfter); // in seconds, not date string } return new DownstreamResponse(http); diff --git a/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddlewareExtensions.cs b/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddlewareExtensions.cs new file mode 100644 index 000000000..68268cb40 --- /dev/null +++ b/src/Ocelot/RateLimiting/Middleware/RateLimitingMiddlewareExtensions.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.RateLimiting.Middleware; + +public static class RateLimitingMiddlewareExtensions +{ + public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } +} diff --git a/src/Ocelot/RateLimit/QuotaExceededError.cs b/src/Ocelot/RateLimiting/QuotaExceededError.cs similarity index 89% rename from src/Ocelot/RateLimit/QuotaExceededError.cs rename to src/Ocelot/RateLimiting/QuotaExceededError.cs index 9c98dc5a6..a46cb4c78 100644 --- a/src/Ocelot/RateLimit/QuotaExceededError.cs +++ b/src/Ocelot/RateLimiting/QuotaExceededError.cs @@ -1,6 +1,6 @@ using Ocelot.Errors; -namespace Ocelot.RateLimit +namespace Ocelot.RateLimiting { public class QuotaExceededError : Error { diff --git a/src/Ocelot/RateLimiting/RateLimitCounter.cs b/src/Ocelot/RateLimiting/RateLimitCounter.cs new file mode 100644 index 000000000..2507a0433 --- /dev/null +++ b/src/Ocelot/RateLimiting/RateLimitCounter.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; + +namespace Ocelot.RateLimiting; + +/// +/// Stores the initial access time and the numbers of calls made from that point. +/// +public struct RateLimitCounter +{ + [JsonConstructor] + public RateLimitCounter(DateTime startedAt, DateTime? exceededAt, long totalRequests) + { + StartedAt = startedAt; + ExceededAt = exceededAt; + TotalRequests = totalRequests; + } + + /// The moment when the counting was started. + /// A value of the moment. + public DateTime StartedAt { get; } + + /// The moment when the limit was exceeded. + /// A value of the moment. + public DateTime? ExceededAt { get; } + + /// Total number of requests counted. + /// A value of total number. + public long TotalRequests { get; set; } +} diff --git a/src/Ocelot/RateLimiting/RateLimitHeaders.cs b/src/Ocelot/RateLimiting/RateLimitHeaders.cs new file mode 100644 index 000000000..860e0d6bb --- /dev/null +++ b/src/Ocelot/RateLimiting/RateLimitHeaders.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Http; + +namespace Ocelot.RateLimiting; + +public class RateLimitHeaders +{ + public RateLimitHeaders(HttpContext context, string limit, string remaining, string reset) + { + Context = context; + Limit = limit; + Remaining = remaining; + Reset = reset; + } + + public HttpContext Context { get; } + public string Limit { get; } + public string Remaining { get; } + public string Reset { get; } +} diff --git a/src/Ocelot/RateLimiting/RateLimiting.cs b/src/Ocelot/RateLimiting/RateLimiting.cs new file mode 100644 index 000000000..9edf4a310 --- /dev/null +++ b/src/Ocelot/RateLimiting/RateLimiting.cs @@ -0,0 +1,194 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using System.Globalization; +using System.Security.Cryptography; + +namespace Ocelot.RateLimiting; + +public class RateLimiting : IRateLimiting +{ + private readonly IRateLimitStorage _storage; + private static readonly object ProcessLocker = new(); + + public RateLimiting(IRateLimitStorage storage) + { + _storage = storage; + } + + /// + /// Main entry point to process the current request and apply the limiting rule. + /// + /// Warning! The method performs the storage operations which MUST BE thread safe. + /// The representation of current request. + /// The current rate limiting options. + /// A value. + public virtual RateLimitCounter ProcessRequest(ClientRequestIdentity identity, RateLimitOptions options) + { + RateLimitCounter counter; + var rule = options.RateLimitRule; + var counterId = GetStorageKey(identity, options); + + // Serial reads/writes from/to the storage which must be thread safe + lock (ProcessLocker) + { + var entry = _storage.Get(counterId); + counter = Count(entry, rule); + var expiration = ToTimespan(rule.Period); // default expiration is set for the Period value + if (counter.TotalRequests > rule.Limit) + { + var retryAfter = RetryAfter(counter, rule); // the calculation depends on the counter returned from CountRequests + if (retryAfter > 0) + { + // Rate Limit exceeded, ban period is active + expiration = TimeSpan.FromSeconds(rule.PeriodTimespan); // current state should expire in the storage after ban period + } + else + { + // Ban period elapsed, start counting + _storage.Remove(counterId); // the store can delete the element on its own using an expiration mechanism, but let's force the element to be deleted + counter = new RateLimitCounter(DateTime.UtcNow, null, 1); + } + } + + _storage.Set(counterId, counter, expiration); + } + + return counter; + } + + /// + /// Counts requests based on the current counter state and taking into account the limiting rule. + /// + /// Old counter with starting moment inside. + /// The limiting rule. + /// A value. + public virtual RateLimitCounter Count(RateLimitCounter? entry, RateLimitRule rule) + { + var now = DateTime.UtcNow; + if (!entry.HasValue) // no entry, start counting + { + return new RateLimitCounter(now, null, 1); // current request is the 1st one + } + + var counter = entry.Value; + var total = counter.TotalRequests + 1; // increment request count + var startedAt = counter.StartedAt; + if (startedAt + ToTimespan(rule.Period) >= now) // counting Period is active + { + var exceededAt = total >= rule.Limit && !counter.ExceededAt.HasValue // current request number equals to the limit + ? now // the exceeding moment is now, the next request will fail but the current one doesn't + : counter.ExceededAt; + return new RateLimitCounter(startedAt, exceededAt, total); // deep copy + } + + var wasExceededAt = counter.ExceededAt; + return wasExceededAt + TimeSpan.FromSeconds(rule.PeriodTimespan) >= now // ban PeriodTimespan is active + ? new RateLimitCounter(startedAt, wasExceededAt, total) // still count + : new RateLimitCounter(now, null, 1); // Ban PeriodTimespan elapsed, start counting NOW! + } + + public virtual RateLimitHeaders GetHeaders(HttpContext context, ClientRequestIdentity identity, RateLimitOptions options) + { + RateLimitHeaders headers; + RateLimitCounter? entry; + lock (ProcessLocker) + { + var counterId = GetStorageKey(identity, options); + entry = _storage.Get(counterId); + } + + var rule = options.RateLimitRule; + if (entry.HasValue) + { + headers = new RateLimitHeaders(context, + limit: rule.Period, + remaining: (rule.Limit - entry.Value.TotalRequests).ToString(), + reset: (entry.Value.StartedAt + ToTimespan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)); + } + else + { + headers = new RateLimitHeaders(context, + limit: rule.Period, // TODO Double check + remaining: rule.Limit.ToString(), // TODO Double check + reset: (DateTime.UtcNow + ToTimespan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)); + } + + return headers; + } + + public virtual string GetStorageKey(ClientRequestIdentity identity, RateLimitOptions options) + { + var key = $"{options.RateLimitCounterPrefix}_{identity.ClientId}_{options.RateLimitRule.Period}_{identity.HttpVerb}_{identity.Path}"; + var idBytes = Encoding.UTF8.GetBytes(key); + + byte[] hashBytes; + using (var algorithm = SHA1.Create()) + { + hashBytes = algorithm.ComputeHash(idBytes); + } + + return BitConverter.ToString(hashBytes).Replace("-", string.Empty); + } + + /// + /// Gets the seconds to wait for the next retry by starting moment and the rule. + /// + /// The method must be called after the one. + /// The counter state. + /// The current rule. + /// An value of seconds. + public virtual double RetryAfter(RateLimitCounter counter, RateLimitRule rule) + { + const double defaultSeconds = 1.0D; // one second + var periodTimespan = rule.PeriodTimespan < defaultSeconds + ? defaultSeconds // allow values which are greater or equal to 1 second + : rule.PeriodTimespan; // good value + var now = DateTime.UtcNow; + if (counter.StartedAt + ToTimespan(rule.Period) >= now) // counting Period is active + { + return counter.TotalRequests < rule.Limit + ? 0.0D // happy path, no need to retry, current request is valid + : counter.ExceededAt.HasValue + ? periodTimespan - (now - counter.ExceededAt.Value).TotalSeconds // minus seconds past + : periodTimespan; // exceeding not yet detected -> let's ban for whole period + } + + if (counter.ExceededAt.HasValue && // limit exceeding was happen + counter.ExceededAt + TimeSpan.FromSeconds(periodTimespan) >= now) // ban PeriodTimespan is active + { + var startedAt = counter.ExceededAt.Value; // ban period was started at + double secondsPast = (now - startedAt).TotalSeconds; + double retryAfter = periodTimespan - secondsPast; + return retryAfter; // it can be negative, which means the wait in PeriodTimespan seconds has ended + } + + return 0.0D; // ban period elapsed, no need to retry, current request is valid + } + + /// + /// Converts to time span from a string, such as "1s", "1m", "1h", "1d". + /// + /// The string value with dimentions: '1s', '1m', '1h', '1d'. + /// A value. + /// By default if the value dimension can't be detected. + public virtual TimeSpan ToTimespan(string timespan) + { + if (string.IsNullOrEmpty(timespan)) + { + return TimeSpan.Zero; + } + + var len = timespan.Length - 1; + var value = timespan.Substring(0, len); + var type = timespan.Substring(len, 1); + + return type switch + { + "d" => TimeSpan.FromDays(double.Parse(value)), + "h" => TimeSpan.FromHours(double.Parse(value)), + "m" => TimeSpan.FromMinutes(double.Parse(value)), + "s" => TimeSpan.FromSeconds(double.Parse(value)), + _ => throw new FormatException($"{timespan} can't be converted to TimeSpan, unknown type {type}"), + }; + } +} diff --git a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs deleted file mode 100644 index dad9af3dc..000000000 --- a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs +++ /dev/null @@ -1,217 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.File; - -namespace Ocelot.AcceptanceTests -{ - public class ClientRateLimitTests : IDisposable - { - private readonly Steps _steps; - private int _counterOne; - private readonly ServiceHandler _serviceHandler; - - public ClientRateLimitTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - - [Fact] - public void should_call_withratelimiting() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/ClientRateLimit", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/ClientRateLimit", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = _steps.RequestIdKey, - RateLimitOptions = new FileRateLimitRule - { - EnableRateLimiting = true, - ClientWhitelist = new List(), - Limit = 3, - Period = "1s", - PeriodTimespan = 1000, - }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - RateLimitOptions = new FileRateLimitOptions - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = string.Empty, - RateLimitCounterPrefix = string.Empty, - HttpStatusCode = 428, - }, - RequestIdKey = "oceclientrequest", - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/ClientRateLimit")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .BDDfy(); - } - - [Fact] - public void should_wait_for_period_timespan_to_elapse_before_making_next_request() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/ClientRateLimit", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/ClientRateLimit", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = _steps.RequestIdKey, - - RateLimitOptions = new FileRateLimitRule - { - EnableRateLimiting = true, - ClientWhitelist = new List(), - Limit = 3, - Period = "1s", - PeriodTimespan = 2, - }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - RateLimitOptions = new FileRateLimitOptions - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = string.Empty, - RateLimitCounterPrefix = string.Empty, - HttpStatusCode = 428, - }, - RequestIdKey = "oceclientrequest", - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/ClientRateLimit")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .And(x => _steps.GivenIWait(1000)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .And(x => _steps.GivenIWait(1000)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .BDDfy(); - } - - [Fact] - public void should_call_middleware_withWhitelistClient() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/ClientRateLimit", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/ClientRateLimit", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = _steps.RequestIdKey, - - RateLimitOptions = new FileRateLimitRule - { - EnableRateLimiting = true, - ClientWhitelist = new List { "ocelotclient1"}, - Limit = 3, - Period = "1s", - PeriodTimespan = 100, - }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - RateLimitOptions = new FileRateLimitOptions - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = string.Empty, - RateLimitCounterPrefix = string.Empty, - }, - RequestIdKey = "oceclientrequest", - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/ClientRateLimit")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 4)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, context => - { - _counterOne++; - context.Response.StatusCode = 200; - context.Response.WriteAsync(_counterOne.ToString()); - return Task.CompletedTask; - }); - } - - public void Dispose() - { - _steps.Dispose(); - } - } -} diff --git a/test/Ocelot.AcceptanceTests/RateLimiting/ClientRateLimitingTests.cs b/test/Ocelot.AcceptanceTests/RateLimiting/ClientRateLimitingTests.cs new file mode 100644 index 000000000..4dd80e7ec --- /dev/null +++ b/test/Ocelot.AcceptanceTests/RateLimiting/ClientRateLimitingTests.cs @@ -0,0 +1,182 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; + +namespace Ocelot.AcceptanceTests.RateLimiting; + +public sealed class ClientRateLimitingTests : Steps, IDisposable +{ + const int OK = (int)HttpStatusCode.OK; + const int TooManyRequests = (int)HttpStatusCode.TooManyRequests; + + private int _counterOne; + private readonly ServiceHandler _serviceHandler; + + public ClientRateLimitingTests() + { + _serviceHandler = new ServiceHandler(); + } + + public override void Dispose() + { + _serviceHandler.Dispose(); + base.Dispose(); + } + + [Fact] + [Trait("Feat", "37")] + public void Should_call_with_rate_limiting() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, null, null, new(), 3, "1s", 1); // periods are equal + var configuration = GivenConfigurationWithRateLimitOptions(route); + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/api/ClientRateLimit")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) + .Then(x => ThenTheStatusCodeShouldBe(TooManyRequests)) + .BDDfy(); + } + + [Fact] + [Trait("Feat", "37")] + public void Should_wait_for_period_timespan_to_elapse_before_making_next_request() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, "/api/ClientRateLimit?count={count}", "/ClientRateLimit/?{count}", new(), 3, "1s", 2); + var configuration = GivenConfigurationWithRateLimitOptions(route); + _counterOne = 0; + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/api/ClientRateLimit")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 2)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) + .Then(x => ThenTheStatusCodeShouldBe(TooManyRequests)) + .And(x => GivenIWait(1000)) + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) + .Then(x => ThenTheStatusCodeShouldBe(TooManyRequests)) + .And(x => GivenIWait(1000)) + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .And(x => ThenTheResponseBodyShouldBe("4")) // total 4 OK responses + .BDDfy(); + } + + private int _count = 0; + private int Count() => ++_count; + private string Url() => $"/ClientRateLimit/?{Count()}"; + + private void WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Func urlDelegate, long times) + { + for (long i = 0; i < times; i++) + { + var url = urlDelegate.Invoke(); + WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(url, 1); + } + } + + [Fact] + [Trait("Feat", "37")] + public void Should_call_middleware_with_white_list_client() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, null, null, whitelist: new() { "ocelotclient1" }, 3, "3s", 2); // main period is greater than ban one + var configuration = GivenConfigurationWithRateLimitOptions(route); + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/api/ClientRateLimit")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 4)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .BDDfy(); + } + + [Fact] + [Trait("Bug", "1590")] + public void StatusShouldNotBeEqualTo429_PeriodTimespanValueIsGreaterThanPeriod() + { + _counterOne = 0; + + // Bug scenario + const string period = "1s"; + const double periodTimespan = /*30*/3; // but decrease 30 to 3 secs, "no wasting time" life hack + const long limit = 100L; + + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, "/api/ClientRateLimit?count={count}", "/ClientRateLimit/?{count}", new(), + limit, period, periodTimespan); // bug scenario, adapted + var configuration = GivenConfigurationWithRateLimitOptions(route); + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(port), "/api/ClientRateLimit")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + + // main scenario + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, route.RateLimitOptions.Limit)) // 100 times to reach the limit + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .And(x => ThenTheResponseBodyShouldBe(route.RateLimitOptions.Limit.ToString())) // total 100 OK responses + + // extra scenario + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) // 101st request should fail + .Then(x => ThenTheStatusCodeShouldBe(TooManyRequests)) + .And(x => GivenIWait((int)TimeSpan.FromSeconds(route.RateLimitOptions.PeriodTimespan).TotalMilliseconds)) // in 3 secs PeriodTimespan will elapse + .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(Url, 1)) + .Then(x => ThenTheStatusCodeShouldBe(OK)) + .And(x => ThenTheResponseBodyShouldBe("101")) // total 101 OK responses + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, context => + { + _counterOne++; + context.Response.StatusCode = OK; + context.Response.WriteAsync(_counterOne.ToString()); + return Task.CompletedTask; + }); + } + + private FileRoute GivenRoute(int port, string downstream, string upstream, List whitelist, long limit, string period, double periodTimespan) => new() + { + DownstreamPathTemplate = downstream ?? "/api/ClientRateLimit", + DownstreamHostAndPorts = new() + { + new("localhost", port), + }, + DownstreamScheme = Uri.UriSchemeHttp, + UpstreamPathTemplate = upstream ?? "/api/ClientRateLimit", + UpstreamHttpMethod = new() { HttpMethods.Get }, + RequestIdKey = RequestIdKey, + RateLimitOptions = new FileRateLimitRule + { + EnableRateLimiting = true, + ClientWhitelist = whitelist ?? new() { "ocelotclient1" }, + Limit = limit, + Period = period ?? "1s", + PeriodTimespan = periodTimespan, + }, + }; + + private static FileConfiguration GivenConfigurationWithRateLimitOptions(params FileRoute[] routes) + { + var config = GivenConfiguration(routes); + config.GlobalConfiguration = new() + { + RateLimitOptions = new() + { + ClientIdHeader = "ClientId", + DisableRateLimitHeaders = false, + QuotaExceededMessage = "Exceeding!", + RateLimitCounterPrefix = "ABC", + HttpStatusCode = TooManyRequests, // 429 + }, + RequestIdKey = "OcelotClientRequest", + }; + return config; + } +} diff --git a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs deleted file mode 100644 index 1c2267ae2..000000000 --- a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs +++ /dev/null @@ -1,183 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Memory; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Logging; -using Ocelot.Middleware; -using Ocelot.RateLimit; -using Ocelot.RateLimit.Middleware; -using Ocelot.Request.Middleware; - -namespace Ocelot.UnitTests.RateLimit -{ - public class ClientRateLimitMiddlewareTests : UnitTest - { - private readonly IRateLimitCounterHandler _rateLimitCounterHandler; - private readonly Mock _loggerFactory; - private readonly Mock _logger; - private readonly ClientRateLimitMiddleware _middleware; - private readonly RequestDelegate _next; - private DownstreamResponse _downstreamResponse; - private readonly string _url; - - public ClientRateLimitMiddlewareTests() - { - _url = "http://localhost:51879"; - var cacheEntryOptions = new MemoryCacheOptions(); - _rateLimitCounterHandler = new MemoryCacheRateLimitCounterHandler(new MemoryCache(cacheEntryOptions)); - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _next = context => Task.CompletedTask; - _middleware = new ClientRateLimitMiddleware(_next, _loggerFactory.Object, _rateLimitCounterHandler); - } - - [Fact] - public void should_call_middleware_and_ratelimiting() - { - var upstreamTemplate = new UpstreamPathTemplateBuilder().Build(); - - var downstreamRoute = new DownstreamRouteBuilder() - .WithEnableRateLimiting(true) - .WithRateLimitOptions(new RateLimitOptions(true, "ClientId", () => new List(), false, string.Empty, string.Empty, new RateLimitRule("1s", 100, 3), 429)) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(upstreamTemplate) - .Build(); - - var route = new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build(); - - var downstreamRouteHolder = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), route); - - this.Given(x => x.WhenICallTheMiddlewareMultipleTimes(2, downstreamRouteHolder)) - .Then(x => x.ThenThereIsNoDownstreamResponse()) - .When(x => x.WhenICallTheMiddlewareMultipleTimes(3, downstreamRouteHolder)) - .Then(x => x.ThenTheResponseIs429()) - .BDDfy(); - } - - [Fact] - public void should_call_middleware_withWhitelistClient() - { - var downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), - new RouteBuilder() - .WithDownstreamRoute(new DownstreamRouteBuilder() - .WithEnableRateLimiting(true) - .WithRateLimitOptions( - new RateLimitOptions(true, "ClientId", () => new List { "ocelotclient2" }, false, string.Empty, string.Empty, new RateLimitRule("1s", 100, 3), 429)) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); - - this.Given(x => x.WhenICallTheMiddlewareWithWhiteClient(downstreamRoute)) - .Then(x => x.ThenThereIsNoDownstreamResponse()) - .BDDfy(); - } - - private void WhenICallTheMiddlewareMultipleTimes(int times, Ocelot.DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute) - { - var httpContexts = new List(); - - for (var i = 0; i < times; i++) - { - var httpContext = new DefaultHttpContext - { - Response = - { - Body = new FakeStream(), - }, - }; - httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); - httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); - httpContext.Items.UpsertDownstreamRoute(downstreamRoute); - var clientId = "ocelotclient1"; - var request = new HttpRequestMessage(new HttpMethod("GET"), _url); - httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(request)); - httpContext.Request.Headers.TryAdd("ClientId", clientId); - httpContexts.Add(httpContext); - } - - foreach (var httpContext in httpContexts) - { - _middleware.Invoke(httpContext).GetAwaiter().GetResult(); - var ds = httpContext.Items.DownstreamResponse(); - _downstreamResponse = ds; - } - } - - private void WhenICallTheMiddlewareWithWhiteClient(Ocelot.DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute) - { - var clientId = "ocelotclient2"; - - for (var i = 0; i < 10; i++) - { - var httpContext = new DefaultHttpContext - { - Response = - { - Body = new FakeStream(), - }, - }; - httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); - httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); - httpContext.Items.UpsertDownstreamRoute(downstreamRoute); - var request = new HttpRequestMessage(new HttpMethod("GET"), _url); - request.Headers.Add("ClientId", clientId); - httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(request)); - httpContext.Request.Headers.TryAdd("ClientId", clientId); - _middleware.Invoke(httpContext).GetAwaiter().GetResult(); - var ds = httpContext.Items.DownstreamResponse(); - _downstreamResponse = ds; - } - } - - private void ThenTheResponseIs429() - { - var code = (int)_downstreamResponse.StatusCode; - code.ShouldBe(429); - } - - private void ThenThereIsNoDownstreamResponse() - { - _downstreamResponse.ShouldBeNull(); - } - } - - internal class FakeStream : Stream - { - public override void Flush() - { - //do nothing - //throw new System.NotImplementedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new System.NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new System.NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new System.NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - //do nothing - } - - public override bool CanRead { get; } - public override bool CanSeek { get; } - public override bool CanWrite => true; - public override long Length { get; } - public override long Position { get; set; } - } -} diff --git a/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs new file mode 100644 index 000000000..29c3b0dee --- /dev/null +++ b/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs @@ -0,0 +1,218 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.RateLimiting; +using Ocelot.RateLimiting.Middleware; +using Ocelot.Request.Middleware; +using System.Text; +using _DownstreamRouteHolder_ = Ocelot.DownstreamRouteFinder.DownstreamRouteHolder; +using _RateLimiting_ = Ocelot.RateLimiting.RateLimiting; + +namespace Ocelot.UnitTests.RateLimiting; + +public class RateLimitingMiddlewareTests : UnitTest +{ + private readonly IRateLimitStorage _storage; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private readonly RateLimitingMiddleware _middleware; + private readonly RequestDelegate _next; + private readonly IRateLimiting _rateLimiting; + private readonly List _downstreamResponses; + private readonly string _url; + + public RateLimitingMiddlewareTests() + { + _url = "http://localhost:51879"; + var cacheEntryOptions = new MemoryCacheOptions(); + _storage = new MemoryCacheRateLimitStorage(new MemoryCache(cacheEntryOptions)); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _next = context => Task.CompletedTask; + _rateLimiting = new _RateLimiting_(_storage); + _middleware = new RateLimitingMiddleware(_next, _loggerFactory.Object, _rateLimiting); + _downstreamResponses = new(); + } + + [Fact] + [Trait("Feat", "37")] + public async Task Should_call_middleware_and_ratelimiting() + { + // Arrange + const long limit = 3L; + var upstreamTemplate = new UpstreamPathTemplateBuilder() + .Build(); + var downstreamRoute = new DownstreamRouteBuilder() + .WithEnableRateLimiting(true) + .WithRateLimitOptions(new( + enableRateLimiting: true, + clientIdHeader: "ClientId", + getClientWhitelist: () => new List(), + disableRateLimitHeaders: false, + quotaExceededMessage: "Exceeding!", + rateLimitCounterPrefix: string.Empty, + new RateLimitRule("1s", 100.0D, limit), + (int)HttpStatusCode.TooManyRequests)) + .WithUpstreamHttpMethod(new() { "Get" }) + .WithUpstreamPathTemplate(upstreamTemplate) + .Build(); + var route = new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(new() { "Get" }) + .Build(); + var downstreamRouteHolder = new _DownstreamRouteHolder_(new(), route); + + // Act, Assert + await WhenICallTheMiddlewareMultipleTimes(limit, downstreamRouteHolder); + _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull()); + + // Act, Assert: the next request should fail + await WhenICallTheMiddlewareMultipleTimes(3, downstreamRouteHolder); + _downstreamResponses.ShouldNotBeNull(); + for (int i = 0; i < _downstreamResponses.Count; i++) + { + var response = _downstreamResponses[i].ShouldNotBeNull(); + response.StatusCode.ShouldBe(HttpStatusCode.TooManyRequests, $"Downstream Response no is {i}"); + var body = await response.Content.ReadAsStringAsync(); + body.ShouldBe("Exceeding!"); + } + } + + [Fact] + [Trait("Feat", "37")] + public async Task Should_call_middleware_withWhitelistClient() + { + // Arrange + var route = new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithEnableRateLimiting(true) + .WithRateLimitOptions(new( + enableRateLimiting: true, + clientIdHeader: "ClientId", + getClientWhitelist: () => new List { "ocelotclient2" }, + disableRateLimitHeaders: false, + quotaExceededMessage: "Exceeding!", + rateLimitCounterPrefix: string.Empty, + new RateLimitRule("1s", 100.0D, 3), + (int)HttpStatusCode.TooManyRequests)) + .WithUpstreamHttpMethod(new() { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new() { "Get" }) + .Build(); + var downstreamRoute = new _DownstreamRouteHolder_(new(), route); + + // Act + await WhenICallTheMiddlewareWithWhiteClient(downstreamRoute); + + // Assert + _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull()); + } + + [Fact] + [Trait("Bug", "1590")] + public async Task MiddlewareInvoke_PeriodTimespanValueIsGreaterThanPeriod_StatusNotEqualTo429() + { + // Arrange + const long limit = 100L; + var upstreamTemplate = new UpstreamPathTemplateBuilder() + .Build(); + var downstreamRoute = new DownstreamRouteBuilder() + .WithEnableRateLimiting(true) + .WithRateLimitOptions(new( + enableRateLimiting: true, + clientIdHeader: "ClientId", + getClientWhitelist: () => new List(), + disableRateLimitHeaders: false, + quotaExceededMessage: "Exceeding!", + rateLimitCounterPrefix: string.Empty, + new RateLimitRule("1s", 30.0D, limit), // bug scenario + (int)HttpStatusCode.TooManyRequests)) + .WithUpstreamHttpMethod(new() { "Get" }) + .WithUpstreamPathTemplate(upstreamTemplate) + .Build(); + var route = new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(new() { "Get" }) + .Build(); + var downstreamRouteHolder = new _DownstreamRouteHolder_(new(), route); + + // Act, Assert: 100 requests must be successful + var contexts = await WhenICallTheMiddlewareMultipleTimes(limit, downstreamRouteHolder); // make 100 requests, but not exceed the limit + _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull()); + contexts.ForEach(ctx => + { + ctx.ShouldNotBeNull(); + ctx.Items.Errors().ShouldNotBeNull().ShouldBeEmpty(); // no errors + ctx.Response.StatusCode.ShouldBe((int)HttpStatusCode.OK); // not 429 aka TooManyRequests + }); + + // Act, Assert: the next 101st request should fail + contexts = await WhenICallTheMiddlewareMultipleTimes(1, downstreamRouteHolder); + _downstreamResponses.ShouldNotBeNull(); + var ds = _downstreamResponses.SingleOrDefault().ShouldNotBeNull(); + ds.StatusCode.ShouldBe(HttpStatusCode.TooManyRequests, $"Downstream Response no {limit + 1}"); + var body = await ds.Content.ReadAsStringAsync(); + body.ShouldBe("Exceeding!"); + contexts[0].Items.Errors().ShouldNotBeNull().ShouldNotBeEmpty(); // having errors + contexts[0].Items.Errors().Single().HttpStatusCode.ShouldBe((int)HttpStatusCode.TooManyRequests); + } + + private async Task> WhenICallTheMiddlewareMultipleTimes(long times, _DownstreamRouteHolder_ downstreamRoute) + { + var contexts = new List(); + _downstreamResponses.Clear(); + for (var i = 0; i < times; i++) + { + var context = new DefaultHttpContext(); + var stream = GetFakeStream($"{i}"); + context.Response.Body = stream; + context.Response.RegisterForDispose(stream); + context.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); + context.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + context.Items.UpsertDownstreamRoute(downstreamRoute); + var request = new HttpRequestMessage(new HttpMethod("GET"), _url); + context.Items.UpsertDownstreamRequest(new DownstreamRequest(request)); + context.Request.Headers.TryAdd("ClientId", "ocelotclient1"); + contexts.Add(context); + + await _middleware.Invoke(context); + + _downstreamResponses.Add(context.Items.DownstreamResponse()); + } + + return contexts; + } + + private static Stream GetFakeStream(string str) + { + byte[] data = Encoding.ASCII.GetBytes(str); + return new MemoryStream(data, 0, data.Length); + } + + private async Task WhenICallTheMiddlewareWithWhiteClient(_DownstreamRouteHolder_ downstreamRoute) + { + const string ClientId = "ocelotclient2"; + for (var i = 0; i < 10; i++) + { + var context = new DefaultHttpContext(); + var stream = GetFakeStream($"{i}"); + context.Response.Body = stream; + context.Response.RegisterForDispose(stream); + context.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); + context.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + context.Items.UpsertDownstreamRoute(downstreamRoute); + var request = new HttpRequestMessage(new HttpMethod("GET"), _url); + request.Headers.Add("ClientId", ClientId); + context.Items.UpsertDownstreamRequest(new DownstreamRequest(request)); + context.Request.Headers.TryAdd("ClientId", ClientId); + + await _middleware.Invoke(context); + + _downstreamResponses.Add(context.Items.DownstreamResponse()); + } + } +} diff --git a/test/Ocelot.UnitTests/RateLimiting/RateLimitingTests.cs b/test/Ocelot.UnitTests/RateLimiting/RateLimitingTests.cs new file mode 100644 index 000000000..a4eb4738e --- /dev/null +++ b/test/Ocelot.UnitTests/RateLimiting/RateLimitingTests.cs @@ -0,0 +1,268 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.RateLimiting; +using System.Runtime.CompilerServices; +using _RateLimiting_ = Ocelot.RateLimiting.RateLimiting; + +namespace Ocelot.UnitTests.RateLimiting; + +public sealed class RateLimitingTests +{ + private readonly Mock _storage; + private readonly _RateLimiting_ _sut; + + public RateLimitingTests() + { + _storage = new(); + _sut = new(_storage.Object); + } + + [Theory] + [Trait("Feat", "37")] + [InlineData(null)] + [InlineData("")] + public void ToTimespan_EmptyValue_ShouldReturnZero(string empty) + { + // Arrange, Act + var actual = _sut.ToTimespan(empty); + + // Assert + Assert.Equal(TimeSpan.Zero, actual); + } + + [Theory] + [Trait("Feat", "37")] + [InlineData("1a")] + [InlineData("2unknown")] + public void ToTimespan_UnknownType_ShouldThrowFormatException(string timespan) + { + // Arrange, Act, Assert + Assert.Throws( + () => _sut.ToTimespan(timespan)); + } + + [Theory] + [Trait("Feat", "37")] + [InlineData("1s", 1 * TimeSpan.TicksPerSecond)] + [InlineData("2m", 2 * TimeSpan.TicksPerMinute)] + [InlineData("3h", 3 * TimeSpan.TicksPerHour)] + [InlineData("4d", 4 * TimeSpan.TicksPerDay)] + public void ToTimespan_KnownType_HappyPath(string timespan, long ticks) + { + // Arrange + var expected = TimeSpan.FromTicks(ticks); + + // Act + var actual = _sut.ToTimespan(timespan); + + // Assert + Assert.Equal(expected, actual); + } + + [Fact] + [Trait("PR", "1592")] + public void Count_NoEntry_StartCounting() + { + // Arrange + RateLimitCounter? arg1 = null; // No Entry + RateLimitRule arg2 = null; + + // Act + RateLimitCounter actual = _sut.Count(arg1, arg2); + + // Assert + Assert.Equal(1L, actual.TotalRequests); + Assert.True(DateTime.UtcNow - actual.StartedAt < TimeSpan.FromSeconds(1.0D)); + } + + [Fact] + [Trait("PR", "1592")] + public void Count_EntryHasNotExpired_IncrementedRequestCount() + { + // Arrange + long total = 2; + RateLimitCounter? arg1 = new RateLimitCounter(DateTime.UtcNow, null, total); // entry has not expired + RateLimitRule arg2 = new("1s", 1.0D, total + 1); // with not exceeding limit + + // Act + RateLimitCounter actual = _sut.Count(arg1, arg2); + + // Assert + Assert.Equal(total + 1, actual.TotalRequests); // incremented request count + Assert.Equal(arg1.Value.StartedAt, actual.StartedAt); // starting point has not changed + } + + [Fact] + [Trait("PR", "1592")] + public void Count_EntryHasNotExpiredAndExceedingLimit_IncrementedRequestCountWithRenewedStartMoment() + { + // Arrange + long total = 2; + RateLimitCounter? arg1 = new RateLimitCounter(DateTime.UtcNow, null, total); // entry has not expired + RateLimitRule arg2 = new("1s", 1.0D, 1L); + + // Act + RateLimitCounter actual = _sut.Count(arg1, arg2); + + // Assert + Assert.Equal(total + 1, actual.TotalRequests); // incremented request count + Assert.InRange(actual.StartedAt, arg1.Value.StartedAt, DateTime.UtcNow); // starting point has renewed and it is between StartedAt and Now + } + + [Fact] + [Trait("PR", "1592")] + public void Count_RateLimitExceeded_StartedCounting() + { + // Arrange + long total = 3, limit = total - 1; + TimeSpan periodTimespan = TimeSpan.FromSeconds(1.0D); + DateTime startedAt = DateTime.UtcNow.AddSeconds(-2.0), // 2 secs ago + exceededAt = startedAt + periodTimespan; // 1 second ago + RateLimitCounter? arg1 = new RateLimitCounter(startedAt, exceededAt, total); // Entry has expired + RateLimitRule arg2 = new("1s", periodTimespan.TotalSeconds, limit); // rate limit exceeded + + // Act + RateLimitCounter actual = _sut.Count(arg1, arg2); + + // Assert + Assert.Equal(1L, actual.TotalRequests); // started counting, the counter was changed + Assert.InRange(actual.StartedAt, arg1.Value.ExceededAt.Value, DateTime.UtcNow); // starting point has renewed and it is between exceededAt and Now + } + + [Fact] + [Trait("PR", "1592")] + public void Count_RateLimitNotExceededAndPeriodIsElapsed_StartedCountingByDefault() + { + // Arrange + long total = 3, limit = 3; + RateLimitCounter? arg1 = new RateLimitCounter(DateTime.UtcNow.AddSeconds(-2.0), null, total); // Entry has expired + RateLimitRule arg2 = new("1s", 1.0D, limit); // Rate limit not exceeded + + // Act + RateLimitCounter actual = _sut.Count(arg1, arg2); + + // Assert + Assert.Equal(1L, actual.TotalRequests); // started counting + Assert.True(DateTime.UtcNow - actual.StartedAt < TimeSpan.FromSeconds(1.0D)); // started now + } + + [Fact] + [Trait("PR", "1592")] + public void ProcessRequest_RateLimitExceededAndBanPeriodElapsed_StartedCounting() + { + // Arrange + const double periodTimespan = 2.0D; + const int millisecondsBeforeAfterEnding = 100; // current processing time of unit test should not take more 100 ms + DateTime now = DateTime.UtcNow, + startedAt = now.AddSeconds(-3).AddMilliseconds(millisecondsBeforeAfterEnding); + DateTime? exceededAt = null; + long totalRequests = 2L; + TimeSpan expiration = TimeSpan.Zero; + + var (identity, options) = SetupProcessRequest("3s", periodTimespan, totalRequests, + () => new RateLimitCounter(startedAt, exceededAt, totalRequests), + (value) => expiration = value); + + // Act 1 + var counter = _sut.ProcessRequest(identity, options); + + // Assert 1 + Assert.Equal(3L, counter.TotalRequests); // old counting -> 3 + Assert.Equal(startedAt, counter.StartedAt); // starting point was not changed + Assert.NotNull(counter.ExceededAt); // exceeded + Assert.Equal(DateTime.UtcNow.Second, counter.ExceededAt.Value.Second); // exceeded now, in the same second + + // Arrange 2 + TimeSpan shift = TimeSpan.FromSeconds(periodTimespan); // don't wait, just move to future + startedAt = counter.StartedAt - shift; // move to past + exceededAt = counter.ExceededAt - shift; // move to past + totalRequests = counter.TotalRequests; // 3 + + // Act 2 + var actual = _sut.ProcessRequest(identity, options); + + // Assert + Assert.Equal(1L, actual.TotalRequests); // started counting + Assert.InRange(actual.StartedAt, now, DateTime.UtcNow); // starting point has renewed and it is between test starting and Now + Assert.Null(actual.ExceededAt); + _storage.Verify(x => x.Remove(It.IsAny()), + Times.Never()); // Once()? Seems Remove is never called because of renewing + _storage.Verify(x => x.Get(It.IsAny()), + Times.Exactly(2)); + _storage.Verify(x => x.Set(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Exactly(2)); + Assert.Equal(TimeSpan.FromSeconds(3), expiration); + } + + private (ClientRequestIdentity Identity, RateLimitOptions Options) SetupProcessRequest(string period, double periodTimespan, long limit, + Func counterFactory, Action expirationAction, [CallerMemberName] string testName = "") + { + ClientRequestIdentity identity = new(nameof(RateLimitingTests), "/" + testName, HttpMethods.Get); + RateLimitOptions options = new RateLimitOptionsBuilder() + .WithEnableRateLimiting(true) + .WithRateLimitCounterPrefix(nameof(_RateLimiting_.ProcessRequest)) + .WithRateLimitRule(new RateLimitRule(period, periodTimespan, limit)) + .Build(); + _storage.Setup(x => x.Get(It.IsAny())) + .Returns(counterFactory); // counter value factory + _storage.Setup(x => x.Remove(It.IsAny())) + .Verifiable(); + expirationAction?.Invoke(TimeSpan.Zero); + _storage.Setup(x => x.Set(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((id, counter, expirationTime) => expirationAction?.Invoke(expirationTime)) + .Verifiable(); + return (identity, options); + } + + [Fact] + [Trait("Bug", "1590")] + public void ProcessRequest_PeriodTimespanValueIsGreaterThanPeriod_ExpectedBehaviorAndExpirationInPeriod() + { + // Arrange: user scenario + const string period = "1s"; + const double periodTimespan = 30.0D; // seconds + const long limit = 100L, requestsPerSecond = 20L; + + // Arrange: setup + DateTime? startedAt = null; + TimeSpan expiration = TimeSpan.Zero; + long total = 1L, count = requestsPerSecond; + RateLimitCounter? current = null; + var (identity, options) = SetupProcessRequest(period, periodTimespan, limit, + () => current, + (value) => expiration = value); + + // Arrange 20 requests per period (1 sec) + var periodSeconds = TimeSpan.FromSeconds(double.Parse(period[0].ToString())); + var periodMilliseconds = periodSeconds.TotalMilliseconds; + int delay = (int)((periodMilliseconds - 200) / requestsPerSecond); // 20 requests per 1 second + + while (count > 0L) + { + // Act + var actual = _sut.ProcessRequest(identity, options); + + // life hack for the 1st request + if (count == requestsPerSecond) + { + startedAt = actual.StartedAt; // for the 1st request get expected value + } + + // Assert + Assert.True(actual.TotalRequests < limit); + actual.TotalRequests.ShouldBe(total++, $"Count is {count}"); + Assert.Equal(startedAt, actual.StartedAt); // starting point is not changed + Assert.Null(actual.ExceededAt); // no exceeding at all + Assert.Equal(periodSeconds, expiration); // expiration in the period + + // Arrange: next micro test + current = actual; + Thread.Sleep(delay); + count--; + } + + Assert.NotEqual(TimeSpan.FromSeconds(periodTimespan), expiration); // Not ban period expiration + Assert.Equal(periodSeconds, expiration); // last 20th request was in counting period + } +} From 6e9a975fae3b26229c3f0b3c1a475a7b05e95fee Mon Sep 17 00:00:00 2001 From: Thiago Loureiro Date: Mon, 13 May 2024 21:03:44 +0200 Subject: [PATCH 06/15] #2054 #2059 Manage `EnableContentHashing` setting by global `CacheOptions` (#2058) * EnableContentHashing not being considered from appsettings * Adding CacheOptionsCreator, Injected IRegionCreator as Singleton. Should still add some acceptance tests that are definitely missing! * Adding caching global configuration since we messed up, ignoring an important breaking change with EnableContentHashing set to false by default * Adding some further acceptance tests, validating EnableContentHashing, validating global config too. * removing some debug content * TtlSeconds must be set * updating documentation * Update docs/features/caching.rst Co-authored-by: Raman Maksimchuk * Update docs/features/caching.rst Co-authored-by: Raman Maksimchuk * Removing RegionCreator, moving service collection extension method to dependencyInjection\Features etc. * adding unit tests for FileCacheOptions * some more null tests... * slight refactoring, updating ICacheOptionsCreator signature * some more design refactoring * Update src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs Co-authored-by: Raman Maksimchuk * Code review by @raman-m * Rename `FileCacheOptions` -> `CacheOptions` * Subtly transition to `CacheOptions`, ensuring compatibility with `FileCacheOptions` to avoid a breaking change * Not obsolete --------- Co-authored-by: Guillaume Gnaegi <58469901+ggnaegi@users.noreply.github.com> Co-authored-by: Raman Maksimchuk --- docs/features/caching.rst | 36 ++++- ...spMemoryCache.cs => DefaultMemoryCache.cs} | 4 +- src/Ocelot/Cache/IRegionCreator.cs | 9 -- src/Ocelot/Cache/RegionCreator.cs | 21 --- .../Builder/DownstreamRouteBuilder.cs | 8 +- src/Ocelot/Configuration/CacheOptions.cs | 71 +++++----- .../Creator/CacheOptionsCreator.cs | 27 ++++ .../Creator/ICacheOptionsCreator.cs | 19 +++ .../Creator/RouteOptionsCreator.cs | 2 + .../Configuration/Creator/RoutesCreator.cs | 18 ++- .../Configuration/File/FileCacheOptions.cs | 41 +++--- .../File/FileGlobalConfiguration.cs | 3 + src/Ocelot/Configuration/File/FileRoute.cs | 4 +- src/Ocelot/DependencyInjection/Features.cs | 20 +++ .../DependencyInjection/OcelotBuilder.cs | 7 +- .../Caching/CachingTests.cs | 131 +++++++++++++++++- .../AdministrationTests.cs | 2 +- ...orTests.cs => CacheOptionsCreatorTests.cs} | 117 ++++++++-------- .../Cache/DefaultCacheKeyGeneratorTests.cs | 2 +- ...cheTests.cs => DefaultMemoryCacheTests.cs} | 8 +- .../Cache/OutputCacheMiddlewareTests.cs | 2 +- .../OutputCacheMiddlewareRealCacheTests.cs | 2 +- .../Configuration/CacheOptionsCreatorTests.cs | 100 +++++++++++++ .../Configuration/RoutesCreatorTests.cs | 22 +-- 24 files changed, 490 insertions(+), 186 deletions(-) rename src/Ocelot/Cache/{AspMemoryCache.cs => DefaultMemoryCache.cs} (94%) delete mode 100644 src/Ocelot/Cache/IRegionCreator.cs delete mode 100644 src/Ocelot/Cache/RegionCreator.cs create mode 100644 src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs create mode 100644 src/Ocelot/Configuration/Creator/ICacheOptionsCreator.cs rename test/Ocelot.UnitTests/Cache/{RegionCreatorTests.cs => CacheOptionsCreatorTests.cs} (73%) rename test/Ocelot.UnitTests/Cache/{AspMemoryCacheTests.cs => DefaultMemoryCacheTests.cs} (90%) create mode 100644 test/Ocelot.UnitTests/Configuration/CacheOptionsCreatorTests.cs diff --git a/docs/features/caching.rst b/docs/features/caching.rst index f2bf302ed..84803060f 100644 --- a/docs/features/caching.rst +++ b/docs/features/caching.rst @@ -39,7 +39,8 @@ Finally, in order to use caching on a route in your Route configuration add this "FileCacheOptions": { "TtlSeconds": 15, "Region": "europe-central", - "Header": "Authorization" + "Header": "OC-Caching-Control", + "EnableContentHashing": false // my route has GET verb only, assigning 'true' for requests with body: POST, PUT etc. } In this example **TtlSeconds** is set to 15 which means the cache will expire after 15 seconds. @@ -48,10 +49,38 @@ The **Region** represents a region of caching. Additionally, if a header name is defined in the **Header** property, that header value is looked up by the key (header name) in the ``HttpRequest`` headers, and if the header is found, its value will be included in caching key. This causes the cache to become invalid due to the header value changing. +``EnableContentHashing`` option +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In version `23.0`_, the new property **EnableContentHashing** has been introduced. Previously, the request body was utilized to compute the cache key. +However, due to potential performance issues arising from request body hashing, it has been disabled by default. +Clearly, this constitutes a breaking change and presents challenges for users who require cache key calculations that consider the request body (e.g., for the POST method). +To address this issue, it is recommended to enable the option either at the route level or globally in the :ref:`cch-global-configuration` section: + +.. code-block:: json + + "CacheOptions": { + // ... + "EnableContentHashing": true + } + +.. _cch-global-configuration: + +Global Configuration +-------------------- + +The positive update is that copying Route-level properties for each route is no longer necessary, as version `23.3`_ allows for setting their values in the ``GlobalConfiguration``. +This convenience extends to **Header** and **Region** as well. +However, an alternative is still being sought for **TtlSeconds**, which must be explicitly set for each route to enable caching. + +Notes +----- + If you look at the example `here `_ you can see how the cache manager is setup and then passed into the Ocelot ``AddCacheManager`` configuration method. You can use any settings supported by the **CacheManager** package and just pass them in. -Anyway, Ocelot currently supports caching on the URL of the downstream service and setting a TTL in seconds to expire the cache. You can also clear the cache for a region by calling Ocelot's administration API. +Anyway, Ocelot currently supports caching on the URL of the downstream service and setting a TTL in seconds to expire the cache. +You can also clear the cache for a region by calling Ocelot's administration API. Your Own Caching ---------------- @@ -68,3 +97,6 @@ If you want to add your own caching method, implement the following interfaces a Please dig into the Ocelot source code to find more. We would really appreciate it if anyone wants to implement `Redis `_, `Memcached `_ etc. Please, open a new `Show and tell `_ thread in `Discussions `_ space of the repository. + +.. _23.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.0.0 +.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 diff --git a/src/Ocelot/Cache/AspMemoryCache.cs b/src/Ocelot/Cache/DefaultMemoryCache.cs similarity index 94% rename from src/Ocelot/Cache/AspMemoryCache.cs rename to src/Ocelot/Cache/DefaultMemoryCache.cs index 2067b3813..f53b6ceaf 100644 --- a/src/Ocelot/Cache/AspMemoryCache.cs +++ b/src/Ocelot/Cache/DefaultMemoryCache.cs @@ -2,12 +2,12 @@ namespace Ocelot.Cache { - public class AspMemoryCache : IOcelotCache + public class DefaultMemoryCache : IOcelotCache { private readonly IMemoryCache _memoryCache; private readonly Dictionary> _regions; - public AspMemoryCache(IMemoryCache memoryCache) + public DefaultMemoryCache(IMemoryCache memoryCache) { _memoryCache = memoryCache; _regions = new Dictionary>(); diff --git a/src/Ocelot/Cache/IRegionCreator.cs b/src/Ocelot/Cache/IRegionCreator.cs deleted file mode 100644 index da1b042da..000000000 --- a/src/Ocelot/Cache/IRegionCreator.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Ocelot.Configuration.File; - -namespace Ocelot.Cache -{ - public interface IRegionCreator - { - string Create(FileRoute route); - } -} diff --git a/src/Ocelot/Cache/RegionCreator.cs b/src/Ocelot/Cache/RegionCreator.cs deleted file mode 100644 index c2dd0ccac..000000000 --- a/src/Ocelot/Cache/RegionCreator.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Ocelot.Configuration.File; - -namespace Ocelot.Cache -{ - public class RegionCreator : IRegionCreator - { - public string Create(FileRoute route) - { - if (!string.IsNullOrEmpty(route?.FileCacheOptions?.Region)) - { - return route?.FileCacheOptions?.Region; - } - - var methods = string.Join(string.Empty, route.UpstreamHttpMethod.Select(m => m)); - - var region = $"{methods}{route.UpstreamPathTemplate.Replace("/", string.Empty)}"; - - return region; - } - } -} diff --git a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs index 9dc93008e..281e93033 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs @@ -19,7 +19,7 @@ public class DownstreamRouteBuilder private List _claimToDownstreamPath; private string _requestIdHeaderKey; private bool _isCached; - private CacheOptions _fileCacheOptions; + private CacheOptions _cacheOptions; private string _downstreamScheme; private LoadBalancerOptions _loadBalancerOptions; private QoSOptions _qosOptions; @@ -87,7 +87,7 @@ public DownstreamRouteBuilder WithUpstreamPathTemplate(UpstreamPathTemplate inpu public DownstreamRouteBuilder WithUpstreamHttpMethod(List input) { - _upstreamHttpMethod = (input.Count == 0) ? new List() : input.Select(x => new HttpMethod(x.Trim())).ToList(); + _upstreamHttpMethod = input.Count == 0 ? new List() : input.Select(x => new HttpMethod(x.Trim())).ToList(); return this; } @@ -147,7 +147,7 @@ public DownstreamRouteBuilder WithIsCached(bool input) public DownstreamRouteBuilder WithCacheOptions(CacheOptions input) { - _fileCacheOptions = input; + _cacheOptions = input; return this; } @@ -276,7 +276,7 @@ public DownstreamRoute Build() _downstreamScheme, _requestIdHeaderKey, _isCached, - _fileCacheOptions, + _cacheOptions, _loadBalancerOptions, _rateLimitOptions, _routeClaimRequirement, diff --git a/src/Ocelot/Configuration/CacheOptions.cs b/src/Ocelot/Configuration/CacheOptions.cs index 352b501d8..dc3c19116 100644 --- a/src/Ocelot/Configuration/CacheOptions.cs +++ b/src/Ocelot/Configuration/CacheOptions.cs @@ -1,39 +1,42 @@ using Ocelot.Request.Middleware; -namespace Ocelot.Configuration -{ - public class CacheOptions - { - internal CacheOptions() { } +namespace Ocelot.Configuration; - public CacheOptions(int ttlSeconds, string region, string header) - { - TtlSeconds = ttlSeconds; - Region = region; - Header = header; - } - - public CacheOptions(int ttlSeconds, string region, string header, bool enableContentHashing) - { - TtlSeconds = ttlSeconds; - Region = region; - Header = header; - EnableContentHashing = enableContentHashing; - } +public class CacheOptions +{ + internal CacheOptions() { } - public int TtlSeconds { get; } - public string Region { get; } - public string Header { get; } - - /// - /// Enables MD5 hash calculation of the of the object. - /// - /// - /// Default value is . No hashing by default. - /// - /// - /// if hashing is enabled, otherwise it is . - /// - public bool EnableContentHashing { get; } + /// + /// Initializes a new instance of the class. + /// + /// + /// Internal defaults: + /// + /// The default value for is , but it is set to null for route-level configuration to allow global configuration usage. + /// The default value for is 0. + /// + /// + /// Time-to-live seconds. If not speciefied, zero value is used by default. + /// The region of caching. + /// The header name to control cached value. + /// The switcher for content hashing. If not speciefied, false value is used by default. + public CacheOptions(int? ttlSeconds, string region, string header, bool? enableContentHashing) + { + TtlSeconds = ttlSeconds ?? 0; + Region = region; + Header = header; + EnableContentHashing = enableContentHashing ?? false; } -} + + /// Time-to-live seconds. + /// Default value is 0. No caching by default. + /// An value of seconds. + public int TtlSeconds { get; } + public string Region { get; } + public string Header { get; } + + /// Enables MD5 hash calculation of the of the object. + /// Default value is . No hashing by default. + /// if hashing is enabled, otherwise it is . + public bool EnableContentHashing { get; } +} diff --git a/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs b/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs new file mode 100644 index 000000000..6d1c8f2b4 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs @@ -0,0 +1,27 @@ +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator; + +public class CacheOptionsCreator : ICacheOptionsCreator +{ + public CacheOptions Create(FileCacheOptions options, FileGlobalConfiguration global, string upstreamPathTemplate, IList upstreamHttpMethods) + { + var region = GetRegion(options.Region ?? global?.CacheOptions.Region, upstreamPathTemplate, upstreamHttpMethods); + var header = options.Header ?? global?.CacheOptions.Header; + var ttlSeconds = options.TtlSeconds ?? global?.CacheOptions.TtlSeconds; + var enableContentHashing = options.EnableContentHashing ?? global?.CacheOptions.EnableContentHashing; + + return new CacheOptions(ttlSeconds, region, header, enableContentHashing); + } + + protected virtual string GetRegion(string region, string upstreamPathTemplate, IList upstreamHttpMethod) + { + if (!string.IsNullOrEmpty(region)) + { + return region; + } + + var methods = string.Join(string.Empty, upstreamHttpMethod); + return $"{methods}{upstreamPathTemplate.Replace("/", string.Empty)}"; + } +} diff --git a/src/Ocelot/Configuration/Creator/ICacheOptionsCreator.cs b/src/Ocelot/Configuration/Creator/ICacheOptionsCreator.cs new file mode 100644 index 000000000..a76a1b20e --- /dev/null +++ b/src/Ocelot/Configuration/Creator/ICacheOptionsCreator.cs @@ -0,0 +1,19 @@ +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator; + +/// +/// This interface is used to create cache options. +/// +public interface ICacheOptionsCreator +{ + /// + /// Creates cache options based on the file cache options, upstream path template and upstream HTTP methods. + /// Upstream path template and upstream HTTP methods are used to get the region name. + /// The file cache options. + /// The global configuration. + /// The upstream path template as string. + /// The upstream http methods as a list of strings. + /// The generated cache options. + CacheOptions Create(FileCacheOptions options, FileGlobalConfiguration global, string upstreamPathTemplate, IList upstreamHttpMethods); +} diff --git a/src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs b/src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs index 8e0911e56..8b6f0e3ea 100644 --- a/src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs @@ -17,6 +17,8 @@ public RouteOptions Create(FileRoute fileRoute) && (!string.IsNullOrEmpty(authOpts.AuthenticationProviderKey) || authOpts.AuthenticationProviderKeys?.Any(k => !string.IsNullOrWhiteSpace(k)) == true); var isAuthorized = fileRoute.RouteClaimsRequirement?.Any() == true; + + // TODO: This sounds more like a hack, it might be better to refactor this at some point. var isCached = fileRoute.FileCacheOptions.TtlSeconds > 0; var enableRateLimiting = fileRoute.RateLimitOptions?.EnableRateLimiting == true; var useServiceDiscovery = !string.IsNullOrEmpty(fileRoute.ServiceName); diff --git a/src/Ocelot/Configuration/Creator/RoutesCreator.cs b/src/Ocelot/Configuration/Creator/RoutesCreator.cs index 8c1f1de63..19bce7b17 100644 --- a/src/Ocelot/Configuration/Creator/RoutesCreator.cs +++ b/src/Ocelot/Configuration/Creator/RoutesCreator.cs @@ -1,5 +1,4 @@ -using Ocelot.Cache; -using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator @@ -14,7 +13,7 @@ public class RoutesCreator : IRoutesCreator private readonly IQoSOptionsCreator _qosOptionsCreator; private readonly IRouteOptionsCreator _fileRouteOptionsCreator; private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; - private readonly IRegionCreator _regionCreator; + private readonly ICacheOptionsCreator _cacheOptionsCreator; private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; private readonly IHeaderFindAndReplaceCreator _headerFAndRCreator; private readonly IDownstreamAddressesCreator _downstreamAddressesCreator; @@ -30,21 +29,20 @@ public RoutesCreator( IQoSOptionsCreator qosOptionsCreator, IRouteOptionsCreator fileRouteOptionsCreator, IRateLimitOptionsCreator rateLimitOptionsCreator, - IRegionCreator regionCreator, + ICacheOptionsCreator cacheOptionsCreator, IHttpHandlerOptionsCreator httpHandlerOptionsCreator, IHeaderFindAndReplaceCreator headerFAndRCreator, IDownstreamAddressesCreator downstreamAddressesCreator, ILoadBalancerOptionsCreator loadBalancerOptionsCreator, IRouteKeyCreator routeKeyCreator, ISecurityOptionsCreator securityOptionsCreator, - IVersionCreator versionCreator - ) + IVersionCreator versionCreator) { _routeKeyCreator = routeKeyCreator; _loadBalancerOptionsCreator = loadBalancerOptionsCreator; _downstreamAddressesCreator = downstreamAddressesCreator; _headerFAndRCreator = headerFAndRCreator; - _regionCreator = regionCreator; + _cacheOptionsCreator = cacheOptionsCreator; _rateLimitOptionsCreator = rateLimitOptionsCreator; _requestIdKeyCreator = requestIdKeyCreator; _upstreamTemplatePatternCreator = upstreamTemplatePatternCreator; @@ -93,8 +91,6 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf var rateLimitOption = _rateLimitOptionsCreator.Create(fileRoute.RateLimitOptions, globalConfiguration); - var region = _regionCreator.Create(fileRoute); - var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileRoute.HttpHandlerOptions); var hAndRs = _headerFAndRCreator.Create(fileRoute); @@ -107,6 +103,8 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf var downstreamHttpVersion = _versionCreator.Create(fileRoute.DownstreamHttpVersion); + var cacheOptions = _cacheOptionsCreator.Create(fileRoute.FileCacheOptions, globalConfiguration, fileRoute.UpstreamPathTemplate, fileRoute.UpstreamHttpMethod); + var route = new DownstreamRouteBuilder() .WithKey(fileRoute.Key) .WithDownstreamPathTemplate(fileRoute.DownstreamPathTemplate) @@ -122,7 +120,7 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf .WithClaimsToDownstreamPath(claimsToDownstreamPath) .WithRequestIdKey(requestIdKey) .WithIsCached(fileRouteOptions.IsCached) - .WithCacheOptions(new CacheOptions(fileRoute.FileCacheOptions.TtlSeconds, region, fileRoute.FileCacheOptions.Header)) + .WithCacheOptions(cacheOptions) .WithDownstreamScheme(fileRoute.DownstreamScheme) .WithLoadBalancerOptions(lbOptions) .WithDownstreamAddresses(downstreamAddresses) diff --git a/src/Ocelot/Configuration/File/FileCacheOptions.cs b/src/Ocelot/Configuration/File/FileCacheOptions.cs index a1b1deed5..42b793390 100644 --- a/src/Ocelot/Configuration/File/FileCacheOptions.cs +++ b/src/Ocelot/Configuration/File/FileCacheOptions.cs @@ -1,21 +1,26 @@ -namespace Ocelot.Configuration.File -{ - public class FileCacheOptions - { - public FileCacheOptions() - { - Region = string.Empty; - TtlSeconds = 0; - } +namespace Ocelot.Configuration.File; - public FileCacheOptions(FileCacheOptions from) - { - Region = from.Region; - TtlSeconds = from.TtlSeconds; - } +public class FileCacheOptions +{ + public FileCacheOptions() { } - public int TtlSeconds { get; set; } - public string Region { get; set; } - public string Header { get; set; } - } + public FileCacheOptions(FileCacheOptions from) + { + Region = from.Region; + TtlSeconds = from.TtlSeconds; + Header = from.Header; + EnableContentHashing = from.EnableContentHashing; + } + + /// Using where T is to have as default value and allowing global configuration usage. + /// If then use global configuration with 0 by default. + /// The time to live seconds, with 0 by default. + public int? TtlSeconds { get; set; } + public string Region { get; set; } + public string Header { get; set; } + + /// Using where T is to have as default value and allowing global configuration usage. + /// If then use global configuration with by default. + /// if content hashing is enabled; otherwise, . + public bool? EnableContentHashing { get; set; } } diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs index 6692ba046..04497cca1 100644 --- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -9,6 +9,7 @@ public FileGlobalConfiguration() LoadBalancerOptions = new FileLoadBalancerOptions(); QoSOptions = new FileQoSOptions(); HttpHandlerOptions = new FileHttpHandlerOptions(); + CacheOptions = new FileCacheOptions(); } public string RequestIdKey { get; set; } @@ -28,5 +29,7 @@ public FileGlobalConfiguration() public FileHttpHandlerOptions HttpHandlerOptions { get; set; } public string DownstreamHttpVersion { get; set; } + + public FileCacheOptions CacheOptions { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileRoute.cs b/src/Ocelot/Configuration/File/FileRoute.cs index 5823113ad..ea076e022 100644 --- a/src/Ocelot/Configuration/File/FileRoute.cs +++ b/src/Ocelot/Configuration/File/FileRoute.cs @@ -41,8 +41,8 @@ public FileRoute(FileRoute from) public string DownstreamHttpMethod { get; set; } public string DownstreamHttpVersion { get; set; } public string DownstreamPathTemplate { get; set; } - public string DownstreamScheme { get; set; } - public FileCacheOptions FileCacheOptions { get; set; } + public string DownstreamScheme { get; set; } + public FileCacheOptions FileCacheOptions { get; set; } public FileHttpHandlerOptions HttpHandlerOptions { get; set; } public string Key { get; set; } public FileLoadBalancerOptions LoadBalancerOptions { get; set; } diff --git a/src/Ocelot/DependencyInjection/Features.cs b/src/Ocelot/DependencyInjection/Features.cs index 51f836ea9..8910145af 100644 --- a/src/Ocelot/DependencyInjection/Features.cs +++ b/src/Ocelot/DependencyInjection/Features.cs @@ -1,4 +1,7 @@ using Microsoft.Extensions.DependencyInjection; +using Ocelot.Cache; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; using Ocelot.RateLimiting; namespace Ocelot.DependencyInjection; @@ -8,9 +11,26 @@ public static class Features /// /// Ocelot feature: Rate Limiting. /// + /// + /// Read The Docs: Rate Limiting. + /// /// The services collection to add the feature to. /// The same object. public static IServiceCollection AddRateLimiting(this IServiceCollection services) => services .AddSingleton() .AddSingleton(); + + /// + /// Ocelot feature: Request Caching. + /// + /// + /// Read The Docs: Caching. + /// + /// The services collection to add the feature to. + /// The same object. + public static IServiceCollection AddOcelotCache(this IServiceCollection services) => services + .AddSingleton, DefaultMemoryCache>() + .AddSingleton, DefaultMemoryCache>() + .AddSingleton() + .AddSingleton(); } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index a501014cc..9dc65a3de 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -54,8 +54,6 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services = services; Services.Configure(configurationRoot); - Services.TryAddSingleton, AspMemoryCache>(); - Services.TryAddSingleton, AspMemoryCache>(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); @@ -81,7 +79,6 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); @@ -114,9 +111,11 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.TryAddSingleton, OcelotConfigurationMonitor>(); + + Services.AddOcelotCache(); Services.AddOcelotMessageInvokerPool(); // See this for why we register this as singleton: diff --git a/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs b/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs index c61ce3864..c029ad56d 100644 --- a/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs +++ b/test/Ocelot.AcceptanceTests/Caching/CachingTests.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; +using System.Text; +using JsonSerializer = System.Text.Json.JsonSerializer; namespace Ocelot.AcceptanceTests.Caching { @@ -10,6 +12,7 @@ public sealed class CachingTests : IDisposable private const string HelloTomContent = "Hello from Tom"; private const string HelloLauraContent = "Hello from Laura"; + private int _counter = 0; public CachingTests() { @@ -113,6 +116,75 @@ public void Should_not_return_cached_response_as_ttl_expires() .BDDfy(); } + [Theory] + [InlineData(true)] + [InlineData(false)] + [Trait("Feat", "2058")] + [Trait("Bug", "2059")] + public void Should_return_different_cached_response_when_request_body_changes_and_EnableContentHashing_is_true(bool asGlobalConfig) + { + var port = PortFinder.GetRandomPort(); + var options = new FileCacheOptions + { + TtlSeconds = 100, + EnableContentHashing = true, + }; + var (testBody1String, testBody2String) = TestBodiesFactory(); + var configuration = GivenFileConfiguration(port, options, asGlobalConfig); + + this.Given(x => x.GivenThereIsAnEchoServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody1String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody2String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody2String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody1String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody2String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody2String)) + .And(x => ThenTheCounterValueShouldBe(2)) + .BDDfy(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [Trait("Feat", "2058")] + [Trait("Bug", "2059")] + public void Should_return_same_cached_response_when_request_body_changes_and_EnableContentHashing_is_false(bool asGlobalConfig) + { + var port = PortFinder.GetRandomPort(); + var options = new FileCacheOptions + { + TtlSeconds = 100, + }; + var (testBody1String, testBody2String) = TestBodiesFactory(); + var configuration = GivenFileConfiguration(port, options, asGlobalConfig); + + this.Given(x => x.GivenThereIsAnEchoServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody1String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody2String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody1String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody2String, Encoding.UTF8, "application/json"))) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(testBody1String)) + .And(x => ThenTheCounterValueShouldBe(1)) + .BDDfy(); + } + [Fact] [Trait("Issue", "1172")] public void Should_clean_cached_response_by_cache_header_via_new_caching_key() @@ -152,7 +224,7 @@ public void Should_clean_cached_response_by_cache_header_via_new_caching_key() .BDDfy(); } - private static FileConfiguration GivenFileConfiguration(int port, FileCacheOptions cacheOptions) => new() + private static FileConfiguration GivenFileConfiguration(int port, FileCacheOptions cacheOptions, bool asGlobalConfig = false) => new() { Routes = new() { @@ -163,12 +235,14 @@ public void Should_clean_cached_response_by_cache_header_via_new_caching_key() { new FileHostAndPort("localhost", port), }, + DownstreamHttpMethod = "Post", DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = "/", - UpstreamHttpMethod = new() { HttpMethods.Get }, - FileCacheOptions = cacheOptions, + UpstreamHttpMethod = new() { HttpMethods.Get, HttpMethods.Post }, + FileCacheOptions = asGlobalConfig ? new FileCacheOptions { TtlSeconds = cacheOptions.TtlSeconds } : cacheOptions, }, }, + GlobalConfiguration = asGlobalConfig ? new FileGlobalConfiguration { CacheOptions = cacheOptions } : null, }; private static void GivenTheCacheExpires() @@ -196,10 +270,61 @@ private void GivenThereIsAServiceRunningOn(string url, HttpStatusCode statusCode }); } + private void GivenThereIsAnEchoServiceRunningOn(string url) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + using var streamReader = new StreamReader(context.Request.Body); + var requestBody = await streamReader.ReadToEndAsync(); + + _counter++; + context.Response.StatusCode = (int)HttpStatusCode.OK; + await context.Response.WriteAsync(requestBody); + }); + } + + private void ThenTheCounterValueShouldBe(int expected) + { + Assert.Equal(expected, _counter); + } + + private (string TestBody1String, string TestBody2String) TestBodiesFactory() + { + var testBody1 = new TestBody + { + Age = 30, + Email = "test.test@email.com", + FirstName = "Jean", + LastName = "Test", + }; + + var testBody1String = JsonSerializer.Serialize(testBody1); + + var testBody2 = new TestBody + { + Age = 31, + Email = "test.test@email.com", + FirstName = "Jean", + LastName = "Test", + }; + + var testBody2String = JsonSerializer.Serialize(testBody2); + + return (testBody1String, testBody2String); + } + public void Dispose() { _serviceHandler?.Dispose(); _steps.Dispose(); } } + + public class TestBody + { + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public int Age { get; set; } + } } diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs index 4fc91e0a8..87ff03fd1 100644 --- a/test/Ocelot.IntegrationTests/AdministrationTests.cs +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -882,7 +882,7 @@ private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) private void ThenTheResultHaveMultiLineIndentedJson() { const string indent = " "; - const int total = 46, skip = 1; + const int total = 52, skip = 1; var lines = _response.Content.ReadAsStringAsync().Result.Split(Environment.NewLine); lines.Length.ShouldBe(total); lines.First().ShouldNotStartWith(indent); diff --git a/test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs b/test/Ocelot.UnitTests/Cache/CacheOptionsCreatorTests.cs similarity index 73% rename from test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs rename to test/Ocelot.UnitTests/Cache/CacheOptionsCreatorTests.cs index 89d552009..f3584e714 100644 --- a/test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs +++ b/test/Ocelot.UnitTests/Cache/CacheOptionsCreatorTests.cs @@ -1,59 +1,60 @@ -using Ocelot.Cache; +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; - -namespace Ocelot.UnitTests.Cache -{ - public class RegionCreatorTests : UnitTest - { - private string _result; - private FileRoute _route; - - [Fact] - public void should_create_region() - { - var route = new FileRoute - { - UpstreamHttpMethod = new List { "Get" }, - UpstreamPathTemplate = "/testdummy", - }; - - this.Given(_ => GivenTheRoute(route)) - .When(_ => WhenICreateTheRegion()) - .Then(_ => ThenTheRegionIs("Gettestdummy")) - .BDDfy(); - } - - [Fact] - public void should_use_region() - { - var route = new FileRoute - { - FileCacheOptions = new FileCacheOptions - { - Region = "region", - }, - }; - - this.Given(_ => GivenTheRoute(route)) - .When(_ => WhenICreateTheRegion()) - .Then(_ => ThenTheRegionIs("region")) - .BDDfy(); - } - - private void GivenTheRoute(FileRoute route) - { - _route = route; - } - - private void WhenICreateTheRegion() - { - var regionCreator = new RegionCreator(); - _result = regionCreator.Create(_route); - } - - private void ThenTheRegionIs(string expected) - { - _result.ShouldBe(expected); - } - } -} + +namespace Ocelot.UnitTests.Cache +{ + public class CacheOptionsCreatorTests : UnitTest + { + private CacheOptions _cacheOptions; + private FileRoute _route; + + [Fact] + public void should_create_region() + { + var route = new FileRoute + { + UpstreamHttpMethod = new List { "Get" }, + UpstreamPathTemplate = "/testdummy", + }; + + this.Given(_ => GivenTheRoute(route)) + .When(_ => WhenICreateTheRegion()) + .Then(_ => ThenTheRegionIs("Gettestdummy")) + .BDDfy(); + } + + [Fact] + public void should_use_region() + { + var route = new FileRoute + { + FileCacheOptions = new FileCacheOptions + { + Region = "region", + }, + }; + + this.Given(_ => GivenTheRoute(route)) + .When(_ => WhenICreateTheRegion()) + .Then(_ => ThenTheRegionIs("region")) + .BDDfy(); + } + + private void GivenTheRoute(FileRoute route) + { + _route = route; + } + + private void WhenICreateTheRegion() + { + var cacheOptionsCreator = new CacheOptionsCreator(); + _cacheOptions = cacheOptionsCreator.Create(_route.FileCacheOptions, new FileGlobalConfiguration(), _route.UpstreamPathTemplate, _route.UpstreamHttpMethod); + } + + private void ThenTheRegionIs(string expected) + { + _cacheOptions.Region.ShouldBe(expected); + } + } +} diff --git a/test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs b/test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs index be9de81e8..50c7de6c9 100644 --- a/test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs +++ b/test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs @@ -59,7 +59,7 @@ public void should_generate_cache_key_without_request_content() [Fact] public void should_generate_cache_key_with_cache_options_header() { - CacheOptions options = new CacheOptions(100, "region", headerName); + CacheOptions options = new CacheOptions(100, "region", headerName, false); var cachekey = MD5Helper.GenerateMd5($"{verb}-{url}-{header}"); this.Given(x => x.GivenDownstreamRoute(options)) diff --git a/test/Ocelot.UnitTests/Cache/AspMemoryCacheTests.cs b/test/Ocelot.UnitTests/Cache/DefaultMemoryCacheTests.cs similarity index 90% rename from test/Ocelot.UnitTests/Cache/AspMemoryCacheTests.cs rename to test/Ocelot.UnitTests/Cache/DefaultMemoryCacheTests.cs index 69cda7c8d..e5d14dbe4 100644 --- a/test/Ocelot.UnitTests/Cache/AspMemoryCacheTests.cs +++ b/test/Ocelot.UnitTests/Cache/DefaultMemoryCacheTests.cs @@ -3,13 +3,13 @@ namespace Ocelot.UnitTests.Cache { - public class AspMemoryCacheTests : UnitTest + public class DefaultMemoryCacheTests : UnitTest { - private readonly AspMemoryCache _cache; + private readonly DefaultMemoryCache _cache; - public AspMemoryCacheTests() + public DefaultMemoryCacheTests() { - _cache = new AspMemoryCache(new MemoryCache(new MemoryCacheOptions())); + _cache = new DefaultMemoryCache(new MemoryCache(new MemoryCacheOptions())); } [Fact] diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs index aaaf263f0..ab00e7675 100644 --- a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs @@ -106,7 +106,7 @@ private void GivenTheDownstreamRouteIs() var route = new RouteBuilder() .WithDownstreamRoute(new DownstreamRouteBuilder() .WithIsCached(true) - .WithCacheOptions(new CacheOptions(100, "kanken", null)) + .WithCacheOptions(new CacheOptions(100, "kanken", null, false)) .WithUpstreamHttpMethod(new List { "Get" }) .Build()) .WithUpstreamHttpMethod(new List { "Get" }) diff --git a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs index fc002eaab..76fe0977d 100644 --- a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs +++ b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs @@ -77,7 +77,7 @@ private void GivenTheDownstreamRouteIs() { var route = new DownstreamRouteBuilder() .WithIsCached(true) - .WithCacheOptions(new CacheOptions(100, "kanken", null)) + .WithCacheOptions(new CacheOptions(100, "kanken", null, false)) .WithUpstreamHttpMethod(new List { "Get" }) .Build(); diff --git a/test/Ocelot.UnitTests/Configuration/CacheOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/CacheOptionsCreatorTests.cs new file mode 100644 index 000000000..12ed54cdf --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/CacheOptionsCreatorTests.cs @@ -0,0 +1,100 @@ +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; + +namespace Ocelot.UnitTests.Configuration; + +[Trait("Feat", "2058")] +[Trait("Bug", "2059")] +public class CacheOptionsCreatorTests +{ + [Fact] + public void ShouldCreateCacheOptions() + { + var options = FileCacheOptionsFactory(); + var cacheOptionsCreator = new CacheOptionsCreator(); + var result = cacheOptionsCreator.Create(options, null, null, null); + + result.TtlSeconds.ShouldBe(options.TtlSeconds.Value); + result.Region.ShouldBe(options.Region); + result.Header.ShouldBe(options.Header); + result.EnableContentHashing.ShouldBe(options.EnableContentHashing.Value); + } + + [Fact] + public void ShouldCreateCacheOptionsUsingGlobalConfiguration() + { + var global = GlobalConfigurationFactory(); + + var cacheOptionsCreator = new CacheOptionsCreator(); + var result = cacheOptionsCreator.Create(new FileCacheOptions(), global, null, null); + + result.TtlSeconds.ShouldBe(global.CacheOptions.TtlSeconds.Value); + result.Region.ShouldBe(global.CacheOptions.Region); + result.Header.ShouldBe(global.CacheOptions.Header); + result.EnableContentHashing.ShouldBe(global.CacheOptions.EnableContentHashing.Value); + } + + [Fact] + public void RouteCacheOptionsShouldOverrideGlobalConfiguration() + { + var global = GlobalConfigurationFactory(); + var options = FileCacheOptionsFactory(); + + var cacheOptionsCreator = new CacheOptionsCreator(); + var result = cacheOptionsCreator.Create(options, global, null, null); + + result.TtlSeconds.ShouldBe(options.TtlSeconds.Value); + result.Region.ShouldBe(options.Region); + result.Header.ShouldBe(options.Header); + result.EnableContentHashing.ShouldBe(options.EnableContentHashing.Value); + } + + [Fact] + public void ShouldCreateCacheOptionsWithDefaults() + { + var cacheOptionsCreator = new CacheOptionsCreator(); + var result = cacheOptionsCreator.Create(new FileCacheOptions(), null, "/", new List { "GET" }); + + result.TtlSeconds.ShouldBe(0); + result.Region.ShouldBe("GET"); + result.Header.ShouldBe(null); + result.EnableContentHashing.ShouldBe(false); + } + + [Fact] + public void ShouldComputeRegionIfNotProvided() + { + var global = GlobalConfigurationFactory(); + var options = FileCacheOptionsFactory(); + + global.CacheOptions.Region = null; + options.Region = null; + + var cacheOptionsCreator = new CacheOptionsCreator(); + var result = cacheOptionsCreator.Create(options, global, "/api/values", new List { "GET", "POST" }); + + result.TtlSeconds.ShouldBe(options.TtlSeconds.Value); + result.Region.ShouldBe("GETPOSTapivalues"); + result.Header.ShouldBe(options.Header); + result.EnableContentHashing.ShouldBe(options.EnableContentHashing.Value); + } + + private static FileGlobalConfiguration GlobalConfigurationFactory() => new() + { + CacheOptions = new FileCacheOptions + { + TtlSeconds = 20, + Region = "globalRegion", + Header = "globalHeader", + EnableContentHashing = false, + }, + }; + + private static FileCacheOptions FileCacheOptionsFactory() => new() + { + TtlSeconds = 10, + Region = "region", + Header = "header", + EnableContentHashing = true, + }; +} diff --git a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs index 382eb3a44..11b5fc3e0 100644 --- a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs @@ -1,5 +1,4 @@ -using Ocelot.Cache; -using Ocelot.Configuration; +using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; @@ -17,7 +16,7 @@ public class RoutesCreatorTests : UnitTest private readonly Mock _qosoCreator; private readonly Mock _rroCreator; private readonly Mock _rloCreator; - private readonly Mock _rCreator; + private readonly Mock _coCreator; private readonly Mock _hhoCreator; private readonly Mock _hfarCreator; private readonly Mock _daCreator; @@ -34,7 +33,7 @@ public class RoutesCreatorTests : UnitTest private List _ctt; private QoSOptions _qoso; private RateLimitOptions _rlo; - private string _region; + private CacheOptions _cacheOptions; private HttpHandlerOptions _hho; private HeaderTransformations _ht; private List _dhp; @@ -51,7 +50,7 @@ public RoutesCreatorTests() _qosoCreator = new Mock(); _rroCreator = new Mock(); _rloCreator = new Mock(); - _rCreator = new Mock(); + _coCreator = new Mock(); _hhoCreator = new Mock(); _hfarCreator = new Mock(); _daCreator = new Mock(); @@ -68,7 +67,7 @@ public RoutesCreatorTests() _qosoCreator.Object, _rroCreator.Object, _rloCreator.Object, - _rCreator.Object, + _coCreator.Object, _hhoCreator.Object, _hfarCreator.Object, _daCreator.Object, @@ -161,7 +160,8 @@ private void GivenTheDependenciesAreSetUpCorrectly() _ctt = new List(); _qoso = new QoSOptionsBuilder().Build(); _rlo = new RateLimitOptionsBuilder().Build(); - _region = "vesty"; + + _cacheOptions = new CacheOptions(0, "vesty", null, false); _hho = new HttpHandlerOptionsBuilder().Build(); _ht = new HeaderTransformations(new List(), new List(), new List(), new List()); _dhp = new List(); @@ -175,7 +175,7 @@ private void GivenTheDependenciesAreSetUpCorrectly() _cthCreator.Setup(x => x.Create(It.IsAny>())).Returns(_ctt); _qosoCreator.Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny>())).Returns(_qoso); _rloCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_rlo); - _rCreator.Setup(x => x.Create(It.IsAny())).Returns(_region); + _coCreator.Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())).Returns(_cacheOptions); _hhoCreator.Setup(x => x.Create(It.IsAny())).Returns(_hho); _hfarCreator.Setup(x => x.Create(It.IsAny())).Returns(_ht); _daCreator.Setup(x => x.Create(It.IsAny())).Returns(_dhp); @@ -222,8 +222,8 @@ private void ThenTheRouteIsSet(FileRoute expected, int routeIndex) _result[routeIndex].DownstreamRoute[0].ClaimsToClaims.ShouldBe(_ctt); _result[routeIndex].DownstreamRoute[0].QosOptions.ShouldBe(_qoso); _result[routeIndex].DownstreamRoute[0].RateLimitOptions.ShouldBe(_rlo); - _result[routeIndex].DownstreamRoute[0].CacheOptions.Region.ShouldBe(_region); - _result[routeIndex].DownstreamRoute[0].CacheOptions.TtlSeconds.ShouldBe(expected.FileCacheOptions.TtlSeconds); + _result[routeIndex].DownstreamRoute[0].CacheOptions.Region.ShouldBe(_cacheOptions.Region); + _result[routeIndex].DownstreamRoute[0].CacheOptions.TtlSeconds.ShouldBe(0); _result[routeIndex].DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_hho); _result[routeIndex].DownstreamRoute[0].UpstreamHeadersFindAndReplace.ShouldBe(_ht.Upstream); _result[routeIndex].DownstreamRoute[0].DownstreamHeadersFindAndReplace.ShouldBe(_ht.Downstream); @@ -264,7 +264,7 @@ private void ThenTheDepsAreCalledFor(FileRoute fileRoute, FileGlobalConfiguratio _cthCreator.Verify(x => x.Create(fileRoute.AddQueriesToRequest), Times.Once); _qosoCreator.Verify(x => x.Create(fileRoute.QoSOptions, fileRoute.UpstreamPathTemplate, fileRoute.UpstreamHttpMethod)); _rloCreator.Verify(x => x.Create(fileRoute.RateLimitOptions, globalConfig), Times.Once); - _rCreator.Verify(x => x.Create(fileRoute), Times.Once); + _coCreator.Verify(x => x.Create(fileRoute.FileCacheOptions, globalConfig, fileRoute.UpstreamPathTemplate, fileRoute.UpstreamHttpMethod), Times.Once); _hhoCreator.Verify(x => x.Create(fileRoute.HttpHandlerOptions), Times.Once); _hfarCreator.Verify(x => x.Create(fileRoute), Times.Once); _daCreator.Verify(x => x.Create(fileRoute), Times.Once); From 6cef42e346a449843cf34788adccdaa646ac9147 Mon Sep 17 00:00:00 2001 From: jlukawska <56401969+jlukawska@users.noreply.github.com> Date: Thu, 18 Apr 2024 18:30:19 +0200 Subject: [PATCH 07/15] #360 Routing based on request header (#1312) * routing based on headers (all specified headers must match) * routing based on headers for aggregated routes * unit tests and small modifications * find placeholders in header templates * match upstream headers to header templates * find placeholders name and values, fix regex for finding placeholders values * fix unit tests * change header placeholder pattern * unit tests * unit tests * unit tests * unit tests * extend validation with checking upstreamheadertemplates, acceptance tests for cases from the issue * update docs and minor changes * SA1649 File name should match first type name * Fix compilation errors by code review after resolving conflicts * Fix warnings * File-scoped namespaces * File-scoped namespace * Target-typed 'new' expressions (C# 9). https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/target-typed-new * IDE1006 Naming rule violation: These words must begin with upper case characters: should_* * Target-typed 'new' expressions (C# 9). https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/target-typed-new * Fix build errors * DownstreamRouteBuilder * AggregatesCreator * IUpstreamHeaderTemplatePatternCreator, RoutesCreator * UpstreamHeaderTemplatePatternCreator * FileAggregateRoute * FileAggregateRoute * FileRoute * Route, IRoute * FileConfigurationFluentValidator * OcelotBuilder * DownstreamRouteCreator * DownstreamRouteFinder * HeaderMatcher * DownstreamRouteFinderMiddleware * UpstreamHeaderTemplate * Routing folder * RoutingBasedOnHeadersTests * Refactor acceptance tests * AAA pattern in unit tests * CS8936: Feature 'collection expressions' is not available in C# 10.0. Please use language version 12.0 or greater. * Code review by @RaynaldM * Convert facts to one `Theory` * AAA pattern * Add traits * Update routing.rst Check grammar and style * Update docs --------- Co-authored-by: raman-m --- docs/features/configuration.rst | 9 +- docs/features/routing.rst | 61 +- .../Builder/DownstreamRouteBuilder.cs | 22 +- .../Configuration/Builder/RouteBuilder.cs | 12 +- .../Creator/AggregatesCreator.cs | 14 +- .../IUpstreamHeaderTemplatePatternCreator.cs | 17 + .../Configuration/Creator/RoutesCreator.cs | 9 +- .../UpstreamHeaderTemplatePatternCreator.cs | 50 ++ src/Ocelot/Configuration/DownstreamRoute.cs | 9 +- .../Configuration/File/FileAggregateRoute.cs | 15 +- src/Ocelot/Configuration/File/FileRoute.cs | 5 +- src/Ocelot/Configuration/File/IRoute.cs | 1 + src/Ocelot/Configuration/Route.cs | 5 +- .../FileConfigurationFluentValidator.cs | 16 +- src/Ocelot/DependencyInjection/Features.cs | 11 + .../DependencyInjection/OcelotBuilder.cs | 3 + .../Finder/DownstreamRouteCreator.cs | 5 +- .../Finder/DownstreamRouteFinder.cs | 48 +- .../Finder/IDownstreamRouteProvider.cs | 10 +- .../HeaderPlaceholderNameAndValueFinder.cs | 24 + .../HeadersToHeaderTemplatesMatcher.cs | 11 + .../IHeaderPlaceholderNameAndValueFinder.cs | 12 + .../IHeadersToHeaderTemplatesMatcher.cs | 11 + .../DownstreamRouteFinderMiddleware.cs | 17 +- src/Ocelot/Values/UpstreamHeaderTemplate.cs | 19 + .../Routing/RoutingBasedOnHeadersTests.cs | 464 +++++++++++ .../{ => Routing}/RoutingTests.cs | 2 +- .../RoutingWithQueryStringTests.cs | 722 +++++++++--------- test/Ocelot.Testing/PortFinder.cs | 3 +- .../Configuration/AggregatesCreatorTests.cs | 31 +- .../Configuration/RoutesCreatorTests.cs | 17 +- ...streamHeaderTemplatePatternCreatorTests.cs | 45 ++ .../FileConfigurationFluentValidatorTests.cs | 116 ++- .../DownstreamRouteCreatorTests.cs | 10 +- .../DownstreamRouteFinderMiddlewareTests.cs | 2 +- .../DownstreamRouteFinderTests.cs | 237 +++++- .../DownstreamRouteProviderFactoryTests.cs | 3 + ...eaderPlaceholderNameAndValueFinderTests.cs | 220 ++++++ .../HeadersToHeaderTemplatesMatcherTests.cs | 293 +++++++ 39 files changed, 2092 insertions(+), 489 deletions(-) create mode 100644 src/Ocelot/Configuration/Creator/IUpstreamHeaderTemplatePatternCreator.cs create mode 100644 src/Ocelot/Configuration/Creator/UpstreamHeaderTemplatePatternCreator.cs create mode 100644 src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinder.cs create mode 100644 src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcher.cs create mode 100644 src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeaderPlaceholderNameAndValueFinder.cs create mode 100644 src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeadersToHeaderTemplatesMatcher.cs create mode 100644 src/Ocelot/Values/UpstreamHeaderTemplate.cs create mode 100644 test/Ocelot.AcceptanceTests/Routing/RoutingBasedOnHeadersTests.cs rename test/Ocelot.AcceptanceTests/{ => Routing}/RoutingTests.cs (97%) rename test/Ocelot.AcceptanceTests/{ => Routing}/RoutingWithQueryStringTests.cs (97%) create mode 100644 test/Ocelot.UnitTests/Configuration/UpstreamHeaderTemplatePatternCreatorTests.cs create mode 100644 test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinderTests.cs create mode 100644 test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcherTests.cs diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 931e4f58d..87198ea1d 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -19,9 +19,11 @@ Here is an example Route configuration. You don't need to set all of these thing .. code-block:: json { - "DownstreamPathTemplate": "/", "UpstreamPathTemplate": "/", + "UpstreamHeaderTemplates": {}, // dictionary + "UpstreamHost": "", "UpstreamHttpMethod": [ "Get" ], + "DownstreamPathTemplate": "/", "DownstreamHttpMethod": "", "DownstreamHttpVersion": "", "AddHeadersToRequest": {}, @@ -37,7 +39,7 @@ Here is an example Route configuration. You don't need to set all of these thing "ServiceName": "", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ - { "Host": "localhost", "Port": 51876 } + { "Host": "localhost", "Port": 12345 } ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 0, @@ -70,7 +72,8 @@ Here is an example Route configuration. You don't need to set all of these thing } } -More information on how to use these options is below. +The actual Route schema for properties can be found in the C# `FileRoute `_ class. +If you're interested in learning more about how to utilize these options, read below! Multiple Environments --------------------- diff --git a/docs/features/routing.rst b/docs/features/routing.rst index 6de03d8e2..e55b33e5c 100644 --- a/docs/features/routing.rst +++ b/docs/features/routing.rst @@ -154,6 +154,58 @@ The Route above will only be matched when the ``Host`` header value is ``somedom If you do not set **UpstreamHost** on a Route then any ``Host`` header will match it. This means that if you have two Routes that are the same, apart from the **UpstreamHost**, where one is null and the other set Ocelot will favour the one that has been set. +.. _routing-upstream-headers: + +Upstream Headers [#f3]_ +----------------------- + +In addition to routing by ``UpstreamPathTemplate``, you can also define ``UpstreamHeaderTemplates``. +For a route to match, all headers specified in this dictionary object must be present in the request headers. + +.. code-block:: json + + { + // ... + "UpstreamPathTemplate": "/", + "UpstreamHttpMethod": [ "Get" ], + "UpstreamHeaderTemplates": { // dictionary + "country": "uk", // 1st header + "version": "v1" // 2nd header + } + } + +In this scenario, the route will only match if a request includes both headers with the specified values. + +Header placeholders +^^^^^^^^^^^^^^^^^^^ + +Let's explore a more intriguing scenario where placeholders can be effectively utilized within your ``UpstreamHeaderTemplates``. + +Consider the following approach using the special placeholder format ``{header:placeholdername}``: + +.. code-block:: json + + { + "DownstreamPathTemplate": "/{versionnumber}/api", // with placeholder + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { "Host": "10.0.10.1", "Port": 80 } + ], + "UpstreamPathTemplate": "/api", + "UpstreamHttpMethod": [ "Get" ], + "UpstreamHeaderTemplates": { + "version": "{header:versionnumber}" // 'header:' prefix vs placeholder + } + } + +In this scenario, the entire value of the request header "**version**" is inserted into the ``DownstreamPathTemplate``. +If necessary, a more intricate upstream header template can be specified, using placeholders such as ``version-{header:version}_country-{header:country}``. + + **Note 1**: Placeholders are not required in ``DownstreamPathTemplate``. + This scenario can be utilized to mandate a specific header regardless of its value. + + **Note 2**: Additionally, the ``UpstreamHeaderTemplates`` dictionary options are applicable for :doc:`../features/requestaggregation` as well. + Priority -------- @@ -294,7 +346,7 @@ Here are two user scenarios. .. _routing-security-options: -Security Options [#f3]_ +Security Options [#f4]_ ----------------------- Ocelot allows you to manage multiple patterns for allowed/blocked IPs using the `IPAddressRange `_ package @@ -326,7 +378,7 @@ The current patterns managed are the following: .. _routing-dynamic: -Dynamic Routing [#f4]_ +Dynamic Routing [#f5]_ ---------------------- The idea is to enable dynamic routing when using a :doc:`../features/servicediscovery` provider so you don't have to provide the Route config. @@ -336,5 +388,6 @@ See the :ref:`sd-dynamic-routing` docs if this sounds interesting to you. .. [#f1] ":ref:`routing-empty-placeholders`" feature is available starting in version `23.0 `_, see issue `748 `_ and the `23.0 `__ release notes for details. .. [#f2] ":ref:`routing-upstream-host`" feature was requested as part of `issue 216 `_. -.. [#f3] ":ref:`routing-security-options`" feature was requested as part of `issue 628 `_ (of `12.0.1 `_ version), then redesigned and improved by `issue 1400 `_, and published in version `20.0 `_ docs. -.. [#f4] ":ref:`routing-dynamic`" feature was requested as part of `issue 340 `_. Complete reference: :ref:`sd-dynamic-routing`. +.. [#f3] ":ref:`routing-upstream-headers`" feature was proposed in `issue 360 `_, and released in version `24.0 `_. +.. [#f4] ":ref:`routing-security-options`" feature was requested as part of `issue 628 `_ (of `12.0.1 `_ version), then redesigned and improved by `issue 1400 `_, and published in version `20.0 `_ docs. +.. [#f5] ":ref:`routing-dynamic`" feature was requested as part of `issue 340 `_. Complete reference: :ref:`sd-dynamic-routing`. diff --git a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs index 281e93033..f8e6dc159 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs @@ -40,13 +40,14 @@ public class DownstreamRouteBuilder private SecurityOptions _securityOptions; private string _downstreamHttpMethod; private Version _downstreamHttpVersion; + private Dictionary _upstreamHeaders; public DownstreamRouteBuilder() { - _downstreamAddresses = new List(); - _delegatingHandlers = new List(); - _addHeadersToDownstream = new List(); - _addHeadersToUpstream = new List(); + _downstreamAddresses = new(); + _delegatingHandlers = new(); + _addHeadersToDownstream = new(); + _addHeadersToUpstream = new(); } public DownstreamRouteBuilder WithDownstreamAddresses(List downstreamAddresses) @@ -87,7 +88,9 @@ public DownstreamRouteBuilder WithUpstreamPathTemplate(UpstreamPathTemplate inpu public DownstreamRouteBuilder WithUpstreamHttpMethod(List input) { - _upstreamHttpMethod = input.Count == 0 ? new List() : input.Select(x => new HttpMethod(x.Trim())).ToList(); + _upstreamHttpMethod = input.Count > 0 + ? input.Select(x => new HttpMethod(x.Trim())).ToList() + : new(); return this; } @@ -259,6 +262,12 @@ public DownstreamRouteBuilder WithDownstreamHttpVersion(Version downstreamHttpVe return this; } + public DownstreamRouteBuilder WithUpstreamHeaders(Dictionary input) + { + _upstreamHeaders = input; + return this; + } + public DownstreamRoute Build() { return new DownstreamRoute( @@ -295,6 +304,7 @@ public DownstreamRoute Build() _dangerousAcceptAnyServerCertificateValidator, _securityOptions, _downstreamHttpMethod, - _downstreamHttpVersion); + _downstreamHttpVersion, + _upstreamHeaders); } } diff --git a/src/Ocelot/Configuration/Builder/RouteBuilder.cs b/src/Ocelot/Configuration/Builder/RouteBuilder.cs index 8e7614a9c..c39062829 100644 --- a/src/Ocelot/Configuration/Builder/RouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/RouteBuilder.cs @@ -10,7 +10,8 @@ public class RouteBuilder private string _upstreamHost; private List _downstreamRoutes; private List _downstreamRoutesConfig; - private string _aggregator; + private string _aggregator; + private IDictionary _upstreamHeaders; public RouteBuilder() { @@ -58,6 +59,12 @@ public RouteBuilder WithAggregator(string aggregator) { _aggregator = aggregator; return this; + } + + public RouteBuilder WithUpstreamHeaders(IDictionary upstreamHeaders) + { + _upstreamHeaders = upstreamHeaders; + return this; } public Route Build() @@ -68,7 +75,8 @@ public Route Build() _upstreamHttpMethod, _upstreamTemplatePattern, _upstreamHost, - _aggregator + _aggregator, + _upstreamHeaders ); } } diff --git a/src/Ocelot/Configuration/Creator/AggregatesCreator.cs b/src/Ocelot/Configuration/Creator/AggregatesCreator.cs index 9a5ae2906..24d3a58b3 100644 --- a/src/Ocelot/Configuration/Creator/AggregatesCreator.cs +++ b/src/Ocelot/Configuration/Creator/AggregatesCreator.cs @@ -5,11 +5,13 @@ namespace Ocelot.Configuration.Creator { public class AggregatesCreator : IAggregatesCreator { - private readonly IUpstreamTemplatePatternCreator _creator; + private readonly IUpstreamTemplatePatternCreator _creator; + private readonly IUpstreamHeaderTemplatePatternCreator _headerCreator; - public AggregatesCreator(IUpstreamTemplatePatternCreator creator) + public AggregatesCreator(IUpstreamTemplatePatternCreator creator, IUpstreamHeaderTemplatePatternCreator headerCreator) { - _creator = creator; + _creator = creator; + _headerCreator = headerCreator; } public List Create(FileConfiguration fileConfiguration, List routes) @@ -35,7 +37,8 @@ private Route SetUpAggregateRoute(IEnumerable routes, FileAggregateRoute applicableRoutes.Add(downstreamRoute); } - var upstreamTemplatePattern = _creator.Create(aggregateRoute); + var upstreamTemplatePattern = _creator.Create(aggregateRoute); + var upstreamHeaderTemplates = _headerCreator.Create(aggregateRoute); var route = new RouteBuilder() .WithUpstreamHttpMethod(aggregateRoute.UpstreamHttpMethod) @@ -43,7 +46,8 @@ private Route SetUpAggregateRoute(IEnumerable routes, FileAggregateRoute .WithDownstreamRoutes(applicableRoutes) .WithAggregateRouteConfig(aggregateRoute.RouteKeysConfig) .WithUpstreamHost(aggregateRoute.UpstreamHost) - .WithAggregator(aggregateRoute.Aggregator) + .WithAggregator(aggregateRoute.Aggregator) + .WithUpstreamHeaders(upstreamHeaderTemplates) .Build(); return route; diff --git a/src/Ocelot/Configuration/Creator/IUpstreamHeaderTemplatePatternCreator.cs b/src/Ocelot/Configuration/Creator/IUpstreamHeaderTemplatePatternCreator.cs new file mode 100644 index 000000000..d2fea8004 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IUpstreamHeaderTemplatePatternCreator.cs @@ -0,0 +1,17 @@ +using Ocelot.Configuration.File; +using Ocelot.Values; + +namespace Ocelot.Configuration.Creator; + +/// +/// Ocelot feature: Routing based on request header. +/// +public interface IUpstreamHeaderTemplatePatternCreator +{ + /// + /// Creates upstream templates based on route headers. + /// + /// The route info. + /// An object where TKey is , TValue is . + IDictionary Create(IRoute route); +} diff --git a/src/Ocelot/Configuration/Creator/RoutesCreator.cs b/src/Ocelot/Configuration/Creator/RoutesCreator.cs index 19bce7b17..cb7a6c451 100644 --- a/src/Ocelot/Configuration/Creator/RoutesCreator.cs +++ b/src/Ocelot/Configuration/Creator/RoutesCreator.cs @@ -1,4 +1,4 @@ -using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator @@ -9,6 +9,7 @@ public class RoutesCreator : IRoutesCreator private readonly IClaimsToThingCreator _claimsToThingCreator; private readonly IAuthenticationOptionsCreator _authOptionsCreator; private readonly IUpstreamTemplatePatternCreator _upstreamTemplatePatternCreator; + private readonly IUpstreamHeaderTemplatePatternCreator _upstreamHeaderTemplatePatternCreator; private readonly IRequestIdKeyCreator _requestIdKeyCreator; private readonly IQoSOptionsCreator _qosOptionsCreator; private readonly IRouteOptionsCreator _fileRouteOptionsCreator; @@ -36,7 +37,8 @@ public RoutesCreator( ILoadBalancerOptionsCreator loadBalancerOptionsCreator, IRouteKeyCreator routeKeyCreator, ISecurityOptionsCreator securityOptionsCreator, - IVersionCreator versionCreator) + IVersionCreator versionCreator, + IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator) { _routeKeyCreator = routeKeyCreator; _loadBalancerOptionsCreator = loadBalancerOptionsCreator; @@ -54,6 +56,7 @@ public RoutesCreator( _loadBalancerOptionsCreator = loadBalancerOptionsCreator; _securityOptionsCreator = securityOptionsCreator; _versionCreator = versionCreator; + _upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator; } public List Create(FileConfiguration fileConfiguration) @@ -149,12 +152,14 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf private Route SetUpRoute(FileRoute fileRoute, DownstreamRoute downstreamRoutes) { var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute); + var upstreamHeaderTemplates = _upstreamHeaderTemplatePatternCreator.Create(fileRoute); var route = new RouteBuilder() .WithUpstreamHttpMethod(fileRoute.UpstreamHttpMethod) .WithUpstreamPathTemplate(upstreamTemplatePattern) .WithDownstreamRoute(downstreamRoutes) .WithUpstreamHost(fileRoute.UpstreamHost) + .WithUpstreamHeaders(upstreamHeaderTemplates) .Build(); return route; diff --git a/src/Ocelot/Configuration/Creator/UpstreamHeaderTemplatePatternCreator.cs b/src/Ocelot/Configuration/Creator/UpstreamHeaderTemplatePatternCreator.cs new file mode 100644 index 000000000..52c653f5e --- /dev/null +++ b/src/Ocelot/Configuration/Creator/UpstreamHeaderTemplatePatternCreator.cs @@ -0,0 +1,50 @@ +using Ocelot.Configuration.File; +using Ocelot.Values; + +namespace Ocelot.Configuration.Creator; + +/// +/// Default creator of upstream templates based on route headers. +/// +/// Ocelot feature: Routing based on request header. +public partial class UpstreamHeaderTemplatePatternCreator : IUpstreamHeaderTemplatePatternCreator +{ + private const string PlaceHolderPattern = @"(\{header:.*?\})"; +#if NET7_0_OR_GREATER + [GeneratedRegex(PlaceHolderPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline, "en-US")] + private static partial Regex RegExPlaceholders(); +#else + private static readonly Regex RegExPlaceholdersVar = new(PlaceHolderPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline, TimeSpan.FromMilliseconds(1000)); + private static Regex RegExPlaceholders() => RegExPlaceholdersVar; +#endif + + public IDictionary Create(IRoute route) + { + var result = new Dictionary(); + + foreach (var headerTemplate in route.UpstreamHeaderTemplates) + { + var headerTemplateValue = headerTemplate.Value; + var matches = RegExPlaceholders().Matches(headerTemplateValue); + + if (matches.Count > 0) + { + var placeholders = matches.Select(m => m.Groups[1].Value).ToArray(); + for (int i = 0; i < placeholders.Length; i++) + { + var indexOfPlaceholder = headerTemplateValue.IndexOf(placeholders[i]); + var placeholderName = placeholders[i][8..^1]; // remove "{header:" and "}" + headerTemplateValue = headerTemplateValue.Replace(placeholders[i], $"(?<{placeholderName}>.+)"); + } + } + + var template = route.RouteIsCaseSensitive + ? $"^{headerTemplateValue}$" + : $"^(?i){headerTemplateValue}$"; // ignore case + + result.Add(headerTemplate.Key, new(template, headerTemplate.Value)); + } + + return result; + } +} diff --git a/src/Ocelot/Configuration/DownstreamRoute.cs b/src/Ocelot/Configuration/DownstreamRoute.cs index 585c2554f..f71563cb0 100644 --- a/src/Ocelot/Configuration/DownstreamRoute.cs +++ b/src/Ocelot/Configuration/DownstreamRoute.cs @@ -39,7 +39,8 @@ public DownstreamRoute( bool dangerousAcceptAnyServerCertificateValidator, SecurityOptions securityOptions, string downstreamHttpMethod, - Version downstreamHttpVersion) + Version downstreamHttpVersion, + Dictionary upstreamHeaders) { DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; AddHeadersToDownstream = addHeadersToDownstream; @@ -74,7 +75,8 @@ public DownstreamRoute( AddHeadersToUpstream = addHeadersToUpstream; SecurityOptions = securityOptions; DownstreamHttpMethod = downstreamHttpMethod; - DownstreamHttpVersion = downstreamHttpVersion; + DownstreamHttpVersion = downstreamHttpVersion; + UpstreamHeaders = upstreamHeaders ?? new(); } public string Key { get; } @@ -110,6 +112,7 @@ public DownstreamRoute( public bool DangerousAcceptAnyServerCertificateValidator { get; } public SecurityOptions SecurityOptions { get; } public string DownstreamHttpMethod { get; } - public Version DownstreamHttpVersion { get; } + public Version DownstreamHttpVersion { get; } + public Dictionary UpstreamHeaders { get; } } } diff --git a/src/Ocelot/Configuration/File/FileAggregateRoute.cs b/src/Ocelot/Configuration/File/FileAggregateRoute.cs index ad47d735f..fa0ef305a 100644 --- a/src/Ocelot/Configuration/File/FileAggregateRoute.cs +++ b/src/Ocelot/Configuration/File/FileAggregateRoute.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; + namespace Ocelot.Configuration.File { public class FileAggregateRoute : IRoute @@ -10,8 +12,15 @@ public class FileAggregateRoute : IRoute public string Aggregator { get; set; } // Only supports GET..are you crazy!! POST, PUT WOULD BE CRAZY!! :) - public List UpstreamHttpMethod => new() { "Get" }; - - public int Priority { get; set; } = 1; + public List UpstreamHttpMethod => new() { HttpMethods.Get }; + public IDictionary UpstreamHeaderTemplates { get; set; } + public int Priority { get; set; } = 1; + + public FileAggregateRoute() + { + RouteKeys = new(); + RouteKeysConfig = new(); + UpstreamHeaderTemplates = new Dictionary(); + } } } diff --git a/src/Ocelot/Configuration/File/FileRoute.cs b/src/Ocelot/Configuration/File/FileRoute.cs index ea076e022..874609e77 100644 --- a/src/Ocelot/Configuration/File/FileRoute.cs +++ b/src/Ocelot/Configuration/File/FileRoute.cs @@ -19,7 +19,8 @@ public FileRoute() QoSOptions = new FileQoSOptions(); RateLimitOptions = new FileRateLimitRule(); RouteClaimsRequirement = new Dictionary(); - SecurityOptions = new FileSecurityOptions(); + SecurityOptions = new FileSecurityOptions(); + UpstreamHeaderTemplates = new Dictionary(); UpstreamHeaderTransform = new Dictionary(); UpstreamHttpMethod = new List(); } @@ -60,6 +61,7 @@ public FileRoute(FileRoute from) public string UpstreamHost { get; set; } public List UpstreamHttpMethod { get; set; } public string UpstreamPathTemplate { get; set; } + public IDictionary UpstreamHeaderTemplates { get; set; } /// /// Clones this object by making a deep copy. @@ -101,6 +103,7 @@ public static void DeepCopy(FileRoute from, FileRoute to) to.ServiceName = from.ServiceName; to.ServiceNamespace = from.ServiceNamespace; to.Timeout = from.Timeout; + to.UpstreamHeaderTemplates = new Dictionary(from.UpstreamHeaderTemplates); to.UpstreamHeaderTransform = new(from.UpstreamHeaderTransform); to.UpstreamHost = from.UpstreamHost; to.UpstreamHttpMethod = new(from.UpstreamHttpMethod); diff --git a/src/Ocelot/Configuration/File/IRoute.cs b/src/Ocelot/Configuration/File/IRoute.cs index 74df79b23..1a70debb3 100644 --- a/src/Ocelot/Configuration/File/IRoute.cs +++ b/src/Ocelot/Configuration/File/IRoute.cs @@ -2,6 +2,7 @@ public interface IRoute { + IDictionary UpstreamHeaderTemplates { get; set; } string UpstreamPathTemplate { get; set; } bool RouteIsCaseSensitive { get; set; } int Priority { get; set; } diff --git a/src/Ocelot/Configuration/Route.cs b/src/Ocelot/Configuration/Route.cs index 8f9c0992f..12c57949c 100644 --- a/src/Ocelot/Configuration/Route.cs +++ b/src/Ocelot/Configuration/Route.cs @@ -10,7 +10,8 @@ public Route(List downstreamRoute, List upstreamHttpMethod, UpstreamPathTemplate upstreamTemplatePattern, string upstreamHost, - string aggregator) + string aggregator, + IDictionary upstreamHeaderTemplates) { UpstreamHost = upstreamHost; DownstreamRoute = downstreamRoute; @@ -18,8 +19,10 @@ public Route(List downstreamRoute, UpstreamHttpMethod = upstreamHttpMethod; UpstreamTemplatePattern = upstreamTemplatePattern; Aggregator = aggregator; + UpstreamHeaderTemplates = upstreamHeaderTemplates; } + public IDictionary UpstreamHeaderTemplates { get; } public UpstreamPathTemplate UpstreamTemplatePattern { get; } public List UpstreamHttpMethod { get; } public string UpstreamHost { get; } diff --git a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs index 7e74251e2..c8596b2d5 100644 --- a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs +++ b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs @@ -123,15 +123,15 @@ private static bool DoesNotContainRoutesWithSpecificRequestIdKeys(FileAggregateR return routesForAggregate.All(r => string.IsNullOrEmpty(r.RequestIdKey)); } - private static bool IsNotDuplicateIn(FileRoute route, - IEnumerable routes) + private static bool IsNotDuplicateIn(FileRoute route, IEnumerable routes) { var matchingRoutes = routes .Where(r => r.UpstreamPathTemplate == route.UpstreamPathTemplate - && r.UpstreamHost == route.UpstreamHost) - .ToList(); + && r.UpstreamHost == route.UpstreamHost + && AreTheSame(r.UpstreamHeaderTemplates, route.UpstreamHeaderTemplates)) + .ToArray(); - if (matchingRoutes.Count == 1) + if (matchingRoutes.Length == 1) { return true; } @@ -150,7 +150,11 @@ private static bool IsNotDuplicateIn(FileRoute route, } return true; - } + } + + private static bool AreTheSame(IDictionary upstreamHeaderTemplates, IDictionary otherHeaderTemplates) + => upstreamHeaderTemplates.Count == otherHeaderTemplates.Count && + upstreamHeaderTemplates.All(x => otherHeaderTemplates.ContainsKey(x.Key) && otherHeaderTemplates[x.Key] == x.Value); private static bool IsNotDuplicateIn(FileRoute route, IEnumerable aggregateRoutes) diff --git a/src/Ocelot/DependencyInjection/Features.cs b/src/Ocelot/DependencyInjection/Features.cs index 8910145af..fa04d9092 100644 --- a/src/Ocelot/DependencyInjection/Features.cs +++ b/src/Ocelot/DependencyInjection/Features.cs @@ -2,6 +2,7 @@ using Ocelot.Cache; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; +using Ocelot.DownstreamRouteFinder.HeaderMatcher; using Ocelot.RateLimiting; namespace Ocelot.DependencyInjection; @@ -33,4 +34,14 @@ public static IServiceCollection AddOcelotCache(this IServiceCollection services .AddSingleton, DefaultMemoryCache>() .AddSingleton() .AddSingleton(); + + /// + /// Ocelot feature: Routing based on request header. + /// + /// The services collection to add the feature to. + /// The same object. + public static IServiceCollection AddHeaderRouting(this IServiceCollection services) => services + .AddSingleton() + .AddSingleton() + .AddSingleton(); } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 9dc65a3de..70bf5338a 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -143,6 +143,9 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); + // Features + Services.AddHeaderRouting(); + // Add ASP.NET services var assembly = typeof(FileConfigurationController).GetTypeInfo().Assembly; MvcCoreBuilder = (customBuilder ?? AddDefaultAspNetServices) diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs index be4d5e32b..ef2590ae5 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs @@ -18,7 +18,8 @@ public DownstreamRouteCreator(IQoSOptionsCreator qoSOptionsCreator) _cache = new ConcurrentDictionary>(); } - public Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost) + public Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, + IInternalConfiguration configuration, string upstreamHost, IDictionary upstreamHeaders) { var serviceName = GetServiceName(upstreamUrlPath); @@ -69,7 +70,7 @@ public Response Get(string upstreamUrlPath, string upstre var route = new RouteBuilder() .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { upstreamHttpMethod }) - .WithUpstreamPathTemplate(upstreamPathTemplate) + .WithUpstreamPathTemplate(upstreamPathTemplate) .Build(); downstreamRouteHolder = new OkResponse(new DownstreamRouteHolder(new List(), route)); diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs index 59cf1f7b7..fab94d4e1 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs @@ -1,21 +1,31 @@ using Ocelot.Configuration; +using Ocelot.DownstreamRouteFinder.HeaderMatcher; using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.Responses; +using Ocelot.Responses; namespace Ocelot.DownstreamRouteFinder.Finder { public class DownstreamRouteFinder : IDownstreamRouteProvider { private readonly IUrlPathToUrlTemplateMatcher _urlMatcher; - private readonly IPlaceholderNameAndValueFinder _placeholderNameAndValueFinder; + private readonly IPlaceholderNameAndValueFinder _pathPlaceholderFinder; + private readonly IHeadersToHeaderTemplatesMatcher _headerMatcher; + private readonly IHeaderPlaceholderNameAndValueFinder _headerPlaceholderFinder; - public DownstreamRouteFinder(IUrlPathToUrlTemplateMatcher urlMatcher, IPlaceholderNameAndValueFinder urlPathPlaceholderNameAndValueFinder) + public DownstreamRouteFinder( + IUrlPathToUrlTemplateMatcher urlMatcher, + IPlaceholderNameAndValueFinder pathPlaceholderFinder, + IHeadersToHeaderTemplatesMatcher headerMatcher, + IHeaderPlaceholderNameAndValueFinder headerPlaceholderFinder) { _urlMatcher = urlMatcher; - _placeholderNameAndValueFinder = urlPathPlaceholderNameAndValueFinder; + _pathPlaceholderFinder = pathPlaceholderFinder; + _headerMatcher = headerMatcher; + _headerPlaceholderFinder = headerPlaceholderFinder; } - public Response Get(string upstreamUrlPath, string upstreamQueryString, string httpMethod, IInternalConfiguration configuration, string upstreamHost) + public Response Get(string upstreamUrlPath, string upstreamQueryString, string httpMethod, + IInternalConfiguration configuration, string upstreamHost, IDictionary upstreamHeaders) { var downstreamRoutes = new List(); @@ -25,20 +35,20 @@ public Response Get(string upstreamUrlPath, string upstre foreach (var route in applicableRoutes) { - var urlMatch = _urlMatcher.Match(upstreamUrlPath, upstreamQueryString, route.UpstreamTemplatePattern); + var urlMatch = _urlMatcher.Match(upstreamUrlPath, upstreamQueryString, route.UpstreamTemplatePattern); + var headersMatch = _headerMatcher.Match(upstreamHeaders, route.UpstreamHeaderTemplates); - if (urlMatch.Data.Match) + if (urlMatch.Data.Match && headersMatch) { - downstreamRoutes.Add(GetPlaceholderNamesAndValues(upstreamUrlPath, upstreamQueryString, route)); + downstreamRoutes.Add(GetPlaceholderNamesAndValues(upstreamUrlPath, upstreamQueryString, route, upstreamHeaders)); } - } - - if (downstreamRoutes.Any()) + } + + if (downstreamRoutes.Count != 0) { var notNullOption = downstreamRoutes.FirstOrDefault(x => !string.IsNullOrEmpty(x.Route.UpstreamHost)); - var nullOption = downstreamRoutes.FirstOrDefault(x => string.IsNullOrEmpty(x.Route.UpstreamHost)); - - return notNullOption != null ? new OkResponse(notNullOption) : new OkResponse(nullOption); + var nullOption = downstreamRoutes.FirstOrDefault(x => string.IsNullOrEmpty(x.Route.UpstreamHost)); + return new OkResponse(notNullOption ?? nullOption); } return new ErrorResponse(new UnableToFindDownstreamRouteError(upstreamUrlPath, httpMethod)); @@ -50,11 +60,15 @@ private static bool RouteIsApplicableToThisRequest(Route route, string httpMetho (string.IsNullOrEmpty(route.UpstreamHost) || route.UpstreamHost == upstreamHost); } - private DownstreamRouteHolder GetPlaceholderNamesAndValues(string path, string query, Route route) + private DownstreamRouteHolder GetPlaceholderNamesAndValues(string path, string query, Route route, IDictionary upstreamHeaders) { - var templatePlaceholderNameAndValues = _placeholderNameAndValueFinder.Find(path, query, route.UpstreamTemplatePattern.OriginalValue); + var templatePlaceholderNameAndValues = _pathPlaceholderFinder + .Find(path, query, route.UpstreamTemplatePattern.OriginalValue) + .Data; + var headerPlaceholders = _headerPlaceholderFinder.Find(upstreamHeaders, route.UpstreamHeaderTemplates); + templatePlaceholderNameAndValues.AddRange(headerPlaceholders); - return new DownstreamRouteHolder(templatePlaceholderNameAndValues.Data, route); + return new DownstreamRouteHolder(templatePlaceholderNameAndValues, route); } } } diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs b/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs index ed2a657ef..c30ba31bc 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs @@ -1,10 +1,10 @@ using Ocelot.Configuration; using Ocelot.Responses; + +namespace Ocelot.DownstreamRouteFinder.Finder; -namespace Ocelot.DownstreamRouteFinder.Finder +public interface IDownstreamRouteProvider { - public interface IDownstreamRouteProvider - { - Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost); - } + Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, + IInternalConfiguration configuration, string upstreamHost, IDictionary upstreamHeaders); } diff --git a/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinder.cs b/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinder.cs new file mode 100644 index 000000000..56e55b2f4 --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinder.cs @@ -0,0 +1,24 @@ +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Values; + +namespace Ocelot.DownstreamRouteFinder.HeaderMatcher; + +public class HeaderPlaceholderNameAndValueFinder : IHeaderPlaceholderNameAndValueFinder +{ + public IList Find(IDictionary upstreamHeaders, IDictionary templateHeaders) + { + var result = new List(); + foreach (var templateHeader in templateHeaders) + { + var upstreamHeader = upstreamHeaders[templateHeader.Key]; + var matches = templateHeader.Value.Pattern.Matches(upstreamHeader); + var placeholders = matches + .SelectMany(g => g.Groups as IEnumerable) + .Where(g => g.Name != "0") + .Select(g => new PlaceholderNameAndValue(string.Concat('{', g.Name, '}'), g.Value)); + result.AddRange(placeholders); + } + + return result; + } +} diff --git a/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcher.cs b/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcher.cs new file mode 100644 index 000000000..42be8dc7f --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcher.cs @@ -0,0 +1,11 @@ +using Ocelot.Values; + +namespace Ocelot.DownstreamRouteFinder.HeaderMatcher; + +public class HeadersToHeaderTemplatesMatcher : IHeadersToHeaderTemplatesMatcher +{ + public bool Match(IDictionary upstreamHeaders, IDictionary routeHeaders) => + routeHeaders == null || + upstreamHeaders != null + && routeHeaders.All(h => upstreamHeaders.ContainsKey(h.Key) && routeHeaders[h.Key].Pattern.IsMatch(upstreamHeaders[h.Key])); +} diff --git a/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeaderPlaceholderNameAndValueFinder.cs b/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeaderPlaceholderNameAndValueFinder.cs new file mode 100644 index 000000000..6f641d278 --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeaderPlaceholderNameAndValueFinder.cs @@ -0,0 +1,12 @@ +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Values; + +namespace Ocelot.DownstreamRouteFinder.HeaderMatcher; + +/// +/// Ocelot feature: Routing based on request header. +/// +public interface IHeaderPlaceholderNameAndValueFinder +{ + IList Find(IDictionary upstreamHeaders, IDictionary templateHeaders); +} diff --git a/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeadersToHeaderTemplatesMatcher.cs b/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeadersToHeaderTemplatesMatcher.cs new file mode 100644 index 000000000..37dcea32a --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeadersToHeaderTemplatesMatcher.cs @@ -0,0 +1,11 @@ +using Ocelot.Values; + +namespace Ocelot.DownstreamRouteFinder.HeaderMatcher; + +/// +/// Ocelot feature: Routing based on request header. +/// +public interface IHeadersToHeaderTemplatesMatcher +{ + bool Match(IDictionary upstreamHeaders, IDictionary routeHeaders); +} diff --git a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs index 63c21b76c..38ab1bd36 100644 --- a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs +++ b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs @@ -24,26 +24,22 @@ IDownstreamRouteProviderFactory downstreamRouteFinder public async Task Invoke(HttpContext httpContext) { var upstreamUrlPath = httpContext.Request.Path.ToString(); - var upstreamQueryString = httpContext.Request.QueryString.ToString(); - - var hostHeader = httpContext.Request.Headers["Host"].ToString(); + var internalConfiguration = httpContext.Items.IInternalConfiguration(); + var hostHeader = httpContext.Request.Headers.Host.ToString(); var upstreamHost = hostHeader.Contains(':') ? hostHeader.Split(':')[0] : hostHeader; + var upstreamHeaders = httpContext.Request.Headers + .ToDictionary(h => h.Key, h => string.Join(';', h.Value)); - Logger.LogDebug(() => $"Upstream url path is {upstreamUrlPath}"); - - var internalConfiguration = httpContext.Items.IInternalConfiguration(); + Logger.LogDebug(() => $"Upstream URL path is '{upstreamUrlPath}'."); var provider = _factory.Get(internalConfiguration); - - var response = provider.Get(upstreamUrlPath, upstreamQueryString, httpContext.Request.Method, internalConfiguration, upstreamHost); - + var response = provider.Get(upstreamUrlPath, upstreamQueryString, httpContext.Request.Method, internalConfiguration, upstreamHost, upstreamHeaders); if (response.IsError) { Logger.LogWarning(() => $"{MiddlewareName} setting pipeline errors. IDownstreamRouteFinder returned {response.Errors.ToErrorString()}"); - httpContext.Items.UpsertErrors(response.Errors); return; } @@ -52,7 +48,6 @@ public async Task Invoke(HttpContext httpContext) // why set both of these on HttpContext httpContext.Items.UpsertTemplatePlaceholderNameAndValues(response.Data.TemplatePlaceholderNameAndValues); - httpContext.Items.UpsertDownstreamRoute(response.Data); await _next.Invoke(httpContext); diff --git a/src/Ocelot/Values/UpstreamHeaderTemplate.cs b/src/Ocelot/Values/UpstreamHeaderTemplate.cs new file mode 100644 index 000000000..3151fbdf8 --- /dev/null +++ b/src/Ocelot/Values/UpstreamHeaderTemplate.cs @@ -0,0 +1,19 @@ +namespace Ocelot.Values; + +/// +/// Upstream template properties of headers and their regular expression. +/// +/// Ocelot feature: Routing based on request header. +public class UpstreamHeaderTemplate +{ + public string Template { get; } + public string OriginalValue { get; } + public Regex Pattern { get; } + + public UpstreamHeaderTemplate(string template, string originalValue) + { + Template = template; + OriginalValue = originalValue; + Pattern = new Regex(template ?? "$^", RegexOptions.Compiled | RegexOptions.Singleline); + } +} diff --git a/test/Ocelot.AcceptanceTests/Routing/RoutingBasedOnHeadersTests.cs b/test/Ocelot.AcceptanceTests/Routing/RoutingBasedOnHeadersTests.cs new file mode 100644 index 000000000..eea54640f --- /dev/null +++ b/test/Ocelot.AcceptanceTests/Routing/RoutingBasedOnHeadersTests.cs @@ -0,0 +1,464 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; + +namespace Ocelot.AcceptanceTests.Routing; + +[Trait("PR", "1312")] +[Trait("Feat", "360")] +public sealed class RoutingBasedOnHeadersTests : Steps, IDisposable +{ + private string _downstreamPath; + private readonly ServiceHandler _serviceHandler; + + public RoutingBasedOnHeadersTests() + { + _serviceHandler = new(); + } + + public override void Dispose() + { + _serviceHandler.Dispose(); + base.Dispose(); + } + + [Fact] + public void Should_match_one_header_value() + { + var port = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var headerValue = "PL"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, new() + { + [headerName] = headerValue, + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, headerValue)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello())) + .BDDfy(); + } + + [Fact] + public void Should_match_one_header_value_when_more_headers() + { + var port = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var headerValue = "PL"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, new() + { + [headerName] = headerValue, + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader("other", "otherValue")) + .And(x => GivenIAddAHeader(headerName, headerValue)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello())) + .BDDfy(); + } + + [Fact] + public void Should_match_two_header_values_when_more_headers() + { + var port = PortFinder.GetRandomPort(); + var headerName1 = "country_code"; + var headerValue1 = "PL"; + var headerName2 = "region"; + var headerValue2 = "MAZ"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, new() + { + [headerName1] = headerValue1, + [headerName2] = headerValue2, + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName1, headerValue1)) + .And(x => GivenIAddAHeader("other", "otherValue")) + .And(x => GivenIAddAHeader(headerName2, headerValue2)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello())) + .BDDfy(); + } + + [Fact] + public void Should_not_match_one_header_value() + { + var port = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var headerValue = "PL"; + var anotherHeaderValue = "UK"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, new() + { + [headerName] = headerValue, + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, anotherHeaderValue)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_not_match_one_header_value_when_no_headers() + { + var port = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var headerValue = "PL"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, new() + { + [headerName] = headerValue, + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_not_match_two_header_values_when_one_different() + { + var port = PortFinder.GetRandomPort(); + var headerName1 = "country_code"; + var headerValue1 = "PL"; + var headerName2 = "region"; + var headerValue2 = "MAZ"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, new() + { + [headerName1] = headerValue1, + [headerName2] = headerValue2, + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName1, headerValue1)) + .And(x => GivenIAddAHeader("other", "otherValue")) + .And(x => GivenIAddAHeader(headerName2, "anothervalue")) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_not_match_two_header_values_when_one_not_existing() + { + var port = PortFinder.GetRandomPort(); + var headerName1 = "country_code"; + var headerValue1 = "PL"; + var headerName2 = "region"; + var headerValue2 = "MAZ"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, new() + { + [headerName1] = headerValue1, + [headerName2] = headerValue2, + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName1, headerValue1)) + .And(x => GivenIAddAHeader("other", "otherValue")) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_not_match_one_header_value_when_header_duplicated() + { + var port = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var headerValue = "PL"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, new() + { + [headerName] = headerValue, + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, headerValue)) + .And(x => GivenIAddAHeader(headerName, "othervalue")) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_aggregated_route_match_header_value() + { + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var headerValue = "PL"; + var routeA = GivenRoute(port1, "/a", "Laura"); + var routeB = GivenRoute(port2, "/b", "Tom"); + var route = GivenAggRouteWithUpstreamHeaderTemplates(new() + { + [headerName] = headerValue, + }); + var configuration = GivenConfiguration(routeA, routeB); + configuration.Aggregates.Add(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port1, "/a", HttpStatusCode.OK, Hello("Laura"))) + .And(x => GivenThereIsAServiceRunningOn(port2, "/b", HttpStatusCode.OK, Hello("Tom"))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, headerValue)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void Should_aggregated_route_not_match_header_value() + { + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var headerValue = "PL"; + var routeA = GivenRoute(port1, "/a", "Laura"); + var routeB = GivenRoute(port2, "/b", "Tom"); + var route = GivenAggRouteWithUpstreamHeaderTemplates(new() + { + [headerName] = headerValue, + }); + var configuration = GivenConfiguration(routeA, routeB); + configuration.Aggregates.Add(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port1, "/a", HttpStatusCode.OK, Hello("Laura"))) + .And(x => x.GivenThereIsAServiceRunningOn(port2, "/b", HttpStatusCode.OK, Hello("Tom"))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_match_header_placeholder() + { + var port = PortFinder.GetRandomPort(); + var headerName = "Region"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, "/products", "/api.internal-{code}/products", + new() + { + [headerName] = "{header:code}", + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api.internal-uk/products", HttpStatusCode.OK, Hello("UK"))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, "uk")) + .When(x => WhenIGetUrlOnTheApiGateway("/products")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello("UK"))) + .BDDfy(); + } + + [Fact] + public void Should_match_header_placeholder_not_in_downstream_path() + { + var port = PortFinder.GetRandomPort(); + var headerName = "ProductName"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, "/products", "/products-info", + new() + { + [headerName] = "product-{header:everything}", + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/products-info", HttpStatusCode.OK, Hello("products"))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, "product-Camera")) + .When(x => WhenIGetUrlOnTheApiGateway("/products")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello("products"))) + .BDDfy(); + } + + [Fact] + public void Should_distinguish_route_for_different_roles() + { + var port = PortFinder.GetRandomPort(); + var headerName = "Origin"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, "/products", "/products-admin", + new() + { + [headerName] = "admin.xxx.com", + }); + var route2 = GivenRouteWithUpstreamHeaderTemplates(port, "/products", "/products", null); + var configuration = GivenConfiguration(route, route2); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/products-admin", HttpStatusCode.OK, Hello("products admin"))) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, "admin.xxx.com")) + .When(x => WhenIGetUrlOnTheApiGateway("/products")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello("products admin"))) + .BDDfy(); + } + + [Fact] + public void Should_match_header_and_url_placeholders() + { + var port = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, "/{aa}", "/{country_code}/{version}/{aa}", + new() + { + [headerName] = "start_{header:country_code}_version_{header:version}_end", + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/pl/v1/bb", HttpStatusCode.OK, Hello())) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, "start_pl_version_v1_end")) + .When(x => WhenIGetUrlOnTheApiGateway("/bb")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello())) + .BDDfy(); + } + + [Fact] + public void Should_match_header_with_braces() + { + var port = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, "/", "/aa", + new() + { + [headerName] = "my_{header}", + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/aa", HttpStatusCode.OK, Hello())) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, "my_{header}")) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello())) + .BDDfy(); + } + + [Fact] + public void Should_match_two_headers_with_the_same_name() + { + var port = PortFinder.GetRandomPort(); + var headerName = "country_code"; + var headerValue1 = "PL"; + var headerValue2 = "UK"; + var route = GivenRouteWithUpstreamHeaderTemplates(port, + new() + { + [headerName] = headerValue1 + ";{header:whatever}", + }); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIAddAHeader(headerName, headerValue1)) + .And(x => GivenIAddAHeader(headerName, headerValue2)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(Hello())) + .BDDfy(); + } + + private static string Hello() => Hello("Jolanta"); + private static string Hello(string who) => $"Hello from {who}"; + + private void GivenThereIsAServiceRunningOn(int port) + => GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, Hello()); + + private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody) + { + basePath ??= "/"; + responseBody ??= Hello(); + var baseUrl = DownstreamUrl(port); + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) + { + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync($"{nameof(_downstreamPath)} is not equal to {nameof(basePath)}"); + } + else + { + context.Response.StatusCode = (int)statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + } + + private void ThenTheDownstreamUrlPathShouldBe(string expected) => _downstreamPath.ShouldBe(expected); + + private static FileRoute GivenRoute(int port, string path = null, string key = null) => new() + { + DownstreamPathTemplate = path ?? "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new() + { + new("localhost", port), + }, + UpstreamPathTemplate = path ?? "/", + UpstreamHttpMethod = new() { HttpMethods.Get }, + Key = key, + }; + + private static FileRoute GivenRouteWithUpstreamHeaderTemplates(int port, Dictionary templates) + { + var route = GivenRoute(port); + route.UpstreamHeaderTemplates = templates; + return route; + } + + private static FileRoute GivenRouteWithUpstreamHeaderTemplates(int port, string upstream, string downstream, Dictionary templates) + { + var route = GivenRoute(port); + route.UpstreamHeaderTemplates = templates; + route.UpstreamPathTemplate = upstream ?? "/"; + route.DownstreamPathTemplate = downstream ?? "/"; + return route; + } + + private static FileAggregateRoute GivenAggRouteWithUpstreamHeaderTemplates(Dictionary templates) => new() + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = new() { "Laura", "Tom" }, + UpstreamHeaderTemplates = templates, + }; +} diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/Routing/RoutingTests.cs similarity index 97% rename from test/Ocelot.AcceptanceTests/RoutingTests.cs rename to test/Ocelot.AcceptanceTests/Routing/RoutingTests.cs index b881a831f..388e690d1 100644 --- a/test/Ocelot.AcceptanceTests/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/Routing/RoutingTests.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; -namespace Ocelot.AcceptanceTests +namespace Ocelot.AcceptanceTests.Routing { public sealed class RoutingTests : IDisposable { diff --git a/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs b/test/Ocelot.AcceptanceTests/Routing/RoutingWithQueryStringTests.cs similarity index 97% rename from test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs rename to test/Ocelot.AcceptanceTests/Routing/RoutingWithQueryStringTests.cs index 5c34167ac..ad4a16ae5 100644 --- a/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs +++ b/test/Ocelot.AcceptanceTests/Routing/RoutingWithQueryStringTests.cs @@ -1,379 +1,379 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; -namespace Ocelot.AcceptanceTests -{ - public class RoutingWithQueryStringTests : IDisposable - { - private readonly Steps _steps; - private readonly ServiceHandler _serviceHandler; - - public RoutingWithQueryStringTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - - [Fact] - public void Should_return_response_200_with_query_string_template() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}", "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/{unitId}/updates")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } +namespace Ocelot.AcceptanceTests.Routing +{ + public class RoutingWithQueryStringTests : IDisposable + { + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public RoutingWithQueryStringTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void Should_return_response_200_with_query_string_template() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}", "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/{unitId}/updates")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } [Theory(DisplayName = "1182: " + nameof(Should_return_200_with_query_string_template_different_keys))] - [InlineData("")] - [InlineData("&x=xxx")] - public void Should_return_200_with_query_string_template_different_keys(string additionalParams) + [InlineData("")] + [InlineData("&x=xxx")] + public void Should_return_200_with_query_string_template_different_keys(string additionalParams) { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unit}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/units/{subscriptionId}/updates?unit={unit}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}{additionalParams}", "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/updates?unit={unitId}{additionalParams}")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unit}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/api/units/{subscriptionId}/updates?unit={unit}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}{additionalParams}", "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/updates?unit={unitId}{additionalParams}")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + [Theory(DisplayName = "1174: " + nameof(Should_return_200_and_forward_query_parameters_without_duplicates))] - [InlineData("projectNumber=45&startDate=2019-12-12&endDate=2019-12-12", "endDate=2019-12-12&projectNumber=45&startDate=2019-12-12")] - [InlineData("$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z", "$filter=ProjectNumber%20eq%2045%20and%20DateOfSale%20ge%202020-03-01T00:00:00z%20and%20DateOfSale%20le%202020-03-15T00:00:00z")] - public void Should_return_200_and_forward_query_parameters_without_duplicates(string everythingelse, string expectedOrdered) + [InlineData("projectNumber=45&startDate=2019-12-12&endDate=2019-12-12", "endDate=2019-12-12&projectNumber=45&startDate=2019-12-12")] + [InlineData("$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z", "$filter=ProjectNumber%20eq%2045%20and%20DateOfSale%20ge%202020-03-01T00:00:00z%20and%20DateOfSale%20le%202020-03-15T00:00:00z")] + public void Should_return_200_and_forward_query_parameters_without_duplicates(string everythingelse, string expectedOrdered) + { + var port = PortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/contracts?{everythingelse}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() { Host = "localhost", Port = port }, + }, + UpstreamPathTemplate = "/contracts?{everythingelse}", + UpstreamHttpMethod = new() { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/contracts", $"?{expectedOrdered}", "Hello from @sunilk3")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/contracts?{everythingelse}")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from @sunilk3")) + .BDDfy(); + } + + [Fact] + public void Should_return_response_200_with_odata_query_string() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{everything}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/{everything}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/odata/customers", "?$filter=Name%20eq%20'Sam'", "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/odata/customers?$filter=Name eq 'Sam' ")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void Should_return_response_200_with_query_string_upstream_template() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void Should_return_response_404_with_query_string_upstream_template_no_query_string() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_return_response_404_with_query_string_upstream_template_different_query_string() { - var port = PortFinder.GetRandomPort(); - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/contracts?{everythingelse}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() { Host = "localhost", Port = port }, - }, - UpstreamPathTemplate = "/contracts?{everythingelse}", - UpstreamHttpMethod = new() { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/contracts", $"?{expectedOrdered}", "Hello from @sunilk3")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/contracts?{everythingelse}")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from @sunilk3")) - .BDDfy(); - } + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?test=1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_return_response_200_with_query_string_upstream_template_multiple_params() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", "?productId=1", "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}&productId=1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } - [Fact] - public void Should_return_response_200_with_odata_query_string() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/{everything}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/{everything}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/odata/customers", "?$filter=Name%20eq%20'Sam'", "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/odata/customers?$filter=Name eq 'Sam' ")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void Should_return_response_200_with_query_string_upstream_template() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void Should_return_response_404_with_query_string_upstream_template_no_query_string() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - [Fact] - public void Should_return_response_404_with_query_string_upstream_template_different_query_string() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?test=1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - [Fact] - public void Should_return_response_200_with_query_string_upstream_template_multiple_params() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", "?productId=1", "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}&productId=1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - /// /// To reproduce 1288: query string should contain the placeholder name and value. /// - [Fact(DisplayName = "1288: " + nameof(Should_copy_query_string_to_downstream_path))] - public void Should_copy_query_string_to_downstream_path() + [Fact(DisplayName = "1288: " + nameof(Should_copy_query_string_to_downstream_path))] + public void Should_copy_query_string_to_downstream_path() { - var idName = "id"; + var idName = "id"; var idValue = "3"; var queryName = idName + "1"; - var queryValue = "2" + idValue + "12"; - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new FileRoute - { - DownstreamPathTemplate = $"/cpx/t1/{{{idName}}}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() { Host = "localhost", Port = port }, - }, - UpstreamPathTemplate = $"/safe/{{{idName}}}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/cpx/t1/{idValue}", $"?{queryName}={queryValue}", "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/safe/{idValue}?{queryName}={queryValue}")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); + var queryValue = "2" + idValue + "12"; + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new FileRoute + { + DownstreamPathTemplate = $"/cpx/t1/{{{idName}}}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() { Host = "localhost", Port = port }, + }, + UpstreamPathTemplate = $"/safe/{{{idName}}}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/cpx/t1/{idValue}", $"?{queryName}={queryValue}", "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/safe/{idValue}?{queryName}={queryValue}")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); } - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string queryString, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - if ((context.Request.PathBase.Value != basePath) || context.Request.QueryString.Value != queryString) - { - context.Response.StatusCode = StatusCodes.Status500InternalServerError; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = StatusCodes.Status200OK; - await context.Response.WriteAsync(responseBody); - } - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string queryString, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + if (context.Request.PathBase.Value != basePath || context.Request.QueryString.Value != queryString) + { + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = StatusCodes.Status200OK; + await context.Response.WriteAsync(responseBody); + } + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); _steps.Dispose(); - GC.SuppressFinalize(this); - } - } -} + GC.SuppressFinalize(this); + } + } +} diff --git a/test/Ocelot.Testing/PortFinder.cs b/test/Ocelot.Testing/PortFinder.cs index 4b661ada7..6eb6b64d4 100644 --- a/test/Ocelot.Testing/PortFinder.cs +++ b/test/Ocelot.Testing/PortFinder.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Net; using System.Net.Sockets; diff --git a/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs index 469e3d7f2..3d4c3ff63 100644 --- a/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs @@ -9,17 +9,21 @@ namespace Ocelot.UnitTests.Configuration public class AggregatesCreatorTests : UnitTest { private readonly AggregatesCreator _creator; - private readonly Mock _utpCreator; + private readonly Mock _utpCreator; + private readonly Mock _uhtpCreator; private FileConfiguration _fileConfiguration; private List _routes; private List _result; private UpstreamPathTemplate _aggregate1Utp; - private UpstreamPathTemplate _aggregate2Utp; + private UpstreamPathTemplate _aggregate2Utp; + private Dictionary _headerTemplates1; + private Dictionary _headerTemplates2; public AggregatesCreatorTests() { - _utpCreator = new Mock(); - _creator = new AggregatesCreator(_utpCreator.Object); + _utpCreator = new Mock(); + _uhtpCreator = new Mock(); + _creator = new AggregatesCreator(_utpCreator.Object, _uhtpCreator.Object); } [Fact] @@ -82,7 +86,8 @@ public void should_create_aggregates() this.Given(_ => GivenThe(fileConfig)) .And(_ => GivenThe(routes)) - .And(_ => GivenTheUtpCreatorReturns()) + .And(_ => GivenTheUtpCreatorReturns()) + .And(_ => GivenTheUhtpCreatorReturns()) .When(_ => WhenICreate()) .Then(_ => ThenTheUtpCreatorIsCalledCorrectly()) .And(_ => ThenTheAggregatesAreCreated()) @@ -96,14 +101,16 @@ private void ThenTheAggregatesAreCreated() _result[0].UpstreamHttpMethod.ShouldContain(x => x == HttpMethod.Get); _result[0].UpstreamHost.ShouldBe(_fileConfiguration.Aggregates[0].UpstreamHost); - _result[0].UpstreamTemplatePattern.ShouldBe(_aggregate1Utp); + _result[0].UpstreamTemplatePattern.ShouldBe(_aggregate1Utp); + _result[0].UpstreamHeaderTemplates.ShouldBe(_headerTemplates1); _result[0].Aggregator.ShouldBe(_fileConfiguration.Aggregates[0].Aggregator); _result[0].DownstreamRoute.ShouldContain(x => x == _routes[0].DownstreamRoute[0]); _result[0].DownstreamRoute.ShouldContain(x => x == _routes[1].DownstreamRoute[0]); _result[1].UpstreamHttpMethod.ShouldContain(x => x == HttpMethod.Get); _result[1].UpstreamHost.ShouldBe(_fileConfiguration.Aggregates[1].UpstreamHost); - _result[1].UpstreamTemplatePattern.ShouldBe(_aggregate2Utp); + _result[1].UpstreamTemplatePattern.ShouldBe(_aggregate2Utp); + _result[1].UpstreamHeaderTemplates.ShouldBe(_headerTemplates2); _result[1].Aggregator.ShouldBe(_fileConfiguration.Aggregates[1].Aggregator); _result[1].DownstreamRoute.ShouldContain(x => x == _routes[2].DownstreamRoute[0]); _result[1].DownstreamRoute.ShouldContain(x => x == _routes[3].DownstreamRoute[0]); @@ -123,6 +130,16 @@ private void GivenTheUtpCreatorReturns() _utpCreator.SetupSequence(x => x.Create(It.IsAny())) .Returns(_aggregate1Utp) .Returns(_aggregate2Utp); + } + + private void GivenTheUhtpCreatorReturns() + { + _headerTemplates1 = new Dictionary(); + _headerTemplates2 = new Dictionary(); + + _uhtpCreator.SetupSequence(x => x.Create(It.IsAny())) + .Returns(_headerTemplates1) + .Returns(_headerTemplates2); } private void ThenTheResultIsEmpty() diff --git a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs index 11b5fc3e0..c9fc698ff 100644 --- a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs @@ -12,6 +12,7 @@ public class RoutesCreatorTests : UnitTest private readonly Mock _cthCreator; private readonly Mock _aoCreator; private readonly Mock _utpCreator; + private readonly Mock _uhtpCreator; private readonly Mock _ridkCreator; private readonly Mock _qosoCreator; private readonly Mock _rroCreator; @@ -39,7 +40,8 @@ public class RoutesCreatorTests : UnitTest private List _dhp; private LoadBalancerOptions _lbo; private List _result; - private Version _expectedVersion; + private Version _expectedVersion; + private Dictionary _uht; public RoutesCreatorTests() { @@ -58,6 +60,7 @@ public RoutesCreatorTests() _rrkCreator = new Mock(); _soCreator = new Mock(); _versionCreator = new Mock(); + _uhtpCreator = new Mock(); _creator = new RoutesCreator( _cthCreator.Object, @@ -74,7 +77,8 @@ public RoutesCreatorTests() _lboCreator.Object, _rrkCreator.Object, _soCreator.Object, - _versionCreator.Object + _versionCreator.Object, + _uhtpCreator.Object ); } @@ -165,7 +169,8 @@ private void GivenTheDependenciesAreSetUpCorrectly() _hho = new HttpHandlerOptionsBuilder().Build(); _ht = new HeaderTransformations(new List(), new List(), new List(), new List()); _dhp = new List(); - _lbo = new LoadBalancerOptionsBuilder().Build(); + _lbo = new LoadBalancerOptionsBuilder().Build(); + _uht = new Dictionary(); _rroCreator.Setup(x => x.Create(It.IsAny())).Returns(_rro); _ridkCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_requestId); @@ -180,7 +185,8 @@ private void GivenTheDependenciesAreSetUpCorrectly() _hfarCreator.Setup(x => x.Create(It.IsAny())).Returns(_ht); _daCreator.Setup(x => x.Create(It.IsAny())).Returns(_dhp); _lboCreator.Setup(x => x.Create(It.IsAny())).Returns(_lbo); - _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersion); + _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersion); + _uhtpCreator.Setup(x => x.Create(It.IsAny())).Returns(_uht); } private void ThenTheRoutesAreCreated() @@ -249,7 +255,8 @@ private void ThenTheRouteIsSet(FileRoute expected, int routeIndex) .ShouldContain(x => x == expected.UpstreamHttpMethod[1]); _result[routeIndex].UpstreamHost.ShouldBe(expected.UpstreamHost); _result[routeIndex].DownstreamRoute.Count.ShouldBe(1); - _result[routeIndex].UpstreamTemplatePattern.ShouldBe(_upt); + _result[routeIndex].UpstreamTemplatePattern.ShouldBe(_upt); + _result[routeIndex].UpstreamHeaderTemplates.ShouldBe(_uht); } private void ThenTheDepsAreCalledFor(FileRoute fileRoute, FileGlobalConfiguration globalConfig) diff --git a/test/Ocelot.UnitTests/Configuration/UpstreamHeaderTemplatePatternCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/UpstreamHeaderTemplatePatternCreatorTests.cs new file mode 100644 index 000000000..49cf841ac --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/UpstreamHeaderTemplatePatternCreatorTests.cs @@ -0,0 +1,45 @@ +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Values; + +namespace Ocelot.UnitTests.Configuration; + +public class UpstreamHeaderTemplatePatternCreatorTests +{ + private readonly UpstreamHeaderTemplatePatternCreator _creator; + + public UpstreamHeaderTemplatePatternCreatorTests() + { + _creator = new(); + } + + [Trait("PR", "1312")] + [Trait("Feat", "360")] + [Theory(DisplayName = "Should create pattern")] + [InlineData("country", "a text without placeholders", "^(?i)a text without placeholders$", " without placeholders")] + [InlineData("country", "a text without placeholders", "^a text without placeholders$", " Route is case sensitive", true)] + [InlineData("country", "{header:start}rest of the text", "^(?i)(?.+)rest of the text$", " with placeholder in the beginning")] + [InlineData("country", "rest of the text{header:end}", "^(?i)rest of the text(?.+)$", " with placeholder at the end")] + [InlineData("country", "{header:countrycode}", "^(?i)(?.+)$", " with placeholder only")] + [InlineData("country", "any text {header:cc} and other {header:version} and {header:bob} the end", "^(?i)any text (?.+) and other (?.+) and (?.+) the end$", " with more placeholders")] + public void Create_WithUpstreamHeaderTemplates_ShouldCreatePattern(string key, string template, string expected, string withMessage, bool? isCaseSensitive = null) + { + // Arrange + var fileRoute = new FileRoute + { + RouteIsCaseSensitive = isCaseSensitive ?? false, + UpstreamHeaderTemplates = new Dictionary + { + [key] = template, + }, + }; + + // Act + var actual = _creator.Create(fileRoute); + + // Assert + var message = nameof(Create_WithUpstreamHeaderTemplates_ShouldCreatePattern).Replace('_', ' ') + withMessage; + actual[key].ShouldNotBeNull() + .Template.ShouldBe(expected, message); + } +} diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs index 3a87dec2f..f3998c878 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs @@ -536,7 +536,7 @@ public void Configuration_is_valid_with_duplicate_routes_but_one_upstreamhost_is .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsValid()) .BDDfy(); - } + } [Fact] public void Configuration_is_invalid_with_invalid_rate_limit_configuration() @@ -718,6 +718,102 @@ public void Configuration_is_not_valid_when_host_and_port_is_empty() .Then(x => x.ThenTheResultIsNotValid()) .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!")) .BDDfy(); + } + + [Fact] + [Trait("PR", "1312")] + [Trait("Feat", "360")] + public void Configuration_is_not_valid_when_upstream_headers_the_same() + { + // Arrange + var route1 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/api/products/", new() + { + { "header1", "value1" }, + { "header2", "value2" }, + }); + var route2 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/www/test/", new() + { + { "header2", "value2" }, + { "header1", "value1" }, + }); + GivenAConfiguration(route1, route2); + + // Act + WhenIValidateTheConfiguration(); + + // Assert + ThenTheResultIsNotValid(); + ThenTheErrorMessageAtPositionIs(0, "route /asdf/ has duplicate"); + } + + [Fact] + [Trait("PR", "1312")] + [Trait("Feat", "360")] + public void Configuration_is_valid_when_upstream_headers_not_the_same() + { + // Arrange + var route1 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/api/products/", new() + { + { "header1", "value1" }, + { "header2", "value2" }, + }); + var route2 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/www/test/", new() + { + { "header2", "value2" }, + { "header1", "valueDIFFERENT" }, + }); + GivenAConfiguration(route1, route2); + + // Act + WhenIValidateTheConfiguration(); + + // Assert + ThenTheResultIsValid(); + } + + [Fact] + [Trait("PR", "1312")] + [Trait("Feat", "360")] + public void Configuration_is_valid_when_upstream_headers_count_not_the_same() + { + // Arrange + var route1 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/api/products/", new() + { + { "header1", "value1" }, + { "header2", "value2" }, + }); + var route2 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/www/test/", new() + { + { "header2", "value2" }, + }); + GivenAConfiguration(route1, route2); + + // Act + WhenIValidateTheConfiguration(); + + // Assert + ThenTheResultIsValid(); + } + + [Fact] + [Trait("PR", "1312")] + [Trait("Feat", "360")] + public void Configuration_is_valid_when_one_upstream_headers_empty_and_other_not_empty() + { + // Arrange + var route1 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/api/products/", new() + { + { "header1", "value1" }, + { "header2", "value2" }, + }); + var route2 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/www/test/", new()); + GivenAConfiguration(route1, route2); + + // Act + WhenIValidateTheConfiguration(); + + // Assert + ThenTheResultIsValid(); } [Theory] @@ -804,6 +900,18 @@ public void Configuration_is_invalid_when_placeholder_is_used_twice_in_downstrea DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, ServiceName = "test", + }; + + private static FileRoute GivenRouteWithUpstreamHeaderTemplates(string upstream, string downstream, Dictionary templates) => new() + { + UpstreamPathTemplate = upstream, + DownstreamPathTemplate = downstream, + DownstreamHostAndPorts = new() + { + new("bbc.co.uk", 123), + }, + UpstreamHttpMethod = new() { HttpMethods.Get }, + UpstreamHeaderTemplates = templates, }; private void GivenAConfiguration(FileConfiguration fileConfiguration) => _fileConfiguration = fileConfiguration; @@ -902,12 +1010,10 @@ private class TestHandler : AuthenticationHandler // It can be set directly or by registering a provider in the dependency injection container. #if NET8_0_OR_GREATER public TestHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) - { - } + { } #else public TestHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) - { - } + { } #endif protected override Task HandleAuthenticateAsync() diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs index 86ba4078a..f4cb323f4 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs @@ -16,7 +16,8 @@ public class DownstreamRouteCreatorTests : UnitTest private Response _result; private string _upstreamHost; private string _upstreamUrlPath; - private string _upstreamHttpMethod; + private string _upstreamHttpMethod; + private Dictionary _upstreamHeaders; private IInternalConfiguration _configuration; private readonly Mock _qosOptionsCreator; private Response _resultTwo; @@ -259,7 +260,8 @@ private void GivenTheConfiguration(IInternalConfiguration config) { _upstreamHost = "doesnt matter"; _upstreamUrlPath = "/auth/test"; - _upstreamHttpMethod = "GET"; + _upstreamHttpMethod = "GET"; + _upstreamHeaders = new Dictionary(); _configuration = config; } @@ -278,12 +280,12 @@ private void ThenTheHandlerOptionsAreSet() private void WhenICreate() { - _result = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost); + _result = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost, _upstreamHeaders); } private void WhenICreateAgain() { - _resultTwo = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost); + _resultTwo = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost, _upstreamHeaders); } private void ThenTheDownstreamRoutesAreTheSameReference() diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs index 6d331a36e..eb6ebe68f 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs @@ -74,7 +74,7 @@ private void GivenTheDownStreamRouteFinderReturns(DownstreamRouteHolder downstre { _downstreamRoute = new OkResponse(downstreamRoute); _finder - .Setup(x => x.Get(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.Get(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) .Returns(_downstreamRoute); } diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index e9b2bf8ed..ec6026a5b 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -2,31 +2,37 @@ using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.Finder; +using Ocelot.DownstreamRouteFinder.HeaderMatcher; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Responses; using Ocelot.Values; namespace Ocelot.UnitTests.DownstreamRouteFinder -{ +{ public class DownstreamRouteFinderTests : UnitTest { private readonly IDownstreamRouteProvider _downstreamRouteFinder; - private readonly Mock _mockMatcher; - private readonly Mock _finder; + private readonly Mock _mockUrlMatcher; + private readonly Mock _mockHeadersMatcher; + private readonly Mock _urlPlaceholderFinder; + private readonly Mock _headerPlaceholderFinder; private string _upstreamUrlPath; private Response _result; private List _routesConfig; private InternalConfiguration _config; private Response _match; private string _upstreamHttpMethod; - private string _upstreamHost; + private string _upstreamHost; + private Dictionary _upstreamHeaders; private string _upstreamQuery; public DownstreamRouteFinderTests() { - _mockMatcher = new Mock(); - _finder = new Mock(); - _downstreamRouteFinder = new Ocelot.DownstreamRouteFinder.Finder.DownstreamRouteFinder(_mockMatcher.Object, _finder.Object); + _mockUrlMatcher = new Mock(); + _mockHeadersMatcher = new Mock(); + _urlPlaceholderFinder = new Mock(); + _headerPlaceholderFinder = new Mock(); + _downstreamRouteFinder = new Ocelot.DownstreamRouteFinder.Finder.DownstreamRouteFinder(_mockUrlMatcher.Object, _urlPlaceholderFinder.Object, _mockHeadersMatcher.Object, _headerPlaceholderFinder.Object); } [Fact] @@ -37,6 +43,7 @@ public void should_return_highest_priority_when_first() this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -58,7 +65,8 @@ public void should_return_highest_priority_when_first() .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 0, false, "someUpstreamPath")) .Build(), }, string.Empty, serviceProviderConfig)) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) .When(x => x.WhenICallTheFinder()) .Then(x => x.ThenTheFollowingIsReturned(new DownstreamRouteHolder(new List(), @@ -83,6 +91,7 @@ public void should_return_highest_priority_when_lowest() this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -104,7 +113,8 @@ public void should_return_highest_priority_when_lowest() .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) .Build(), }, string.Empty, serviceProviderConfig)) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) .When(x => x.WhenICallTheFinder()) .Then(x => x.ThenTheFollowingIsReturned(new DownstreamRouteHolder(new List(), @@ -130,6 +140,7 @@ public void should_return_route() .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>( new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -143,7 +154,8 @@ public void should_return_route() .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then( @@ -172,6 +184,8 @@ public void should_not_append_slash_to_upstream_url_path() .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>( new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -185,7 +199,8 @@ public void should_not_append_slash_to_upstream_url_path() .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then( @@ -215,6 +230,7 @@ public void should_return_route_if_upstream_path_and_upstream_template_are_the_s x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -228,7 +244,8 @@ public void should_return_route_if_upstream_path_and_upstream_template_are_the_s .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then( @@ -256,6 +273,7 @@ public void should_return_correct_route_for_http_verb() x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -278,7 +296,8 @@ public void should_return_correct_route_for_http_verb() .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) .When(x => x.WhenICallTheFinder()) .Then( @@ -315,7 +334,8 @@ public void should_not_return_route() .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(false)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(false)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then( @@ -334,6 +354,7 @@ public void should_return_correct_route_for_http_verb_setting_multiple_upstream_ x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -347,7 +368,8 @@ public void should_return_correct_route_for_http_verb_setting_multiple_upstream_ .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) .When(x => x.WhenICallTheFinder()) .Then( @@ -375,6 +397,7 @@ public void should_return_correct_route_for_http_verb_setting_all_upstream_http_ x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -388,7 +411,8 @@ public void should_return_correct_route_for_http_verb_setting_all_upstream_http_ .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) .When(x => x.WhenICallTheFinder()) .Then( @@ -416,6 +440,7 @@ public void should_not_return_route_for_http_verb_not_setting_in_upstream_http_m x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -429,7 +454,8 @@ public void should_not_return_route_for_http_verb_not_setting_in_upstream_http_m .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) .When(x => x.WhenICallTheFinder()) .Then(x => x.ThenAnErrorResponseIsReturned()) @@ -447,6 +473,7 @@ public void should_return_route_when_host_matches() .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>( new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -461,7 +488,8 @@ public void should_return_route_when_host_matches() .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then( @@ -491,6 +519,7 @@ public void should_return_route_when_upstreamhost_is_null() .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>( new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -504,7 +533,8 @@ public void should_return_route_when_upstreamhost_is_null() .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then( @@ -532,6 +562,7 @@ public void should_not_return_route_when_host_doesnt_match() this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) .And(x => GivenTheUpstreamHostIs("DONTMATCH")) .And(x => x.GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -556,7 +587,8 @@ public void should_not_return_route_when_host_doesnt_match() .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then(x => x.ThenAnErrorResponseIsReturned()) @@ -572,6 +604,7 @@ public void should_not_return_route_when_host_doesnt_match_with_empty_upstream_h this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) .And(x => GivenTheUpstreamHostIs("DONTMATCH")) .And(x => x.GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -586,7 +619,8 @@ public void should_not_return_route_when_host_doesnt_match_with_empty_upstream_h .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then(x => x.ThenAnErrorResponseIsReturned()) @@ -602,6 +636,7 @@ public void should_return_route_when_host_does_match_with_empty_upstream_http_me this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) .And(x => GivenTheUpstreamHostIs("MATCH")) .And(x => x.GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -616,7 +651,8 @@ public void should_return_route_when_host_does_match_with_empty_upstream_http_me .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .And(x => x.ThenTheUrlMatcherIsCalledCorrectly(1, 0)) @@ -633,6 +669,7 @@ public void should_return_route_when_host_matches_but_null_host_on_same_path_fir .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( new OkResponse>( new List()))) + .And(x => x.GivenTheHeaderPlaceholderAndNameFinderReturns(new List())) .And(x => x.GivenTheConfigurationIs(new List { new RouteBuilder() @@ -656,7 +693,8 @@ public void should_return_route_when_host_matches_but_null_host_on_same_path_fir .Build(), }, string.Empty, serviceProviderConfig )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheHeadersMatcherReturns(true)) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) .When(x => x.WhenICallTheFinder()) .Then( @@ -675,8 +713,124 @@ public void should_return_route_when_host_matches_but_null_host_on_same_path_fir .And(x => x.ThenTheUrlMatcherIsCalledCorrectly(1, 0)) .And(x => x.ThenTheUrlMatcherIsCalledCorrectly(1, 1)) .BDDfy(); - } - + } + + [Fact] + [Trait("PR", "1312")] + [Trait("Feat", "360")] + public void Should_return_route_when_upstream_headers_match() + { + // Arrange + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + var upstreamHeaders = new Dictionary() + { + ["header1"] = "headerValue1", + ["header2"] = "headerValue2", + ["header3"] = "headerValue3", + }; + var upstreamHeadersConfig = new Dictionary() + { + ["header1"] = new UpstreamHeaderTemplate("headerValue1", "headerValue1"), + ["header2"] = new UpstreamHeaderTemplate("headerValue2", "headerValue2"), + }; + var urlPlaceholders = new List { new PlaceholderNameAndValue("url", "urlValue") }; + var headerPlaceholders = new List { new PlaceholderNameAndValue("header", "headerValue") }; + + GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/"); + GivenTheUpstreamHeadersIs(upstreamHeaders); + GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(urlPlaceholders)); + GivenTheHeaderPlaceholderAndNameFinderReturns(headerPlaceholders); + GivenTheConfigurationIs( + new() + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new() {"Get"}) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new() {"Get"}) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .WithUpstreamHeaders(upstreamHeadersConfig) + .Build(), + }, + string.Empty, + serviceProviderConfig); + GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true))); + GivenTheHeadersMatcherReturns(true); + GivenTheUpstreamHttpMethodIs("Get"); + + // Act + WhenICallTheFinder(); + + // Assert + ThenTheFollowingIsReturned(new DownstreamRouteHolder( + urlPlaceholders.Union(headerPlaceholders).ToList(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new() { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new() { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build() + )); + ThenTheUrlMatcherIsCalledCorrectly(); + } + + [Fact] + [Trait("PR", "1312")] + [Trait("Feat", "360")] + public void Should_not_return_route_when_upstream_headers_dont_match() + { + // Arrange + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + var upstreamHeadersConfig = new Dictionary() + { + ["header1"] = new UpstreamHeaderTemplate("headerValue1", "headerValue1"), + ["header2"] = new UpstreamHeaderTemplate("headerValue2", "headerValue2"), + }; + + GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/"); + GivenTheUpstreamHeadersIs(new Dictionary() { { "header1", "headerValue1" } }); + GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List())); + GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); + GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new() { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new() { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .WithUpstreamHeaders(upstreamHeadersConfig) + .Build(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new() { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new() { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .WithUpstreamHeaders(upstreamHeadersConfig) + .Build(), + }, string.Empty, serviceProviderConfig + ); + GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true))); + GivenTheHeadersMatcherReturns(false); + GivenTheUpstreamHttpMethodIs("Get"); + + // Act + WhenICallTheFinder(); + + // Assert + ThenAnErrorResponseIsReturned(); + } + private void GivenTheUpstreamHostIs(string upstreamHost) { _upstreamHost = upstreamHost; @@ -684,14 +838,26 @@ private void GivenTheUpstreamHostIs(string upstreamHost) private void GivenTheTemplateVariableAndNameFinderReturns(Response> response) { - _finder + _urlPlaceholderFinder .Setup(x => x.Find(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response); } + private void GivenTheHeaderPlaceholderAndNameFinderReturns(List placeholders) + { + _headerPlaceholderFinder + .Setup(x => x.Find(It.IsAny>(), It.IsAny>())) + .Returns(placeholders); + } + private void GivenTheUpstreamHttpMethodIs(string upstreamHttpMethod) { _upstreamHttpMethod = upstreamHttpMethod; + } + + private void GivenTheUpstreamHeadersIs(Dictionary upstreamHeaders) + { + _upstreamHeaders = upstreamHeaders; } private void ThenAnErrorResponseIsReturned() @@ -701,34 +867,41 @@ private void ThenAnErrorResponseIsReturned() private void ThenTheUrlMatcherIsCalledCorrectly() { - _mockMatcher + _mockUrlMatcher .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _routesConfig[0].UpstreamTemplatePattern), Times.Once); } private void ThenTheUrlMatcherIsCalledCorrectly(int times, int index = 0) { - _mockMatcher + _mockUrlMatcher .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _routesConfig[index].UpstreamTemplatePattern), Times.Exactly(times)); } private void ThenTheUrlMatcherIsCalledCorrectly(string expectedUpstreamUrlPath) { - _mockMatcher + _mockUrlMatcher .Verify(x => x.Match(expectedUpstreamUrlPath, _upstreamQuery, _routesConfig[0].UpstreamTemplatePattern), Times.Once); } private void ThenTheUrlMatcherIsNotCalled() { - _mockMatcher + _mockUrlMatcher .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _routesConfig[0].UpstreamTemplatePattern), Times.Never); } private void GivenTheUrlMatcherReturns(Response match) { _match = match; - _mockMatcher + _mockUrlMatcher .Setup(x => x.Match(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_match); + } + + private void GivenTheHeadersMatcherReturns(bool headersMatch) + { + _mockHeadersMatcher + .Setup(x => x.Match(It.IsAny>(), It.IsAny>())) + .Returns(headersMatch); } private void GivenTheConfigurationIs(List routesConfig, string adminPath, ServiceProviderConfiguration serviceProviderConfig) @@ -745,7 +918,7 @@ private void GivenThereIsAnUpstreamUrlPath(string upstreamUrlPath) private void WhenICallTheFinder() { - _result = _downstreamRouteFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost); + _result = _downstreamRouteFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); } private void ThenTheFollowingIsReturned(DownstreamRouteHolder expected) diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs index db89a5eeb..20426362a 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs @@ -2,6 +2,7 @@ using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; +using Ocelot.DownstreamRouteFinder.HeaderMatcher; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Logging; @@ -21,7 +22,9 @@ public DownstreamRouteProviderFactoryTests() { var services = new ServiceCollection(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinderTests.cs new file mode 100644 index 000000000..b1a767602 --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinderTests.cs @@ -0,0 +1,220 @@ +using Ocelot.DownstreamRouteFinder.HeaderMatcher; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Values; + +namespace Ocelot.UnitTests.DownstreamRouteFinder.HeaderMatcher; + +[Trait("PR", "1312")] +[Trait("Feat", "360")] +public class HeaderPlaceholderNameAndValueFinderTests : UnitTest +{ + private readonly IHeaderPlaceholderNameAndValueFinder _finder; + private Dictionary _upstreamHeaders; + private Dictionary _upstreamHeaderTemplates; + private List _result; + + public HeaderPlaceholderNameAndValueFinderTests() + { + _finder = new HeaderPlaceholderNameAndValueFinder(); + } + + [Fact] + public void Should_return_no_placeholders() + { + // Arrange + var upstreamHeaderTemplates = new Dictionary(); + var upstreamHeaders = new Dictionary(); + var expected = new List(); + GivenUpstreamHeaderTemplatesAre(upstreamHeaderTemplates); + GivenUpstreamHeadersAre(upstreamHeaders); + + // Act + WhenICallFindPlaceholders(); + + // Assert + TheResultIs(expected); + } + + [Fact] + public void Should_return_one_placeholder_with_value_when_no_other_text() + { + // Arrange + var upstreamHeaderTemplates = new Dictionary + { + ["country"] = new("^(?i)(?.+)$", "{header:countrycode}"), + }; + var upstreamHeaders = new Dictionary + { + ["country"] = "PL", + }; + var expected = new List + { + new("{countrycode}", "PL"), + }; + GivenUpstreamHeaderTemplatesAre(upstreamHeaderTemplates); + GivenUpstreamHeadersAre(upstreamHeaders); + + // Act + WhenICallFindPlaceholders(); + + // Assert + TheResultIs(expected); + } + + [Fact] + public void Should_return_one_placeholder_with_value_when_other_text_on_the_right() + { + // Arrange + var upstreamHeaderTemplates = new Dictionary + { + ["country"] = new("^(?.+)-V1$", "{header:countrycode}-V1"), + }; + var upstreamHeaders = new Dictionary + { + ["country"] = "PL-V1", + }; + var expected = new List + { + new("{countrycode}", "PL"), + }; + GivenUpstreamHeaderTemplatesAre(upstreamHeaderTemplates); + GivenUpstreamHeadersAre(upstreamHeaders); + + // Act + WhenICallFindPlaceholders(); + + // Assert + TheResultIs(expected); + } + + [Fact] + public void Should_return_one_placeholder_with_value_when_other_text_on_the_left() + { + // Arrange + var upstreamHeaderTemplates = new Dictionary + { + ["country"] = new("^V1-(?.+)$", "V1-{header:countrycode}"), + }; + var upstreamHeaders = new Dictionary + { + ["country"] = "V1-PL", + }; + var expected = new List + { + new("{countrycode}", "PL"), + }; + GivenUpstreamHeaderTemplatesAre(upstreamHeaderTemplates); + GivenUpstreamHeadersAre(upstreamHeaders); + + // Act + WhenICallFindPlaceholders(); + + // Assert + TheResultIs(expected); + } + + [Fact] + public void Should_return_one_placeholder_with_value_when_other_texts_surrounding() + { + // Arrange + var upstreamHeaderTemplates = new Dictionary + { + ["country"] = new("^cc:(?.+)-V1$", "cc:{header:countrycode}-V1"), + }; + var upstreamHeaders = new Dictionary + { + ["country"] = "cc:PL-V1", + }; + var expected = new List + { + new("{countrycode}", "PL"), + }; + GivenUpstreamHeaderTemplatesAre(upstreamHeaderTemplates); + GivenUpstreamHeadersAre(upstreamHeaders); + + // Act + WhenICallFindPlaceholders(); + + // Assert + TheResultIs(expected); + } + + [Fact] + public void Should_return_two_placeholders_with_text_between() + { + // Arrange + var upstreamHeaderTemplates = new Dictionary + { + ["countryAndVersion"] = new("^(?i)(?.+)-(?.+)$", "{header:countrycode}-{header:version}"), + }; + var upstreamHeaders = new Dictionary + { + ["countryAndVersion"] = "PL-v1", + }; + var expected = new List + { + new("{countrycode}", "PL"), + new("{version}", "v1"), + }; + GivenUpstreamHeaderTemplatesAre(upstreamHeaderTemplates); + GivenUpstreamHeadersAre(upstreamHeaders); + + // Act + WhenICallFindPlaceholders(); + + // Assert + TheResultIs(expected); + } + + [Fact] + public void Should_return_placeholders_from_different_headers() + { + // Arrange + var upstreamHeaderTemplates = new Dictionary + { + ["country"] = new("^(?i)(?.+)$", "{header:countrycode}"), + ["version"] = new("^(?i)(?.+)$", "{header:version}"), + }; + var upstreamHeaders = new Dictionary + { + ["country"] = "PL", + ["version"] = "v1", + }; + var expected = new List + { + new("{countrycode}", "PL"), + new("{version}", "v1"), + }; + GivenUpstreamHeaderTemplatesAre(upstreamHeaderTemplates); + GivenUpstreamHeadersAre(upstreamHeaders); + + // Act + WhenICallFindPlaceholders(); + + // Assert + TheResultIs(expected); + } + + private void GivenUpstreamHeaderTemplatesAre(Dictionary upstreaHeaderTemplates) + { + _upstreamHeaderTemplates = upstreaHeaderTemplates; + } + + private void GivenUpstreamHeadersAre(Dictionary upstreamHeaders) + { + _upstreamHeaders = upstreamHeaders; + } + + private void WhenICallFindPlaceholders() + { + var result = _finder.Find(_upstreamHeaders, _upstreamHeaderTemplates); + _result = new(result); + } + + private void TheResultIs(List expected) + { + _result.ShouldNotBeNull(); + _result.Count.ShouldBe(expected.Count); + _result.ForEach(x => expected.Any(e => e.Name == x.Name && e.Value == x.Value).ShouldBeTrue()); + } +} diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcherTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcherTests.cs new file mode 100644 index 000000000..fae3cc0c5 --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcherTests.cs @@ -0,0 +1,293 @@ +using Ocelot.DownstreamRouteFinder.HeaderMatcher; +using Ocelot.Values; + +namespace Ocelot.UnitTests.DownstreamRouteFinder.HeaderMatcher; + +[Trait("PR", "1312")] +[Trait("Feat", "360")] +public class HeadersToHeaderTemplatesMatcherTests : UnitTest +{ + private readonly IHeadersToHeaderTemplatesMatcher _headerMatcher; + private Dictionary _upstreamHeaders; + private Dictionary _templateHeaders; + private bool _result; + + public HeadersToHeaderTemplatesMatcherTests() + { + _headerMatcher = new HeadersToHeaderTemplatesMatcher(); + } + + [Fact] + public void Should_match_when_no_template_headers() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "anyHeaderValue", + }; + var templateHeaders = new Dictionary(); + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsTrue(); + } + + [Fact] + public void Should_match_the_same_headers() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "anyHeaderValue", + }; + var templateHeaders = new Dictionary() + { + ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsTrue(); + } + + [Fact] + public void Should_not_match_the_same_headers_when_differ_case_and_case_sensitive() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "ANYHEADERVALUE", + }; + var templateHeaders = new Dictionary() + { + ["anyHeader"] = new("^anyHeaderValue$", "anyHeaderValue"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsFalse(); + } + + [Fact] + public void Should_match_the_same_headers_when_differ_case_and_case_insensitive() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "ANYHEADERVALUE", + }; + var templateHeaders = new Dictionary() + { + ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsTrue(); + } + + [Fact] + public void Should_not_match_different_headers_values() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "anyHeaderValueDifferent", + }; + var templateHeaders = new Dictionary() + { + ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsFalse(); + } + + [Fact] + public void Should_not_match_the_same_headers_names() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeaderDifferent"] = "anyHeaderValue", + }; + var templateHeaders = new Dictionary() + { + ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsFalse(); + } + + [Fact] + public void Should_match_all_the_same_headers() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "anyHeaderValue", + ["notNeededHeader"] = "notNeededHeaderValue", + ["secondHeader"] = "secondHeaderValue", + ["thirdHeader"] = "thirdHeaderValue", + }; + var templateHeaders = new Dictionary() + { + ["secondHeader"] = new("^(?i)secondHeaderValue$", "secondHeaderValue"), + ["thirdHeader"] = new("^(?i)thirdHeaderValue$", "thirdHeaderValue"), + ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsTrue(); + } + + [Fact] + public void Should_not_match_the_headers_when_one_of_them_different() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "anyHeaderValue", + ["notNeededHeader"] = "notNeededHeaderValue", + ["secondHeader"] = "secondHeaderValueDIFFERENT", + ["thirdHeader"] = "thirdHeaderValue", + }; + var templateHeaders = new Dictionary() + { + ["secondHeader"] = new("^(?i)secondHeaderValue$", "secondHeaderValue"), + ["thirdHeader"] = new("^(?i)thirdHeaderValue$", "thirdHeaderValue"), + ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsFalse(); + } + + [Fact] + public void Should_match_the_header_with_placeholder() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "PL", + }; + var templateHeaders = new Dictionary() + { + ["anyHeader"] = new("^(?i)(?.+)$", "{header:countrycode}"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsTrue(); + } + + [Fact] + public void Should_match_the_header_with_placeholders() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "PL-V1", + }; + var templateHeaders = new Dictionary() + { + ["anyHeader"] = new("^(?i)(?.+)-(?.+)$", "{header:countrycode}-{header:version}"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsTrue(); + } + + [Fact] + public void Should_not_match_the_header_with_placeholders() + { + // Arrange + var upstreamHeaders = new Dictionary() + { + ["anyHeader"] = "PL", + }; + var templateHeaders = new Dictionary() + { + ["anyHeader"] = new("^(?i)(?.+)-(?.+)$", "{header:countrycode}-{header:version}"), + }; + GivenIHaveUpstreamHeaders(upstreamHeaders); + GivenIHaveTemplateHeadersInRoute(templateHeaders); + + // Act + WhenIMatchTheHeaders(); + + // Assert + ThenTheResultIsFalse(); + } + + private void GivenIHaveUpstreamHeaders(Dictionary upstreamHeaders) + { + _upstreamHeaders = upstreamHeaders; + } + + private void GivenIHaveTemplateHeadersInRoute(Dictionary templateHeaders) + { + _templateHeaders = templateHeaders; + } + + private void WhenIMatchTheHeaders() + { + _result = _headerMatcher.Match(_upstreamHeaders, _templateHeaders); + } + + private void ThenTheResultIsTrue() + { + _result.ShouldBeTrue(); + } + + private void ThenTheResultIsFalse() + { + _result.ShouldBeFalse(); + } +} From 176a4c8b2b84bb9b4bcacb03be112eeb1e8dfdc0 Mon Sep 17 00:00:00 2001 From: Ibnu Daru Aji Date: Fri, 19 Apr 2024 19:21:27 +0700 Subject: [PATCH 08/15] #1672 Custom Default Version Policy (#1673) * feature written, tests passed * actualy passes almost all the test. * resolve conflict, hopefully. * please. * let it cook. * uses constants instead of string for version policies. * conflict res * swapped downstream method and version. * #1731 Read the Docs configuration file v2 (#1733) * fixing the documentation, using Release/20.0 as base branch * using latest conf.py, created with sphinx-quickstart, fixing the warnings during documentation generation * Update .readthedocs.yaml * switching to threemammals.org for copyright * adding requirements file, updating readthedocs.yaml, adding formats pdf / epub and config for requirements file * fixing code block in websockets.rst * ok, now it should be fine... * Update kubernetes.rst: Review and fix markup code * Update websockets.rst: Review and fix markup * Update conf.py: Update release, author and copyright --------- Co-authored-by: Raman Maksimchuk * * When using the QoS option "ExceptionsAllowedBeforeBreaking" the circuit breaker never opens the circuit. * merge issue, PortFinder * some code improvements, using httpresponsemessage status codes as a base for circuit breaker * Adding more unit tests, and trying to mitigate the test issues with the method "GivenThereIsAPossiblyBrokenServiceRunningOn" * fixing some test issues * setting timeout value to 5000 to avoid side effects * again timing issues * timing issues again * ok, first one ok * Revert "ok, first one ok" This reverts commit 2e4a673c28894a39f7e057907badb448247ff9d7. * inline method * putting back logging for http request exception * removing logger configuration, back to default * adding a bit more tests to check the policy wrap * Removing TimeoutStrategy from parameters, it's set by default to pessimistic, at least one policy will be returned, so using First() in circuit breaker and removing the branch Policy == null from delegating handler. * Fix StyleCop warnings * Format parameters * Sort usings * since we might have two policies wrapped, timeout and circuit breaker, we can't use the name CircuitBreaker for polly qos provider, it's not right. Using PollyPolicyWrapper and AsnycPollyPolicy instead. * modifying circuit breaker delegating handler name, usin Polly policies instead * renaming CircuitBreakerFactory to PolicyWrapperFactory in tests * DRY for FileConfiguration, using FileConfigurationFactory * Add copy constructor * Refactor setup * Use expression body for method * Fix acceptance test * IDE1006 Naming rule violation: These words must begin with upper case characters * CA1816 Change ReturnsErrorTests.Dispose() to call GC.SuppressFinalize(object) * Sort usings * Use expression body for method * Return back named arguments --------- Co-authored-by: raman-m * feature written, tests passed * actualy passes almost all the test. * resolve conflict, hopefully. * missed this one. * please. * come on... * let it build. * let it cook. * copied from main branch. * conflict res * resolving conflicts. * another attempt. * lf * re-incorporate downstream version policy. * renamed the version policies and added acceptance tests. * trust the dotnet dev cert. * accepts cert from dotnet. * Fix compiling errors * Refactor tests * a bit of code cleanup, removing some usings * a bit more cleanup in fileroute * try and error with the tests * "Yahoo!...", said @ibnuda :) * FileRoute: let it go... Binary copy! :LoL: * FileRoute: let it cook... Re-add sweet props * `dotnet dev-certs` for the `build` job * Recover `kubernetes.rst` * docs/make.bat original version * OcelotBuilderExtensions * original src/Ocelot.Provider.Polly/v7/PollyPolicyWrapper.cs * `IVersionPolicyCreator` XML docs * Code review by @raman-m (part 1) * RequestMapper : care about diff * Code review by @raman-m (part 2) * Fix Should_return_OK_status_and_multiline_indented_json_response_with_json_options_for_custom_builder * Update configuration.rst Add DownstreamVersionPolicy section * Update docs * Rename `DownstreamVersionPolicy` to `DownstreamHttpVersionPolicy` * update docs after prop renaming * Sort props --------- Co-authored-by: Guillaume Gnaegi <58469901+ggnaegi@users.noreply.github.com> Co-authored-by: Raman Maksimchuk --- .circleci/config.yml | 4 +- docs/features/configuration.rst | 68 +++++++ docs/make.bat | 0 .../Builder/DownstreamRouteBuilder.cs | 8 + .../Configuration/Builder/RouteBuilder.cs | 3 +- .../Creator/ConfigurationCreator.cs | 10 +- .../Configuration/Creator/DynamicsCreator.cs | 6 +- .../Creator/HttpVersionPolicyCreator.cs | 20 +++ .../Creator/IVersionPolicyCreator.cs | 14 ++ .../Configuration/Creator/RoutesCreator.cs | 12 +- .../Configuration/Creator/VersionPolicies.cs | 11 ++ src/Ocelot/Configuration/DownstreamRoute.cs | 18 +- .../Configuration/File/FileDynamicRoute.cs | 14 ++ .../File/FileGlobalConfiguration.cs | 16 +- src/Ocelot/Configuration/File/FileRoute.cs | 21 ++- .../Configuration/IInternalConfiguration.cs | 6 + .../Configuration/InternalConfiguration.cs | 10 +- .../Validator/RouteFluentValidator.cs | 6 + .../DependencyInjection/OcelotBuilder.cs | 1 + src/Ocelot/Request/Mapper/RequestMapper.cs | 4 +- .../DefaultVersionPolicyTests.cs | 166 ++++++++++++++++++ test/Ocelot.AcceptanceTests/PollyQoSTests.cs | 10 +- .../ReturnsErrorTests.cs | 2 +- ...wnstreamRouteFinderMiddlewareBenchmarks.cs | 2 +- .../AdministrationTests.cs | 2 +- .../ConfigurationCreatorTests.cs | 4 +- .../Configuration/DynamicsCreatorTests.cs | 21 ++- .../FileConfigurationSetterTests.cs | 12 +- .../FileInternalConfigurationCreatorTests.cs | 2 +- .../HttpVersionPolicyCreatorTests.cs | 50 ++++++ .../InMemoryConfigurationRepositoryTests.cs | 3 +- .../Configuration/RoutesCreatorTests.cs | 18 +- .../FileConfigurationFluentValidatorTests.cs | 2 +- .../DownstreamRouteCreatorTests.cs | 132 ++++++++++++-- .../DownstreamRouteFinderMiddlewareTests.cs | 12 +- .../DownstreamRouteFinderTests.cs | 12 +- .../DownstreamRouteProviderFactoryTests.cs | 24 ++- .../DownstreamUrlCreatorMiddlewareTests.cs | 2 +- .../Errors/ExceptionHandlerMiddlewareTests.cs | 8 +- ...ekaMiddlewareConfigurationProviderTests.cs | 4 +- .../LoadBalancerMiddlewareTests.cs | 2 +- .../Polly/PollyQoSProviderTests.cs | 29 ++- 42 files changed, 692 insertions(+), 79 deletions(-) mode change 100755 => 100644 docs/make.bat create mode 100644 src/Ocelot/Configuration/Creator/HttpVersionPolicyCreator.cs create mode 100644 src/Ocelot/Configuration/Creator/IVersionPolicyCreator.cs create mode 100644 src/Ocelot/Configuration/Creator/VersionPolicies.cs create mode 100644 test/Ocelot.AcceptanceTests/DefaultVersionPolicyTests.cs create mode 100644 test/Ocelot.UnitTests/Configuration/HttpVersionPolicyCreatorTests.cs diff --git a/.circleci/config.yml b/.circleci/config.yml index 876d46b5c..114d2b2e1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,13 +8,13 @@ jobs: resource_class: medium+ steps: - checkout - - run: dotnet tool restore && dotnet cake + - run: dotnet dev-certs https && dotnet tool restore && dotnet cake release: docker: - image: ocelot2/circleci-build:latest steps: - checkout - - run: dotnet tool restore && dotnet cake --target=Release + - run: dotnet dev-certs https && dotnet tool restore && dotnet cake --target=Release workflows: version: 2 main: diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 87198ea1d..51631b768 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -26,6 +26,7 @@ Here is an example Route configuration. You don't need to set all of these thing "DownstreamPathTemplate": "/", "DownstreamHttpMethod": "", "DownstreamHttpVersion": "", + "DownstreamHttpVersionPolicy": "", "AddHeadersToRequest": {}, "AddClaimsToRequest": {}, "RouteClaimsRequirement": {}, @@ -404,11 +405,76 @@ Registering a callback } } +.. _config-http-version: + DownstreamHttpVersion --------------------- Ocelot allows you to choose the HTTP version it will use to make the proxy request. It can be set as ``1.0``, ``1.1`` or ``2.0``. +* `HttpVersion Class `_ + +.. _config-version-policy: + +DownstreamHttpVersionPolicy [#f5]_ +---------------------------------- + +This routing property enables the configuration of the ``VersionPolicy`` property within ``HttpRequestMessage`` objects for downstream HTTP requests. +For additional details, refer to the following documentation: + +* `HttpRequestMessage.VersionPolicy Property `_ +* `HttpVersionPolicy Enum `_ +* `HttpVersion Class `_ + +The ``DownstreamHttpVersionPolicy`` option is intricately linked with the :ref:`config-http-version` setting. +Therefore, merely specifying ``DownstreamHttpVersion`` may sometimes be inadequate, particularly if your downstream services or Ocelot logs report HTTP connection errors such as ``PROTOCOL_ERROR``. +In these routes, selecting the precise ``DownstreamHttpVersionPolicy`` value is crucial for the ``HttpVersion`` policy to prevent such protocol errors. + +HTTP/2 version policy +^^^^^^^^^^^^^^^^^^^^^ + +**Given** you aim to ensure a smooth HTTP/2 connection setup for the Ocelot app and downstream services with SSL enabled: + +.. code-block:: json + + { + "DownstreamScheme": "https", + "DownstreamHttpVersion": "2.0", + "DownstreamHttpVersionPolicy": "", // empty + "DangerousAcceptAnyServerCertificateValidator": true + } + +**And** you configure global settings to use Kestrel with this snippet: + +.. code-block:: csharp + + var builder = WebApplication.CreateBuilder(args); + builder.WebHost.ConfigureKestrel(serverOptions => + { + serverOptions.ConfigureEndpointDefaults(listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); + }); + +**When** all components are set to communicate exclusively via HTTP/2 without TLS (plain HTTP). + +**Then** the downstream services may display error messages such as: + +.. code-block:: + + HTTP/2 connection error (PROTOCOL_ERROR): Invalid HTTP/2 connection preface + +To resolve the issue, ensure that ``HttpRequestMessage`` has its ``VersionPolicy`` set to ``RequestVersionOrHigher``. +Therefore, the ``DownstreamHttpVersionPolicy`` should be defined as follows: + +.. code-block:: json + + { + "DownstreamHttpVersion": "2.0", + "DownstreamHttpVersionPolicy": "RequestVersionOrHigher" // ! + } + Dependency Injection -------------------- @@ -434,8 +500,10 @@ You can find additional details in the dedicated :ref:`di-configuration-overview .. [#f2] ":ref:`config-merging-tomemory`" subfeature is based on the ``MergeOcelotJson`` enumeration type with values: ``ToFile`` and ``ToMemory``. The 1st one is implicit by default, and the second one is exactly what you need when merging to memory. See more details on implementations in the `ConfigurationBuilderExtensions`_ class. .. [#f3] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. .. [#f4] ":ref:`config-consul-key`" feature was requested in `issue 346 `_ as a part of version `7.0.0 `_. +.. [#f5] ":ref:`config-version-policy`" feature was requested in `issue 1672 `_ as a part of version `24.0`_. .. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0 .. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 +.. _24.0: https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0 .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/ocelot.json .. _ConfigurationBuilderExtensions: https://github.com/ThreeMammals/Ocelot/blob/develop/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs diff --git a/docs/make.bat b/docs/make.bat old mode 100755 new mode 100644 diff --git a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs index f8e6dc159..ffff82e17 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs @@ -40,6 +40,7 @@ public class DownstreamRouteBuilder private SecurityOptions _securityOptions; private string _downstreamHttpMethod; private Version _downstreamHttpVersion; + private HttpVersionPolicy _downstreamHttpVersionPolicy; private Dictionary _upstreamHeaders; public DownstreamRouteBuilder() @@ -268,6 +269,12 @@ public DownstreamRouteBuilder WithUpstreamHeaders(Dictionary(); @@ -28,6 +30,7 @@ IVersionCreator versionCreator _qosOptionsCreator = qosOptionsCreator; _httpHandlerOptionsCreator = httpHandlerOptionsCreator; _versionCreator = versionCreator; + _versionPolicyCreator = versionPolicyCreator; } public InternalConfiguration Create(FileConfiguration fileConfiguration, List routes) @@ -43,6 +46,8 @@ public InternalConfiguration Create(FileConfiguration fileConfiguration, List Create(FileConfiguration fileConfiguration) @@ -27,12 +29,14 @@ private Route SetUpDynamicRoute(FileDynamicRoute fileDynamicRoute, FileGlobalCon .Create(fileDynamicRoute.RateLimitRule, globalConfiguration); var version = _versionCreator.Create(fileDynamicRoute.DownstreamHttpVersion); + var versionPolicy = _versionPolicyCreator.Create(fileDynamicRoute.DownstreamHttpVersionPolicy); var downstreamRoute = new DownstreamRouteBuilder() .WithEnableRateLimiting(rateLimitOption.EnableRateLimiting) .WithRateLimitOptions(rateLimitOption) .WithServiceName(fileDynamicRoute.ServiceName) .WithDownstreamHttpVersion(version) + .WithDownstreamHttpVersionPolicy(versionPolicy) .Build(); var route = new RouteBuilder() diff --git a/src/Ocelot/Configuration/Creator/HttpVersionPolicyCreator.cs b/src/Ocelot/Configuration/Creator/HttpVersionPolicyCreator.cs new file mode 100644 index 000000000..0c8af7123 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/HttpVersionPolicyCreator.cs @@ -0,0 +1,20 @@ +namespace Ocelot.Configuration.Creator; + +/// +/// Default implementation of the interface. +/// +public class HttpVersionPolicyCreator : IVersionPolicyCreator +{ + /// + /// Creates a by a string. + /// + /// The string representation of the version policy. + /// An enumeration value. + public HttpVersionPolicy Create(string downstreamHttpVersionPolicy) => downstreamHttpVersionPolicy switch + { + VersionPolicies.RequestVersionExact => HttpVersionPolicy.RequestVersionExact, + VersionPolicies.RequestVersionOrHigher => HttpVersionPolicy.RequestVersionOrHigher, + VersionPolicies.RequestVersionOrLower => HttpVersionPolicy.RequestVersionOrLower, + _ => HttpVersionPolicy.RequestVersionOrLower, + }; +} diff --git a/src/Ocelot/Configuration/Creator/IVersionPolicyCreator.cs b/src/Ocelot/Configuration/Creator/IVersionPolicyCreator.cs new file mode 100644 index 000000000..3d25d8cea --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IVersionPolicyCreator.cs @@ -0,0 +1,14 @@ +namespace Ocelot.Configuration.Creator; + +/// +/// Defines conversions from version policy strings to enumeration values. +/// +public interface IVersionPolicyCreator +{ + /// + /// Creates a by a string. + /// + /// The string representation of the version policy. + /// An enumeration value. + HttpVersionPolicy Create(string downstreamHttpVersionPolicy); +} diff --git a/src/Ocelot/Configuration/Creator/RoutesCreator.cs b/src/Ocelot/Configuration/Creator/RoutesCreator.cs index cb7a6c451..4c94f6c71 100644 --- a/src/Ocelot/Configuration/Creator/RoutesCreator.cs +++ b/src/Ocelot/Configuration/Creator/RoutesCreator.cs @@ -1,6 +1,6 @@ using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; - + namespace Ocelot.Configuration.Creator { public class RoutesCreator : IRoutesCreator @@ -21,6 +21,7 @@ public class RoutesCreator : IRoutesCreator private readonly IRouteKeyCreator _routeKeyCreator; private readonly ISecurityOptionsCreator _securityOptionsCreator; private readonly IVersionCreator _versionCreator; + private readonly IVersionPolicyCreator _versionPolicyCreator; public RoutesCreator( IClaimsToThingCreator claimsToThingCreator, @@ -38,6 +39,7 @@ public RoutesCreator( IRouteKeyCreator routeKeyCreator, ISecurityOptionsCreator securityOptionsCreator, IVersionCreator versionCreator, + IVersionPolicyCreator versionPolicyCreator, IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator) { _routeKeyCreator = routeKeyCreator; @@ -56,6 +58,7 @@ public RoutesCreator( _loadBalancerOptionsCreator = loadBalancerOptionsCreator; _securityOptionsCreator = securityOptionsCreator; _versionCreator = versionCreator; + _versionPolicyCreator = versionPolicyCreator; _upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator; } @@ -106,6 +109,8 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf var downstreamHttpVersion = _versionCreator.Create(fileRoute.DownstreamHttpVersion); + var downstreamHttpVersionPolicy = _versionPolicyCreator.Create(fileRoute.DownstreamHttpVersionPolicy); + var cacheOptions = _cacheOptionsCreator.Create(fileRoute.FileCacheOptions, globalConfiguration, fileRoute.UpstreamPathTemplate, fileRoute.UpstreamHttpMethod); var route = new DownstreamRouteBuilder() @@ -143,6 +148,7 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf .WithDangerousAcceptAnyServerCertificateValidator(fileRoute.DangerousAcceptAnyServerCertificateValidator) .WithSecurityOptions(securityOptions) .WithDownstreamHttpVersion(downstreamHttpVersion) + .WithDownstreamHttpVersionPolicy(downstreamHttpVersionPolicy) .WithDownStreamHttpMethod(fileRoute.DownstreamHttpMethod) .Build(); @@ -151,14 +157,14 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf private Route SetUpRoute(FileRoute fileRoute, DownstreamRoute downstreamRoutes) { - var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute); + var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute); var upstreamHeaderTemplates = _upstreamHeaderTemplatePatternCreator.Create(fileRoute); var route = new RouteBuilder() .WithUpstreamHttpMethod(fileRoute.UpstreamHttpMethod) .WithUpstreamPathTemplate(upstreamTemplatePattern) .WithDownstreamRoute(downstreamRoutes) - .WithUpstreamHost(fileRoute.UpstreamHost) + .WithUpstreamHost(fileRoute.UpstreamHost) .WithUpstreamHeaders(upstreamHeaderTemplates) .Build(); diff --git a/src/Ocelot/Configuration/Creator/VersionPolicies.cs b/src/Ocelot/Configuration/Creator/VersionPolicies.cs new file mode 100644 index 000000000..8073d47ab --- /dev/null +++ b/src/Ocelot/Configuration/Creator/VersionPolicies.cs @@ -0,0 +1,11 @@ +namespace Ocelot.Configuration.Creator; + +/// +/// Constants for conversions in concrete classes for the interface. +/// +public class VersionPolicies +{ + public const string RequestVersionExact = nameof(RequestVersionExact); + public const string RequestVersionOrLower = nameof(RequestVersionOrLower); + public const string RequestVersionOrHigher = nameof(RequestVersionOrHigher); +} diff --git a/src/Ocelot/Configuration/DownstreamRoute.cs b/src/Ocelot/Configuration/DownstreamRoute.cs index f71563cb0..ab4b6d5de 100644 --- a/src/Ocelot/Configuration/DownstreamRoute.cs +++ b/src/Ocelot/Configuration/DownstreamRoute.cs @@ -40,6 +40,7 @@ public DownstreamRoute( SecurityOptions securityOptions, string downstreamHttpMethod, Version downstreamHttpVersion, + HttpVersionPolicy downstreamHttpVersionPolicy, Dictionary upstreamHeaders) { DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; @@ -75,7 +76,8 @@ public DownstreamRoute( AddHeadersToUpstream = addHeadersToUpstream; SecurityOptions = securityOptions; DownstreamHttpMethod = downstreamHttpMethod; - DownstreamHttpVersion = downstreamHttpVersion; + DownstreamHttpVersion = downstreamHttpVersion; + DownstreamHttpVersionPolicy = downstreamHttpVersionPolicy; UpstreamHeaders = upstreamHeaders ?? new(); } @@ -112,7 +114,19 @@ public DownstreamRoute( public bool DangerousAcceptAnyServerCertificateValidator { get; } public SecurityOptions SecurityOptions { get; } public string DownstreamHttpMethod { get; } - public Version DownstreamHttpVersion { get; } + public Version DownstreamHttpVersion { get; } + + /// The enum specifies behaviors for selecting and negotiating the HTTP version for a request. + /// An enum value being mapped from a constant. + /// + /// Related to the property. + /// + /// HttpVersionPolicy Enum + /// HttpVersion Class + /// HttpRequestMessage.VersionPolicy Property + /// + /// + public HttpVersionPolicy DownstreamHttpVersionPolicy { get; } public Dictionary UpstreamHeaders { get; } } } diff --git a/src/Ocelot/Configuration/File/FileDynamicRoute.cs b/src/Ocelot/Configuration/File/FileDynamicRoute.cs index fb93c2f91..21ad814af 100644 --- a/src/Ocelot/Configuration/File/FileDynamicRoute.cs +++ b/src/Ocelot/Configuration/File/FileDynamicRoute.cs @@ -1,3 +1,5 @@ +using Ocelot.Configuration.Creator; + namespace Ocelot.Configuration.File { public class FileDynamicRoute @@ -5,5 +7,17 @@ public class FileDynamicRoute public string ServiceName { get; set; } public FileRateLimitRule RateLimitRule { get; set; } public string DownstreamHttpVersion { get; set; } + + /// The enum specifies behaviors for selecting and negotiating the HTTP version for a request. + /// A value of defined constants. + /// + /// Related to the property. + /// + /// HttpVersionPolicy Enum + /// HttpVersion Class + /// HttpRequestMessage.VersionPolicy Property + /// + /// + public string DownstreamHttpVersionPolicy { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs index 04497cca1..340ca04ff 100644 --- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -1,4 +1,6 @@ -namespace Ocelot.Configuration.File +using Ocelot.Configuration.Creator; + +namespace Ocelot.Configuration.File { public class FileGlobalConfiguration { @@ -30,6 +32,18 @@ public FileGlobalConfiguration() public string DownstreamHttpVersion { get; set; } + /// The enum specifies behaviors for selecting and negotiating the HTTP version for a request. + /// A value of defined constants. + /// + /// Related to the property. + /// + /// HttpVersionPolicy Enum + /// HttpVersion Class + /// HttpRequestMessage.VersionPolicy Property + /// + /// + public string DownstreamHttpVersionPolicy { get; set; } + public FileCacheOptions CacheOptions { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileRoute.cs b/src/Ocelot/Configuration/File/FileRoute.cs index 874609e77..fe5eedf89 100644 --- a/src/Ocelot/Configuration/File/FileRoute.cs +++ b/src/Ocelot/Configuration/File/FileRoute.cs @@ -1,4 +1,6 @@ -namespace Ocelot.Configuration.File +using Ocelot.Configuration.Creator; + +namespace Ocelot.Configuration.File { public class FileRoute : IRoute, ICloneable { @@ -41,9 +43,21 @@ public FileRoute(FileRoute from) public List DownstreamHostAndPorts { get; set; } public string DownstreamHttpMethod { get; set; } public string DownstreamHttpVersion { get; set; } + + /// The enum specifies behaviors for selecting and negotiating the HTTP version for a request. + /// A value of defined constants. + /// + /// Related to the property. + /// + /// HttpVersionPolicy Enum + /// HttpVersion Class + /// HttpRequestMessage.VersionPolicy Property + /// + /// + public string DownstreamHttpVersionPolicy { get; set; } public string DownstreamPathTemplate { get; set; } - public string DownstreamScheme { get; set; } - public FileCacheOptions FileCacheOptions { get; set; } + public string DownstreamScheme { get; set; } + public FileCacheOptions FileCacheOptions { get; set; } public FileHttpHandlerOptions HttpHandlerOptions { get; set; } public string Key { get; set; } public FileLoadBalancerOptions LoadBalancerOptions { get; set; } @@ -87,6 +101,7 @@ public static void DeepCopy(FileRoute from, FileRoute to) to.DownstreamHostAndPorts = from.DownstreamHostAndPorts.Select(x => new FileHostAndPort(x)).ToList(); to.DownstreamHttpMethod = from.DownstreamHttpMethod; to.DownstreamHttpVersion = from.DownstreamHttpVersion; + to.DownstreamHttpVersionPolicy = from.DownstreamHttpVersionPolicy; to.DownstreamPathTemplate = from.DownstreamPathTemplate; to.DownstreamScheme = from.DownstreamScheme; to.FileCacheOptions = new(from.FileCacheOptions); diff --git a/src/Ocelot/Configuration/IInternalConfiguration.cs b/src/Ocelot/Configuration/IInternalConfiguration.cs index da839cf8b..59ef78549 100644 --- a/src/Ocelot/Configuration/IInternalConfiguration.cs +++ b/src/Ocelot/Configuration/IInternalConfiguration.cs @@ -1,3 +1,5 @@ +using Ocelot.Configuration.File; + namespace Ocelot.Configuration { public interface IInternalConfiguration @@ -19,5 +21,9 @@ public interface IInternalConfiguration HttpHandlerOptions HttpHandlerOptions { get; } Version DownstreamHttpVersion { get; } + + /// Global HTTP version policy. It is related to property. + /// An enumeration value. + HttpVersionPolicy? DownstreamHttpVersionPolicy { get; } } } diff --git a/src/Ocelot/Configuration/InternalConfiguration.cs b/src/Ocelot/Configuration/InternalConfiguration.cs index 86eb5472b..7d047cfe6 100644 --- a/src/Ocelot/Configuration/InternalConfiguration.cs +++ b/src/Ocelot/Configuration/InternalConfiguration.cs @@ -1,3 +1,5 @@ +using Ocelot.Configuration.File; + namespace Ocelot.Configuration { public class InternalConfiguration : IInternalConfiguration @@ -11,7 +13,8 @@ public InternalConfiguration( string downstreamScheme, QoSOptions qoSOptions, HttpHandlerOptions httpHandlerOptions, - Version downstreamHttpVersion) + Version downstreamHttpVersion, + HttpVersionPolicy? downstreamHttpVersionPolicy) { Routes = routes; AdministrationPath = administrationPath; @@ -22,6 +25,7 @@ public InternalConfiguration( QoSOptions = qoSOptions; HttpHandlerOptions = httpHandlerOptions; DownstreamHttpVersion = downstreamHttpVersion; + DownstreamHttpVersionPolicy = downstreamHttpVersionPolicy; } public List Routes { get; } @@ -34,5 +38,9 @@ public InternalConfiguration( public HttpHandlerOptions HttpHandlerOptions { get; } public Version DownstreamHttpVersion { get; } + + /// Global HTTP version policy. It is related to property. + /// An enumeration value. + public HttpVersionPolicy? DownstreamHttpVersionPolicy { get; } } } diff --git a/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs b/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs index f383bf75c..900c53189 100644 --- a/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs +++ b/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs @@ -1,6 +1,7 @@ using FluentValidation; using Microsoft.AspNetCore.Authentication; using Ocelot.Configuration.File; +using Ocelot.Configuration.Creator; namespace Ocelot.Configuration.Validator { @@ -84,6 +85,11 @@ public RouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemePr { RuleFor(r => r.DownstreamHttpVersion).Matches("^[0-9]([.,][0-9]{1,1})?$"); }); + + When(route => !string.IsNullOrEmpty(route.DownstreamHttpVersionPolicy), () => + { + RuleFor(r => r.DownstreamHttpVersionPolicy).Matches($@"^({VersionPolicies.RequestVersionExact}|{VersionPolicies.RequestVersionOrHigher}|{VersionPolicies.RequestVersionOrLower})$"); + }); } private async Task IsSupportedAuthenticationProviders(FileAuthenticationOptions options, CancellationToken cancellationToken) diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 70bf5338a..f25f5a5f8 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -137,6 +137,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.TryAddSingleton(); // Add security diff --git a/src/Ocelot/Request/Mapper/RequestMapper.cs b/src/Ocelot/Request/Mapper/RequestMapper.cs index 883c42350..aa78e0a63 100644 --- a/src/Ocelot/Request/Mapper/RequestMapper.cs +++ b/src/Ocelot/Request/Mapper/RequestMapper.cs @@ -18,10 +18,10 @@ public HttpRequestMessage Map(HttpRequest request, DownstreamRoute downstreamRou Method = MapMethod(request, downstreamRoute), RequestUri = MapUri(request), Version = downstreamRoute.DownstreamHttpVersion, + VersionPolicy = downstreamRoute.DownstreamHttpVersionPolicy, }; MapHeaders(request, requestMessage); - return requestMessage; } @@ -55,7 +55,7 @@ private static void AddContentHeaders(HttpRequest request, HttpContent content) // The performance might be improved by retrieving the matching headers from the request // instead of calling request.Headers.TryGetValue for each used content header - var matchingHeaders = ContentHeaders.Where(header => request.Headers.ContainsKey(header)); + var matchingHeaders = ContentHeaders.Where(request.Headers.ContainsKey); foreach (var key in matchingHeaders) { diff --git a/test/Ocelot.AcceptanceTests/DefaultVersionPolicyTests.cs b/test/Ocelot.AcceptanceTests/DefaultVersionPolicyTests.cs new file mode 100644 index 000000000..c9a1d1419 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/DefaultVersionPolicyTests.cs @@ -0,0 +1,166 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; + +namespace Ocelot.AcceptanceTests; + +[Trait("Feat", "1672")] +public sealed class DefaultVersionPolicyTests : Steps +{ + private const string Body = "supercalifragilistic"; + + public DefaultVersionPolicyTests() + { + } + + [Fact] + public void Should_return_bad_gateway_when_request_higher_receive_lower() + { + var port = PortFinder.GetRandomPort(); + var route = GivenHttpsRoute(port, "2.0", VersionPolicies.RequestVersionOrHigher); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAServiceRunningOn(port, HttpProtocols.Http1)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) + .BDDfy(); + } + + [Fact] + public void Should_return_bad_gateway_when_request_lower_receive_higher() + { + var port = PortFinder.GetRandomPort(); + var route = GivenHttpsRoute(port, "1.1", VersionPolicies.RequestVersionOrLower); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAServiceRunningOn(port, HttpProtocols.Http2)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) + .BDDfy(); + } + + [Fact] + public void Should_return_bad_gateway_when_request_exact_receive_different() + { + var port = PortFinder.GetRandomPort(); + var route = GivenHttpsRoute(port, "1.1", VersionPolicies.RequestVersionExact); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAServiceRunningOn(port, HttpProtocols.Http2)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) + .BDDfy(); + } + + [Fact] + public void Should_return_ok_when_request_version_exact_receive_exact() + { + var port = PortFinder.GetRandomPort(); + var route = GivenHttpsRoute(port, "2.0", VersionPolicies.RequestVersionExact); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAServiceRunningOn(port, HttpProtocols.Http2)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void Should_return_ok_when_request_version_lower_receive_lower() + { + var port = PortFinder.GetRandomPort(); + var route = GivenHttpsRoute(port, "2.0", VersionPolicies.RequestVersionOrLower); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAServiceRunningOn(port, HttpProtocols.Http1)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void Should_return_ok_when_request_version_lower_receive_exact() + { + var port = PortFinder.GetRandomPort(); + var route = GivenHttpsRoute(port, "2.0", VersionPolicies.RequestVersionOrLower); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAServiceRunningOn(port, HttpProtocols.Http2)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void Should_return_ok_when_request_version_higher_receive_higher() + { + var port = PortFinder.GetRandomPort(); + var route = GivenHttpsRoute(port, "1.1", VersionPolicies.RequestVersionOrHigher); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAServiceRunningOn(port, HttpProtocols.Http2)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void Should_return_ok_when_request_version_higher_receive_exact() + { + var port = PortFinder.GetRandomPort(); + var route = GivenHttpsRoute(port, "1.1", VersionPolicies.RequestVersionOrHigher); + var configuration = GivenConfiguration(route); + this.Given(x => GivenThereIsAServiceRunningOn(port, HttpProtocols.Http1)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + private static void GivenThereIsAServiceRunningOn(int port, HttpProtocols protocols) + { + var url = $"{Uri.UriSchemeHttps}://localhost:{port}"; + var builder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .ConfigureKestrel(serverOptions => + { + serverOptions.ConfigureEndpointDefaults(listenOptions => { listenOptions.Protocols = protocols; }); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .Configure(app => + { + app.Run(async context => + { + context.Response.StatusCode = (int)HttpStatusCode.OK; + await context.Response.WriteAsync(Body); + }); + }) + .Build(); + + builder.Start(); + } + + private static FileRoute GivenHttpsRoute(int port, string httpVersion, string versionPolicy) => new() + { + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() { HttpMethods.Get }, + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new() { new("localhost", port) }, + DownstreamScheme = Uri.UriSchemeHttps, // !!! + DownstreamHttpVersion = httpVersion, + DownstreamHttpVersionPolicy = versionPolicy, + DangerousAcceptAnyServerCertificateValidator = true, + }; +} diff --git a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs index 58481afa3..597731755 100644 --- a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs +++ b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs @@ -198,11 +198,11 @@ private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string resp { if (requestCount == 2) { - // in Polly v8 - // MinimumThroughput (ExceptionsAllowedBeforeBreaking) must be 2 or more - // BreakDuration (DurationOfBreak) must be 500 or more - // Timeout (TimeoutValue) must be 1000 or more - // so we wait for 2.1 seconds to make sure the circuit is open + // In Polly v8: + // MinimumThroughput (ExceptionsAllowedBeforeBreaking) must be 2 or more + // BreakDuration (DurationOfBreak) must be 500 or more + // Timeout (TimeoutValue) must be 1000 or more + // So, we wait for 2.1 seconds to make sure the circuit is open // DurationOfBreak * ExceptionsAllowedBeforeBreaking + Timeout // 500 * 2 + 1000 = 2000 minimum + 100 milliseconds to exceed the minimum await Task.Delay(2100); diff --git a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs index c2157e24d..a5c1d7c94 100644 --- a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs +++ b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs @@ -2,7 +2,7 @@ namespace Ocelot.AcceptanceTests { - public class ReturnsErrorTests : IDisposable + public sealed class ReturnsErrorTests : IDisposable { private readonly Steps _steps; private readonly ServiceHandler _serviceHandler; diff --git a/test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs b/test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs index 4e99d1fe0..95124e6bf 100644 --- a/test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs +++ b/test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs @@ -52,7 +52,7 @@ public void SetUp() }, }; httpContext.Request.Headers.Append("Host", "most"); - httpContext.Items.SetIInternalConfiguration(new InternalConfiguration(new List(), null, null, null, null, null, null, null, null)); + httpContext.Items.SetIInternalConfiguration(new InternalConfiguration(new List(), null, null, null, null, null, null, null, null, null)); _httpContext = httpContext; } diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs index 87ff03fd1..7495307a6 100644 --- a/test/Ocelot.IntegrationTests/AdministrationTests.cs +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -884,7 +884,7 @@ private void ThenTheResultHaveMultiLineIndentedJson() const string indent = " "; const int total = 52, skip = 1; var lines = _response.Content.ReadAsStringAsync().Result.Split(Environment.NewLine); - lines.Length.ShouldBe(total); + lines.Length.ShouldBeGreaterThanOrEqualTo(total); lines.First().ShouldNotStartWith(indent); lines.Skip(skip).Take(total - skip - 1).ToList() diff --git a/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs index 8f6c7f803..7b3f98a2a 100644 --- a/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs @@ -15,6 +15,7 @@ public class ConfigurationCreatorTests : UnitTest private readonly Mock _hhoCreator; private readonly Mock _lboCreator; private readonly Mock _vCreator; + private readonly Mock _versionPolicyCreator; private FileConfiguration _fileConfig; private List _routes; private ServiceProviderConfiguration _spc; @@ -27,6 +28,7 @@ public class ConfigurationCreatorTests : UnitTest public ConfigurationCreatorTests() { _vCreator = new Mock(); + _versionPolicyCreator = new Mock(); _lboCreator = new Mock(); _hhoCreator = new Mock(); _qosCreator = new Mock(); @@ -114,7 +116,7 @@ private void GivenTheDependenciesAreSetUp() private void WhenICreate() { var serviceProvider = _serviceCollection.BuildServiceProvider(); - _creator = new ConfigurationCreator(_spcCreator.Object, _qosCreator.Object, _hhoCreator.Object, serviceProvider, _lboCreator.Object, _vCreator.Object); + _creator = new ConfigurationCreator(_spcCreator.Object, _qosCreator.Object, _hhoCreator.Object, serviceProvider, _lboCreator.Object, _vCreator.Object, _versionPolicyCreator.Object); _result = _creator.Create(_fileConfig, _routes); } } diff --git a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs index aec6ffb75..63c7c5237 100644 --- a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs @@ -2,7 +2,7 @@ using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; - + namespace Ocelot.UnitTests.Configuration { public class DynamicsCreatorTests : UnitTest @@ -10,17 +10,20 @@ public class DynamicsCreatorTests : UnitTest private readonly DynamicsCreator _creator; private readonly Mock _rloCreator; private readonly Mock _versionCreator; + private readonly Mock _versionPolicyCreator; private List _result; private FileConfiguration _fileConfig; private RateLimitOptions _rlo1; private RateLimitOptions _rlo2; private Version _version; + private HttpVersionPolicy _versionPolicy; public DynamicsCreatorTests() { _versionCreator = new Mock(); + _versionPolicyCreator = new Mock(); _rloCreator = new Mock(); - _creator = new DynamicsCreator(_rloCreator.Object, _versionCreator.Object); + _creator = new DynamicsCreator(_rloCreator.Object, _versionCreator.Object, _versionPolicyCreator.Object); } [Fact] @@ -50,6 +53,7 @@ public void should_return_re_routes() EnableRateLimiting = false, }, DownstreamHttpVersion = "1.1", + DownstreamHttpVersionPolicy = VersionPolicies.RequestVersionOrLower, }, new() { @@ -59,6 +63,7 @@ public void should_return_re_routes() EnableRateLimiting = true, }, DownstreamHttpVersion = "2.0", + DownstreamHttpVersionPolicy = VersionPolicies.RequestVersionOrHigher, }, }, }; @@ -66,6 +71,7 @@ public void should_return_re_routes() this.Given(_ => GivenThe(fileConfig)) .And(_ => GivenTheRloCreatorReturns()) .And(_ => GivenTheVersionCreatorReturns()) + .And(_ => GivenTheVersionPolicyCreatorReturns()) .When(_ => WhenICreate()) .Then(_ => ThenTheRoutesAreReturned()) .And(_ => ThenTheRloCreatorIsCalledCorrectly()) @@ -86,6 +92,9 @@ private void ThenTheVersionCreatorIsCalledCorrectly() { _versionCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].DownstreamHttpVersion), Times.Once); _versionCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].DownstreamHttpVersion), Times.Once); + + _versionPolicyCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].DownstreamHttpVersionPolicy), Times.Once); + _versionPolicyCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].DownstreamHttpVersionPolicy), Times.Once); } private void ThenTheRoutesAreReturned() @@ -94,11 +103,13 @@ private void ThenTheRoutesAreReturned() _result[0].DownstreamRoute[0].EnableEndpointEndpointRateLimiting.ShouldBeFalse(); _result[0].DownstreamRoute[0].RateLimitOptions.ShouldBe(_rlo1); _result[0].DownstreamRoute[0].DownstreamHttpVersion.ShouldBe(_version); + _result[0].DownstreamRoute[0].DownstreamHttpVersionPolicy.ShouldBe(_versionPolicy); _result[0].DownstreamRoute[0].ServiceName.ShouldBe(_fileConfig.DynamicRoutes[0].ServiceName); _result[1].DownstreamRoute[0].EnableEndpointEndpointRateLimiting.ShouldBeTrue(); _result[1].DownstreamRoute[0].RateLimitOptions.ShouldBe(_rlo2); _result[1].DownstreamRoute[0].DownstreamHttpVersion.ShouldBe(_version); + _result[1].DownstreamRoute[0].DownstreamHttpVersionPolicy.ShouldBe(_versionPolicy); _result[1].DownstreamRoute[0].ServiceName.ShouldBe(_fileConfig.DynamicRoutes[1].ServiceName); } @@ -107,6 +118,12 @@ private void GivenTheVersionCreatorReturns() _version = new Version("1.1"); _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_version); } + + private void GivenTheVersionPolicyCreatorReturns() + { + _versionPolicy = HttpVersionPolicy.RequestVersionOrLower; + _versionPolicyCreator.Setup(x => x.Create(It.IsAny())).Returns(_versionPolicy); + } private void GivenTheRloCreatorReturns() { diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs index 2c2201a33..74709d949 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs @@ -32,7 +32,17 @@ public void should_set_configuration() { var fileConfig = new FileConfiguration(); var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - var config = new InternalConfiguration(new List(), string.Empty, serviceProviderConfig, "asdf", new LoadBalancerOptionsBuilder().Build(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1")); + var config = new InternalConfiguration( + new List(), + string.Empty, + serviceProviderConfig, + "asdf", + new LoadBalancerOptionsBuilder().Build(), + string.Empty, + new QoSOptionsBuilder().Build(), + new HttpHandlerOptionsBuilder().Build(), + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(x => GivenTheFollowingConfiguration(fileConfig)) .And(x => GivenTheRepoReturns(new OkResponse())) diff --git a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs index b8fb5b7f6..802a4588e 100644 --- a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs @@ -79,7 +79,7 @@ private void GivenTheDependenciesAreSetUp() _routes = new List { new RouteBuilder().Build() }; _aggregates = new List { new RouteBuilder().Build() }; _dynamics = new List { new RouteBuilder().Build() }; - _internalConfig = new InternalConfiguration(null, string.Empty, null, string.Empty, null, string.Empty, null, null, null); + _internalConfig = new InternalConfiguration(null, string.Empty, null, string.Empty, null, string.Empty, null, null, null, null); _routesCreator.Setup(x => x.Create(It.IsAny())).Returns(_routes); _aggregatesCreator.Setup(x => x.Create(It.IsAny(), It.IsAny>())).Returns(_aggregates); diff --git a/test/Ocelot.UnitTests/Configuration/HttpVersionPolicyCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HttpVersionPolicyCreatorTests.cs new file mode 100644 index 000000000..813752d60 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/HttpVersionPolicyCreatorTests.cs @@ -0,0 +1,50 @@ +using Ocelot.Configuration.Creator; + +namespace Ocelot.UnitTests.Configuration; + +[Trait("Feat", "1672")] +public sealed class HttpVersionPolicyCreatorTests : UnitTest +{ + private readonly HttpVersionPolicyCreator _creator; + + public HttpVersionPolicyCreatorTests() + { + _creator = new HttpVersionPolicyCreator(); + } + + [Theory] + [InlineData(VersionPolicies.RequestVersionOrLower, HttpVersionPolicy.RequestVersionOrLower)] + [InlineData(VersionPolicies.RequestVersionExact, HttpVersionPolicy.RequestVersionExact)] + [InlineData(VersionPolicies.RequestVersionOrHigher, HttpVersionPolicy.RequestVersionOrHigher)] + public void Should_create_version_policy_based_on_input(string versionPolicy, HttpVersionPolicy expected) + { + // Arrange, Act + var actual = _creator.Create(versionPolicy); + + // Assert + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData("invalid version")] + public void Should_default_to_request_version_or_lower(string versionPolicy) + { + // Arrange, Act + var actual = _creator.Create(versionPolicy); + + // Assert + Assert.Equal(HttpVersionPolicy.RequestVersionOrLower, actual); + } + + [Fact] + public void Should_default_to_request_version_or_lower_when_setting_gibberish() + { + // Arrange, Act + var actual = _creator.Create("string is gibberish"); + + // Assert + Assert.Equal(HttpVersionPolicy.RequestVersionOrLower, actual); + } +} diff --git a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs index 85d6bf210..e6f502438 100644 --- a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs @@ -3,7 +3,7 @@ using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.Repository; using Ocelot.Responses; - + namespace Ocelot.UnitTests.Configuration { public class InMemoryConfigurationRepositoryTests : UnitTest @@ -115,6 +115,7 @@ public List Routes public QoSOptions QoSOptions { get; } public HttpHandlerOptions HttpHandlerOptions { get; } public Version DownstreamHttpVersion { get; } + public HttpVersionPolicy? DownstreamHttpVersionPolicy { get; } } } } diff --git a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs index c9fc698ff..5f8a481bc 100644 --- a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs @@ -25,6 +25,7 @@ public class RoutesCreatorTests : UnitTest private readonly Mock _rrkCreator; private readonly Mock _soCreator; private readonly Mock _versionCreator; + private readonly Mock _versionPolicyCreator; private FileConfiguration _fileConfig; private RouteOptions _rro; private string _requestId; @@ -40,7 +41,8 @@ public class RoutesCreatorTests : UnitTest private List _dhp; private LoadBalancerOptions _lbo; private List _result; - private Version _expectedVersion; + private Version _expectedVersion; + private HttpVersionPolicy _expectedVersionPolicy; private Dictionary _uht; public RoutesCreatorTests() @@ -60,6 +62,7 @@ public RoutesCreatorTests() _rrkCreator = new Mock(); _soCreator = new Mock(); _versionCreator = new Mock(); + _versionPolicyCreator = new Mock(); _uhtpCreator = new Mock(); _creator = new RoutesCreator( @@ -78,8 +81,8 @@ public RoutesCreatorTests() _rrkCreator.Object, _soCreator.Object, _versionCreator.Object, - _uhtpCreator.Object - ); + _versionPolicyCreator.Object, + _uhtpCreator.Object); } [Fact] @@ -156,6 +159,7 @@ private void ThenTheDependenciesAreCalledCorrectly() private void GivenTheDependenciesAreSetUpCorrectly() { _expectedVersion = new Version("1.1"); + _expectedVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; _rro = new RouteOptions(false, false, false, false, false); _requestId = "testy"; _rrk = "besty"; @@ -169,7 +173,7 @@ private void GivenTheDependenciesAreSetUpCorrectly() _hho = new HttpHandlerOptionsBuilder().Build(); _ht = new HeaderTransformations(new List(), new List(), new List(), new List()); _dhp = new List(); - _lbo = new LoadBalancerOptionsBuilder().Build(); + _lbo = new LoadBalancerOptionsBuilder().Build(); _uht = new Dictionary(); _rroCreator.Setup(x => x.Create(It.IsAny())).Returns(_rro); @@ -185,7 +189,8 @@ private void GivenTheDependenciesAreSetUpCorrectly() _hfarCreator.Setup(x => x.Create(It.IsAny())).Returns(_ht); _daCreator.Setup(x => x.Create(It.IsAny())).Returns(_dhp); _lboCreator.Setup(x => x.Create(It.IsAny())).Returns(_lbo); - _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersion); + _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersion); + _versionPolicyCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersionPolicy); _uhtpCreator.Setup(x => x.Create(It.IsAny())).Returns(_uht); } @@ -215,6 +220,7 @@ private void WhenICreate() private void ThenTheRouteIsSet(FileRoute expected, int routeIndex) { _result[routeIndex].DownstreamRoute[0].DownstreamHttpVersion.ShouldBe(_expectedVersion); + _result[routeIndex].DownstreamRoute[0].DownstreamHttpVersionPolicy.ShouldBe(_expectedVersionPolicy); _result[routeIndex].DownstreamRoute[0].IsAuthenticated.ShouldBe(_rro.IsAuthenticated); _result[routeIndex].DownstreamRoute[0].IsAuthorized.ShouldBe(_rro.IsAuthorized); _result[routeIndex].DownstreamRoute[0].IsCached.ShouldBe(_rro.IsCached); @@ -255,7 +261,7 @@ private void ThenTheRouteIsSet(FileRoute expected, int routeIndex) .ShouldContain(x => x == expected.UpstreamHttpMethod[1]); _result[routeIndex].UpstreamHost.ShouldBe(expected.UpstreamHost); _result[routeIndex].DownstreamRoute.Count.ShouldBe(1); - _result[routeIndex].UpstreamTemplatePattern.ShouldBe(_upt); + _result[routeIndex].UpstreamTemplatePattern.ShouldBe(_upt); _result[routeIndex].UpstreamHeaderTemplates.ShouldBe(_uht); } diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs index f3998c878..54b61aed0 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs @@ -33,7 +33,7 @@ public FileConfigurationFluentValidatorTests() _authProvider = new Mock(); _provider = _services.BuildServiceProvider(); - // Todo - replace with mocks + // TODO Replace with mocks _configurationValidator = new FileConfigurationFluentValidator(_provider, new RouteFluentValidator(_authProvider.Object, new HostAndPortValidator(), new FileQoSOptionsFluentValidator(_provider)), new FileGlobalConfigurationFluentValidator(new FileQoSOptionsFluentValidator(_provider))); } diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs index f4cb323f4..741af9d54 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs @@ -39,7 +39,17 @@ public DownstreamRouteCreatorTests() [Fact] public void should_create_downstream_route() { - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration)) .When(_ => WhenICreate()) @@ -66,7 +76,17 @@ public void should_create_downstream_route_with_rate_limit_options() var routes = new List { route }; - var configuration = new InternalConfiguration(routes, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + routes, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration)) .When(_ => WhenICreate()) @@ -78,7 +98,17 @@ public void should_create_downstream_route_with_rate_limit_options() [Fact] public void should_cache_downstream_route() { - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration, "/geoffisthebest/")) .When(_ => WhenICreate()) @@ -91,7 +121,17 @@ public void should_cache_downstream_route() [Fact] public void should_not_cache_downstream_route() { - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration, "/geoffistheworst/")) .When(_ => WhenICreate()) @@ -105,7 +145,17 @@ public void should_not_cache_downstream_route() public void should_create_downstream_route_with_no_path() { var upstreamUrlPath = "/auth/"; - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration, upstreamUrlPath)) .When(_ => WhenICreate()) @@ -117,7 +167,17 @@ public void should_create_downstream_route_with_no_path() public void should_create_downstream_route_with_only_first_segment_no_traling_slash() { var upstreamUrlPath = "/auth"; - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration, upstreamUrlPath)) .When(_ => WhenICreate()) @@ -129,7 +189,17 @@ public void should_create_downstream_route_with_only_first_segment_no_traling_sl public void should_create_downstream_route_with_segments_no_traling_slash() { var upstreamUrlPath = "/auth/test"; - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrHigher); this.Given(_ => GivenTheConfiguration(configuration, upstreamUrlPath)) .When(_ => WhenICreate()) @@ -141,7 +211,17 @@ public void should_create_downstream_route_with_segments_no_traling_slash() public void should_create_downstream_route_and_remove_query_string() { var upstreamUrlPath = "/auth/test?test=1&best=2"; - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration, upstreamUrlPath)) .When(_ => WhenICreate()) @@ -153,7 +233,17 @@ public void should_create_downstream_route_and_remove_query_string() public void should_create_downstream_route_for_sticky_sessions() { var loadBalancerOptions = new LoadBalancerOptionsBuilder().WithType(nameof(CookieStickySessions)).WithKey("boom").WithExpiryInMs(1).Build(); - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration)) .When(_ => WhenICreate()) @@ -169,7 +259,17 @@ public void should_create_downstream_route_with_qos() .WithTimeoutValue(1) .Build(); - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration)) .And(_ => GivenTheQosCreatorReturns(qoSOptions)) @@ -181,7 +281,17 @@ public void should_create_downstream_route_with_qos() [Fact] public void should_create_downstream_route_with_handler_options() { - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + var configuration = new InternalConfiguration( + null, + "doesnt matter", + null, + "doesnt matter", + _loadBalancerOptions, + "http", + _qoSOptions, + _handlerOptions, + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); this.Given(_ => GivenTheConfiguration(configuration)) .When(_ => WhenICreate()) diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs index eb6ebe68f..c96b81e69 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs @@ -39,7 +39,17 @@ public DownstreamRouteFinderMiddlewareTests() [Fact] public void should_call_scoped_data_repository_correctly() { - var config = new InternalConfiguration(null, null, new ServiceProviderConfigurationBuilder().Build(), string.Empty, new LoadBalancerOptionsBuilder().Build(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1")); + var config = new InternalConfiguration( + null, + null, + new ServiceProviderConfigurationBuilder().Build(), + string.Empty, + new LoadBalancerOptionsBuilder().Build(), + string.Empty, + new QoSOptionsBuilder().Build(), + new HttpHandlerOptionsBuilder().Build(), + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index ec6026a5b..2c2e04510 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -907,7 +907,17 @@ private void GivenTheHeadersMatcherReturns(bool headersMatch) private void GivenTheConfigurationIs(List routesConfig, string adminPath, ServiceProviderConfiguration serviceProviderConfig) { _routesConfig = routesConfig; - _config = new InternalConfiguration(_routesConfig, adminPath, serviceProviderConfig, string.Empty, new LoadBalancerOptionsBuilder().Build(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1")); + _config = new InternalConfiguration( + _routesConfig, + adminPath, + serviceProviderConfig, + string.Empty, + new LoadBalancerOptionsBuilder().Build(), + string.Empty, + new QoSOptionsBuilder().Build(), + new HttpHandlerOptionsBuilder().Build(), + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); } private void GivenThereIsAnUpstreamUrlPath(string upstreamUrlPath) diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs index 20426362a..2eb2a092b 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs @@ -151,12 +151,32 @@ private void WhenIGet() private void GivenTheRoutes(List routes) { - _config = new InternalConfiguration(routes, string.Empty, null, string.Empty, new LoadBalancerOptionsBuilder().Build(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1")); + _config = new InternalConfiguration( + routes, + string.Empty, + null, + string.Empty, + new LoadBalancerOptionsBuilder().Build(), + string.Empty, + new QoSOptionsBuilder().Build(), + new HttpHandlerOptionsBuilder().Build(), + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); } private void GivenTheRoutes(List routes, ServiceProviderConfiguration config) { - _config = new InternalConfiguration(routes, string.Empty, config, string.Empty, new LoadBalancerOptionsBuilder().Build(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1")); + _config = new InternalConfiguration( + routes, + string.Empty, + config, + string.Empty, + new LoadBalancerOptionsBuilder().Build(), + string.Empty, + new QoSOptionsBuilder().Build(), + new HttpHandlerOptionsBuilder().Build(), + new Version("1.1"), + HttpVersionPolicy.RequestVersionOrLower); } } } diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs index 615eb704e..6f2d5333c 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs @@ -545,7 +545,7 @@ public void should_fix_issue_748(string upstreamTemplate, string downstreamTempl private void GivenTheServiceProviderConfigIs(ServiceProviderConfiguration config) { - var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null); + var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null, null); _httpContext.Items.SetIInternalConfiguration(configuration); } diff --git a/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs b/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs index ab72f90e2..9bb883aa3 100644 --- a/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs @@ -42,7 +42,7 @@ public ExceptionHandlerMiddlewareTests() [Fact] public void NoDownstreamException() { - var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null); + var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null, null); this.Given(_ => GivenAnExceptionWillNotBeThrownDownstream()) .And(_ => GivenTheConfigurationIs(config)) @@ -55,7 +55,7 @@ public void NoDownstreamException() [Fact] public void DownstreamException() { - var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null); + var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null, null); this.Given(_ => GivenAnExceptionWillBeThrownDownstream()) .And(_ => GivenTheConfigurationIs(config)) @@ -67,7 +67,7 @@ public void DownstreamException() [Fact] public void ShouldSetRequestId() { - var config = new InternalConfiguration(null, null, null, "requestidkey", null, null, null, null, null); + var config = new InternalConfiguration(null, null, null, "requestidkey", null, null, null, null, null, null); this.Given(_ => GivenAnExceptionWillNotBeThrownDownstream()) .And(_ => GivenTheConfigurationIs(config)) @@ -80,7 +80,7 @@ public void ShouldSetRequestId() [Fact] public void ShouldSetAspDotNetRequestId() { - var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null); + var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null, null); this.Given(_ => GivenAnExceptionWillNotBeThrownDownstream()) .And(_ => GivenTheConfigurationIs(config)) diff --git a/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs b/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs index ad981a59b..f020707be 100644 --- a/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs +++ b/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs @@ -16,7 +16,7 @@ public void ShouldNotBuild() { var configRepo = new Mock(); configRepo.Setup(x => x.Get()) - .Returns(new OkResponse(new InternalConfiguration(null, null, null, null, null, null, null, null, null))); + .Returns(new OkResponse(new InternalConfiguration(null, null, null, null, null, null, null, null, null, null))); var services = new ServiceCollection(); services.AddSingleton(configRepo.Object); var sp = services.BuildServiceProvider(); @@ -31,7 +31,7 @@ public void ShouldBuild() var client = new Mock(); var configRepo = new Mock(); configRepo.Setup(x => x.Get()) - .Returns(new OkResponse(new InternalConfiguration(null, null, serviceProviderConfig, null, null, null, null, null, null))); + .Returns(new OkResponse(new InternalConfiguration(null, null, serviceProviderConfig, null, null, null, null, null, null, null))); var services = new ServiceCollection(); services.AddSingleton(configRepo.Object); services.AddSingleton(client.Object); diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs index f5b5dd925..fd46e9a2a 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs @@ -131,7 +131,7 @@ private void WhenICallTheMiddleware() private void GivenTheConfigurationIs(ServiceProviderConfiguration config) { _config = config; - var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null); + var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null, null); _httpContext.Items.SetIInternalConfiguration(configuration); } diff --git a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs index 8c08a46e0..bc56a1a1f 100644 --- a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs +++ b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs @@ -1,6 +1,5 @@ using Ocelot.Configuration.Builder; using Ocelot.Logging; -using Ocelot.Provider.Polly; using Ocelot.Provider.Polly.v7; using Polly; using Polly.CircuitBreaker; @@ -29,7 +28,7 @@ public void Should_build() } [Fact] - public void should_build_and_wrap_contains_two_policies() + public void Should_build_and_wrap_contains_two_policies() { var pollyQosProvider = PollyQoSProviderFactory(); var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider); @@ -70,7 +69,7 @@ public void should_build_and_wrap_contains_two_policies() } [Fact] - public void should_build_and_contains_one_policy_when_with_exceptions_allowed_before_breaking_is_zero() + public void Should_build_and_contains_one_policy_when_with_exceptions_allowed_before_breaking_is_zero() { var pollyQosProvider = PollyQoSProviderFactory(); var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider, true); @@ -86,7 +85,7 @@ public void should_build_and_contains_one_policy_when_with_exceptions_allowed_be } [Fact] - public void should_return_same_circuit_breaker_for_given_route() + public void Should_return_same_circuit_breaker_for_given_route() { var pollyQosProvider = PollyQoSProviderFactory(); var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider); @@ -95,7 +94,7 @@ public void should_return_same_circuit_breaker_for_given_route() } [Fact] - public void should_return_different_circuit_breaker_for_two_different_routes() + public void Should_return_different_circuit_breaker_for_two_different_routes() { var pollyQosProvider = PollyQoSProviderFactory(); var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider); @@ -113,7 +112,7 @@ public void should_return_different_circuit_breaker_for_two_different_routes() [InlineData(HttpStatusCode.VariantAlsoNegotiates)] [InlineData(HttpStatusCode.InsufficientStorage)] [InlineData(HttpStatusCode.LoopDetected)] - public async Task should_throw_broken_circuit_exception_after_two_exceptions(HttpStatusCode errorCode) + public async Task Should_throw_broken_circuit_exception_after_two_exceptions(HttpStatusCode errorCode) { var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); @@ -125,7 +124,7 @@ await Assert.ThrowsAsync>(async () = } [Fact] - public async Task should_not_throw_broken_circuit_exception_if_status_code_ok() + public async Task Should_not_throw_broken_circuit_exception_if_status_code_ok() { var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); @@ -135,9 +134,9 @@ public async Task should_not_throw_broken_circuit_exception_if_status_code_ok() Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); } - [Fact(Skip = "TODO", DisplayName = "TODO " + nameof(should_throw_and_before_delay_should_not_allow_requests))] + [Fact(Skip = "TODO", DisplayName = "TODO " + nameof(Should_throw_and_before_delay_should_not_allow_requests))] [Trait("TODO", "Fix after the release")] - public async Task should_throw_and_before_delay_should_not_allow_requests() + public async Task Should_throw_and_before_delay_should_not_allow_requests() { var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); @@ -154,7 +153,7 @@ await Assert.ThrowsAsync>(async () = } [Fact] - public async Task should_throw_but_after_delay_should_allow_one_more_internal_server_error() + public async Task Should_throw_but_after_delay_should_allow_one_more_internal_server_error() { var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); @@ -170,7 +169,7 @@ await Assert.ThrowsAsync>(async () = } [Fact] - public async Task should_throw_but_after_delay_should_allow_one_more_internal_server_error_and_throw() + public async Task Should_throw_but_after_delay_should_allow_one_more_internal_server_error_and_throw() { var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); @@ -188,7 +187,7 @@ await Assert.ThrowsAsync>(async () = } [Fact] - public async Task should_throw_but_after_delay_should_allow_one_more_ok_request_and_put_counter_back_to_zero() + public async Task Should_throw_but_after_delay_should_allow_one_more_ok_request_and_put_counter_back_to_zero() { var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); @@ -208,14 +207,12 @@ await Assert.ThrowsAsync>(async () = await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); } - private PollyQoSProvider PollyQoSProviderFactory() + private static PollyQoSProvider PollyQoSProviderFactory() { var factory = new Mock(); factory.Setup(x => x.CreateLogger()) .Returns(new Mock().Object); - - var pollyQoSProvider = new PollyQoSProvider(factory.Object); - return pollyQoSProvider; + return new PollyQoSProvider(factory.Object); } private static PollyPolicyWrapper PolicyWrapperFactory(string routeTemplate, PollyQoSProvider pollyQoSProvider, bool inactiveExceptionsAllowedBeforeBreaking = false) From 573a9d98e3eb730a53a555d881df15de9017b43d Mon Sep 17 00:00:00 2001 From: Van Tran Date: Wed, 22 May 2024 03:06:21 +0700 Subject: [PATCH 09/15] #738 #1990 Route Metadata as custom properties (#1843) * feat(configuration): adding route metadata * feat(configuration): update docs * feat(configuration): replace Dictionary<> by IDictionary<>, code cleaning * feat(configuration): replace Dictionary<> by IDictionary<> * feat(configuration): replace Dictionary<> by IDictionary<> * feat(configuration): update the data type of FileDynamicRoute Metadata * formatting * feat(configuration): fix integration tests * feat !1843 add extension methods for DownstreamRoute to get metadata * feat !1843 add extension methods for DownstreamRoute * feat !1843 update docs * feat !1843 update docs * feat !1843 cleanup split string logic * SA1505: An opening brace should not be followed by a blank line * IDE1006: Naming rule violation: These words must begin with upper case characters: should_xxx * Fix compile errors after rebasing * Fix unit tests + AAA pattern * First Version, providing a generic extension method GetMetadata with global configuration * Adding ConvertToNumericType method to be able to use the NumberStyles enum * adding first acceptance tests * The tests are now passing again... * adding latest test cases. That should be enough (includes global configuration changes too) * Update metadata.rst * adding the xml docs for IMetadataCreator and MetadataCreator * renaming MetadataCreator to DefaultMetadataCreator * number tests for .net 6 too * Moving Metadata specific downstream route extensions to the Metadata folder * cleanup * applying some of the requested changes * Final code review by @raman-m * Add traits * Fix docs build error --------- Co-authored-by: Raman Maksimchuk Co-authored-by: Guillaume Gnaegi <58469901+ggnaegi@users.noreply.github.com> --- docs/features/configuration.rst | 61 +- docs/features/metadata.rst | 99 +++ docs/index.rst | 1 + .../Builder/DownstreamRouteBuilder.cs | 10 +- .../Builder/MetadataOptionsBuilder.cs | 54 ++ .../Creator/DefaultMetadataCreator.cs | 34 + .../Configuration/Creator/DynamicsCreator.cs | 12 +- .../Configuration/Creator/IMetadataCreator.cs | 11 + .../Configuration/Creator/RoutesCreator.cs | 16 +- src/Ocelot/Configuration/DownstreamRoute.cs | 9 +- .../Configuration/File/FileDynamicRoute.cs | 1 + .../File/FileGlobalConfiguration.cs | 3 + .../Configuration/File/FileMetadataOptions.cs | 33 + src/Ocelot/Configuration/File/FileRoute.cs | 9 +- src/Ocelot/Configuration/MetadataOptions.cs | 45 ++ src/Ocelot/DependencyInjection/Features.cs | 8 + .../DependencyInjection/OcelotBuilder.cs | 1 + .../Metadata/DownstreamRouteExtensions.cs | 165 +++++ .../HttpDelegatingHandlersTests.cs | 2 +- .../Metadata/DownstreamMetadataTests.cs | 611 ++++++++++++++++++ .../EurekaServiceDiscoveryTests.cs | 6 +- test/Ocelot.AcceptanceTests/Steps.cs | 5 +- .../AdministrationTests.cs | 4 +- .../CustomOcelotMiddleware.cs | 34 + test/Ocelot.ManualTest/Program.cs | 5 +- test/Ocelot.ManualTest/appsettings.json | 3 +- test/Ocelot.ManualTest/ocelot.json | 15 + .../DefaultMetadataCreatorTests.cs | 118 ++++ .../DownstreamRouteExtensionsTests.cs | 281 ++++++++ .../Configuration/DynamicsCreatorTests.cs | 116 ++-- .../Configuration/RoutesCreatorTests.cs | 34 +- 31 files changed, 1732 insertions(+), 74 deletions(-) create mode 100644 docs/features/metadata.rst create mode 100644 src/Ocelot/Configuration/Builder/MetadataOptionsBuilder.cs create mode 100644 src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs create mode 100644 src/Ocelot/Configuration/Creator/IMetadataCreator.cs create mode 100644 src/Ocelot/Configuration/File/FileMetadataOptions.cs create mode 100644 src/Ocelot/Configuration/MetadataOptions.cs create mode 100644 src/Ocelot/Metadata/DownstreamRouteExtensions.cs create mode 100644 test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs create mode 100644 test/Ocelot.ManualTest/CustomOcelotMiddleware.cs create mode 100644 test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs create mode 100644 test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 51631b768..4447dac3d 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -70,7 +70,8 @@ Here is an example Route configuration. You don't need to set all of these thing "IPAllowedList": [], "IPBlockedList": [], "ExcludeAllowedFromBlocked": false - } + }, + "Metadata": {} } The actual Route schema for properties can be found in the C# `FileRoute `_ class. @@ -494,6 +495,64 @@ You can utilize these methods in the ``ConfigureAppConfiguration`` method (locat You can find additional details in the dedicated :ref:`di-configuration-overview` section and in subsequent sections related to the :doc:`../features/dependencyinjection` chapter. +Route Metadata +-------------- + +Ocelot provides various features such as routing, authentication, caching, load balancing, and more. However, some users may encounter situations where Ocelot does not meet their specific needs or they want to customize its behavior. In such cases, Ocelot allows users to add metadata to the route configuration. This property can store any arbitrary data that users can access in middlewares or delegating handlers. By using the metadata, users can implement their own logic and extend the functionality of Ocelot. + +Here is an example: + +.. code-block:: json + + { + "Routes": [ + { + "UpstreamHttpMethod": [ "GET" ], + "UpstreamPathTemplate": "/posts/{postId}", + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamHostAndPorts": [ + { "Host": "localhost", "Port": 80 } + ], + "Metadata": { + "api-id": "FindPost", + "my-extension/param1": "overwritten-value", + "other-extension/param1": "value1", + "other-extension/param2": "value2", + "tags": "tag1, tag2, area1, area2, func1", + "json": "[1, 2, 3, 4, 5]" + } + } + ], + "GlobalConfiguration": { + "Metadata": { + "instance_name": "dc-1-54abcz", + "my-extension/param1": "default-value" + } + } + } + +Now, the route metadata can be accessed through the `DownstreamRoute` object: + +.. code-block:: csharp + + public static class OcelotMiddlewares + { + public static Task PreAuthenticationMiddleware(HttpContext context, Func next) + { + var downstreamRoute = context.Items.DownstreamRoute(); + + if(downstreamRoute?.Metadata is {} metadata) + { + var param1 = metadata.GetValueOrDefault("my-extension/param1") ?? throw new MyExtensionException("Param 1 is null"); + var param2 = metadata.GetValueOrDefault("my-extension/param2", "custom-value"); + + // working with metadata + } + + return next(); + } + } + """" .. [#f1] ":ref:`config-merging-files`" feature was requested in `issue 296 `_, since then we extended it in `issue 1216 `_ (PR `1227 `_) as ":ref:`config-merging-tomemory`" subfeature which was released as a part of version `23.2`_. diff --git a/docs/features/metadata.rst b/docs/features/metadata.rst new file mode 100644 index 000000000..902ff972e --- /dev/null +++ b/docs/features/metadata.rst @@ -0,0 +1,99 @@ +Metadata +======== + +Configuration +------------- + +Ocelot provides various features such as routing, authentication, caching, load +balancing, and more. +However, some users may encounter situations where Ocelot does not meet their +specific needs or they want to customize its behavior. +In such cases, Ocelot allows users to add metadata to the route configuration. +This property can store any arbitrary data that users can access in middlewares +or delegating handlers. + +By using the metadata, users can implement their own logic and extend the +functionality of Ocelot e.g. + +.. code-block:: json + + { + "Routes": [ + { + "UpstreamHttpMethod": [ "GET" ], + "UpstreamPathTemplate": "/posts/{postId}", + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamHostAndPorts": [ + { "Host": "localhost", "Port": 80 } + ], + "Metadata": { + "id": "FindPost", + "tags": "tag1, tag2, area1, area2, func1", + "plugin1.enabled": "true", + "plugin1.values": "[1, 2, 3, 4, 5]", + "plugin1.param": "value2", + "plugin1.param2": "123", + "plugin2/param1": "overwritten-value", + "plugin2/param2": "{\"name\":\"John Doe\",\"age\":30,\"city\":\"New York\",\"is_student\":false,\"hobbies\":[\"reading\",\"hiking\",\"cooking\"]}" + } + } + ], + "GlobalConfiguration": { + "Metadata": { + "instance_name": "machine-1", + "plugin2/param1": "default-value" + } + } + } + +Now, the route metadata can be accessed through the ``DownstreamRoute`` object: + +.. code-block:: csharp + + public class MyMiddleware + { + public Task Invoke(HttpContext context, Func next) + { + var route = context.Items.DownstreamRoute(); + var enabled = route.GetMetadata("plugin1.enabled"); + var values = route.GetMetadata("plugin1.values"); + var param1 = route.GetMetadata("plugin1.param", "system-default-value"); + var param2 = route.GetMetadata("plugin1.param2"); + + // working on the plugin1's function + + return next?.Invoke(); + } + } + +Extension Methods +----------------- + +Ocelot provides one DowstreamRoute extension method to help you retrieve your metadata values effortlessly. +With the exception of the types string, bool, bool?, string[] and numeric, all strings passed as parameters are treated as json strings and an attempt is made to convert them into objects of generic type T. +If the value is null, then, if not explicitely specified, the default for the chosen target type is returned. + +.. list-table:: + :widths: 20 40 40 + + * - Method + - Description + - Notes + * - ``GetMetadata`` + - The metadata value is returned as string without further parsing + - + * - ``GetMetadata`` + - The metadata value is splitted by a given separator (default ``,``) and returned as a string array. + - Several parameters can be set in the global configuration, such as Separators (default = ``[","]``), StringSplitOptions (default ``None``) and TrimChars, the characters that should be trimmed (default = ``[' ']``). + * - ``GetMetadata`` + - The metadata value is parsed to a number. + - Some parameters can be set in the global configuration, such as NumberStyle (default ``Any``) and CurrentCulture (default ``CultureInfo.CurrentCulture``) + * - ``GetMetadata`` + - The metadata value is converted to the given generic type. The value is treated as a json string and the json serializer tries to deserialize the string to the target type. + - A JsonSerializerOptions object can be passed as method parameter, Web is used as default. + * - ``GetMetadata`` + - Check if the metadata value is a truthy value, otherwise return false. + - The truthy values are: ``true``, ``yes``, ``ok``, ``on``, ``enable``, ``enabled`` + * - ``GetMetadata`` + - Check if the metadata value is a truthy value (return true), or falsy value (return false), otherwise return null. + - The known truthy values are: ``true``, ``yes``, ``ok``, ``on``, ``enable``, ``enabled``, ``1``, the known falsy values are: ``false``, ``no``, ``off``, ``disable``, ``disabled``, ``0`` diff --git a/docs/index.rst b/docs/index.rst index be0b6deb4..c313c5104 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -113,6 +113,7 @@ Updated Documentation features/kubernetes features/loadbalancer features/logging + features/metadata features/methodtransformation features/middlewareinjection features/qualityofservice diff --git a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs index ffff82e17..2f01cd429 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs @@ -42,6 +42,7 @@ public class DownstreamRouteBuilder private Version _downstreamHttpVersion; private HttpVersionPolicy _downstreamHttpVersionPolicy; private Dictionary _upstreamHeaders; + private MetadataOptions _metadataOptions; public DownstreamRouteBuilder() { @@ -275,6 +276,12 @@ public DownstreamRouteBuilder WithDownstreamHttpVersionPolicy(HttpVersionPolicy return this; } + public DownstreamRouteBuilder WithMetadata(MetadataOptions metadataOptions) + { + _metadataOptions = metadataOptions; + return this; + } + public DownstreamRoute Build() { return new DownstreamRoute( @@ -313,6 +320,7 @@ public DownstreamRoute Build() _downstreamHttpMethod, _downstreamHttpVersion, _downstreamHttpVersionPolicy, - _upstreamHeaders); + _upstreamHeaders, + _metadataOptions); } } diff --git a/src/Ocelot/Configuration/Builder/MetadataOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/MetadataOptionsBuilder.cs new file mode 100644 index 000000000..79b765672 --- /dev/null +++ b/src/Ocelot/Configuration/Builder/MetadataOptionsBuilder.cs @@ -0,0 +1,54 @@ +using System.Globalization; + +namespace Ocelot.Configuration.Builder; + +public class MetadataOptionsBuilder +{ + private string[] _separators; + private char[] _trimChars; + private StringSplitOptions _stringSplitOption; + private NumberStyles _numberStyle; + private CultureInfo _currentCulture; + private IDictionary _metadata; + + public MetadataOptionsBuilder WithSeparators(string[] separators) + { + _separators = separators; + return this; + } + + public MetadataOptionsBuilder WithTrimChars(char[] trimChars) + { + _trimChars = trimChars; + return this; + } + + public MetadataOptionsBuilder WithStringSplitOption(string stringSplitOption) + { + _stringSplitOption = Enum.Parse(stringSplitOption); + return this; + } + + public MetadataOptionsBuilder WithNumberStyle(string numberStyle) + { + _numberStyle = Enum.Parse(numberStyle); + return this; + } + + public MetadataOptionsBuilder WithCurrentCulture(string currentCulture) + { + _currentCulture = CultureInfo.GetCultureInfo(currentCulture); + return this; + } + + public MetadataOptionsBuilder WithMetadata(IDictionary metadata) + { + _metadata = metadata; + return this; + } + + public MetadataOptions Build() + { + return new MetadataOptions(_separators, _trimChars, _stringSplitOption, _numberStyle, _currentCulture, _metadata); + } +} diff --git a/src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs b/src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs new file mode 100644 index 000000000..4ffcafe67 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs @@ -0,0 +1,34 @@ +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator; + +/// +/// This class implements the interface. +/// +public class DefaultMetadataCreator : IMetadataCreator +{ + public MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration globalConfiguration) + { + // metadata from the route could be null when no metadata is defined + metadata ??= new Dictionary(); + + // metadata from the global configuration is never null + var options = globalConfiguration.MetadataOptions; + var mergedMetadata = new Dictionary(options.Metadata); + + foreach (var (key, value) in metadata) + { + mergedMetadata[key] = value; + } + + return new MetadataOptionsBuilder() + .WithMetadata(mergedMetadata) + .WithSeparators(options.Separators) + .WithTrimChars(options.TrimChars) + .WithStringSplitOption(options.StringSplitOption) + .WithNumberStyle(options.NumberStyle) + .WithCurrentCulture(options.CurrentCulture) + .Build(); + } +} diff --git a/src/Ocelot/Configuration/Creator/DynamicsCreator.cs b/src/Ocelot/Configuration/Creator/DynamicsCreator.cs index 36d591ab9..cb5cdf419 100644 --- a/src/Ocelot/Configuration/Creator/DynamicsCreator.cs +++ b/src/Ocelot/Configuration/Creator/DynamicsCreator.cs @@ -8,12 +8,18 @@ public class DynamicsCreator : IDynamicsCreator private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; private readonly IVersionCreator _versionCreator; private readonly IVersionPolicyCreator _versionPolicyCreator; + private readonly IMetadataCreator _metadataCreator; - public DynamicsCreator(IRateLimitOptionsCreator rateLimitOptionsCreator, IVersionCreator versionCreator, IVersionPolicyCreator versionPolicyCreator) + public DynamicsCreator( + IRateLimitOptionsCreator rateLimitOptionsCreator, + IVersionCreator versionCreator, + IVersionPolicyCreator versionPolicyCreator, + IMetadataCreator metadataCreator) { _rateLimitOptionsCreator = rateLimitOptionsCreator; _versionCreator = versionCreator; _versionPolicyCreator = versionPolicyCreator; + _metadataCreator = metadataCreator; } public List Create(FileConfiguration fileConfiguration) @@ -29,7 +35,8 @@ private Route SetUpDynamicRoute(FileDynamicRoute fileDynamicRoute, FileGlobalCon .Create(fileDynamicRoute.RateLimitRule, globalConfiguration); var version = _versionCreator.Create(fileDynamicRoute.DownstreamHttpVersion); - var versionPolicy = _versionPolicyCreator.Create(fileDynamicRoute.DownstreamHttpVersionPolicy); + var versionPolicy = _versionPolicyCreator.Create(fileDynamicRoute.DownstreamHttpVersionPolicy); + var metadata = _metadataCreator.Create(fileDynamicRoute.Metadata, globalConfiguration); var downstreamRoute = new DownstreamRouteBuilder() .WithEnableRateLimiting(rateLimitOption.EnableRateLimiting) @@ -37,6 +44,7 @@ private Route SetUpDynamicRoute(FileDynamicRoute fileDynamicRoute, FileGlobalCon .WithServiceName(fileDynamicRoute.ServiceName) .WithDownstreamHttpVersion(version) .WithDownstreamHttpVersionPolicy(versionPolicy) + .WithMetadata(metadata) .Build(); var route = new RouteBuilder() diff --git a/src/Ocelot/Configuration/Creator/IMetadataCreator.cs b/src/Ocelot/Configuration/Creator/IMetadataCreator.cs new file mode 100644 index 000000000..5252d620b --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IMetadataCreator.cs @@ -0,0 +1,11 @@ +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator; + +/// +/// This interface describes the creation of metadata options. +/// +public interface IMetadataCreator +{ + MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration globalConfiguration); +} diff --git a/src/Ocelot/Configuration/Creator/RoutesCreator.cs b/src/Ocelot/Configuration/Creator/RoutesCreator.cs index 4c94f6c71..015944014 100644 --- a/src/Ocelot/Configuration/Creator/RoutesCreator.cs +++ b/src/Ocelot/Configuration/Creator/RoutesCreator.cs @@ -1,6 +1,6 @@ using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; - + namespace Ocelot.Configuration.Creator { public class RoutesCreator : IRoutesCreator @@ -22,6 +22,7 @@ public class RoutesCreator : IRoutesCreator private readonly ISecurityOptionsCreator _securityOptionsCreator; private readonly IVersionCreator _versionCreator; private readonly IVersionPolicyCreator _versionPolicyCreator; + private readonly IMetadataCreator _metadataCreator; public RoutesCreator( IClaimsToThingCreator claimsToThingCreator, @@ -38,9 +39,10 @@ public RoutesCreator( ILoadBalancerOptionsCreator loadBalancerOptionsCreator, IRouteKeyCreator routeKeyCreator, ISecurityOptionsCreator securityOptionsCreator, - IVersionCreator versionCreator, + IVersionCreator versionCreator, IVersionPolicyCreator versionPolicyCreator, - IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator) + IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator, + IMetadataCreator metadataCreator) { _routeKeyCreator = routeKeyCreator; _loadBalancerOptionsCreator = loadBalancerOptionsCreator; @@ -60,6 +62,7 @@ public RoutesCreator( _versionCreator = versionCreator; _versionPolicyCreator = versionPolicyCreator; _upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator; + _metadataCreator = metadataCreator; } public List Create(FileConfiguration fileConfiguration) @@ -113,6 +116,8 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf var cacheOptions = _cacheOptionsCreator.Create(fileRoute.FileCacheOptions, globalConfiguration, fileRoute.UpstreamPathTemplate, fileRoute.UpstreamHttpMethod); + var metadata = _metadataCreator.Create(fileRoute.Metadata, globalConfiguration); + var route = new DownstreamRouteBuilder() .WithKey(fileRoute.Key) .WithDownstreamPathTemplate(fileRoute.DownstreamPathTemplate) @@ -150,6 +155,7 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf .WithDownstreamHttpVersion(downstreamHttpVersion) .WithDownstreamHttpVersionPolicy(downstreamHttpVersionPolicy) .WithDownStreamHttpMethod(fileRoute.DownstreamHttpMethod) + .WithMetadata(metadata) .Build(); return route; @@ -157,14 +163,14 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf private Route SetUpRoute(FileRoute fileRoute, DownstreamRoute downstreamRoutes) { - var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute); + var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute); var upstreamHeaderTemplates = _upstreamHeaderTemplatePatternCreator.Create(fileRoute); var route = new RouteBuilder() .WithUpstreamHttpMethod(fileRoute.UpstreamHttpMethod) .WithUpstreamPathTemplate(upstreamTemplatePattern) .WithDownstreamRoute(downstreamRoutes) - .WithUpstreamHost(fileRoute.UpstreamHost) + .WithUpstreamHost(fileRoute.UpstreamHost) .WithUpstreamHeaders(upstreamHeaderTemplates) .Build(); diff --git a/src/Ocelot/Configuration/DownstreamRoute.cs b/src/Ocelot/Configuration/DownstreamRoute.cs index ab4b6d5de..0241f9b61 100644 --- a/src/Ocelot/Configuration/DownstreamRoute.cs +++ b/src/Ocelot/Configuration/DownstreamRoute.cs @@ -41,7 +41,8 @@ public DownstreamRoute( string downstreamHttpMethod, Version downstreamHttpVersion, HttpVersionPolicy downstreamHttpVersionPolicy, - Dictionary upstreamHeaders) + Dictionary upstreamHeaders, + MetadataOptions metadataOptions) { DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; AddHeadersToDownstream = addHeadersToDownstream; @@ -76,9 +77,10 @@ public DownstreamRoute( AddHeadersToUpstream = addHeadersToUpstream; SecurityOptions = securityOptions; DownstreamHttpMethod = downstreamHttpMethod; - DownstreamHttpVersion = downstreamHttpVersion; + DownstreamHttpVersion = downstreamHttpVersion; DownstreamHttpVersionPolicy = downstreamHttpVersionPolicy; UpstreamHeaders = upstreamHeaders ?? new(); + MetadataOptions = metadataOptions; } public string Key { get; } @@ -114,7 +116,7 @@ public DownstreamRoute( public bool DangerousAcceptAnyServerCertificateValidator { get; } public SecurityOptions SecurityOptions { get; } public string DownstreamHttpMethod { get; } - public Version DownstreamHttpVersion { get; } + public Version DownstreamHttpVersion { get; } /// The enum specifies behaviors for selecting and negotiating the HTTP version for a request. /// An enum value being mapped from a constant. @@ -128,5 +130,6 @@ public DownstreamRoute( /// public HttpVersionPolicy DownstreamHttpVersionPolicy { get; } public Dictionary UpstreamHeaders { get; } + public MetadataOptions MetadataOptions { get; } } } diff --git a/src/Ocelot/Configuration/File/FileDynamicRoute.cs b/src/Ocelot/Configuration/File/FileDynamicRoute.cs index 21ad814af..a42581531 100644 --- a/src/Ocelot/Configuration/File/FileDynamicRoute.cs +++ b/src/Ocelot/Configuration/File/FileDynamicRoute.cs @@ -19,5 +19,6 @@ public class FileDynamicRoute /// /// public string DownstreamHttpVersionPolicy { get; set; } + public IDictionary Metadata { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs index 340ca04ff..7ce35f99e 100644 --- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -12,6 +12,7 @@ public FileGlobalConfiguration() QoSOptions = new FileQoSOptions(); HttpHandlerOptions = new FileHttpHandlerOptions(); CacheOptions = new FileCacheOptions(); + MetadataOptions = new FileMetadataOptions(); } public string RequestIdKey { get; set; } @@ -45,5 +46,7 @@ public FileGlobalConfiguration() public string DownstreamHttpVersionPolicy { get; set; } public FileCacheOptions CacheOptions { get; set; } + + public FileMetadataOptions MetadataOptions { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileMetadataOptions.cs b/src/Ocelot/Configuration/File/FileMetadataOptions.cs new file mode 100644 index 000000000..8d34bbe44 --- /dev/null +++ b/src/Ocelot/Configuration/File/FileMetadataOptions.cs @@ -0,0 +1,33 @@ +using System.Globalization; + +namespace Ocelot.Configuration.File; + +public class FileMetadataOptions +{ + public FileMetadataOptions() + { + Separators = new[] { "," }; + TrimChars = new[] { ' ' }; + StringSplitOption = Enum.GetName(typeof(StringSplitOptions), StringSplitOptions.None); + NumberStyle = Enum.GetName(typeof(NumberStyles), NumberStyles.Any); + CurrentCulture = CultureInfo.CurrentCulture.Name; + Metadata = new Dictionary(); + } + + public FileMetadataOptions(FileMetadataOptions from) + { + Separators = from.Separators; + TrimChars = from.TrimChars; + StringSplitOption = from.StringSplitOption; + NumberStyle = from.NumberStyle; + CurrentCulture = from.CurrentCulture; + Metadata = from.Metadata; + } + + public IDictionary Metadata { get; set; } + public string[] Separators { get; set; } + public char[] TrimChars { get; set; } + public string StringSplitOption { get; set; } + public string NumberStyle { get; set; } + public string CurrentCulture { get; set; } +} diff --git a/src/Ocelot/Configuration/File/FileRoute.cs b/src/Ocelot/Configuration/File/FileRoute.cs index fe5eedf89..b9a3c4eda 100644 --- a/src/Ocelot/Configuration/File/FileRoute.cs +++ b/src/Ocelot/Configuration/File/FileRoute.cs @@ -17,11 +17,12 @@ public FileRoute() FileCacheOptions = new FileCacheOptions(); HttpHandlerOptions = new FileHttpHandlerOptions(); LoadBalancerOptions = new FileLoadBalancerOptions(); + Metadata = new Dictionary(); Priority = 1; QoSOptions = new FileQoSOptions(); RateLimitOptions = new FileRateLimitRule(); RouteClaimsRequirement = new Dictionary(); - SecurityOptions = new FileSecurityOptions(); + SecurityOptions = new FileSecurityOptions(); UpstreamHeaderTemplates = new Dictionary(); UpstreamHeaderTransform = new Dictionary(); UpstreamHttpMethod = new List(); @@ -56,11 +57,12 @@ public FileRoute(FileRoute from) /// public string DownstreamHttpVersionPolicy { get; set; } public string DownstreamPathTemplate { get; set; } - public string DownstreamScheme { get; set; } + public string DownstreamScheme { get; set; } public FileCacheOptions FileCacheOptions { get; set; } public FileHttpHandlerOptions HttpHandlerOptions { get; set; } public string Key { get; set; } public FileLoadBalancerOptions LoadBalancerOptions { get; set; } + public IDictionary Metadata { get; set; } public int Priority { get; set; } public FileQoSOptions QoSOptions { get; set; } public FileRateLimitRule RateLimitOptions { get; set; } @@ -103,11 +105,12 @@ public static void DeepCopy(FileRoute from, FileRoute to) to.DownstreamHttpVersion = from.DownstreamHttpVersion; to.DownstreamHttpVersionPolicy = from.DownstreamHttpVersionPolicy; to.DownstreamPathTemplate = from.DownstreamPathTemplate; - to.DownstreamScheme = from.DownstreamScheme; + to.DownstreamScheme = from.DownstreamScheme; to.FileCacheOptions = new(from.FileCacheOptions); to.HttpHandlerOptions = new(from.HttpHandlerOptions); to.Key = from.Key; to.LoadBalancerOptions = new(from.LoadBalancerOptions); + to.Metadata = new Dictionary(from.Metadata); to.Priority = from.Priority; to.QoSOptions = new(from.QoSOptions); to.RateLimitOptions = new(from.RateLimitOptions); diff --git a/src/Ocelot/Configuration/MetadataOptions.cs b/src/Ocelot/Configuration/MetadataOptions.cs new file mode 100644 index 000000000..b94a1fb7c --- /dev/null +++ b/src/Ocelot/Configuration/MetadataOptions.cs @@ -0,0 +1,45 @@ +using Ocelot.Configuration.File; +using System.Globalization; + +namespace Ocelot.Configuration; + +public class MetadataOptions +{ + public MetadataOptions(MetadataOptions from) + { + Separators = from.Separators; + TrimChars = from.TrimChars; + StringSplitOption = from.StringSplitOption; + NumberStyle = from.NumberStyle; + CurrentCulture = from.CurrentCulture; + Metadata = from.Metadata; + } + + public MetadataOptions(FileMetadataOptions from) + { + StringSplitOption = Enum.Parse(from.StringSplitOption); + NumberStyle = Enum.Parse(from.NumberStyle); + CurrentCulture = CultureInfo.GetCultureInfo(from.CurrentCulture); + Separators = from.Separators; + TrimChars = from.TrimChars; + Metadata = from.Metadata; + } + + public MetadataOptions(string[] separators, char[] trimChars, StringSplitOptions stringSplitOption, + NumberStyles numberStyle, CultureInfo currentCulture, IDictionary metadata) + { + Separators = separators; + TrimChars = trimChars; + StringSplitOption = stringSplitOption; + NumberStyle = numberStyle; + CurrentCulture = currentCulture; + Metadata = metadata; + } + + public string[] Separators { get; } + public char[] TrimChars { get; } + public StringSplitOptions StringSplitOption { get; } + public NumberStyles NumberStyle { get; } + public CultureInfo CurrentCulture { get; } + public IDictionary Metadata { get; set; } +} diff --git a/src/Ocelot/DependencyInjection/Features.cs b/src/Ocelot/DependencyInjection/Features.cs index fa04d9092..1ea558932 100644 --- a/src/Ocelot/DependencyInjection/Features.cs +++ b/src/Ocelot/DependencyInjection/Features.cs @@ -44,4 +44,12 @@ public static IServiceCollection AddHeaderRouting(this IServiceCollection servic .AddSingleton() .AddSingleton() .AddSingleton(); + + /// + /// Ocelot feature: Inject custom metadata and use it in delegating handlers. + /// + /// The services collection to add the feature to. + /// The same object. + public static IServiceCollection AddOcelotMetadata(this IServiceCollection services) => + services.AddSingleton(); } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index f25f5a5f8..217280e37 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -116,6 +116,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton, OcelotConfigurationMonitor>(); Services.AddOcelotCache(); + Services.AddOcelotMetadata(); Services.AddOcelotMessageInvokerPool(); // See this for why we register this as singleton: diff --git a/src/Ocelot/Metadata/DownstreamRouteExtensions.cs b/src/Ocelot/Metadata/DownstreamRouteExtensions.cs new file mode 100644 index 000000000..ca3ba25e6 --- /dev/null +++ b/src/Ocelot/Metadata/DownstreamRouteExtensions.cs @@ -0,0 +1,165 @@ +using Ocelot.Configuration; +using System.Globalization; +using System.Reflection; +using System.Text.Json; + +namespace Ocelot.Metadata; + +public static class DownstreamRouteExtensions +{ + /// + /// The known truthy values. + /// + private static readonly HashSet TruthyValues = + new(StringComparer.OrdinalIgnoreCase) + { + "true", + "yes", + "on", + "ok", + "enable", + "enabled", + "1", + }; + + /// + /// The known falsy values. + /// + private static readonly HashSet FalsyValues = + new(StringComparer.OrdinalIgnoreCase) + { + "false", + "no", + "off", + "disable", + "disabled", + "0", + }; + + /// + /// The known numeric types. + /// + private static readonly HashSet NumericTypes = new() + { + typeof(byte), + typeof(sbyte), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(float), + typeof(double), + typeof(decimal), + }; + + /// + /// Extension method to get metadata from a downstream route. + /// + /// The metadata target type. + /// The current downstream route. + /// The metadata key in downstream route Metadata dictionary. + /// The fallback value if no value found. + /// Custom json serializer options if needed. + /// The parsed metadata value. + public static T GetMetadata(this DownstreamRoute downstreamRoute, string key, T defaultValue = default, + JsonSerializerOptions jsonSerializerOptions = null) + { + var metadata = downstreamRoute?.MetadataOptions.Metadata; + + if (metadata == null || !metadata.TryGetValue(key, out var metadataValue) || metadataValue == null) + { + return defaultValue; + } + + return (T)ConvertTo(typeof(T), metadataValue, downstreamRoute.MetadataOptions, + jsonSerializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web)); + } + + /// + /// Converting a string value to the target type. + /// Some custom conversion has been for the following types: + /// , , , numeric types; + /// otherwise trying to deserialize the value using the JsonSerializer. + /// + /// The target type. + /// The string value. + /// The metadata options, it includes the global configuration. + /// If needed, some custom json serializer options. + /// The converted string. + private static object ConvertTo(Type targetType, string value, MetadataOptions metadataOptions, + JsonSerializerOptions jsonSerializerOptions) + { + if (targetType == typeof(string)) + { + return value; + } + + if (targetType == typeof(bool)) + { + return TruthyValues.Contains(value.Trim()); + } + + if (targetType == typeof(bool?)) + { + if (TruthyValues.Contains(value.Trim())) + { + return true; + } + + if (FalsyValues.Contains(value.Trim())) + { + return false; + } + + return null; + } + + if (targetType == typeof(string[])) + { + if (value == null) + { + return Array.Empty(); + } + + return value.Split(metadataOptions.Separators, metadataOptions.StringSplitOption) + .Select(s => s.Trim(metadataOptions.TrimChars)) + .Where(s => !string.IsNullOrEmpty(s)) + .ToArray(); + } + + return NumericTypes.Contains(targetType) + ? ConvertToNumericType(value, targetType, metadataOptions.CurrentCulture, metadataOptions.NumberStyle) + : JsonSerializer.Deserialize(value, targetType, jsonSerializerOptions); + } + + /// + /// Converting string to the known numeric types. + /// + /// The number as string. + /// The target numeric type. + /// The current format provider. + /// The current number style configuration. + /// The parsed string as object of type targetType. + /// Exception thrown if the conversion for the type target type can't be found. + private static object ConvertToNumericType(string value, Type targetType, IFormatProvider provider, + NumberStyles numberStyle) + { + return targetType switch + { + { } t when t == typeof(byte) => byte.Parse(value, numberStyle, provider), + { } t when t == typeof(sbyte) => sbyte.Parse(value, numberStyle, provider), + { } t when t == typeof(short) => short.Parse(value, numberStyle, provider), + { } t when t == typeof(ushort) => ushort.Parse(value, numberStyle, provider), + { } t when t == typeof(int) => int.Parse(value, numberStyle, provider), + { } t when t == typeof(uint) => uint.Parse(value, numberStyle, provider), + { } t when t == typeof(long) => long.Parse(value, numberStyle, provider), + { } t when t == typeof(ulong) => ulong.Parse(value, numberStyle, provider), + { } t when t == typeof(float) => float.Parse(value, numberStyle, provider), + { } t when t == typeof(double) => double.Parse(value, numberStyle, provider), + { } t when t == typeof(decimal) => decimal.Parse(value, numberStyle, provider), + _ => throw new NotImplementedException($"No conversion available for the type: {targetType.Name}"), + }; + } +} diff --git a/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs index 83f79e720..330861317 100644 --- a/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs +++ b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs @@ -123,7 +123,7 @@ public void should_call_global_di_handlers_multiple_times() this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithGlobalHandlerRegisteredInDi()) + .And(x => _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(true)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) diff --git a/test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs b/test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs new file mode 100644 index 000000000..ea09880df --- /dev/null +++ b/test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs @@ -0,0 +1,611 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using Ocelot.Metadata; +using Ocelot.Middleware; +using System.Globalization; + +namespace Ocelot.AcceptanceTests.Metadata; + +[Trait("Feat", "738")] +public class DownstreamMetadataTests : IDisposable +{ + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public enum StringArrayConfig + { + Default = 1, + AlternateSeparators, + AlternateTrimChars, + AlternateStringSplitOptions, + Mix, + } + + public enum NumberConfig + { + Default = 1, + AlternateNumberStyle, + AlternateCulture, + } + + public DownstreamMetadataTests() + { + _steps = new Steps(); + _serviceHandler = new ServiceHandler(); + } + + public void Dispose() + { + _steps?.Dispose(); + _serviceHandler?.Dispose(); + } + + [Theory] + [InlineData(typeof(StringDownStreamMetadataHandler))] + [InlineData(typeof(StringArrayDownStreamMetadataHandler))] + [InlineData(typeof(BoolDownStreamMetadataHandler))] + [InlineData(typeof(DoubleDownStreamMetadataHandler))] + [InlineData(typeof(SuperDataContainerDownStreamMetadataHandler))] + public void ShouldMatchTargetObjects(Type currentType) + { + (Dictionary sourceDictionary, Dictionary _) = + GetSourceAndTargetDictionary(currentType); + + var port = PortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = port, }, }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + Metadata = sourceDictionary, + DelegatingHandlers = new List { currentType.Name, }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => x.GivenOcelotIsRunningWithSpecificHandlerForType(currentType)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + /// + /// Testing the string array type with different configurations. + /// + /// The possible separators. + /// The trimmed characters. + /// If the empty entries should be removed. + /// The current test configuration. + [Theory] + [InlineData(new[] { "," }, new[] { ' ' }, nameof(StringSplitOptions.None), StringArrayConfig.Default)] + [InlineData( + new[] { ";", ".", "," }, + new[] { ' ' }, + nameof(StringSplitOptions.None), + StringArrayConfig.AlternateSeparators)] + [InlineData( + new[] { "," }, + new[] { ' ', ';', ':' }, + nameof(StringSplitOptions.None), + StringArrayConfig.AlternateTrimChars)] + [InlineData( + new[] { "," }, + new[] { ' ' }, + nameof(StringSplitOptions.RemoveEmptyEntries), + StringArrayConfig.AlternateStringSplitOptions)] + [InlineData( + new[] { ";", ".", "," }, + new[] { ' ', '_', ':' }, + nameof(StringSplitOptions.RemoveEmptyEntries), + StringArrayConfig.Mix)] + public void ShouldMatchTargetStringArrayAccordingToConfiguration( + string[] separators, + char[] trimChars, + string stringSplitOption, + StringArrayConfig currentConfig) + { + (Dictionary sourceDictionary, Dictionary _) = + GetSourceAndTargetDictionariesForStringArrayType(currentConfig); + + sourceDictionary.Add(nameof(StringArrayConfig), currentConfig.ToString()); + + var port = PortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = port, }, }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + Metadata = sourceDictionary, + DelegatingHandlers = new List { nameof(StringArrayDownStreamMetadataHandler) }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + MetadataOptions = new FileMetadataOptions + { + Separators = separators, + TrimChars = trimChars, + StringSplitOption = stringSplitOption, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => x.GivenOcelotIsRunningWithSpecificHandlerForType(typeof(StringArrayDownStreamMetadataHandler))) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Theory] + [InlineData(NumberStyles.Any, "de-CH", NumberConfig.Default)] + [InlineData(NumberStyles.AllowParentheses | NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite | NumberStyles.AllowLeadingSign, "de-CH", NumberConfig.AlternateNumberStyle)] + public void ShouldMatchTargetNumberAccordingToConfiguration( + NumberStyles numberStyles, + string cultureName, + NumberConfig currentConfig) + { + (Dictionary sourceDictionary, Dictionary _) = + GetSourceAndTargetDictionariesForNumberType(); + + sourceDictionary.Add(nameof(NumberConfig), currentConfig.ToString()); + + var port = PortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = port, }, }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + Metadata = sourceDictionary, + DelegatingHandlers = new List { nameof(IntDownStreamMetadataHandler) }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + MetadataOptions = new FileMetadataOptions + { + NumberStyle = numberStyles.ToString(), + CurrentCulture = cultureName, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => x.GivenOcelotIsRunningWithSpecificHandlerForType(typeof(IntDownStreamMetadataHandler))) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url) + { + _serviceHandler.GivenThereIsAServiceRunningOn( + url, + context => + { + context.Response.StatusCode = 200; + return Task.CompletedTask; + }); + } + + /// + /// Starting ocelot with the delegating handler of type currentType. + /// + /// The current delegating handler type. + /// Throws if delegating handler type doesn't match. + private void GivenOcelotIsRunningWithSpecificHandlerForType(Type currentType) + { + switch (currentType) + { + case { } t when t == typeof(StringDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); + break; + case { } t when t == typeof(StringArrayDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); + break; + case { } t when t == typeof(BoolDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); + break; + case { } t when t == typeof(DoubleDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); + break; + case { } t when t == typeof(SuperDataContainerDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); + break; + case { } t when t == typeof(IntDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); + break; + default: + throw new NotImplementedException(); + } + } + + // It would have been better to use a generic method, but it is not possible to use a generic type as a parameter + // for the delegating handler name + private class StringDownStreamMetadataHandler : DownstreamMetadataHandler + { + public StringDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) + { + } + } + + private class StringArrayDownStreamMetadataHandler : DownstreamMetadataHandler + { + public StringArrayDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base( + httpContextAccessor) + { + } + } + + private class BoolDownStreamMetadataHandler : DownstreamMetadataHandler + { + public BoolDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) + { + } + } + + private class DoubleDownStreamMetadataHandler : DownstreamMetadataHandler + { + public DoubleDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) + { + } + } + + private class IntDownStreamMetadataHandler : DownstreamMetadataHandler + { + public IntDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) + { + } + } + + private class SuperDataContainerDownStreamMetadataHandler : DownstreamMetadataHandler + { + public SuperDataContainerDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base( + httpContextAccessor) + { + } + } + + /// + /// Simple delegating handler that checks if the metadata is correctly passed to the downstream route + /// and checking if the extension method GetMetadata returns the correct value. + /// + /// The current type. + private class DownstreamMetadataHandler : DelegatingHandler + { + private readonly IHttpContextAccessor _httpContextAccessor; + + public DownstreamMetadataHandler(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + var downstreamRoute = _httpContextAccessor.HttpContext?.Items.DownstreamRoute(); + + if (downstreamRoute.MetadataOptions.Metadata.ContainsKey(nameof(StringArrayConfig))) + { + var currentConfig = + Enum.Parse(downstreamRoute.MetadataOptions.Metadata[nameof(StringArrayConfig)]); + downstreamRoute.MetadataOptions.Metadata.Remove(nameof(StringArrayConfig)); + + (Dictionary _, Dictionary targetDictionary) = + GetSourceAndTargetDictionariesForStringArrayType(currentConfig); + + foreach (var key in targetDictionary.Keys) + { + Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata(key)); + } + } + else if (downstreamRoute.MetadataOptions.Metadata.ContainsKey(nameof(NumberConfig))) + { + downstreamRoute.MetadataOptions.Metadata.Remove(nameof(NumberConfig)); + + (Dictionary _, Dictionary targetDictionary) = + GetSourceAndTargetDictionariesForNumberType(); + + foreach (var key in targetDictionary.Keys) + { + Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata(key)); + } + } + else + { + (Dictionary _, Dictionary targetDictionary) = + GetSourceAndTargetDictionary(typeof(T)); + + foreach (var key in targetDictionary.Keys) + { + Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata(key)); + } + } + + return base.SendAsync(request, cancellationToken); + } + } + + public static (Dictionary SourceDictionary, Dictionary TargetDictionary) + GetSourceAndTargetDictionariesForStringArrayType(StringArrayConfig currentConfig) + { + Dictionary sourceDictionary; + Dictionary targetDictionary; + + if (currentConfig == StringArrayConfig.Default) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1, Value2, Value3" }, + { "Key2", "Value2, Value3, Value4" }, + { "Key3", "Value3, ,Value4, Value5" }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentConfig == StringArrayConfig.AlternateSeparators) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1; Value2. Value3" }, + { "Key2", "Value2. Value3, Value4" }, + { "Key3", "Value3, ,Value4; Value5" }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentConfig == StringArrayConfig.AlternateTrimChars) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1; :, Value2 :, Value3 " }, + { "Key2", " Value2, Value3; , Value4" }, + { "Key3", "Value3 , ,Value4, Value5 " }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentConfig == StringArrayConfig.AlternateStringSplitOptions) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1, ,Value2, Value3, " }, + { "Key2", "Value2, , ,Value3, Value4, , ," }, + { "Key3", "Value3, ,Value4, , ,Value5" }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentConfig == StringArrayConfig.Mix) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1; :, Value2. :, Value3 " }, + { "Key2", " Value2_, , , Value3; , Value4" }, + { "Key3", "Value3:; , ,Value4, Value5 " }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + throw new NotImplementedException(); + } + + public static (Dictionary SourceDictionary, Dictionary TargetDictionary) + GetSourceAndTargetDictionariesForNumberType() + { + return ( + new Dictionary + { + { "Key1", "-2" }, { "Key2", " (1000000) " }, { "Key3", "-1000000000 " }, + }, + new Dictionary { { "Key1", -2 }, { "Key2", -1000000 }, { "Key3", -1000000000 } }); + } + + /// + /// Method retrieving the source and target dictionary for the current type. + /// The source value is of type string and the target is of type object. + /// + /// The current type. + /// A source and a target directory to compare the results. + /// Throws if type not found. + public static (Dictionary SourceDictionary, Dictionary TargetDictionary) + GetSourceAndTargetDictionary(Type currentType) + { + Dictionary sourceDictionary; + Dictionary targetDictionary; + if (currentType == typeof(StringDownStreamMetadataHandler) || currentType == typeof(string)) + { + sourceDictionary = new Dictionary { { "Key1", "Value1" }, { "Key2", "Value2" }, }; + + targetDictionary = new Dictionary { { "Key1", "Value1" }, { "Key2", "Value2" }, }; + + return (sourceDictionary, targetDictionary); + } + + if (currentType == typeof(StringArrayDownStreamMetadataHandler) || currentType == typeof(string[])) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1, Value2, Value3" }, + { "Key2", "Value2, Value3, Value4" }, + { "Key3", "Value3, ,Value4, Value5" }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentType == typeof(BoolDownStreamMetadataHandler) || currentType == typeof(bool?)) + { + sourceDictionary = new Dictionary + { + { "Key1", "true" }, + { "Key2", "false" }, + { "Key3", "null" }, + { "Key4", "disabled" }, + { "Key5", "0" }, + { "Key6", "1" }, + { "Key7", "yes" }, + { "Key8", "enabled" }, + { "Key9", "on" }, + { "Key10", "off" }, + { "Key11", "test" }, + }; + + targetDictionary = new Dictionary + { + { "Key1", true }, + { "Key2", false }, + { "Key3", null }, + { "Key4", false }, + { "Key5", false }, + { "Key6", true }, + { "Key7", true }, + { "Key8", true }, + { "Key9", true }, + { "Key10", false }, + { "Key11", null }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentType == typeof(DoubleDownStreamMetadataHandler) || currentType == typeof(double)) + { + sourceDictionary = new Dictionary { { "Key1", "0.00001" }, { "Key2", "0.00000001" }, }; + + targetDictionary = new Dictionary { { "Key1", 0.00001 }, { "Key2", 0.00000001 }, }; + + return (sourceDictionary, targetDictionary); + } + + if (currentType == typeof(SuperDataContainerDownStreamMetadataHandler) || + currentType == typeof(SuperDataContainer)) + { + sourceDictionary = new Dictionary + { + { "Key1", "{\"key1\":\"Bonjour\",\"key2\":\"Hello\",\"key3\":0.00001,\"key4\":true}" }, + }; + + targetDictionary = new Dictionary + { + { + "Key1", new SuperDataContainer + { + Key1 = "Bonjour", Key2 = "Hello", Key3 = 0.00001, Key4 = true, + } + }, + }; + + return (sourceDictionary, targetDictionary); + } + + throw new NotImplementedException(); + } + + public class SuperDataContainer + { + public string Key1 { get; set; } + + public string Key2 { get; set; } + + public double Key3 { get; set; } + + public bool? Key4 { get; set; } + + public override bool Equals(object obj) + { + // Check for null and compare run-time types. + if (obj == null || this.GetType() != obj.GetType()) + { + return false; + } + + SuperDataContainer other = (SuperDataContainer)obj; + return Key1 == other.Key1 && Key2 == other.Key2 && Key3.Equals(other.Key3) && Key4 == other.Key4; + } + + // https://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-overriding-gethashcode + public override int GetHashCode() + { + unchecked + { + int hash = 17; + hash = (hash * 23) + (Key1?.GetHashCode() ?? 0); + hash = (hash * 23) + (Key2?.GetHashCode() ?? 0); + hash = (hash * 23) + Key3.GetHashCode(); + hash = (hash * 23) + (Key4?.GetHashCode() ?? 0); + return hash; + } + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs index a12c20641..5ec8f194b 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs @@ -112,7 +112,7 @@ private void GivenThereIsAFakeEurekaServiceDiscoveryProvider(string url, string evictionTimestamp = 0, serviceUpTimestamp = 1457714988223, }, - metadata = new Metadata + metadata = new() { value = "java.util.Collections$EmptyMap", }, @@ -232,7 +232,7 @@ public class LeaseInfo public long serviceUpTimestamp { get; set; } } - public class Metadata + public class ValueMetadata { [JsonProperty("@class")] public string value { get; set; } @@ -251,7 +251,7 @@ public class Instance public int countryId { get; set; } public DataCenterInfo dataCenterInfo { get; set; } public LeaseInfo leaseInfo { get; set; } - public Metadata metadata { get; set; } + public ValueMetadata metadata { get; set; } public string homePageUrl { get; set; } public string statusPageUrl { get; set; } public string healthCheckUrl { get; set; } diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 0d5505fcd..9b1e4b927 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -29,6 +29,7 @@ using Serilog.Core; using System.IO.Compression; using System.Net.Http.Headers; +using System.Security.Policy; using System.Text; using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests; using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; @@ -536,7 +537,7 @@ public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi() _ocelotClient = _ocelotServer.CreateClient(); } - public void GivenOcelotIsRunningWithGlobalHandlerRegisteredInDi() + public void GivenOcelotIsRunningWithHandlerRegisteredInDi(bool global = false) where TOne : DelegatingHandler { _webHostBuilder = new WebHostBuilder(); @@ -555,7 +556,7 @@ public void GivenOcelotIsRunningWithGlobalHandlerRegisteredInDi() { s.AddSingleton(_webHostBuilder); s.AddOcelot() - .AddDelegatingHandler(true); + .AddDelegatingHandler(global); }) .Configure(a => { a.UseOcelot().Wait(); }); diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs index 7495307a6..5ae8d0819 100644 --- a/test/Ocelot.IntegrationTests/AdministrationTests.cs +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -118,7 +118,7 @@ public void Should_return_OK_status_and_multiline_indented_json_response_with_js .Then(x => ThenTheResultHaveMultiLineIndentedJson()) .BDDfy(); } - + [Fact] public void Should_be_able_to_use_token_from_ocelot_a_on_ocelot_b() { @@ -878,7 +878,7 @@ private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) { _response.StatusCode.ShouldBe(expectedHttpStatusCode); } - + private void ThenTheResultHaveMultiLineIndentedJson() { const string indent = " "; diff --git a/test/Ocelot.ManualTest/CustomOcelotMiddleware.cs b/test/Ocelot.ManualTest/CustomOcelotMiddleware.cs new file mode 100644 index 000000000..f3d251887 --- /dev/null +++ b/test/Ocelot.ManualTest/CustomOcelotMiddleware.cs @@ -0,0 +1,34 @@ +using Ocelot.Logging; +using Ocelot.Middleware; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Ocelot.ManualTest; + +public static class CustomOcelotMiddleware +{ + public static Task Invoke(HttpContext context, Func next) + { + var logger = GetLogger(context); + var downstreamRoute = context.Items.DownstreamRoute(); + + if (downstreamRoute?.MetadataOptions?.Metadata is { } metadata) + { + logger.LogInformation(() => + { + var metadataInJson = JsonSerializer.Serialize(metadata); + var message = $"My custom middleware found some metadata: {metadataInJson}"; + return message; + }); + } + + return next(); + } + + private static IOcelotLogger GetLogger(HttpContext context) + { + var loggerFactory = context.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger(); + return logger; + } +} diff --git a/test/Ocelot.ManualTest/Program.cs b/test/Ocelot.ManualTest/Program.cs index 135e7e2c7..eaab89ec9 100644 --- a/test/Ocelot.ManualTest/Program.cs +++ b/test/Ocelot.ManualTest/Program.cs @@ -55,7 +55,10 @@ public static void Main(string[] args) .UseIISIntegration() .Configure(app => { - app.UseOcelot().Wait(); + app.UseOcelot(options => + { + options.PreAuthenticationMiddleware = CustomOcelotMiddleware.Invoke; + }).Wait(); }) .Build() .Run(); diff --git a/test/Ocelot.ManualTest/appsettings.json b/test/Ocelot.ManualTest/appsettings.json index e2192fccf..87d3f31b3 100644 --- a/test/Ocelot.ManualTest/appsettings.json +++ b/test/Ocelot.ManualTest/appsettings.json @@ -4,7 +4,8 @@ "LogLevel": { "Default": "Error", "System": "Error", - "Microsoft": "Error" + "Microsoft": "Error", + "Ocelot": "Information" } }, "eureka": { diff --git a/test/Ocelot.ManualTest/ocelot.json b/test/Ocelot.ManualTest/ocelot.json index df8b1f976..8eb947163 100644 --- a/test/Ocelot.ManualTest/ocelot.json +++ b/test/Ocelot.ManualTest/ocelot.json @@ -336,6 +336,21 @@ ], "UpstreamPathTemplate": "/bbc/", "UpstreamHttpMethod": [ "Get" ] + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 443 + } + ], + "UpstreamPathTemplate": "/list-post", + "UpstreamHttpMethod": [ "GET" ], + "Metadata": { + "api_id": "e99d7ce0-d918-443e-b243-1960a8212b5d" + } } ], diff --git a/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs new file mode 100644 index 000000000..b150c6f3c --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs @@ -0,0 +1,118 @@ +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; + +namespace Ocelot.UnitTests.Configuration; + +[Trait("Feat", "738")] +public class DefaultMetadataCreatorTests : UnitTest +{ + private FileGlobalConfiguration _globalConfiguration; + private Dictionary _metadataInRoute; + private MetadataOptions _result; + private readonly DefaultMetadataCreator _sut = new(); + + [Fact] + public void Should_return_empty_metadata() + { + // Arrange + GivenEmptyMetadataInGlobalConfiguration(); + GivenEmptyMetadataInRoute(); + + // Act + WhenICreate(); + + // Assert + ThenDownstreamRouteMetadataMustBeEmpty(); + } + + [Fact] + public void Should_return_global_metadata() + { + // Arrange + GivenSomeMetadataInGlobalConfiguration(); + GivenEmptyMetadataInRoute(); + + // Act + WhenICreate(); + + // Assert + ThenDownstreamMetadataMustContain("foo", "bar"); + } + + [Fact] + public void Should_return_route_metadata() + { + // Arrange + GivenEmptyMetadataInGlobalConfiguration(); + GivenSomeMetadataInRoute(); + + // Act + WhenICreate(); + + // Assert + ThenDownstreamMetadataMustContain("foo", "baz"); + } + + [Fact] + public void Should_overwrite_global_metadata() + { + // Arrange + GivenSomeMetadataInGlobalConfiguration(); + GivenSomeMetadataInRoute(); + + // Act + WhenICreate(); + + // Assert + ThenDownstreamMetadataMustContain("foo", "baz"); + } + + private void WhenICreate() + { + _result = _sut.Create( _metadataInRoute, _globalConfiguration); + } + + private void GivenEmptyMetadataInGlobalConfiguration() + { + _globalConfiguration = new FileGlobalConfiguration(); + } + + private void GivenSomeMetadataInGlobalConfiguration() + { + _globalConfiguration = new FileGlobalConfiguration + { + MetadataOptions = new FileMetadataOptions + { + Metadata = new Dictionary + { + ["foo"] = "bar", + }, + }, + }; + } + + private void GivenEmptyMetadataInRoute() + { + _metadataInRoute = new Dictionary(); + } + + private void GivenSomeMetadataInRoute() + { + _metadataInRoute = new Dictionary + { + ["foo"] = "baz", + }; + } + + private void ThenDownstreamRouteMetadataMustBeEmpty() + { + _result.Metadata.Keys.ShouldBeEmpty(); + } + + private void ThenDownstreamMetadataMustContain(string key, string value) + { + _result.Metadata.Keys.ShouldContain(key); + _result.Metadata[key].ShouldBeEquivalentTo(value); + } +} diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs new file mode 100644 index 000000000..0c3b3bcc3 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs @@ -0,0 +1,281 @@ +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Metadata; +using Ocelot.Values; +using System.Text.Json; + +namespace Ocelot.UnitTests.Configuration; + +[Trait("Feat", "738")] +public class DownstreamRouteExtensionsTests +{ + private readonly DownstreamRoute _downstreamRoute; + + public DownstreamRouteExtensionsTests() + { + _downstreamRoute = new DownstreamRoute( + null, + new UpstreamPathTemplate(null, 0, false, null), + new List(), + new List(), + new List(), + null, + null, + new HttpHandlerOptions(false, false, false, false, 0, TimeSpan.Zero), + default, + default, + new QoSOptions(0, 0, 0, null), + null, + null, + default, + new CacheOptions(0, null, null, null), + new LoadBalancerOptions(null, null, 0), + new RateLimitOptions(false, null, null, false, null, null, null, 0), + new Dictionary(), + new List(), + new List(), + new List(), + new List(), + default, + default, + new AuthenticationOptions(null, null, null), + new DownstreamPathTemplate(null), + null, + new List(), + new List(), + new List(), + default, + new SecurityOptions(), + null, + new Version(), + HttpVersionPolicy.RequestVersionExact, + new(), + new MetadataOptions(new FileMetadataOptions())); + } + + [Theory] + [InlineData("key1", null)] + [InlineData("hello", "world")] + public void Should_return_default_value_when_key_not_found(string key, string defaultValue) + { + // Arrange + _downstreamRoute.MetadataOptions.Metadata.Add(key, defaultValue); + + // Act + var metadataValue = _downstreamRoute.GetMetadata(key, defaultValue); + + // Assert + metadataValue.ShouldBe(defaultValue); + } + + [Theory] + [InlineData("hello", "world")] + [InlineData("object.key", "value1,value2,value3")] + public void Should_return_found_metadata_value(string key, string value) + { + // Arrange + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); + + // Act + var metadataValue = _downstreamRoute.GetMetadata(key); + + //Assert + metadataValue.ShouldBe(value); + } + + [Theory] + [InlineData("mykey", "")] + [InlineData("mykey", "value1", "value1")] + [InlineData("mykey", "value1,value2", "value1", "value2")] + [InlineData("mykey", "value1, value2", "value1", "value2")] + [InlineData("mykey", "value1,,,value2", "value1", "value2")] + [InlineData("mykey", "value1, ,value2", "value1", "value2")] + [InlineData("mykey", "value1, value2, value3", "value1", "value2", "value3")] + [InlineData("mykey", ", ,value1, ,, ,,,,,value2,,, ", "value1", "value2")] + public void Should_split_strings(string key, string value, params string[] expected) + { + // Arrange + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); + + // Act + var metadataValue = _downstreamRoute.GetMetadata(key); + + //Assert + metadataValue.ShouldBe(expected); + } + + [Fact] + public void Should_parse_from_json_null() => Should_parse_object_from_json("mykey", "null", null); + + [Fact] + public void Should_parse_from_json_string() => Should_parse_object_from_json("mykey", "string", "string"); + + [Fact] + public void Should_parse_from_json_numbers() => Should_parse_object_from_json("mykey", "123", 123); + + [Fact] + public void Should_parse_from_object() + => Should_parse_object_from_json( + "mykey", + "{\"Id\": 88, \"Value\": \"Hello World!\", \"MyTime\": \"2024-01-01T10:10:10.000Z\"}", + new FakeObject { Id = 88, Value = "Hello World!", MyTime = new DateTime(2024, 1, 1, 10, 10, 10, DateTimeKind.Unspecified) }); + + private void Should_parse_object_from_json(string key, string value, object expected) + { + // Arrange + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); + + // Act + var metadataValue = _downstreamRoute.GetMetadata(key); + + //Assert + metadataValue.ShouldBeEquivalentTo(expected); + } + + [Fact] + public void Should_parse_from_json_array() + { + // Arrange + var key = "mykey"; + _downstreamRoute.MetadataOptions.Metadata.Add(key, "[\"value1\", \"value2\", \"value3\"]"); + + // Act + var metadataValue = _downstreamRoute.GetMetadata>(key); + + //Assert + IEnumerable enumerable = metadataValue as string[] ?? metadataValue.ToArray(); + enumerable.ShouldNotBeNull(); + enumerable.ElementAt(0).ShouldBe("value1"); + enumerable.ElementAt(1).ShouldBe("value2"); + enumerable.ElementAt(2).ShouldBe("value3"); + } + + [Fact] + public void Should_throw_error_when_invalid_json() + { + // Arrange + var key = "mykey"; + _downstreamRoute.MetadataOptions.Metadata.Add(key, "[[["); + + // Act + + //Assert + Assert.Throws(() => + { + _ = _downstreamRoute.GetMetadata>(key); + }); + } + + [Fact] + public void Should_parse_json_with_custom_json_settings_options() + { + // Arrange + var key = "mykey"; + var value = "{\"id\": 88, \"value\": \"Hello World!\", \"myTime\": \"2024-01-01T10:10:10.000Z\"}"; + var expected = new FakeObject + { + Id = 88, + Value = "Hello World!", + MyTime = new DateTime(2024, 1, 1, 10, 10, 10, DateTimeKind.Unspecified), + }; + var serializerOptions = new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); + + // Act + var metadataValue = _downstreamRoute.GetMetadata(key, jsonSerializerOptions: serializerOptions); + + //Assert + metadataValue.ShouldBeEquivalentTo(expected); + } + + record FakeObject + { + public int Id { get; set; } + public string Value { get; set; } + public DateTime MyTime { get; set; } + } + + [Theory] + [InlineData("0", 0)] + [InlineData("99", 99)] + [InlineData("500", 500)] + [InlineData("999999999", 999999999)] + public void Should_parse_integers(string value, int expected) => Should_parse_number(value, expected); + + [Theory] + [InlineData("0", 0)] + [InlineData("0.5", 0.5)] + [InlineData("99", 99)] + [InlineData("99.5", 99.5)] + [InlineData("999999999", 999999999)] + [InlineData("999999999.5", 999999999.5)] + public void Should_parse_double(string value, double expected) => Should_parse_number(value, expected); + + private void Should_parse_number(string value, T expected) + { + // Arrange + var key = "mykey"; + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); + + // Act + var metadataValue = _downstreamRoute.GetMetadata(key); + + //Assert + metadataValue.ShouldBe(expected); + } + + [Fact] + public void Should_throw_error_when_invalid_number() + { + // Arrange + var key = "mykey"; + _downstreamRoute.MetadataOptions.Metadata.Add(key, "xyz"); + + // Act + + // Assert + Assert.Throws(() => + { + _ = _downstreamRoute.GetMetadata(key); + }); + } + + [Theory] + [InlineData("true", true)] + [InlineData("yes", true)] + [InlineData("on", true)] + [InlineData("enabled", true)] + [InlineData("enable", true)] + [InlineData("ok", true)] + [InlineData(" true ", true)] + [InlineData(" yes ", true)] + [InlineData(" on ", true)] + [InlineData(" enabled ", true)] + [InlineData(" enable ", true)] + [InlineData(" ok ", true)] + [InlineData("", false)] + [InlineData(" ", false)] + [InlineData(null, false)] + [InlineData("false", false)] + [InlineData("off", false)] + [InlineData("disabled", false)] + [InlineData("disable", false)] + [InlineData("no", false)] + [InlineData("abcxyz", false)] + public void Should_parse_truthy_values(string value, bool expected) + { + // Arrange + var key = "mykey"; + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); + + // Act + var isTrusthy = _downstreamRoute.GetMetadata(key); + + //Assert + isTrusthy.ShouldBe(expected); + } +} diff --git a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs index 63c7c5237..3d06802fc 100644 --- a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs @@ -2,7 +2,7 @@ using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; - + namespace Ocelot.UnitTests.Configuration { public class DynamicsCreatorTests : UnitTest @@ -11,74 +11,83 @@ public class DynamicsCreatorTests : UnitTest private readonly Mock _rloCreator; private readonly Mock _versionCreator; private readonly Mock _versionPolicyCreator; + private readonly Mock _metadataCreator; private List _result; private FileConfiguration _fileConfig; private RateLimitOptions _rlo1; private RateLimitOptions _rlo2; private Version _version; private HttpVersionPolicy _versionPolicy; + private Dictionary _expectedMetadata; public DynamicsCreatorTests() { _versionCreator = new Mock(); _versionPolicyCreator = new Mock(); + _metadataCreator = new Mock(); _rloCreator = new Mock(); - _creator = new DynamicsCreator(_rloCreator.Object, _versionCreator.Object, _versionPolicyCreator.Object); + _creator = new DynamicsCreator(_rloCreator.Object, _versionCreator.Object, _versionPolicyCreator.Object, _metadataCreator.Object); } [Fact] - public void should_return_nothing() + public void Should_return_nothing() { + // Arrange var fileConfig = new FileConfiguration(); + GivenThe(fileConfig); + + // Act + WhenICreate(); - this.Given(_ => GivenThe(fileConfig)) - .When(_ => WhenICreate()) - .Then(_ => ThenNothingIsReturned()) - .And(_ => ThenTheRloCreatorIsNotCalled()) - .BDDfy(); + // Assert + ThenNothingIsReturned(); + ThenTheRloCreatorIsNotCalled(); + ThenTheMetadataCreatorIsNotCalled(); } [Fact] - public void should_return_re_routes() + public void Should_return_routes() { + // Arrange var fileConfig = new FileConfiguration { - DynamicRoutes = new List + DynamicRoutes = new() { - new() - { - ServiceName = "1", - RateLimitRule = new FileRateLimitRule - { - EnableRateLimiting = false, - }, - DownstreamHttpVersion = "1.1", - DownstreamHttpVersionPolicy = VersionPolicies.RequestVersionOrLower, - }, - new() - { - ServiceName = "2", - RateLimitRule = new FileRateLimitRule - { - EnableRateLimiting = true, - }, - DownstreamHttpVersion = "2.0", - DownstreamHttpVersionPolicy = VersionPolicies.RequestVersionOrHigher, - }, + GivenDynamicRoute("1", false, "1.1", "foo", "bar"), + GivenDynamicRoute("2", true, "2.0", "foo", "baz"), }, }; - - this.Given(_ => GivenThe(fileConfig)) - .And(_ => GivenTheRloCreatorReturns()) - .And(_ => GivenTheVersionCreatorReturns()) - .And(_ => GivenTheVersionPolicyCreatorReturns()) - .When(_ => WhenICreate()) - .Then(_ => ThenTheRoutesAreReturned()) - .And(_ => ThenTheRloCreatorIsCalledCorrectly()) - .And(_ => ThenTheVersionCreatorIsCalledCorrectly()) - .BDDfy(); + GivenThe(fileConfig); + GivenTheRloCreatorReturns(); + GivenTheVersionCreatorReturns(); + GivenTheVersionPolicyCreatorReturns(); + GivenTheMetadataCreatorReturns(); + + // Act + WhenICreate(); + + // Assert + ThenTheRoutesAreReturned(); + ThenTheRloCreatorIsCalledCorrectly(); + ThenTheVersionCreatorIsCalledCorrectly(); + ThenTheMetadataCreatorIsCalledCorrectly(); } + private FileDynamicRoute GivenDynamicRoute(string serviceName, bool enableRateLimiting, + string downstreamHttpVersion, string key, string value) => new() + { + ServiceName = serviceName, + RateLimitRule = new FileRateLimitRule + { + EnableRateLimiting = enableRateLimiting, + }, + DownstreamHttpVersion = downstreamHttpVersion, + Metadata = new Dictionary + { + [key] = value, + }, + }; + private void ThenTheRloCreatorIsCalledCorrectly() { _rloCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].RateLimitRule, @@ -93,8 +102,14 @@ private void ThenTheVersionCreatorIsCalledCorrectly() _versionCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].DownstreamHttpVersion), Times.Once); _versionCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].DownstreamHttpVersion), Times.Once); - _versionPolicyCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].DownstreamHttpVersionPolicy), Times.Once); - _versionPolicyCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].DownstreamHttpVersionPolicy), Times.Once); + _versionPolicyCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].DownstreamHttpVersionPolicy), Times.Exactly(2)); + _versionPolicyCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].DownstreamHttpVersionPolicy), Times.Exactly(2)); + } + + private void ThenTheMetadataCreatorIsCalledCorrectly() + { + _metadataCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].Metadata, It.IsAny()), Times.Once); + _metadataCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].Metadata, It.IsAny()), Times.Once); } private void ThenTheRoutesAreReturned() @@ -117,7 +132,7 @@ private void GivenTheVersionCreatorReturns() { _version = new Version("1.1"); _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_version); - } + } private void GivenTheVersionPolicyCreatorReturns() { @@ -125,6 +140,16 @@ private void GivenTheVersionPolicyCreatorReturns() _versionPolicyCreator.Setup(x => x.Create(It.IsAny())).Returns(_versionPolicy); } + private void GivenTheMetadataCreatorReturns() + { + _expectedMetadata = new() + { + ["foo"] = "bar", + }; + _metadataCreator.Setup(x => x.Create(It.IsAny>(), It.IsAny())) + .Returns(new MetadataOptions(new FileMetadataOptions{Metadata = _expectedMetadata})); + } + private void GivenTheRloCreatorReturns() { _rlo1 = new RateLimitOptionsBuilder().Build(); @@ -140,6 +165,11 @@ private void ThenTheRloCreatorIsNotCalled() { _rloCreator.Verify(x => x.Create(It.IsAny(), It.IsAny()), Times.Never); } + + private void ThenTheMetadataCreatorIsNotCalled() + { + _metadataCreator.Verify(x => x.Create(It.IsAny>(), It.IsAny()), Times.Never); + } private void ThenNothingIsReturned() { diff --git a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs index 5f8a481bc..dbb9ef25a 100644 --- a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs @@ -26,6 +26,7 @@ public class RoutesCreatorTests : UnitTest private readonly Mock _soCreator; private readonly Mock _versionCreator; private readonly Mock _versionPolicyCreator; + private readonly Mock _metadataCreator; private FileConfiguration _fileConfig; private RouteOptions _rro; private string _requestId; @@ -41,9 +42,10 @@ public class RoutesCreatorTests : UnitTest private List _dhp; private LoadBalancerOptions _lbo; private List _result; - private Version _expectedVersion; + private Version _expectedVersion; private HttpVersionPolicy _expectedVersionPolicy; private Dictionary _uht; + private Dictionary _expectedMetadata; public RoutesCreatorTests() { @@ -64,6 +66,7 @@ public RoutesCreatorTests() _versionCreator = new Mock(); _versionPolicyCreator = new Mock(); _uhtpCreator = new Mock(); + _metadataCreator = new Mock(); _creator = new RoutesCreator( _cthCreator.Object, @@ -82,11 +85,12 @@ public RoutesCreatorTests() _soCreator.Object, _versionCreator.Object, _versionPolicyCreator.Object, - _uhtpCreator.Object); + _uhtpCreator.Object, + _metadataCreator.Object); } [Fact] - public void should_return_nothing() + public void Should_return_nothing() { var fileConfig = new FileConfiguration(); @@ -97,7 +101,7 @@ public void should_return_nothing() } [Fact] - public void should_return_re_routes() + public void Should_return_routes() { var fileConfig = new FileConfiguration { @@ -119,7 +123,11 @@ public void should_return_re_routes() { { "e","f" }, }, - UpstreamHttpMethod = new List { "GET", "POST" }, + UpstreamHttpMethod = new List { "GET", "POST" }, + Metadata = new Dictionary + { + ["foo"] = "bar", + }, }, new() { @@ -138,6 +146,10 @@ public void should_return_re_routes() { "k","l" }, }, UpstreamHttpMethod = new List { "PUT", "DELETE" }, + Metadata = new Dictionary + { + ["foo"] = "baz", + }, }, }, }; @@ -175,6 +187,10 @@ private void GivenTheDependenciesAreSetUpCorrectly() _dhp = new List(); _lbo = new LoadBalancerOptionsBuilder().Build(); _uht = new Dictionary(); + _expectedMetadata = new Dictionary() + { + ["foo"] = "bar", + }; _rroCreator.Setup(x => x.Create(It.IsAny())).Returns(_rro); _ridkCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_requestId); @@ -192,6 +208,10 @@ private void GivenTheDependenciesAreSetUpCorrectly() _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersion); _versionPolicyCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersionPolicy); _uhtpCreator.Setup(x => x.Create(It.IsAny())).Returns(_uht); + _metadataCreator.Setup(x => x.Create(It.IsAny>(), It.IsAny())).Returns(new MetadataOptions(new FileMetadataOptions + { + Metadata = _expectedMetadata, + })); } private void ThenTheRoutesAreCreated() @@ -251,6 +271,7 @@ private void ThenTheRouteIsSet(FileRoute expected, int routeIndex) _result[routeIndex].DownstreamRoute[0].RouteClaimsRequirement.ShouldBe(expected.RouteClaimsRequirement); _result[routeIndex].DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe(expected.DownstreamPathTemplate); _result[routeIndex].DownstreamRoute[0].Key.ShouldBe(expected.Key); + _result[routeIndex].DownstreamRoute[0].MetadataOptions.Metadata.ShouldBe(_expectedMetadata); _result[routeIndex].UpstreamHttpMethod .Select(x => x.Method) .ToList() @@ -282,7 +303,8 @@ private void ThenTheDepsAreCalledFor(FileRoute fileRoute, FileGlobalConfiguratio _hfarCreator.Verify(x => x.Create(fileRoute), Times.Once); _daCreator.Verify(x => x.Create(fileRoute), Times.Once); _lboCreator.Verify(x => x.Create(fileRoute.LoadBalancerOptions), Times.Once); - _soCreator.Verify(x => x.Create(fileRoute.SecurityOptions), Times.Once); + _soCreator.Verify(x => x.Create(fileRoute.SecurityOptions), Times.Once); + _metadataCreator.Verify(x => x.Create(fileRoute.Metadata, globalConfig), Times.Once); } } } From 34cb3ebf9768ac8cd8d2c75139da2123e23fdba4 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Mon, 27 May 2024 13:03:54 +0300 Subject: [PATCH 10/15] #954 #957 #1026 Customize Consul services creation in `Consul` service discovery provider (#2067) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Interfaces folder and namespace * `IConsulServiceBuilder` interface vs `ConsulServiceBuilder` class * Inject services into `ConsulServiceBuilder` * Extend `IConsulServiceBuilder` interface * Finalize design of the customization. No JSON options because `ServiceDiscoveryProvider` are generalized for all types of providers * Remove BDDfy in favor of AAA pattern * Refactor original unit tests * Rename to `DefaultConsulServiceBuilder` * Update src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs Update `IsValid(ServiceEntry entry)` Co-authored-by: Raynald Messié * Code review by @RaynaldM * Read the node instance from `ServiceEntry`. If it is null, search for a node in the common collection. * Refactor `OcelotBuilderExtensionsTests` * The generic `AddConsul(IOcelotBuilder)` method * Rename to `ConsulTests` * Unit tests: 100% coverage * Move to `ServiceDiscovery` folder * DRY: GivenServiceEntry, GivenRoute * Convert to file-scoped namespace * Inherit from `Steps` * Refactor acceptance tests * Acceptance test for #954 user scenario * Move "Store Configuration in Consul" to "Service Discovery" * Feature docs --------- Co-authored-by: Raynald Messié --- docs/features/configuration.rst | 68 +- docs/features/servicediscovery.rst | 164 ++- src/Ocelot.Provider.Consul/Consul.cs | 83 +- .../ConsulClientFactory.cs | 4 +- .../ConsulFileConfigurationRepository.cs | 1 + .../ConsulProviderFactory.cs | 11 +- .../DefaultConsulServiceBuilder.cs | 103 ++ .../{ => Interfaces}/IConsulClientFactory.cs | 2 +- .../Interfaces/IConsulServiceBuilder.cs | 11 + .../OcelotBuilderExtensions.cs | 36 + .../ConsulConfigurationInConsulTests.cs | 942 +++++++------- .../ConsulServiceDiscoveryTests.cs | 1151 +++++++---------- .../ConsulWebSocketTests.cs | 678 +++++----- .../ConsulFileConfigurationRepositoryTests.cs | 1 + .../ConsulServiceDiscoveryProviderTests.cs | 223 ---- test/Ocelot.UnitTests/Consul/ConsulTests.cs | 209 +++ .../DefaultConsulServiceBuilderTests.cs | 200 +++ .../Consul/OcelotBuilderExtensionsTests.cs | 128 +- .../Consul/ProviderFactoryTests.cs | 1 + 19 files changed, 2124 insertions(+), 1892 deletions(-) create mode 100644 src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs rename src/Ocelot.Provider.Consul/{ => Interfaces}/IConsulClientFactory.cs (68%) create mode 100644 src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs rename test/Ocelot.AcceptanceTests/{ => ServiceDiscovery}/ConsulConfigurationInConsulTests.cs (97%) rename test/Ocelot.AcceptanceTests/{ => ServiceDiscovery}/ConsulWebSocketTests.cs (97%) delete mode 100644 test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs create mode 100644 test/Ocelot.UnitTests/Consul/ConsulTests.cs create mode 100644 test/Ocelot.UnitTests/Consul/DefaultConsulServiceBuilderTests.cs diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 4447dac3d..5a29a6b48 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -216,62 +216,14 @@ For example: Examining the code within the `ConfigurationBuilderExtensions class `_ would be helpful for gaining a better understanding of the signatures of the overloaded methods [#f2]_. -Store Configuration in Consul ------------------------------ +Store Configuration in `Consul`_ +-------------------------------- -The first thing you need to do is install the `NuGet package `_ that provides `Consul `_ support in Ocelot. +As a developer, if you have enabled :doc:`../features/servicediscovery` with `Consul`_ support in Ocelot, you may choose to manage your configuration saving to the *Consul* `KV store`_. -.. code-block:: powershell +Beyond the traditional methods of storing configuration in a file vs folder (:ref:`config-merging-files`), or in-memory (:ref:`config-merging-tomemory`), you also have the alternative to utilize the `Consul`_ server's storage capabilities. - Install-Package Ocelot.Provider.Consul - -Then you add the following when you register your services Ocelot will attempt to store and retrieve its configuration in Consul KV store. -In order to register Consul services we must call the ``AddConsul()`` and ``AddConfigStoredInConsul()`` extensions using the ``OcelotBuilder`` being returned by ``AddOcelot()`` [#f3]_ like below: - -.. code-block:: csharp - - services.AddOcelot() - .AddConsul() - .AddConfigStoredInConsul(); - -You also need to add the following to your `ocelot.json`_. This is how Ocelot finds your Consul agent and interacts to load and store the configuration from Consul. - -.. code-block:: json - - "GlobalConfiguration": { - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 9500 - } - } - -The team decided to create this feature after working on the Raft consensus algorithm and finding out its super hard. -Why not take advantage of the fact Consul already gives you this! -We guess it means if you want to use Ocelot to its fullest, you take on Consul as a dependency for now. - -This feature has a `3 seconds `_ TTL cache before making a new request to your local Consul agent. - -.. _config-consul-key: - -Consul Configuration Key [#f4]_ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If you are using Consul for configuration (or other providers in the future), you might want to key your configurations: so you can have multiple configurations. - -In order to specify the key you need to set the **ConfigurationKey** property in the **ServiceDiscoveryProvider** options of the configuration JSON file e.g. - -.. code-block:: json - - "GlobalConfiguration": { - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 9500, - "ConfigurationKey": "Ocelot_A" - } - } - -In this example Ocelot will use ``Ocelot_A`` as the key for your configuration when looking it up in Consul. -If you do not set the **ConfigurationKey**, Ocelot will use the string ``InternalConfiguration`` as the key. +For further details on managing Ocelot configurations via a Consul instance, please consult the ":ref:`sd-consul-configuration-in-kv`" section. Follow Redirects aka HttpHandlerOptions --------------------------------------- @@ -417,7 +369,7 @@ Ocelot allows you to choose the HTTP version it will use to make the proxy reque .. _config-version-policy: -DownstreamHttpVersionPolicy [#f5]_ +DownstreamHttpVersionPolicy [#f3]_ ---------------------------------- This routing property enables the configuration of the ``VersionPolicy`` property within ``HttpRequestMessage`` objects for downstream HTTP requests. @@ -557,12 +509,12 @@ Now, the route metadata can be accessed through the `DownstreamRoute` object: .. [#f1] ":ref:`config-merging-files`" feature was requested in `issue 296 `_, since then we extended it in `issue 1216 `_ (PR `1227 `_) as ":ref:`config-merging-tomemory`" subfeature which was released as a part of version `23.2`_. .. [#f2] ":ref:`config-merging-tomemory`" subfeature is based on the ``MergeOcelotJson`` enumeration type with values: ``ToFile`` and ``ToMemory``. The 1st one is implicit by default, and the second one is exactly what you need when merging to memory. See more details on implementations in the `ConfigurationBuilderExtensions`_ class. -.. [#f3] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. -.. [#f4] ":ref:`config-consul-key`" feature was requested in `issue 346 `_ as a part of version `7.0.0 `_. -.. [#f5] ":ref:`config-version-policy`" feature was requested in `issue 1672 `_ as a part of version `24.0`_. +.. [#f3] ":ref:`config-version-policy`" feature was requested in `issue 1672 `_ as a part of version `23.3`_. .. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0 .. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 -.. _24.0: https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0 +.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/ocelot.json .. _ConfigurationBuilderExtensions: https://github.com/ThreeMammals/Ocelot/blob/develop/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs +.. _Consul: https://www.consul.io/ +.. _KV Store: https://developer.hashicorp.com/consul/docs/dynamic-app-config/kv diff --git a/docs/features/servicediscovery.rst b/docs/features/servicediscovery.rst index 6d9d8d4ef..a4e4f96c5 100644 --- a/docs/features/servicediscovery.rst +++ b/docs/features/servicediscovery.rst @@ -9,28 +9,85 @@ At the moment this is only supported in the **GlobalConfiguration** section, whi Consul ------ - | **Namespace**: `Ocelot.Provider.Consul `_ + | **Namespace**: ``Ocelot.Provider.Consul`` -The first thing you need to do is install the `Ocelot.Provider.Consul `__ package that provides `Consul `_ support in Ocelot: +The first thing you need to do is install the `Ocelot.Provider.Consul `_ package that provides `Consul`_ support in Ocelot: .. code-block:: powershell Install-Package Ocelot.Provider.Consul -Then add the following to your ``ConfigureServices`` method: +To register *Consul* services, you must invoke the ``AddConsul()`` extension using the ``OcelotBuilder`` returned by ``AddOcelot()`` [#f1]_. +Therefore, include the following in your ``ConfigureServices`` method: .. code-block:: csharp services.AddOcelot() - .AddConsul(); + .AddConsul(); // or .AddConsul() -Currently there are 2 types of Consul *service discovery* providers: ``Consul`` and ``PollConsul``. -The default provider is ``Consul``, which means that if ``ConsulProviderFactory`` cannot read, understand, or parse the **Type** property of the ``ServiceProviderConfiguration`` object, then a ``Consul`` provider instance is created by the factory. +Currently there are 2 types of *Consul* service discovery providers: ``Consul`` and ``PollConsul``. +The default provider is ``Consul``, which means that if ``ConsulProviderFactory`` cannot read, understand, or parse the **Type** property of the ``ServiceProviderConfiguration`` object, +then a :ref:`sd-consul-provider` instance is created by the factory. -Explore these types of providers and understand the differences in the subsections below. +Explore these types of providers and understand the differences in the subsections: :ref:`sd-consul-provider` and :ref:`sd-pollconsul-provider`. -Consul Provider Type -^^^^^^^^^^^^^^^^^^^^ +.. _sd-consul-configuration-in-kv: + +Configuration in `KV Store`_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Add the following when you register your services Ocelot will attempt to store and retrieve its :doc:`../features/configuration` in *Consul* `KV Store`_: + +.. code-block:: csharp + + services.AddOcelot() + .AddConsul() + .AddConfigStoredInConsul(); // ! + +You also need to add the following to your `ocelot.json`_. +This is how Ocelot finds your *Consul* agent and interacts to load and store the configuration from *Consul*. + +.. code-block:: json + + "GlobalConfiguration": { + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 9500 + } + } + +The team decided to create this feature after working on the Raft consensus algorithm and finding out its super hard. +Why not take advantage of the fact Consul already gives you this! +We guess it means if you want to use Ocelot to its fullest, you take on Consul as a dependency for now. + + **Note!** This feature has a `3 seconds TTL`_ cache before making a new request to your local *Consul* agent. + +.. _sd-consul-configuration-key: + +Consul Configuration Key [#f2]_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you are using *Consul* for :doc:`../features/configuration` (or other providers in the future), you might want to key your configurations: so you can have multiple configurations. + +In order to specify the key you need to set the **ConfigurationKey** property in the **ServiceDiscoveryProvider** options of the configuration JSON file e.g. + +.. code-block:: json + + "GlobalConfiguration": { + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 9500, + "ConfigurationKey": "Ocelot_A" // ! + } + } + +In this example Ocelot will use ``Ocelot_A`` as the key for your configuration when looking it up in *Consul*. +If you do not set the **ConfigurationKey**, Ocelot will use the string ``InternalConfiguration`` as the key. + +.. _sd-consul-provider: + +``Consul`` Provider +^^^^^^^^^^^^^^^^^^^ | **Class**: `Ocelot.Provider.Consul.Consul `_ @@ -67,8 +124,10 @@ If no load balancer is specified, Ocelot will not load balance requests. When this is set up Ocelot will lookup the downstream host and port from the *service discovery* provider and load balance requests across any available services. -PollConsul Provider Type -^^^^^^^^^^^^^^^^^^^^^^^^ +.. _sd-pollconsul-provider: + +``PollConsul`` Provider +^^^^^^^^^^^^^^^^^^^^^^^ | **Class**: `Ocelot.Provider.Consul.PollConsul `_ @@ -98,7 +157,7 @@ Service Definition Your services need to be added to Consul something like below (C# style but hopefully this make sense)... The only important thing to note is not to add ``http`` or ``https`` to the ``Address`` field. We have been contacted before about not accepting scheme in ``Address``. -After reading `this `_ we do not think the scheme should be in there. +After reading `Agents Overview `_ and `Define services `_ docs we do not think the **scheme** should be in there. In C# @@ -140,6 +199,68 @@ In order so this to work you must add the additional property below: Ocelot will add this token to the Consul client that it uses to make requests and that is then used for every request. +.. _sd-consul-service-builder: + +Consul Service Builder [#f3]_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + | **Interface**: ``IConsulServiceBuilder`` + | **Implementation**: ``DefaultConsulServiceBuilder`` + +The Ocelot community has consistently reported, both in the past and presently, issues with *Consul* services (such as connectivity) due to a variety of *Consul* agent definitions. +Some DevOps engineers prefer to group services as *Consul* `catalog nodes`_ by customizing the assignment of host names to node names, +while others focus on defining agent services with pure IP addresses as hosts, which relates to the `954`_ bug dilemma. + +Since version `13.5.2`_, the building of service downstream host/port in PR `909`_ has been altered to favor the node name as the host over the agent service address IP. + +Version `23.3`_ saw the introduction of a customization feature that allows control over the service building process through the ``DefaultConsulServiceBuilder`` class. +This class has virtual methods that can be overridden to meet the needs of developers and DevOps. + +The present logic in the ``DefaultConsulServiceBuilder`` class is as follows: + +.. code-block:: csharp + + protected virtual string GetDownstreamHost(ServiceEntry entry, Node node) + => node != null ? node.Name : entry.Service.Address; + +Some DevOps engineers choose to ignore node names, opting instead for abstract identifiers rather than actual hostnames. +Our team, however, advocates for the assignment of real hostnames or IP addresses to node names, upholding this as a best practice. +If this approach does not align with your needs, or if you prefer not to spend time detailing your nodes for downstream services, you might consider defining agent services without node names. +In such cases within a *Consul* setup, you would need to override the behavior of the ``DefaultConsulServiceBuilder`` class. +For further details, refer to the subsequent section below. + +.. _sd-addconsul-generic-method: + +``AddConsul`` method +""""""""""""""""""""""" + + | **Signature**: ``IOcelotBuilder AddConsul(this IOcelotBuilder builder)`` + +Overriding the ``DefaultConsulServiceBuilder`` behavior involves two steps: defining a new class that inherits from the ``IConsulServiceBuilder`` interface, +and then injecting this new behavior into DI using the ``AddConsul`` helper. +However, the quickest and most streamlined approach is to inherit directly from the ``DefaultConsulServiceBuilder`` class, which offers greater flexibility. + +**First**, we need to define a new service building class: + +.. code-block:: csharp + + public class MyConsulServiceBuilder : DefaultConsulServiceBuilder + { + public MyConsulServiceBuilder(Func configurationFactory, IConsulClientFactory clientFactory, IOcelotLoggerFactory loggerFactory) + : base(configurationFactory, clientFactory, loggerFactory) { } + // I want to use the agent service IP address as the downstream hostname + protected override string GetDownstreamHost(ServiceEntry entry, Node node) => entry.Service.Address; + } + +**Second**, we must inject the new behavior into DI, as demonstrated in the Ocelot versus Consul setup: + +.. code-block:: csharp + + services.AddOcelot() + .AddConsul(); + +You can refer to `the acceptance test`_ in the repository for an example. + Eureka ------ @@ -410,3 +531,22 @@ After this, you need to add the ``IServiceDiscoveryProviderFactory`` interface t Note that in this case the Ocelot pipeline will not use ``ServiceDiscoveryProviderFactory`` by default. Additionally, you do not need to specify ``"Type": "MyServiceDiscoveryProvider"`` in the **ServiceDiscoveryProvider** properties of the **GlobalConfiguration** settings. But you can leave this ``Type`` option for compatibility between both designs. + +"""" + +.. [#f1] :ref:`di-the-addocelot-method` adds default ASP.NET services to DI container. You could call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of :doc:`../features/dependencyinjection` feature. +.. [#f2] *"Consul Configuration Key"* feature was requested in issue `346`_ as a part of version `7.0.0`_. +.. [#f3] Customization of *"Consul Service Builder"* was implemented as a part of bug `954`_ fixing and the feature was delivered in version `23.3`_. + +.. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/ocelot.json +.. _Consul: https://www.consul.io/ +.. _KV Store: https://developer.hashicorp.com/consul/docs/dynamic-app-config/kv +.. _3 seconds TTL: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+TimeSpan.FromSeconds%283%29&type=code +.. _catalog nodes: https://developer.hashicorp.com/consul/api-docs/catalog#list-nodes +.. _the acceptance test: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+Should_return_service_address_by_overridden_service_builder_when_there_is_a_node&type=code +.. _346: https://github.com/ThreeMammals/Ocelot/issues/346 +.. _909: https://github.com/ThreeMammals/Ocelot/pull/909 +.. _954: https://github.com/ThreeMammals/Ocelot/issues/954 +.. _7.0.0: https://github.com/ThreeMammals/Ocelot/releases/tag/7.0.0 +.. _13.5.2: https://github.com/ThreeMammals/Ocelot/releases/tag/13.5.2 +.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 diff --git a/src/Ocelot.Provider.Consul/Consul.cs b/src/Ocelot.Provider.Consul/Consul.cs index 273fca2ab..27b5b4422 100644 --- a/src/Ocelot.Provider.Consul/Consul.cs +++ b/src/Ocelot.Provider.Consul/Consul.cs @@ -1,5 +1,5 @@ -using Ocelot.Infrastructure.Extensions; -using Ocelot.Logging; +using Ocelot.Logging; +using Ocelot.Provider.Consul.Interfaces; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; @@ -7,70 +7,49 @@ namespace Ocelot.Provider.Consul; public class Consul : IServiceDiscoveryProvider { - private const string VersionPrefix = "version-"; - private readonly ConsulRegistryConfiguration _config; + private readonly ConsulRegistryConfiguration _configuration; private readonly IConsulClient _consul; private readonly IOcelotLogger _logger; + private readonly IConsulServiceBuilder _serviceBuilder; - public Consul(ConsulRegistryConfiguration config, IOcelotLoggerFactory factory, IConsulClientFactory clientFactory) + public Consul( + ConsulRegistryConfiguration config, + IOcelotLoggerFactory factory, + IConsulClientFactory clientFactory, + IConsulServiceBuilder serviceBuilder) { - _config = config; - _consul = clientFactory.Get(_config); + _configuration = config; + _consul = clientFactory.Get(_configuration); _logger = factory.CreateLogger(); + _serviceBuilder = serviceBuilder; } - public async Task> GetAsync() + public virtual async Task> GetAsync() { - var queryResult = await _consul.Health.Service(_config.KeyOfServiceInConsul, string.Empty, true); + var entriesTask = _consul.Health.Service(_configuration.KeyOfServiceInConsul, string.Empty, true); + var nodesTask = _consul.Catalog.Nodes(); + await Task.WhenAll(entriesTask, nodesTask); + + var entries = entriesTask.Result.Response ?? Array.Empty(); + var nodes = nodesTask.Result.Response ?? Array.Empty(); var services = new List(); - foreach (var serviceEntry in queryResult.Response) + if (entries.Length != 0) { - var service = serviceEntry.Service; - if (IsValid(service)) - { - var nodes = await _consul.Catalog.Nodes(); - if (nodes.Response == null) - { - services.Add(BuildService(serviceEntry, null)); - } - else - { - var serviceNode = nodes.Response.FirstOrDefault(n => n.Address == service.Address); - services.Add(BuildService(serviceEntry, serviceNode)); - } - } - else - { - _logger.LogWarning( - () => $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."); - } + _logger.LogDebug(() => $"{nameof(Consul)} Provider: Found total {entries.Length} service entries for '{_configuration.KeyOfServiceInConsul}' service."); + _logger.LogDebug(() => $"{nameof(Consul)} Provider: Found total {nodes.Length} catalog nodes."); + var collection = BuildServices(entries, nodes); + services.AddRange(collection); + } + else + { + _logger.LogWarning(() => $"{nameof(Consul)} Provider: No service entries found for '{_configuration.KeyOfServiceInConsul}' service!"); } - return services.ToList(); - } - - private static Service BuildService(ServiceEntry serviceEntry, Node serviceNode) - { - var service = serviceEntry.Service; - return new Service( - service.Service, - new ServiceHostAndPort( - serviceNode == null ? service.Address : serviceNode.Name, - service.Port), - service.ID, - GetVersionFromStrings(service.Tags), - service.Tags ?? Enumerable.Empty()); + return services; } - private static bool IsValid(AgentService service) - => !string.IsNullOrEmpty(service.Address) - && !service.Address.Contains($"{Uri.UriSchemeHttp}://") - && !service.Address.Contains($"{Uri.UriSchemeHttps}://") - && service.Port > 0; - - private static string GetVersionFromStrings(IEnumerable strings) - => strings?.FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal)) - .TrimStart(VersionPrefix); + protected virtual IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes) + => _serviceBuilder.BuildServices(entries, nodes); } diff --git a/src/Ocelot.Provider.Consul/ConsulClientFactory.cs b/src/Ocelot.Provider.Consul/ConsulClientFactory.cs index 4a7478c59..f7c5c0c0c 100644 --- a/src/Ocelot.Provider.Consul/ConsulClientFactory.cs +++ b/src/Ocelot.Provider.Consul/ConsulClientFactory.cs @@ -1,4 +1,6 @@ -namespace Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; + +namespace Ocelot.Provider.Consul; public class ConsulClientFactory : IConsulClientFactory { diff --git a/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs b/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs index 2f9569362..c95146f46 100644 --- a/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs +++ b/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs @@ -5,6 +5,7 @@ using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.Logging; +using Ocelot.Provider.Consul.Interfaces; using Ocelot.Responses; using System.Text; diff --git a/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs b/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs index c769f49c0..00c2715ee 100644 --- a/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs +++ b/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Logging; +using Ocelot.Provider.Consul.Interfaces; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.Provider.Consul; @@ -17,16 +18,20 @@ public static class ConsulProviderFactory public static ServiceDiscoveryFinderDelegate Get { get; } = CreateProvider; + private static ConsulRegistryConfiguration configuration; + private static ConsulRegistryConfiguration ConfigurationGetter() => configuration; + public static Func GetConfiguration { get; } = ConfigurationGetter; + private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route) { var factory = provider.GetService(); var consulFactory = provider.GetService(); - var consulRegistryConfiguration = new ConsulRegistryConfiguration( - config.Scheme, config.Host, config.Port, route.ServiceName, config.Token); + configuration = new ConsulRegistryConfiguration(config.Scheme, config.Host, config.Port, route.ServiceName, config.Token); + var serviceBuilder = provider.GetService(); - var consulProvider = new Consul(consulRegistryConfiguration, factory, consulFactory); + var consulProvider = new Consul(configuration, factory, consulFactory, serviceBuilder); if (PollConsul.Equals(config.Type, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs b/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs new file mode 100644 index 000000000..7526bea65 --- /dev/null +++ b/src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs @@ -0,0 +1,103 @@ +using Ocelot.Infrastructure.Extensions; +using Ocelot.Logging; +using Ocelot.Provider.Consul.Interfaces; +using Ocelot.Values; + +namespace Ocelot.Provider.Consul; + +public class DefaultConsulServiceBuilder : IConsulServiceBuilder +{ + private readonly ConsulRegistryConfiguration _configuration; + private readonly IConsulClient _client; + private readonly IOcelotLogger _logger; + + public DefaultConsulServiceBuilder( + Func configurationFactory, + IConsulClientFactory clientFactory, + IOcelotLoggerFactory loggerFactory) + { + _configuration = configurationFactory.Invoke(); + _client = clientFactory.Get(_configuration); + _logger = loggerFactory.CreateLogger(); + } + + public ConsulRegistryConfiguration Configuration => _configuration; + protected IConsulClient Client => _client; + protected IOcelotLogger Logger => _logger; + + public virtual bool IsValid(ServiceEntry entry) + { + var service = entry.Service; + var address = service.Address; + bool valid = !string.IsNullOrEmpty(address) + && !address.StartsWith(Uri.UriSchemeHttp + "://", StringComparison.OrdinalIgnoreCase) + && !address.StartsWith(Uri.UriSchemeHttps + "://", StringComparison.OrdinalIgnoreCase) + && service.Port > 0; + + if (!valid) + { + _logger.LogWarning( + () => $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."); + } + + return valid; + } + + public virtual IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes) + { + ArgumentNullException.ThrowIfNull(entries); + var services = new List(entries.Length); + + foreach (var serviceEntry in entries) + { + if (IsValid(serviceEntry)) + { + var serviceNode = GetNode(serviceEntry, nodes); + var item = CreateService(serviceEntry, serviceNode); + if (item != null) + { + services.Add(item); + } + } + } + + return services; + } + + protected virtual Node GetNode(ServiceEntry entry, Node[] nodes) + => entry?.Node ?? nodes?.FirstOrDefault(n => n.Address == entry?.Service?.Address); + + public virtual Service CreateService(ServiceEntry entry, Node node) + => new( + GetServiceName(entry, node), + GetServiceHostAndPort(entry, node), + GetServiceId(entry, node), + GetServiceVersion(entry, node), + GetServiceTags(entry, node) + ); + + protected virtual string GetServiceName(ServiceEntry entry, Node node) + => entry.Service.Service; + + protected virtual ServiceHostAndPort GetServiceHostAndPort(ServiceEntry entry, Node node) + => new( + GetDownstreamHost(entry, node), + entry.Service.Port); + + protected virtual string GetDownstreamHost(ServiceEntry entry, Node node) + => node != null ? node.Name : entry.Service.Address; + + protected virtual string GetServiceId(ServiceEntry entry, Node node) + => entry.Service.ID; + + protected virtual string GetServiceVersion(ServiceEntry entry, Node node) + => entry.Service.Tags + ?.FirstOrDefault(tag => tag.StartsWith(VersionPrefix, StringComparison.Ordinal)) + ?.TrimStart(VersionPrefix) + ?? string.Empty; + + protected virtual IEnumerable GetServiceTags(ServiceEntry entry, Node node) + => entry.Service.Tags ?? Enumerable.Empty(); + + private const string VersionPrefix = "version-"; +} diff --git a/src/Ocelot.Provider.Consul/IConsulClientFactory.cs b/src/Ocelot.Provider.Consul/Interfaces/IConsulClientFactory.cs similarity index 68% rename from src/Ocelot.Provider.Consul/IConsulClientFactory.cs rename to src/Ocelot.Provider.Consul/Interfaces/IConsulClientFactory.cs index 3ee3a2b25..0fe12aa08 100644 --- a/src/Ocelot.Provider.Consul/IConsulClientFactory.cs +++ b/src/Ocelot.Provider.Consul/Interfaces/IConsulClientFactory.cs @@ -1,4 +1,4 @@ -namespace Ocelot.Provider.Consul; +namespace Ocelot.Provider.Consul.Interfaces; public interface IConsulClientFactory { diff --git a/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs b/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs new file mode 100644 index 000000000..0555b0144 --- /dev/null +++ b/src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs @@ -0,0 +1,11 @@ +using Ocelot.Values; + +namespace Ocelot.Provider.Consul.Interfaces; + +public interface IConsulServiceBuilder +{ + ConsulRegistryConfiguration Configuration { get; } + bool IsValid(ServiceEntry entry); + IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes); + Service CreateService(ServiceEntry serviceEntry, Node serviceNode); +} diff --git a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs index dac7aecff..0c064f780 100644 --- a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs @@ -2,21 +2,57 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Ocelot.Configuration.Repository; using Ocelot.DependencyInjection; +using Ocelot.Provider.Consul.Interfaces; namespace Ocelot.Provider.Consul; public static class OcelotBuilderExtensions { + /// + /// Integrates Consul service discovery into the DI, atop the existing Ocelot services. + /// + /// + /// Default services: + /// + /// The service is an instance of . + /// The service is an instance of . + /// + /// + /// The Ocelot Builder instance, default. + /// The reference to the same extended object. public static IOcelotBuilder AddConsul(this IOcelotBuilder builder) { builder.Services .AddSingleton(ConsulProviderFactory.Get) + .AddSingleton(ConsulProviderFactory.GetConfiguration) .AddSingleton() + .AddSingleton() .RemoveAll(typeof(IFileConfigurationPollerOptions)) .AddSingleton(); return builder; } + /// + /// Integrates Consul service discovery into the DI, atop the existing Ocelot services, with service builder overriding. + /// + /// + /// Services to override: + /// + /// The service has been substituted with a instance. + /// + /// + /// The service builder type. + /// The Ocelot Builder instance, default. + /// The reference to the same extended object. + public static IOcelotBuilder AddConsul(this IOcelotBuilder builder) + where TServiceBuilder : class, IConsulServiceBuilder + { + AddConsul(builder).Services + .RemoveAll() + .AddSingleton(typeof(IConsulServiceBuilder), typeof(TServiceBuilder)); + return builder; + } + public static IOcelotBuilder AddConfigStoredInConsul(this IOcelotBuilder builder) { builder.Services diff --git a/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulConfigurationInConsulTests.cs similarity index 97% rename from test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs rename to test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulConfigurationInConsulTests.cs index 978626c14..d97d98c09 100644 --- a/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulConfigurationInConsulTests.cs @@ -6,474 +6,474 @@ using Ocelot.Cache; using Ocelot.Configuration.File; using System.Text; - -namespace Ocelot.AcceptanceTests -{ - public class ConsulConfigurationInConsulTests : IDisposable - { - private IWebHost _builder; - private readonly Steps _steps; - private IWebHost _fakeConsulBuilder; - private FileConfiguration _config; - private readonly List _consulServices; - - public ConsulConfigurationInConsulTests() - { - _consulServices = new List(); - _steps = new Steps(); - } - - [Fact] - public void should_return_response_200_with_simple_url() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = servicePort, - }, - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", string.Empty, 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_load_configuration_out_of_consul() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - - var consulConfig = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/status", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = servicePort, - }, - }, - UpstreamPathTemplate = "/cs/status", - UpstreamHttpMethod = new List {"Get"}, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) - .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", "/status", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_load_configuration_out_of_consul_if_it_is_changed() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - - var consulConfig = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/status", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = servicePort, - }, - }, - UpstreamPathTemplate = "/cs/status", - UpstreamHttpMethod = new List {"Get"}, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - var secondConsulConfig = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/status", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = servicePort, - }, - }, - UpstreamPathTemplate = "/cs/status/awesome", - UpstreamHttpMethod = new List {"Get"}, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) - .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", "/status", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .When(x => GivenTheConsulConfigurationIs(secondConsulConfig)) - .Then(x => ThenTheConfigIsUpdatedInOcelot()) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes_and_rate_limit() - { - var consulPort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = downstreamServicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - - var consulConfig = new FileConfiguration - { - DynamicRoutes = new List - { - new() - { - ServiceName = serviceName, - RateLimitRule = new FileRateLimitRule - { - EnableRateLimiting = true, - ClientWhitelist = new List(), - Limit = 3, - Period = "1s", - PeriodTimespan = 1000, - }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - RateLimitOptions = new FileRateLimitOptions - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = string.Empty, - RateLimitCounterPrefix = string.Empty, - HttpStatusCode = 428, - }, - DownstreamScheme = "http", - }, - }; - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) - .And(x => GivenTheConsulConfigurationIs(consulConfig)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 2)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .BDDfy(); - } - - private void ThenTheConfigIsUpdatedInOcelot() - { - var result = Wait.WaitFor(20000).Until(() => - { - try - { - _steps.WhenIGetUrlOnTheApiGateway("/cs/status/awesome"); - _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK); - _steps.ThenTheResponseBodyShouldBe("Hello from Laura"); - return true; - } - catch (Exception) - { - return false; - } - }); - result.ShouldBeTrue(); - } - - private void GivenTheConsulConfigurationIs(FileConfiguration config) - { - _config = config; - } - - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) - { - foreach (var serviceEntry in serviceEntries) - { - _consulServices.Add(serviceEntry); - } - } - - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) - { - _fakeConsulBuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.Run(async context => - { - if (context.Request.Method.ToLower() == "get" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") - { - var json = JsonConvert.SerializeObject(_config); - - var bytes = Encoding.UTF8.GetBytes(json); - - var base64 = Convert.ToBase64String(bytes); - - var kvp = new FakeConsulGetResponse(base64); - json = JsonConvert.SerializeObject(new[] { kvp }); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") - { - try - { - var reader = new StreamReader(context.Request.Body); - - // Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. - // var json = reader.ReadToEnd(); - var json = await reader.ReadToEndAsync(); - - _config = JsonConvert.DeserializeObject(json); - - var response = JsonConvert.SerializeObject(true); - - await context.Response.WriteAsync(response); - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } - } - else if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") - { - var json = JsonConvert.SerializeObject(_consulServices); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - }) - .Build(); - - _fakeConsulBuilder.Start(); - } - - public class FakeConsulGetResponse - { - public FakeConsulGetResponse(string value) - { - Value = value; - } - - public int CreateIndex => 100; - public int ModifyIndex => 200; - public int LockIndex => 200; - public string Key => "InternalConfiguration"; - public int Flags => 0; - public string Value { get; } - public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e"; - } - - private void GivenThereIsAServiceRunningOn(string url, string basePath, int statusCode, string responseBody) - { - _builder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.UsePathBase(basePath); - - app.Run(async context => - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - }) - .Build(); - - _builder.Start(); - } - - public void Dispose() - { - _builder?.Dispose(); - _steps.Dispose(); - } - - private class FakeCache : IOcelotCache - { - public void Add(string key, FileConfiguration value, TimeSpan ttl, string region) - { - throw new NotImplementedException(); - } - - public FileConfiguration Get(string key, string region) - { - throw new NotImplementedException(); - } - - public void ClearRegion(string region) - { - throw new NotImplementedException(); - } - - public void AddAndDelete(string key, FileConfiguration value, TimeSpan ttl, string region) - { - throw new NotImplementedException(); - } - } - } -} + +namespace Ocelot.AcceptanceTests.ServiceDiscovery +{ + public class ConsulConfigurationInConsulTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + private IWebHost _fakeConsulBuilder; + private FileConfiguration _config; + private readonly List _consulServices; + + public ConsulConfigurationInConsulTests() + { + _consulServices = new List(); + _steps = new Steps(); + } + + [Fact] + public void should_return_response_200_with_simple_url() + { + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + + this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", string.Empty, 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_load_configuration_out_of_consul() + { + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + + var consulConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/status", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/cs/status", + UpstreamHttpMethod = new List {"Get"}, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) + .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", "/status", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_load_configuration_out_of_consul_if_it_is_changed() + { + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + + var consulConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/status", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/cs/status", + UpstreamHttpMethod = new List {"Get"}, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var secondConsulConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/status", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/cs/status/awesome", + UpstreamHttpMethod = new List {"Get"}, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) + .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", "/status", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(x => GivenTheConsulConfigurationIs(secondConsulConfig)) + .Then(x => ThenTheConfigIsUpdatedInOcelot()) + .BDDfy(); + } + + [Fact] + public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes_and_rate_limit() + { + var consulPort = PortFinder.GetRandomPort(); + const string serviceName = "web"; + var downstreamServicePort = PortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = downstreamServicePort, + ID = "web_90_0_2_224_8080", + Tags = new[] { "version-v1" }, + }, + }; + + var consulConfig = new FileConfiguration + { + DynamicRoutes = new List + { + new() + { + ServiceName = serviceName, + RateLimitRule = new FileRateLimitRule + { + EnableRateLimiting = true, + ClientWhitelist = new List(), + Limit = 3, + Period = "1s", + PeriodTimespan = 1000, + }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + RateLimitOptions = new FileRateLimitOptions + { + ClientIdHeader = "ClientId", + DisableRateLimitHeaders = false, + QuotaExceededMessage = string.Empty, + RateLimitCounterPrefix = string.Empty, + HttpStatusCode = 428, + }, + DownstreamScheme = "http", + }, + }; + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) + .And(x => GivenTheConsulConfigurationIs(consulConfig)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 2)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) + .BDDfy(); + } + + private void ThenTheConfigIsUpdatedInOcelot() + { + var result = Wait.WaitFor(20000).Until(() => + { + try + { + _steps.WhenIGetUrlOnTheApiGateway("/cs/status/awesome"); + _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK); + _steps.ThenTheResponseBodyShouldBe("Hello from Laura"); + return true; + } + catch (Exception) + { + return false; + } + }); + result.ShouldBeTrue(); + } + + private void GivenTheConsulConfigurationIs(FileConfiguration config) + { + _config = config; + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach (var serviceEntry in serviceEntries) + { + _consulServices.Add(serviceEntry); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) + { + _fakeConsulBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + if (context.Request.Method.ToLower() == "get" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") + { + var json = JsonConvert.SerializeObject(_config); + + var bytes = Encoding.UTF8.GetBytes(json); + + var base64 = Convert.ToBase64String(bytes); + + var kvp = new FakeConsulGetResponse(base64); + json = JsonConvert.SerializeObject(new[] { kvp }); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") + { + try + { + var reader = new StreamReader(context.Request.Body); + + // Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. + // var json = reader.ReadToEnd(); + var json = await reader.ReadToEndAsync(); + + _config = JsonConvert.DeserializeObject(json); + + var response = JsonConvert.SerializeObject(true); + + await context.Response.WriteAsync(response); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + else if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") + { + var json = JsonConvert.SerializeObject(_consulServices); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + }) + .Build(); + + _fakeConsulBuilder.Start(); + } + + public class FakeConsulGetResponse + { + public FakeConsulGetResponse(string value) + { + Value = value; + } + + public int CreateIndex => 100; + public int ModifyIndex => 200; + public int LockIndex => 200; + public string Key => "InternalConfiguration"; + public int Flags => 0; + public string Value { get; } + public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e"; + } + + private void GivenThereIsAServiceRunningOn(string url, string basePath, int statusCode, string responseBody) + { + _builder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.UsePathBase(basePath); + + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _builder.Start(); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + + private class FakeCache : IOcelotCache + { + public void Add(string key, FileConfiguration value, TimeSpan ttl, string region) + { + throw new NotImplementedException(); + } + + public FileConfiguration Get(string key, string region) + { + throw new NotImplementedException(); + } + + public void ClearRegion(string region) + { + throw new NotImplementedException(); + } + + public void AddAndDelete(string key, FileConfiguration value, TimeSpan ttl, string region) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs index d25c2075b..b98f93c0d 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs @@ -1,744 +1,527 @@ using Consul; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Logging; +using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; +using System.Runtime.CompilerServices; using System.Text.RegularExpressions; -namespace Ocelot.AcceptanceTests.ServiceDiscovery +namespace Ocelot.AcceptanceTests.ServiceDiscovery; + +public sealed class ConsulServiceDiscoveryTests : Steps, IDisposable { - public class ConsulServiceDiscoveryTests : IDisposable + private readonly List _consulServices; + private readonly List _consulNodes; + private int _counterOne; + private int _counterTwo; + private int _counterConsul; + private int _counterNodes; + private static readonly object SyncLock = new(); + private string _downstreamPath; + private string _receivedToken; + private readonly ServiceHandler _serviceHandler; + private readonly ServiceHandler _serviceHandler2; + private readonly ServiceHandler _consulHandler; + + public ConsulServiceDiscoveryTests() { - private readonly Steps _steps; - private readonly List _consulServices; - private int _counterOne; - private int _counterTwo; - private int _counterConsul; - private static readonly object SyncLock = new(); - private string _downstreamPath; - private string _receivedToken; - private readonly ServiceHandler _serviceHandler; - private readonly ServiceHandler _serviceHandler2; - private readonly ServiceHandler _consulHandler; - - public ConsulServiceDiscoveryTests() - { - _serviceHandler = new ServiceHandler(); - _serviceHandler2 = new ServiceHandler(); - _consulHandler = new ServiceHandler(); - _steps = new Steps(); - _consulServices = new List(); - } + _serviceHandler = new ServiceHandler(); + _serviceHandler2 = new ServiceHandler(); + _consulHandler = new ServiceHandler(); + _consulServices = new(); + _consulNodes = new(); + } - [Fact] - public void should_use_consul_service_discovery_and_load_balance_request() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort1 = PortFinder.GetRandomPort(); - var servicePort2 = PortFinder.GetRandomPort(); - var serviceName = "product"; - var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; - var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort1, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort2, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) - .BDDfy(); - } + public override void Dispose() + { + _serviceHandler?.Dispose(); + _serviceHandler2?.Dispose(); + _consulHandler?.Dispose(); + } - [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request() - { - var consulPort = PortFinder.GetRandomPort(); - var servicePort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/home")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + [Fact] + public void Should_use_consul_service_discovery_and_load_balance_request() + { + const string serviceName = "product"; + var consulPort = PortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var serviceEntryOne = GivenServiceEntry(port1, serviceName: serviceName); + var serviceEntryTwo = GivenServiceEntry(port2, serviceName: serviceName); + var route = GivenRoute(serviceName: serviceName); + var configuration = GivenServiceDiscovery(consulPort, route); + this.Given(x => x.GivenProductServiceOneIsRunning(DownstreamUrl(port1), 200)) + .And(x => x.GivenProductServiceTwoIsRunning(DownstreamUrl(port2), 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) .BDDfy(); - } + } - [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes() - { - var consulPort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = downstreamServicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - DownstreamScheme = "http", - HttpHandlerOptions = new FileHttpHandlerOptions - { - AllowAutoRedirect = true, - UseCookieContainer = true, - UseTracing = false, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + [Fact] + public void Should_handle_request_to_consul_for_downstream_service_and_make_request() + { + const string serviceName = "web"; + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + var serviceEntryOne = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); + var route = GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }); + var configuration = GivenServiceDiscovery(consulPort, route); + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", HttpStatusCode.OK, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/web/something")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGateway("/home")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); - } + } - [Fact] - public void should_use_consul_service_discovery_and_load_balance_request_no_re_routes() + [Fact] + public void Should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes() + { + const string serviceName = "web"; + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + var serviceEntry = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); + + var configuration = GivenServiceDiscovery(consulPort); + configuration.GlobalConfiguration.DownstreamScheme = "http"; + configuration.GlobalConfiguration.HttpHandlerOptions = new() { - var consulPort = PortFinder.GetRandomPort(); - var serviceName = "product"; - var serviceOnePort = PortFinder.GetRandomPort(); - var serviceTwoPort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{serviceOnePort}"; - var downstreamServiceTwoUrl = $"http://localhost:{serviceTwoPort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = serviceOnePort, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = serviceTwoPort, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - DownstreamScheme = "http", - }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) - .BDDfy(); - } + AllowAutoRedirect = true, + UseCookieContainer = true, + UseTracing = false, + }; - [Fact] - public void should_use_token_to_make_request_to_consul() - { - var token = "abctoken"; - var consulPort = PortFinder.GetRandomPort(); - var serviceName = "web"; - var servicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" }, - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - Token = token, - }, - }, - }; - - this.Given(_ => GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(_ => _steps.GivenThereIsAConfiguration(configuration)) - .And(_ => _steps.GivenOcelotIsRunningWithConsul()) - .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/home")) - .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(_ => ThenTheTokenIs(token)) - .BDDfy(); - } + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/something", HttpStatusCode.OK, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGateway("/web/something")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } - [Fact] - public void should_send_request_to_service_after_it_becomes_available_in_consul() - { - var consulPort = PortFinder.GetRandomPort(); - var serviceName = "product"; - var servicePort1 = PortFinder.GetRandomPort(); - var servicePort2 = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; - var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort1, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = servicePort2, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .And(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) - .And(x => WhenIRemoveAService(serviceEntryTwo)) - .And(x => GivenIResetCounters()) - .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .And(x => ThenOnlyOneServiceHasBeenCalled()) - .And(x => WhenIAddAServiceBackIn(serviceEntryTwo)) - .And(x => GivenIResetCounters()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) - .BDDfy(); - } + [Fact] + public void Should_use_consul_service_discovery_and_load_balance_request_no_re_routes() + { + const string serviceName = "product"; + var consulPort = PortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var serviceEntry1 = GivenServiceEntry(port1, serviceName: serviceName); + var serviceEntry2 = GivenServiceEntry(port2, serviceName: serviceName); + + var configuration = GivenServiceDiscovery(consulPort); + configuration.GlobalConfiguration.LoadBalancerOptions = new() { Type = nameof(LeastConnection) }; + configuration.GlobalConfiguration.DownstreamScheme = "http"; + + this.Given(x => x.GivenProductServiceOneIsRunning(DownstreamUrl(port1), 200)) + .And(x => x.GivenProductServiceTwoIsRunning(DownstreamUrl(port2), 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry1, serviceEntry2)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) + .BDDfy(); + } - [Fact] - public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request() - { - var consulPort = PortFinder.GetRandomPort(); - const string serviceName = "web"; - var downstreamServicePort = PortFinder.GetRandomPort(); - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = "localhost", - Port = downstreamServicePort, - ID = $"web_90_0_2_224_{downstreamServicePort}", - Tags = new[] { "version-v1" }, - }, - }; - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - Type = "PollConsul", - PollingInterval = 0, - Namespace = string.Empty, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + [Fact] + public void Should_use_token_to_make_request_to_consul() + { + const string serviceName = "web"; + const string token = "abctoken"; + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + var serviceEntry = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", new[] { "version-v1" }, serviceName); + var route = GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }); + + var configuration = GivenServiceDiscovery(consulPort, route); + configuration.GlobalConfiguration.ServiceDiscoveryProvider.Token = token; + + this.Given(_ => GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", HttpStatusCode.OK, "Hello from Laura")) + .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntry)) + .And(_ => GivenThereIsAConfiguration(configuration)) + .And(_ => GivenOcelotIsRunningWithConsul()) + .When(_ => WhenIGetUrlOnTheApiGateway("/home")) + .Then(_ => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(_ => ThenTheTokenIs(token)) .BDDfy(); - } + } - [Theory] - [Trait("PR", "1944")] - [Trait("Issues", "849 1496")] - [InlineData("LeastConnection")] - [InlineData("RoundRobin")] - [InlineData("NoLoadBalancer")] - [InlineData("CookieStickySessions")] - public void Should_use_consul_service_discovery_based_on_upstream_host(string loadBalancerType) - { - // Simulate two DIFFERENT downstream services (e.g. product services for US and EU markets) - // with different ServiceNames (e.g. product-us and product-eu), - // UpstreamHost is used to determine which ServiceName to use when making a request to Consul (e.g. Host: us-shop goes to product-us) - var consulPort = PortFinder.GetRandomPort(); - var servicePortUS = PortFinder.GetRandomPort(); - var servicePortEU = PortFinder.GetRandomPort(); - var serviceNameUS = "product-us"; - var serviceNameEU = "product-eu"; - var downstreamServiceUrlUS = $"http://localhost:{servicePortUS}"; - var downstreamServiceUrlEU = $"http://localhost:{servicePortEU}"; - var upstreamHostUS = "us-shop"; - var upstreamHostEU = "eu-shop"; - var publicUrlUS = $"http://{upstreamHostUS}"; - var publicUrlEU = $"http://{upstreamHostEU}"; - var responseBodyUS = "Phone chargers with US plug"; - var responseBodyEU = "Phone chargers with EU plug"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryUS = new ServiceEntry - { - Service = new AgentService - { - Service = serviceNameUS, - Address = "localhost", - Port = servicePortUS, - ID = Guid.NewGuid().ToString(), - Tags = new string[] { "US" }, - }, - }; - var serviceEntryEU = new ServiceEntry - { - Service = new AgentService - { - Service = serviceNameEU, - Address = "localhost", - Port = servicePortEU, - ID = Guid.NewGuid().ToString(), - Tags = new string[] { "EU" }, - }, - }; - - var configuration = new FileConfiguration - { - Routes = new() - { - new() - { - DownstreamPathTemplate = "/products", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new() { "Get" }, - UpstreamHost = upstreamHostUS, - ServiceName = serviceNameUS, - LoadBalancerOptions = new() { Type = loadBalancerType }, - }, - new() - { - DownstreamPathTemplate = "/products", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new() {"Get" }, - UpstreamHost = upstreamHostEU, - ServiceName = serviceNameEU, - LoadBalancerOptions = new() { Type = loadBalancerType }, - }, - }, - GlobalConfiguration = new() - { - ServiceDiscoveryProvider = new() - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - }, - }, - }; - - // Ocelot request for http://us-shop/ should find 'product-us' in Consul, call /products and return "Phone chargers with US plug" - // Ocelot request for http://eu-shop/ should find 'product-eu' in Consul, call /products and return "Phone chargers with EU plug" - this.Given(x => x._serviceHandler.GivenThereIsAServiceRunningOn(downstreamServiceUrlUS, "/products", MapGet("/products", responseBodyUS))) - .And(x => x._serviceHandler2.GivenThereIsAServiceRunningOn(downstreamServiceUrlEU, "/products", MapGet("/products", responseBodyEU))) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryUS, serviceEntryEU)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul(publicUrlUS, publicUrlEU)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop for the first time") - .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(1)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyUS)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop for the first time") - .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(2)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyEU)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop again") - .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(3)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyUS)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop again") - .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(4)) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(responseBodyEU)) - .BDDfy(); - } + [Fact] + public void Should_send_request_to_service_after_it_becomes_available_in_consul() + { + const string serviceName = "product"; + var consulPort = PortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var serviceEntry1 = GivenServiceEntry(port1, serviceName: serviceName); + var serviceEntry2 = GivenServiceEntry(port2, serviceName: serviceName); + var route = GivenRoute(serviceName: serviceName); + var configuration = GivenServiceDiscovery(consulPort, route); + this.Given(x => x.GivenProductServiceOneIsRunning(DownstreamUrl(port1), 200)) + .And(x => x.GivenProductServiceTwoIsRunning(DownstreamUrl(port2), 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry1, serviceEntry2)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .And(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .And(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) + .And(x => WhenIRemoveAService(serviceEntry2)) + .And(x => GivenIResetCounters()) + .And(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .And(x => ThenOnlyOneServiceHasBeenCalled()) + .And(x => WhenIAddAServiceBackIn(serviceEntry2)) + .And(x => GivenIResetCounters()) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) + .BDDfy(); + } - private void ThenTheTokenIs(string token) - { - _receivedToken.ShouldBe(token); - } + [Fact] + public void Should_handle_request_to_poll_consul_for_downstream_service_and_make_request() + { + const string serviceName = "web"; + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); + var serviceEntry = GivenServiceEntry(servicePort, "localhost", $"web_90_0_2_224_{servicePort}", new[] { "version-v1" }, serviceName); + var route = GivenRoute("/api/home", "/home", serviceName, httpMethods: new[] { "Get", "Options" }); + var configuration = GivenServiceDiscovery(consulPort, route); + + var sd = configuration.GlobalConfiguration.ServiceDiscoveryProvider; + sd.Type = nameof(PollConsul); + sd.PollingInterval = 0; + sd.Namespace = string.Empty; + + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", HttpStatusCode.OK, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) + .When(x => WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } - private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo) - { - _consulServices.Add(serviceEntryTwo); - } + [Theory] + [Trait("PR", "1944")] + [Trait("Bugs", "849 1496")] + [InlineData(nameof(LeastConnection))] + [InlineData(nameof(RoundRobin))] + [InlineData(nameof(NoLoadBalancer))] + [InlineData(nameof(CookieStickySessions))] + public void Should_use_consul_service_discovery_based_on_upstream_host(string loadBalancerType) + { + // Simulate two DIFFERENT downstream services (e.g. product services for US and EU markets) + // with different ServiceNames (e.g. product-us and product-eu), + // UpstreamHost is used to determine which ServiceName to use when making a request to Consul (e.g. Host: us-shop goes to product-us) + const string serviceNameUS = "product-us"; + const string serviceNameEU = "product-eu"; + var consulPort = PortFinder.GetRandomPort(); + var servicePortUS = PortFinder.GetRandomPort(); + var servicePortEU = PortFinder.GetRandomPort(); + const string upstreamHostUS = "us-shop"; + const string upstreamHostEU = "eu-shop"; + var publicUrlUS = $"http://{upstreamHostUS}"; + var publicUrlEU = $"http://{upstreamHostEU}"; + const string responseBodyUS = "Phone chargers with US plug"; + const string responseBodyEU = "Phone chargers with EU plug"; + var serviceEntryUS = GivenServiceEntry(servicePortUS, serviceName: serviceNameUS, tags: new[] { "US" }); + var serviceEntryEU = GivenServiceEntry(servicePortEU, serviceName: serviceNameEU, tags: new[] { "EU" }); + var routeUS = GivenRoute("/products", "/", serviceNameUS, loadBalancerType, upstreamHostUS); + var routeEU = GivenRoute("/products", "/", serviceNameEU, loadBalancerType, upstreamHostEU); + var configuration = GivenServiceDiscovery(consulPort, routeUS, routeEU); + + // Ocelot request for http://us-shop/ should find 'product-us' in Consul, call /products and return "Phone chargers with US plug" + // Ocelot request for http://eu-shop/ should find 'product-eu' in Consul, call /products and return "Phone chargers with EU plug" + this.Given(x => x._serviceHandler.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePortUS), "/products", MapGet("/products", responseBodyUS))) + .And(x => x._serviceHandler2.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePortEU), "/products", MapGet("/products", responseBodyEU))) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryUS, serviceEntryEU)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul(publicUrlUS, publicUrlEU)) + .When(x => WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop for the first time") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(1)) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(responseBodyUS)) + .When(x => WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop for the first time") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(2)) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(responseBodyEU)) + .When(x => WhenIGetUrlOnTheApiGateway(publicUrlUS), "When I get US shop again") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(3)) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(responseBodyUS)) + .When(x => WhenIGetUrlOnTheApiGateway(publicUrlEU), "When I get EU shop again") + .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(4)) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe(responseBodyEU)) + .BDDfy(); + } - private void ThenOnlyOneServiceHasBeenCalled() - { - _counterOne.ShouldBe(10); - _counterTwo.ShouldBe(0); - } + [Fact] + [Trait("Bug", "954")] + public void Should_return_service_address_by_overridden_service_builder_when_there_is_a_node() + { + const string serviceName = "OpenTestService"; + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); // 9999 + var serviceEntry = GivenServiceEntry(servicePort, + id: "OPEN_TEST_01", + serviceName: serviceName, + tags: new[] { serviceName }); + var serviceNode = new Node() { Name = "n1" }; // cornerstone of the bug + serviceEntry.Node = serviceNode; + var route = GivenRoute("/api/{url}", "/open/{url}", serviceName, httpMethods: new[] { "POST", "GET" }); + var configuration = GivenServiceDiscovery(consulPort, route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", HttpStatusCode.OK, "Hello from Raman")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) + .And(x => x.GivenTheServiceNodesAreRegisteredWithConsul(serviceNode)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithConsul()) // default services registration results with the bug: "n1" host issue + .When(x => WhenIGetUrlOnTheApiGateway("/open/home")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) + .And(x => ThenTheResponseBodyShouldBe("")) + .And(x => ThenConsulShouldHaveBeenCalledTimes(1)) + .And(x => ThenConsulNodesShouldHaveBeenCalledTimes(1)) + + // Override default service builder + .Given(x => GivenOcelotIsRunningWithServices(WithOverriddenConsulServiceBuilder)) + .When(x => WhenIGetUrlOnTheApiGateway("/open/home")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Raman")) + .And(x => ThenConsulShouldHaveBeenCalledTimes(2)) + .And(x => ThenConsulNodesShouldHaveBeenCalledTimes(2)) + .BDDfy(); + } - private void WhenIRemoveAService(ServiceEntry serviceEntryTwo) - { - _consulServices.Remove(serviceEntryTwo); - } + private static void WithOverriddenConsulServiceBuilder(IServiceCollection services) + => services.AddOcelot().AddConsul(); - private void GivenIResetCounters() - { - _counterOne = 0; - _counterTwo = 0; - _counterConsul = 0; - } + public class MyConsulServiceBuilder : DefaultConsulServiceBuilder + { + public MyConsulServiceBuilder(Func configurationFactory, IConsulClientFactory clientFactory, IOcelotLoggerFactory loggerFactory) + : base(configurationFactory, clientFactory, loggerFactory) { } - private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) - { - _counterOne.ShouldBeInRange(bottom, top); - _counterOne.ShouldBeInRange(bottom, top); - } + protected override string GetDownstreamHost(ServiceEntry entry, Node node) => entry.Service.Address; + } - private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) + private static ServiceEntry GivenServiceEntry(int port, string address = null, string id = null, string[] tags = null, [CallerMemberName] string serviceName = null) => new() + { + Service = new AgentService { - var total = _counterOne + _counterTwo; - total.ShouldBe(expected); - } + Service = serviceName, + Address = address ?? "localhost", + Port = port, + ID = id ?? Guid.NewGuid().ToString(), + Tags = tags ?? Array.Empty(), + }, + }; + + private static FileRoute GivenRoute(string downstream = null, string upstream = null, [CallerMemberName] string serviceName = null, string loadBalancerType = null, string upstreamHost = null, string[] httpMethods = null) => new() + { + DownstreamPathTemplate = downstream ?? "/", + DownstreamScheme = Uri.UriSchemeHttp, + UpstreamPathTemplate = upstream ?? "/", + UpstreamHttpMethod = httpMethods != null ? new(httpMethods) : new() { HttpMethods.Get }, + UpstreamHost = upstreamHost, + ServiceName = serviceName, + LoadBalancerOptions = new() { Type = loadBalancerType ?? nameof(LeastConnection) }, + }; + + private static FileConfiguration GivenServiceDiscovery(int consulPort, params FileRoute[] routes) + { + var config = GivenConfiguration(routes); + config.GlobalConfiguration.ServiceDiscoveryProvider = new() + { + Scheme = Uri.UriSchemeHttp, + Host = "localhost", + Port = consulPort, + Type = nameof(Provider.Consul.Consul), + }; + return config; + } + + private void ThenTheTokenIs(string token) + { + _receivedToken.ShouldBe(token); + } - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + private void WhenIAddAServiceBackIn(ServiceEntry serviceEntry) + { + _consulServices.Add(serviceEntry); + } + + private void ThenOnlyOneServiceHasBeenCalled() + { + _counterOne.ShouldBe(10); + _counterTwo.ShouldBe(0); + } + + private void WhenIRemoveAService(ServiceEntry serviceEntry) + { + _consulServices.Remove(serviceEntry); + } + + private void GivenIResetCounters() + { + _counterOne = 0; + _counterTwo = 0; + _counterConsul = 0; + } + + private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) + { + _counterOne.ShouldBeInRange(bottom, top); + _counterOne.ShouldBeInRange(bottom, top); + } + + private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) + { + var total = _counterOne + _counterTwo; + total.ShouldBe(expected); + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) => _consulServices.AddRange(serviceEntries); + private void GivenTheServiceNodesAreRegisteredWithConsul(params Node[] nodes) => _consulNodes.AddRange(nodes); + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) + { + _consulHandler.GivenThereIsAServiceRunningOn(url, async context => { - foreach (var serviceEntry in serviceEntries) + if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) { - _consulServices.Add(serviceEntry); + _receivedToken = values.First(); } - } - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) - { - _consulHandler.GivenThereIsAServiceRunningOn(url, async context => + // Parse the request path to get the service name + var pathMatch = Regex.Match(context.Request.Path.Value, "/v1/health/service/(?[^/]+)"); + if (pathMatch.Success) { - if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) - { - _receivedToken = values.First(); - } + _counterConsul++; + + // Use the parsed service name to filter the registered Consul services + var serviceName = pathMatch.Groups["serviceName"].Value; + var services = _consulServices.Where(x => x.Service.Service == serviceName).ToList(); + var json = JsonConvert.SerializeObject(services); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + return; + } - // Parse the request path to get the service name - var pathMatch = Regex.Match(context.Request.Path.Value, "/v1/health/service/(?[^/]+)"); - if (pathMatch.Success) - { - _counterConsul++; - - // Use the parsed service name to filter the registered Consul services - var serviceName = pathMatch.Groups["serviceName"].Value; - var services = _consulServices.Where(x => x.Service.Service == serviceName).ToList(); - var json = JsonConvert.SerializeObject(services); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - } + if (context.Request.Path.Value == "/v1/catalog/nodes") + { + _counterNodes++; + var json = JsonConvert.SerializeObject(_consulNodes); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + } - private void ThenConsulShouldHaveBeenCalledTimes(int expected) - { - _counterConsul.ShouldBe(expected); - } + private void ThenConsulShouldHaveBeenCalledTimes(int expected) => _counterConsul.ShouldBe(expected); + private void ThenConsulNodesShouldHaveBeenCalledTimes(int expected) => _counterNodes.ShouldBe(expected); - private void GivenProductServiceOneIsRunning(string url, int statusCode) + private void GivenProductServiceOneIsRunning(string url, int statusCode) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + try { - try - { - string response; - lock (SyncLock) - { - _counterOne++; - response = _counterOne.ToString(); - } - - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(response); - } - catch (Exception exception) + string response; + lock (SyncLock) { - await context.Response.WriteAsync(exception.StackTrace); + _counterOne++; + response = _counterOne.ToString(); } - }); - } - private void GivenProductServiceTwoIsRunning(string url, int statusCode) - { - _serviceHandler2.GivenThereIsAServiceRunningOn(url, async context => + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (Exception exception) { - try - { - string response; - lock (SyncLock) - { - _counterTwo++; - response = _counterTwo.ToString(); - } - - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(response); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + private void GivenProductServiceTwoIsRunning(string url, int statusCode) + { + _serviceHandler2.GivenThereIsAServiceRunningOn(url, async context => { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + try { - _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPath != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else + string response; + lock (SyncLock) { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); + _counterTwo++; + response = _counterTwo.ToString(); } - }); - } - private RequestDelegate MapGet(string path, string responseBody) => async context => + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, HttpStatusCode statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => { - var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - if (downstreamPath == path) + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) { - context.Response.StatusCode = 200; - await context.Response.WriteAsync(responseBody); + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync("Downstream path doesn't match base path"); } else { - context.Response.StatusCode = 404; - await context.Response.WriteAsync("Not Found"); + context.Response.StatusCode = (int)statusCode; + await context.Response.WriteAsync(responseBody); } - }; + }); + } - public void Dispose() + private static RequestDelegate MapGet(string path, string responseBody) => async context => + { + var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + if (downstreamPath == path) { - _serviceHandler?.Dispose(); - _serviceHandler2?.Dispose(); - _consulHandler?.Dispose(); - _steps.Dispose(); + context.Response.StatusCode = (int)HttpStatusCode.OK; + await context.Response.WriteAsync(responseBody); } - } + else + { + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + await context.Response.WriteAsync("Not Found"); + } + }; } diff --git a/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulWebSocketTests.cs similarity index 97% rename from test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs rename to test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulWebSocketTests.cs index dceafc0d4..9afa1b154 100644 --- a/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulWebSocketTests.cs @@ -5,343 +5,343 @@ using Ocelot.WebSockets; using System.Net.WebSockets; using System.Text; - -namespace Ocelot.AcceptanceTests -{ - public class ConsulWebSocketTests : IDisposable - { - private readonly List _secondRecieved; - private readonly List _firstRecieved; - private readonly List _serviceEntries; - private readonly Steps _steps; - private readonly ServiceHandler _serviceHandler; - - public ConsulWebSocketTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - _firstRecieved = new List(); - _secondRecieved = new List(); - _serviceEntries = new List(); - } - - [Fact] - public void ShouldProxyWebsocketInputToDownstreamServiceAndUseServiceDiscoveryAndLoadBalancer() - { - var downstreamPort = PortFinder.GetRandomPort(); - var downstreamHost = "localhost"; - - var secondDownstreamPort = PortFinder.GetRandomPort(); - var secondDownstreamHost = "localhost"; - - var serviceName = "websockets"; - var consulPort = PortFinder.GetRandomPort(); - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = downstreamHost, - Port = downstreamPort, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - var serviceEntryTwo = new ServiceEntry - { - Service = new AgentService - { - Service = serviceName, - Address = secondDownstreamHost, - Port = secondDownstreamPort, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - var config = new FileConfiguration - { - Routes = new List - { - new() - { - UpstreamPathTemplate = "/", - DownstreamPathTemplate = "/ws", - DownstreamScheme = "ws", - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "RoundRobin" }, - ServiceName = serviceName, - }, - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Scheme = "http", - Host = "localhost", - Port = consulPort, - Type = "consul", - }, - }, - }; - - this.Given(_ => _steps.GivenThereIsAConfiguration(config)) - .And(_ => _steps.StartFakeOcelotWithWebSocketsWithConsul()) - .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws")) - .And(_ => StartSecondFakeDownstreamService($"http://{secondDownstreamHost}:{secondDownstreamPort}", "/ws")) - .When(_ => WhenIStartTheClients()) - .Then(_ => ThenBothDownstreamServicesAreCalled()) - .BDDfy(); - } - - private void ThenBothDownstreamServicesAreCalled() - { - _firstRecieved.Count.ShouldBe(10); - _firstRecieved.ForEach(x => - { - x.ShouldBe("test"); - }); - - _secondRecieved.Count.ShouldBe(10); - _secondRecieved.ForEach(x => - { - x.ShouldBe("chocolate"); - }); - } - - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) - { - foreach (var serviceEntry in serviceEntries) - { - _serviceEntries.Add(serviceEntry); - } - } - - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") - { - var json = JsonConvert.SerializeObject(_serviceEntries); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - } - - private async Task WhenIStartTheClients() - { - var firstClient = StartClient("ws://localhost:5000/"); - - var secondClient = StartSecondClient("ws://localhost:5000/"); - - await Task.WhenAll(firstClient, secondClient); - } - - private async Task StartClient(string url) - { - IClientWebSocket client = new ClientWebSocketProxy(); - - await client.ConnectAsync(new Uri(url), CancellationToken.None); - - var sending = Task.Run(async () => - { - var line = "test"; - for (var i = 0; i < 10; i++) - { - var bytes = Encoding.UTF8.GetBytes(line); - - await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, - CancellationToken.None); - await Task.Delay(10); - } - - await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - }); - - var receiving = Task.Run(async () => - { - var buffer = new byte[1024 * 4]; - - while (true) - { - var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - if (result.MessageType == WebSocketMessageType.Text) - { - _firstRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); - } - else if (result.MessageType == WebSocketMessageType.Close) - { - if (client.State != WebSocketState.Closed) - { - // Last version, the client state is CloseReceived - // Valid states are: Open, CloseReceived, CloseSent - await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - } - - break; - } - } - }); - - await Task.WhenAll(sending, receiving); - } - - private async Task StartSecondClient(string url) - { - await Task.Delay(500); - - IClientWebSocket client = new ClientWebSocketProxy(); - - await client.ConnectAsync(new Uri(url), CancellationToken.None); - - var sending = Task.Run(async () => - { - var line = "test"; - for (var i = 0; i < 10; i++) - { - var bytes = Encoding.UTF8.GetBytes(line); - - await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, - CancellationToken.None); - await Task.Delay(10); - } - - await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - }); - - var receiving = Task.Run(async () => - { - var buffer = new byte[1024 * 4]; - - while (true) - { - var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - if (result.MessageType == WebSocketMessageType.Text) - { - _secondRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); - } - else if (result.MessageType == WebSocketMessageType.Close) - { - if (client.State != WebSocketState.Closed) - { - // Last version, the client state is CloseReceived - // Valid states are: Open, CloseReceived, CloseSent - await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); - } - - break; - } - } - }); - - await Task.WhenAll(sending, receiving); - } - - private async Task StartFakeDownstreamService(string url, string path) - { - await _serviceHandler.StartFakeDownstreamService(url, async (context, next) => - { - if (context.Request.Path == path) - { - if (context.WebSockets.IsWebSocketRequest) - { - var webSocket = await context.WebSockets.AcceptWebSocketAsync(); - await Echo(webSocket); - } - else - { - context.Response.StatusCode = 400; - } - } - else - { - await next(); - } - }); - } - - private async Task StartSecondFakeDownstreamService(string url, string path) - { - await _serviceHandler.StartFakeDownstreamService(url, async (context, next) => - { - if (context.Request.Path == path) - { - if (context.WebSockets.IsWebSocketRequest) - { - var webSocket = await context.WebSockets.AcceptWebSocketAsync(); - await Message(webSocket); - } - else - { - context.Response.StatusCode = 400; - } - } - else - { - await next(); - } - }); - } - - private static async Task Echo(WebSocket webSocket) - { - try - { - var buffer = new byte[1024 * 4]; - - var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - while (!result.CloseStatus.HasValue) - { - await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); - - result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - } - - await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - - private static async Task Message(WebSocket webSocket) - { - try - { - var buffer = new byte[1024 * 4]; - - var bytes = Encoding.UTF8.GetBytes("chocolate"); - - var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - while (!result.CloseStatus.HasValue) - { - await webSocket.SendAsync(new ArraySegment(bytes), result.MessageType, result.EndOfMessage, CancellationToken.None); - - result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - } - - await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - - public void Dispose() - { - _serviceHandler?.Dispose(); + +namespace Ocelot.AcceptanceTests.ServiceDiscovery +{ + public class ConsulWebSocketTests : IDisposable + { + private readonly List _secondRecieved; + private readonly List _firstRecieved; + private readonly List _serviceEntries; + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public ConsulWebSocketTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + _firstRecieved = new List(); + _secondRecieved = new List(); + _serviceEntries = new List(); + } + + [Fact] + public void ShouldProxyWebsocketInputToDownstreamServiceAndUseServiceDiscoveryAndLoadBalancer() + { + var downstreamPort = PortFinder.GetRandomPort(); + var downstreamHost = "localhost"; + + var secondDownstreamPort = PortFinder.GetRandomPort(); + var secondDownstreamHost = "localhost"; + + var serviceName = "websockets"; + var consulPort = PortFinder.GetRandomPort(); + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = downstreamHost, + Port = downstreamPort, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + var serviceEntryTwo = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = secondDownstreamHost, + Port = secondDownstreamPort, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + + var config = new FileConfiguration + { + Routes = new List + { + new() + { + UpstreamPathTemplate = "/", + DownstreamPathTemplate = "/ws", + DownstreamScheme = "ws", + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "RoundRobin" }, + ServiceName = serviceName, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + Type = "consul", + }, + }, + }; + + this.Given(_ => _steps.GivenThereIsAConfiguration(config)) + .And(_ => _steps.StartFakeOcelotWithWebSocketsWithConsul()) + .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws")) + .And(_ => StartSecondFakeDownstreamService($"http://{secondDownstreamHost}:{secondDownstreamPort}", "/ws")) + .When(_ => WhenIStartTheClients()) + .Then(_ => ThenBothDownstreamServicesAreCalled()) + .BDDfy(); + } + + private void ThenBothDownstreamServicesAreCalled() + { + _firstRecieved.Count.ShouldBe(10); + _firstRecieved.ForEach(x => + { + x.ShouldBe("test"); + }); + + _secondRecieved.Count.ShouldBe(10); + _secondRecieved.ForEach(x => + { + x.ShouldBe("chocolate"); + }); + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach (var serviceEntry in serviceEntries) + { + _serviceEntries.Add(serviceEntry); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") + { + var json = JsonConvert.SerializeObject(_serviceEntries); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + } + + private async Task WhenIStartTheClients() + { + var firstClient = StartClient("ws://localhost:5000/"); + + var secondClient = StartSecondClient("ws://localhost:5000/"); + + await Task.WhenAll(firstClient, secondClient); + } + + private async Task StartClient(string url) + { + IClientWebSocket client = new ClientWebSocketProxy(); + + await client.ConnectAsync(new Uri(url), CancellationToken.None); + + var sending = Task.Run(async () => + { + var line = "test"; + for (var i = 0; i < 10; i++) + { + var bytes = Encoding.UTF8.GetBytes(line); + + await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, + CancellationToken.None); + await Task.Delay(10); + } + + await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + }); + + var receiving = Task.Run(async () => + { + var buffer = new byte[1024 * 4]; + + while (true) + { + var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + if (result.MessageType == WebSocketMessageType.Text) + { + _firstRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); + } + else if (result.MessageType == WebSocketMessageType.Close) + { + if (client.State != WebSocketState.Closed) + { + // Last version, the client state is CloseReceived + // Valid states are: Open, CloseReceived, CloseSent + await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + } + + break; + } + } + }); + + await Task.WhenAll(sending, receiving); + } + + private async Task StartSecondClient(string url) + { + await Task.Delay(500); + + IClientWebSocket client = new ClientWebSocketProxy(); + + await client.ConnectAsync(new Uri(url), CancellationToken.None); + + var sending = Task.Run(async () => + { + var line = "test"; + for (var i = 0; i < 10; i++) + { + var bytes = Encoding.UTF8.GetBytes(line); + + await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, + CancellationToken.None); + await Task.Delay(10); + } + + await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + }); + + var receiving = Task.Run(async () => + { + var buffer = new byte[1024 * 4]; + + while (true) + { + var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + if (result.MessageType == WebSocketMessageType.Text) + { + _secondRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); + } + else if (result.MessageType == WebSocketMessageType.Close) + { + if (client.State != WebSocketState.Closed) + { + // Last version, the client state is CloseReceived + // Valid states are: Open, CloseReceived, CloseSent + await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + } + + break; + } + } + }); + + await Task.WhenAll(sending, receiving); + } + + private async Task StartFakeDownstreamService(string url, string path) + { + await _serviceHandler.StartFakeDownstreamService(url, async (context, next) => + { + if (context.Request.Path == path) + { + if (context.WebSockets.IsWebSocketRequest) + { + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await Echo(webSocket); + } + else + { + context.Response.StatusCode = 400; + } + } + else + { + await next(); + } + }); + } + + private async Task StartSecondFakeDownstreamService(string url, string path) + { + await _serviceHandler.StartFakeDownstreamService(url, async (context, next) => + { + if (context.Request.Path == path) + { + if (context.WebSockets.IsWebSocketRequest) + { + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await Message(webSocket); + } + else + { + context.Response.StatusCode = 400; + } + } + else + { + await next(); + } + }); + } + + private static async Task Echo(WebSocket webSocket) + { + try + { + var buffer = new byte[1024 * 4]; + + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + private static async Task Message(WebSocket webSocket) + { + try + { + var buffer = new byte[1024 * 4]; + + var bytes = Encoding.UTF8.GetBytes("chocolate"); + + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(bytes), result.MessageType, result.EndOfMessage, CancellationToken.None); + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + public void Dispose() + { + _serviceHandler?.Dispose(); _steps.Dispose(); - GC.SuppressFinalize(this); - } - } -} + GC.SuppressFinalize(this); + } + } +} diff --git a/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs index 2b2ee6557..0f8eaf192 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs @@ -5,6 +5,7 @@ using Ocelot.Configuration.File; using Ocelot.Logging; using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; using Ocelot.Responses; using System.Text; diff --git a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs deleted file mode 100644 index 7df0a2c60..000000000 --- a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs +++ /dev/null @@ -1,223 +0,0 @@ -using Consul; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Newtonsoft.Json; -using Ocelot.Logging; -using Ocelot.Provider.Consul; -using Ocelot.Values; -using ConsulProvider = Ocelot.Provider.Consul.Consul; - -namespace Ocelot.UnitTests.Consul -{ - public class ConsulServiceDiscoveryProviderTests : UnitTest, IDisposable - { - private IWebHost _fakeConsulBuilder; - private readonly List _serviceEntries; - private ConsulProvider _provider; - private readonly string _serviceName; - private readonly int _port; - private readonly string _consulHost; - private readonly string _consulScheme; - private readonly string _fakeConsulServiceDiscoveryUrl; - private List _services; - private readonly Mock _factory; - private readonly Mock _logger; - private string _receivedToken; - private readonly IConsulClientFactory _clientFactory; - - public ConsulServiceDiscoveryProviderTests() - { - _serviceName = "test"; - _port = 8500; - _consulHost = "localhost"; - _consulScheme = "http"; - _fakeConsulServiceDiscoveryUrl = $"{_consulScheme}://{_consulHost}:{_port}"; - _serviceEntries = new List(); - _factory = new Mock(); - _clientFactory = new ConsulClientFactory(); - _logger = new Mock(); - _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, _serviceName, null); - _provider = new ConsulProvider(config, _factory.Object, _clientFactory); - } - - [Fact] - public void should_return_service_from_consul() - { - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = _serviceName, - Address = "localhost", - Port = 50881, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) - .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .When(x => WhenIGetTheServices()) - .Then(x => ThenTheCountIs(1)) - .BDDfy(); - } - - [Fact] - public void should_use_token() - { - var token = "test token"; - var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, _serviceName, token); - _provider = new ConsulProvider(config, _factory.Object, _clientFactory); - - var serviceEntryOne = new ServiceEntry - { - Service = new AgentService - { - Service = _serviceName, - Address = "localhost", - Port = 50881, - ID = Guid.NewGuid().ToString(), - Tags = Array.Empty(), - }, - }; - - this.Given(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) - .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .When(_ => WhenIGetTheServices()) - .Then(_ => ThenTheCountIs(1)) - .And(_ => ThenTheTokenIs(token)) - .BDDfy(); - } - - [Fact] - public void should_not_return_services_with_invalid_address() - { - var serviceEntryOne = GivenService(address: "http://localhost", port: 50881) - .ToServiceEntry(); - var serviceEntryTwo = GivenService(address: "http://localhost", port: 50888) - .ToServiceEntry(); - - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) - .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .When(x => WhenIGetTheServices()) - .Then(x => ThenTheCountIs(0)) - .And(x => ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(serviceEntryOne, serviceEntryTwo)) - .BDDfy(); - } - - [Fact] - public void should_not_return_services_with_empty_address() - { - var serviceEntryOne = GivenService(port: 50881) - .WithAddress(string.Empty) - .ToServiceEntry(); - var serviceEntryTwo = GivenService(port: 50888) - .WithAddress(null) - .ToServiceEntry(); - - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) - .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .When(x => WhenIGetTheServices()) - .Then(x => ThenTheCountIs(0)) - .And(x => ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(serviceEntryOne, serviceEntryTwo)) - .BDDfy(); - } - - [Fact] - public void should_not_return_services_with_invalid_port() - { - var serviceEntryOne = GivenService(port: -1) - .ToServiceEntry(); - var serviceEntryTwo = GivenService(port: 0) - .ToServiceEntry(); - - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) - .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .When(x => WhenIGetTheServices()) - .Then(x => ThenTheCountIs(0)) - .And(x => ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(serviceEntryOne, serviceEntryTwo)) - .BDDfy(); - } - - private AgentService GivenService(string address = null, int? port = null, string id = null, string[] tags = null) - => new() - { - Service = _serviceName, - Address = address ?? "localhost", - Port = port ?? 123, - ID = id ?? Guid.NewGuid().ToString(), - Tags = tags ?? Array.Empty(), - }; - - private void ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(params ServiceEntry[] serviceEntries) - { - foreach (var entry in serviceEntries) - { - var service = entry.Service; - var expected = $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."; - _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == expected)), Times.Once); - } - } - - private void ThenTheCountIs(int count) - { - _services.Count.ShouldBe(count); - } - - private void WhenIGetTheServices() - { - _services = _provider.GetAsync().GetAwaiter().GetResult(); - } - - private void ThenTheTokenIs(string token) - { - _receivedToken.ShouldBe(token); - } - - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) - { - foreach (var serviceEntry in serviceEntries) - { - _serviceEntries.Add(serviceEntry); - } - } - - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) - { - _fakeConsulBuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.Run(async context => - { - if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") - { - if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) - { - _receivedToken = values.First(); - } - - var json = JsonConvert.SerializeObject(_serviceEntries); - context.Response.Headers.Append("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - }) - .Build(); - - _fakeConsulBuilder.Start(); - } - - public void Dispose() - { - _fakeConsulBuilder?.Dispose(); - } - } -} diff --git a/test/Ocelot.UnitTests/Consul/ConsulTests.cs b/test/Ocelot.UnitTests/Consul/ConsulTests.cs new file mode 100644 index 000000000..b9009d488 --- /dev/null +++ b/test/Ocelot.UnitTests/Consul/ConsulTests.cs @@ -0,0 +1,209 @@ +using Consul; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Ocelot.Logging; +using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; +using System.Runtime.CompilerServices; +using ConsulProvider = Ocelot.Provider.Consul.Consul; + +namespace Ocelot.UnitTests.Consul; + +public sealed class ConsulTests : UnitTest, IDisposable +{ + private readonly int _port; + private readonly string _consulHost; + private readonly string _consulScheme; + private readonly string _fakeConsulServiceDiscoveryUrl; + private readonly List _consulServiceEntries; + private readonly Mock _factory; + private readonly Mock _logger; + private IConsulClientFactory _clientFactory; + private IConsulServiceBuilder _serviceBuilder; + private ConsulRegistryConfiguration _config; + private IWebHost _fakeConsulBuilder; + private ConsulProvider _provider; + private string _receivedToken; + + public ConsulTests() + { + _port = 8500; + _consulHost = "localhost"; + _consulScheme = "http"; + _fakeConsulServiceDiscoveryUrl = $"{_consulScheme}://{_consulHost}:{_port}"; + _consulServiceEntries = new List(); + _factory = new Mock(); + _logger = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + } + + public void Dispose() + { + _fakeConsulBuilder?.Dispose(); + } + + private void Arrange([CallerMemberName] string serviceName = null) + { + _config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, serviceName, null); + _clientFactory = new ConsulClientFactory(); + _serviceBuilder = new DefaultConsulServiceBuilder(() => _config, _clientFactory, _factory.Object); + _provider = new ConsulProvider(_config, _factory.Object, _clientFactory, _serviceBuilder); + } + + [Fact] + public async Task Should_return_service_from_consul() + { + Arrange(); + var service1 = GivenService(50881); + _consulServiceEntries.Add(service1.ToServiceEntry()); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().Count.ShouldBe(1); + } + + [Fact] + public async Task Should_use_token() + { + Arrange(); + const string token = "test token"; + var service1 = GivenService(50881); + _consulServiceEntries.Add(service1.ToServiceEntry()); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, nameof(Should_use_token), token); + _provider = new ConsulProvider(config, _factory.Object, _clientFactory, _serviceBuilder); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().Count.ShouldBe(1); + _receivedToken.ShouldBe(token); + } + + [Fact] + public async Task Should_not_return_services_with_invalid_address() + { + Arrange(); + var service1 = GivenService(50881, "http://localhost"); + var service2 = GivenService(50888, "http://localhost"); + _consulServiceEntries.Add(service1.ToServiceEntry()); + _consulServiceEntries.Add(service2.ToServiceEntry()); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().Count.ShouldBe(0); + ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(); + } + + [Fact] + public async Task Should_not_return_services_with_empty_address() + { + Arrange(); + var service1 = GivenService(50881).WithAddress(string.Empty); + var service2 = GivenService(50888).WithAddress(null); + _consulServiceEntries.Add(service1.ToServiceEntry()); + _consulServiceEntries.Add(service2.ToServiceEntry()); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().Count.ShouldBe(0); + ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(); + } + + [Fact] + public async Task Should_not_return_services_with_invalid_port() + { + Arrange(); + var service1 = GivenService(-1); + var service2 = GivenService(0); + _consulServiceEntries.Add(service1.ToServiceEntry()); + _consulServiceEntries.Add(service2.ToServiceEntry()); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().Count.ShouldBe(0); + ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(); + } + + [Fact] + public async Task GetAsync_NoEntries_ShouldLogWarning() + { + Arrange(); + _consulServiceEntries.Clear(); // NoEntries + _logger.Setup(x => x.LogWarning(It.IsAny>())).Verifiable(); + GivenThereIsAFakeConsulServiceDiscoveryProvider(); + + // Act + var actual = await _provider.GetAsync(); + + // Assert + actual.ShouldNotBeNull().ShouldBeEmpty(); + var expected = $"Consul Provider: No service entries found for '{nameof(GetAsync_NoEntries_ShouldLogWarning)}' service!"; + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == expected)), Times.Once); + } + + private static AgentService GivenService(int port, string address = null, string id = null, string[] tags = null, [CallerMemberName] string serviceName = null) => new() + { + Service = serviceName, + Address = address ?? "localhost", + Port = port, + ID = id ?? Guid.NewGuid().ToString(), + Tags = tags ?? Array.Empty(), + }; + + private void ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning() + { + foreach (var entry in _consulServiceEntries) + { + var service = entry.Service; + var expected = $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."; + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == expected)), Times.Once); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider([CallerMemberName] string serviceName = "test") + { + _fakeConsulBuilder = new WebHostBuilder() + .UseUrls(_fakeConsulServiceDiscoveryUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_fakeConsulServiceDiscoveryUrl) + .Configure(app => + { + app.Run(async context => + { + if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") + { + if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) + { + _receivedToken = values.First(); + } + + var json = JsonConvert.SerializeObject(_consulServiceEntries); + context.Response.Headers.Append("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + }) + .Build(); + _fakeConsulBuilder.Start(); + } +} diff --git a/test/Ocelot.UnitTests/Consul/DefaultConsulServiceBuilderTests.cs b/test/Ocelot.UnitTests/Consul/DefaultConsulServiceBuilderTests.cs new file mode 100644 index 000000000..25dc8d950 --- /dev/null +++ b/test/Ocelot.UnitTests/Consul/DefaultConsulServiceBuilderTests.cs @@ -0,0 +1,200 @@ +using Castle.Components.DictionaryAdapter.Xml; +using Consul; +using Ocelot.Logging; +using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Xml.Linq; + +namespace Ocelot.UnitTests.Consul; + +public sealed class DefaultConsulServiceBuilderTests +{ + private DefaultConsulServiceBuilder sut; + private readonly Func configurationFactory; + private readonly Mock clientFactory; + private readonly Mock loggerFactory; + private readonly Mock logger; + private ConsulRegistryConfiguration _configuration; + + private ConsulRegistryConfiguration GetConfiguration() => _configuration; + + public DefaultConsulServiceBuilderTests() + { + configurationFactory = GetConfiguration; + clientFactory = new(); + clientFactory.Setup(x => x.Get(It.IsAny())) + .Returns(new ConsulClient()); + logger = new(); + loggerFactory = new(); + loggerFactory.Setup(x => x.CreateLogger()) + .Returns(logger.Object); + } + + private void Arrange([CallerMemberName] string testName = null) + { + _configuration = new(null, null, 0, testName, null); + sut = new DefaultConsulServiceBuilder(configurationFactory, clientFactory.Object, loggerFactory.Object); + } + + [Fact] + public void Ctor_PrivateMembers_PropertiesAreInitialized() + { + Arrange(); + var methodClient = sut.GetType().GetProperty("Client", BindingFlags.NonPublic | BindingFlags.Instance); + var methodLogger = sut.GetType().GetProperty("Logger", BindingFlags.NonPublic | BindingFlags.Instance); + + // Act + var actualConfiguration = sut.Configuration; + var actualClient = methodClient.GetValue(sut); + var actualLogger = methodLogger.GetValue(sut); + + // Assert + actualConfiguration.ShouldNotBeNull().ShouldBe(_configuration); + actualClient.ShouldNotBeNull(); + actualLogger.ShouldNotBeNull(); + } + + private static Type Me { get; } = typeof(DefaultConsulServiceBuilder); + private static MethodInfo GetNode { get; } = Me.GetMethod("GetNode", BindingFlags.NonPublic | BindingFlags.Instance); + + [Fact] + public void GetNode_EntryBranch_ReturnsEntryNode() + { + Arrange(); + Node node = new() { Name = nameof(GetNode_EntryBranch_ReturnsEntryNode) }; + ServiceEntry entry = new() { Node = node }; + + // Act + var actual = GetNode.Invoke(sut, new object[] { entry, null }) as Node; + + // Assert + actual.ShouldNotBeNull().ShouldBe(node); + actual.Name.ShouldBe(node.Name); + } + + [Fact] + public void GetNode_NodesBranch_ReturnsNodeFromCollection() + { + Arrange(); + ServiceEntry entry = new() + { + Node = null, + Service = new() { Address = nameof(GetNode_NodesBranch_ReturnsNodeFromCollection) }, + }; + Node[] nodes = null; + + // Act, Assert: nodes is null + var actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; + actual.ShouldBeNull(); + + // Arrange, Act, Assert: nodes has items, happy path + var node = new Node { Address = nameof(GetNode_NodesBranch_ReturnsNodeFromCollection) }; + nodes = new[] { node }; + actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; + actual.ShouldNotBeNull().ShouldBe(node); + actual.Address.ShouldBe(entry.Service.Address); + + // Arrange, Act, Assert: nodes has items, some nulls in entry + entry.Service.Address = null; + actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; + actual.ShouldBeNull(); + + entry.Service = null; + actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; + actual.ShouldBeNull(); + + entry = null; + actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; + actual.ShouldBeNull(); + } + + private static MethodInfo GetDownstreamHost { get; } = Me.GetMethod("GetDownstreamHost", BindingFlags.NonPublic | BindingFlags.Instance); + + [Fact] + public void GetDownstreamHost_BothBranches_NameOrAddress() + { + Arrange(); + + // Arrange, Act, Assert: node branch + ServiceEntry entry = new() + { + Service = new() { Address = nameof(GetDownstreamHost_BothBranches_NameOrAddress) }, + }; + var node = new Node { Name = "test1" }; + var actual = GetDownstreamHost.Invoke(sut, new object[] { entry, node }) as string; + actual.ShouldNotBeNull().ShouldBe("test1"); + + // Arrange, Act, Assert: entry branch + node = null; + actual = GetDownstreamHost.Invoke(sut, new object[] { entry, node }) as string; + actual.ShouldNotBeNull().ShouldBe(nameof(GetDownstreamHost_BothBranches_NameOrAddress)); + } + + private static MethodInfo GetServiceVersion { get; } = Me.GetMethod("GetServiceVersion", BindingFlags.NonPublic | BindingFlags.Instance); + + [Fact] + public void GetServiceVersion_TagsIsNull_EmptyString() + { + Arrange(); + + // Arrange, Act, Assert: collection is null + ServiceEntry entry = new() + { + Service = new() { Tags = null }, + }; + Node node = null; + var actual = GetServiceVersion.Invoke(sut, new object[] { entry, node }) as string; + actual.ShouldBe(string.Empty); + + // Arrange, Act, Assert: collection has no version tag + entry.Service.Tags = new[] { "test" }; + actual = GetServiceVersion.Invoke(sut, new object[] { entry, node }) as string; + actual.ShouldBe(string.Empty); + } + + [Fact] + public void GetServiceVersion_HasTags_HappyPath() + { + Arrange(); + + // Arrange + var tags = new string[] { "test", "version-v2" }; + ServiceEntry entry = new() + { + Service = new() { Tags = tags }, + }; + Node node = null; + + // Act + var actual = GetServiceVersion.Invoke(sut, new object[] { entry, node }) as string; + + // Assert + actual.ShouldBe("v2"); + } + + private static MethodInfo GetServiceTags { get; } = Me.GetMethod("GetServiceTags", BindingFlags.NonPublic | BindingFlags.Instance); + + [Fact] + public void GetServiceTags_BothBranches() + { + Arrange(); + + // Arrange, Act, Assert: collection is null + ServiceEntry entry = new() + { + Service = new() { Tags = null }, + }; + Node node = null; + var actual = GetServiceTags.Invoke(sut, new object[] { entry, node }) as IEnumerable; + actual.ShouldNotBeNull().ShouldBeEmpty(); + + // Arrange, Act, Assert: happy path + entry.Service.Tags = new string[] { "1", "2", "3" }; + actual = GetServiceTags.Invoke(sut, new object[] { entry, node }) as IEnumerable; + actual.ShouldNotBeNull().ShouldNotBeEmpty(); + actual.Count().ShouldBe(3); + actual.ShouldContain("3"); + } +} diff --git a/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs index b9c7532c0..c1a2ed096 100644 --- a/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs @@ -1,73 +1,105 @@ -using Microsoft.AspNetCore.Hosting; +using Consul; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; +using Ocelot.Values; using System.Reflection; -namespace Ocelot.UnitTests.Consul +namespace Ocelot.UnitTests.Consul; + +public class OcelotBuilderExtensionsTests : UnitTest { - public class OcelotBuilderExtensionsTests : UnitTest + private readonly IServiceCollection _services; + private readonly IConfiguration _configRoot; + + public OcelotBuilderExtensionsTests() + { + _configRoot = new ConfigurationRoot(new List()); + _services = new ServiceCollection(); + _services.AddSingleton(GetHostingEnvironment()); + _services.AddSingleton(_configRoot); + } + + private static IWebHostEnvironment GetHostingEnvironment() { - private readonly IServiceCollection _services; - private readonly IConfiguration _configRoot; - private IOcelotBuilder _ocelotBuilder; - private Exception _ex; + var environment = new Mock(); + environment.Setup(e => e.ApplicationName) + .Returns(typeof(OcelotBuilderExtensionsTests).GetTypeInfo().Assembly.GetName().Name); + return environment.Object; + } - public OcelotBuilderExtensionsTests() + [Fact] + public void AddConsul_ShouldSetUpConsul() + { + // Arrange + Exception ex = null; + try { - _configRoot = new ConfigurationRoot(new List()); - _services = new ServiceCollection(); - _services.AddSingleton(GetHostingEnvironment()); - _services.AddSingleton(_configRoot); + // Act + var builder = _services.AddOcelot(_configRoot); + builder.AddConsul(); } - - private static IWebHostEnvironment GetHostingEnvironment() + catch (Exception e) { - var environment = new Mock(); - environment - .Setup(e => e.ApplicationName) - .Returns(typeof(OcelotBuilderExtensionsTests).GetTypeInfo().Assembly.GetName().Name); - - return environment.Object; + ex = e; } - [Fact] - public void should_set_up_consul() + // Assert + ex.ShouldBeNull(); + } + + [Fact] + public void AddConfigStoredInConsul_ShouldSetUpConsul() + { + // Arrange + Exception ex = null; + try { - this.Given(x => WhenISetUpOcelotServices()) - .When(x => WhenISetUpConsul()) - .Then(x => ThenAnExceptionIsntThrown()) - .BDDfy(); + // Act + var builder = _services.AddOcelot(_configRoot); + builder.AddConsul().AddConfigStoredInConsul(); } - - private void WhenISetUpOcelotServices() + catch (Exception e) { - try - { - _ocelotBuilder = _services.AddOcelot(_configRoot); - } - catch (Exception e) - { - _ex = e; - } + ex = e; } - private void WhenISetUpConsul() + // Assert + ex.ShouldBeNull(); + } + + [Fact] + public void AddConsulGeneric_TServiceBuilder_ShouldSetUpConsul() + { + // Arrange + Exception ex = null; + IOcelotBuilder builder = null; + try { - try - { - _ocelotBuilder.AddConsul().AddConfigStoredInConsul(); - } - catch (Exception e) - { - _ex = e; - } + // Act + builder = _services + .AddOcelot(_configRoot) + .AddConsul(); } - - private void ThenAnExceptionIsntThrown() + catch (Exception e) { - _ex.ShouldBeNull(); + ex = e; } + + // Assert + ex.ShouldBeNull(); + builder.ShouldNotBeNull(); + builder.Services.SingleOrDefault(s => s.ServiceType == typeof(IConsulServiceBuilder)).ShouldNotBeNull(); } } + +internal class FakeConsulServiceBuilder : IConsulServiceBuilder +{ + public ConsulRegistryConfiguration Configuration => throw new NotImplementedException(); + public IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes) => throw new NotImplementedException(); + public Service CreateService(ServiceEntry serviceEntry, Node serviceNode) => throw new NotImplementedException(); + public bool IsValid(ServiceEntry entry) => throw new NotImplementedException(); +} diff --git a/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs b/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs index f71b4ffe5..d7b676a23 100644 --- a/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs @@ -3,6 +3,7 @@ using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Provider.Consul; +using Ocelot.Provider.Consul.Interfaces; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.UnitTests.Consul; From cc8f5c5dfc7c56e4446a2e80d97391429c8b364d Mon Sep 17 00:00:00 2001 From: Zhannur Akhmetkhanov <63536315+hogwartsdeveloper@users.noreply.github.com> Date: Tue, 28 May 2024 18:11:43 +0500 Subject: [PATCH 11/15] Ocelot NuGet package README file (#2074) * Add Ocelot.csproj README.md link * Code review --------- Co-authored-by: Raman Maksimchuk --- src/Ocelot/Ocelot.csproj | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Ocelot/Ocelot.csproj b/src/Ocelot/Ocelot.csproj index 19f0a5bdc..b876ca4b7 100644 --- a/src/Ocelot/Ocelot.csproj +++ b/src/Ocelot/Ocelot.csproj @@ -4,7 +4,7 @@ disable disable true - Ocelot is an API Gateway. The project is aimed at people using .NET running a micro services / service orientated architecture that need a unified point of entry into their system. In particular I want easy integration with IdentityServer reference and bearer tokens. reference tokens. Ocelot is a bunch of middlewares in a specific order. Ocelot manipulates the HttpRequest object into a state specified by its configuration until it reaches a request builder middleware where it creates a HttpRequestMessage object which is used to make a request to a downstream service. The middleware that makes the request is the last thing in the Ocelot pipeline. It does not call the next middleware. The response from the downstream service is stored in a per request scoped repository and retrived as the requests goes back up the Ocelot pipeline. There is a piece of middleware that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client. That is basically it with a bunch of other features. + Ocelot is an API gateway based on .NET stack. Ocelot 0.0.0-dev Ocelot @@ -12,6 +12,7 @@ API Gateway;.NET core https://github.com/ThreeMammals/Ocelot https://raw.githubusercontent.com/ThreeMammals/Ocelot/develop/images/ocelot_logo.png + README.md win-x64;osx-x64 false false @@ -55,4 +56,7 @@ + + + From ee1fb97fcf08c8785a45a5576591f0bb95baeda7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raynald=20Messi=C3=A9?= Date: Fri, 31 May 2024 17:36:24 +0200 Subject: [PATCH 12/15] #2078 V7 polly syntax not longer supported (#2079) * #2078 V7 polly syntax not longer supported * fix doc * Update qualityofservice.rst Consolidate all notes into the Notes section --------- Co-authored-by: Raman Maksimchuk --- docs/features/qualityofservice.rst | 26 +- .../Ocelot.Provider.Polly.csproj | 2 +- .../OcelotBuilderExtensions.cs | 97 +------ src/Ocelot.Provider.Polly/Usings.cs | 6 +- .../v7/IPollyQoSProvider.cs | 10 - .../v7/OcelotBuilderExtensions.cs | 120 --------- .../v7/PollyPoliciesDelegatingHandler.cs | 54 ---- .../v7/PollyPolicyWrapper.cs | 21 -- .../v7/PollyQoSProvider.cs | 72 ----- test/Ocelot.UnitTests/Ocelot.UnitTests.csproj | 2 +- .../PollyPoliciesDelegatingHandlerTests.cs | 255 ------------------ .../Polly/PollyQoSProviderTests.cs | 242 ----------------- 12 files changed, 10 insertions(+), 897 deletions(-) delete mode 100644 src/Ocelot.Provider.Polly/v7/IPollyQoSProvider.cs delete mode 100644 src/Ocelot.Provider.Polly/v7/OcelotBuilderExtensions.cs delete mode 100644 src/Ocelot.Provider.Polly/v7/PollyPoliciesDelegatingHandler.cs delete mode 100644 src/Ocelot.Provider.Polly/v7/PollyPolicyWrapper.cs delete mode 100644 src/Ocelot.Provider.Polly/v7/PollyQoSProvider.cs delete mode 100644 test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs delete mode 100644 test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs diff --git a/docs/features/qualityofservice.rst b/docs/features/qualityofservice.rst index 799efdc08..2046ff802 100644 --- a/docs/features/qualityofservice.rst +++ b/docs/features/qualityofservice.rst @@ -43,30 +43,12 @@ You can set the **TimeoutValue** in isolation of the **ExceptionsAllowedBeforeBr There is no point setting the other two in isolation as they affect each other! -Defaults --------- +Notes +----- -If you do not add a QoS section, QoS will not be used, however Ocelot will default to a **90** seconds timeout on all downstream requests. -If someone needs this to be configurable, open an issue. [#f2]_ +1. If you do not add a QoS section, QoS will not be used, however Ocelot will default to a **90** seconds timeout on all downstream requests. If someone needs this to be configurable, open an issue. [#f2]_ -.. _qos-polly-v7-vs-v8: - -`Polly`_ v7 vs v8 ------------------ - -Important changes in version `23.2`_: [#f3]_ - - - With `Polly`_ version 8+, the ``ExceptionsAllowedBeforeBreaking`` value must be equal to or greater than **2**! - - The ``AddPolly`` method has been migrated from v7 policy wrappers to v8 resilience pipelines. Consequently, it now exhibits different behavior based on v8 pipelines. - -If you prefer not to modify your settings, you can continue using `Polly`_ v7 as follows: - -.. code-block:: csharp - - services.AddOcelot() - .AddPollyV7(); - -**Note**: Support for `Polly`_ v7 will be removed in a future version. We recommend avoiding this method (which is tagged as ``Obsolete``) unless absolutely necessary. +2. `Polly`_ V7 syntax no longer supported. In version `23.2`_ [#f3]_ with `Polly`_ version 8+, the ``ExceptionsAllowedBeforeBreaking`` value must be equal to or greater than **2**! .. _qos-extensibility: diff --git a/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj b/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj index 176e4769f..0d9c84e67 100644 --- a/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj +++ b/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj @@ -34,7 +34,7 @@ all - + diff --git a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs index bfa26cf50..76fcc35d5 100644 --- a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs @@ -6,7 +6,6 @@ using Ocelot.Errors.QoS; using Ocelot.Logging; using Ocelot.Provider.Polly.Interfaces; -using Ocelot.Provider.Polly.v7; using Ocelot.Requester; using Polly.CircuitBreaker; using Polly.Registry; @@ -32,7 +31,7 @@ public static class OcelotBuilderExtensions /// /// Adds Polly QoS provider to Ocelot by custom delegate and with custom error mapping. /// - /// QoS provider to use (by default use ). + /// QoS provider to use (by default use ). /// Ocelot builder to extend. /// Your customized delegating handler (to manage QoS behavior by yourself). /// Your customized error mapping. @@ -94,7 +93,7 @@ public static IOcelotBuilder AddPolly(this IOcelotBuilder builder) /// Defaults: /// /// - /// + /// /// /// /// @@ -112,96 +111,4 @@ public static IOcelotBuilder AddPolly(this IOcelotBuilder builder) /// A object, but concrete type is the class. private static DelegatingHandler GetDelegatingHandler(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) => new PollyResiliencePipelineDelegatingHandler(route, contextAccessor, loggerFactory); - - #region Obsolete extensions will be removed in future version - - /// - /// Adds Polly QoS provider to Ocelot by custom delegate and with custom error mapping. - /// - /// QoS provider to use (by default use ). - /// Ocelot builder to extend. - /// Your customized delegating handler (to manage QoS behavior by yourself). - /// Your customized error mapping. - /// The reference to the same extended object. - [Obsolete("Use AddPolly instead, it will be remove in future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler, IDictionary> errorMapping) - where TProvider : class, IPollyQoSProvider - { - builder.Services - .AddSingleton(errorMapping) - .AddSingleton, TProvider>() - .AddSingleton(delegatingHandler); - return builder; - } - - /// - /// Adds Polly QoS provider to Ocelot with custom error mapping, but default is used. - /// - /// QoS provider to use (by default use ). - /// Ocelot builder to extend. - /// Your customized error mapping. - /// The reference to the same extended object. - [Obsolete("Use AddPolly instead, it will be remove in future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, IDictionary> errorMapping) - where TProvider : class, IPollyQoSProvider - => AddPollyV7(builder, GetDelegatingHandlerV7, errorMapping); - - /// - /// Adds Polly QoS provider to Ocelot with custom delegate, but default error mapping is used. - /// - /// QoS provider to use (by default use ). - /// Ocelot builder to extend. - /// Your customized delegating handler (to manage QoS behavior by yourself). - /// The reference to the same extended object. - [Obsolete("Use AddPolly instead, it will be remove in future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler) - where TProvider : class, IPollyQoSProvider - => AddPollyV7(builder, delegatingHandler, DefaultErrorMapping); - - /// - /// Adds Polly QoS provider to Ocelot by defaults. - /// - /// - /// Defaults: - /// - /// - /// - /// - /// - /// QoS provider to use (by default use ). - /// Ocelot builder to extend. - /// The reference to the same extended object. - [Obsolete("Use AddPolly instead, it will be remove in future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder) - where TProvider : class, IPollyQoSProvider - => AddPollyV7(builder, GetDelegatingHandlerV7, DefaultErrorMapping); - - /// - /// Adds Polly QoS provider to Ocelot by defaults with default QoS provider. - /// - /// - /// Defaults: - /// - /// - /// - /// - /// - /// - /// Ocelot builder to extend. - /// The reference to the same extended object. - [Obsolete("Use AddPolly instead, it will be remove in future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder) - => AddPollyV7(builder, GetDelegatingHandlerV7, DefaultErrorMapping); - - /// - /// Creates default delegating handler based on the type. - /// - /// The downstream route to apply the handler for. - /// The context accessor of the route. - /// The factory of logger. - /// A object, but concrete type is the class. - private static DelegatingHandler GetDelegatingHandlerV7(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) - => new PollyPoliciesDelegatingHandler(route, contextAccessor, loggerFactory); - - #endregion } diff --git a/src/Ocelot.Provider.Polly/Usings.cs b/src/Ocelot.Provider.Polly/Usings.cs index fb5c12dc6..be2de9d09 100644 --- a/src/Ocelot.Provider.Polly/Usings.cs +++ b/src/Ocelot.Provider.Polly/Usings.cs @@ -1,10 +1,8 @@ // Default Microsoft.NET.Sdk namespaces +// Project extra global namespaces +global using Polly; global using System; global using System.Collections.Generic; -global using System.Linq; global using System.Net.Http; global using System.Threading; global using System.Threading.Tasks; - -// Project extra global namespaces -global using Polly; diff --git a/src/Ocelot.Provider.Polly/v7/IPollyQoSProvider.cs b/src/Ocelot.Provider.Polly/v7/IPollyQoSProvider.cs deleted file mode 100644 index 1537439b9..000000000 --- a/src/Ocelot.Provider.Polly/v7/IPollyQoSProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Ocelot.Configuration; - -namespace Ocelot.Provider.Polly.v7; - -[Obsolete("It is obsolete because now, we use IPollyQoSResiliencePipelineProvider with new v8 resilience strategies")] -public interface IPollyQoSProvider - where TResult : class -{ - PollyPolicyWrapper GetPollyPolicyWrapper(DownstreamRoute route); -} diff --git a/src/Ocelot.Provider.Polly/v7/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Polly/v7/OcelotBuilderExtensions.cs deleted file mode 100644 index 6098bbe01..000000000 --- a/src/Ocelot.Provider.Polly/v7/OcelotBuilderExtensions.cs +++ /dev/null @@ -1,120 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Ocelot.Configuration; -using Ocelot.DependencyInjection; -using Ocelot.Errors; -using Ocelot.Errors.QoS; -using Ocelot.Logging; -using Ocelot.Requester; -using Polly.CircuitBreaker; -using Polly.Timeout; - -namespace Ocelot.Provider.Polly.v7; - -public static class OcelotBuilderExtensions -{ - /// - /// Default mapping of Polly s to objects. - /// - public static readonly IDictionary> DefaultErrorMapping = new Dictionary> - { - {typeof(TaskCanceledException), CreateRequestTimedOutError}, - {typeof(TimeoutRejectedException), CreateRequestTimedOutError}, - {typeof(BrokenCircuitException), CreateRequestTimedOutError}, - {typeof(BrokenCircuitException), CreateRequestTimedOutError}, - }; - - private static Error CreateRequestTimedOutError(Exception e) => new RequestTimedOutError(e); - - #region Obsolete extensions will be removed in future version - - /// - /// Adds Polly QoS provider to Ocelot by custom delegate and with custom error mapping. - /// - /// QoS provider to use (by default use ). - /// Ocelot builder to extend. - /// Your customized delegating handler (to manage QoS behavior by yourself). - /// Your customized error mapping. - /// The reference to the same extended object. - [Obsolete("We advise you to use Ocelot.Provider.Polly.AddPolly rather than this one. Polly v7 support will be removed in a future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler, IDictionary> errorMapping) - where TProvider : class, IPollyQoSProvider - { - builder.Services - .AddSingleton(errorMapping) - .AddSingleton, TProvider>() - .AddSingleton(delegatingHandler); - return builder; - } - - /// - /// Adds Polly QoS provider to Ocelot with custom error mapping, but default is used. - /// - /// QoS provider to use (by default use ). - /// Ocelot builder to extend. - /// Your customized error mapping. - /// The reference to the same extended object. - [Obsolete("We advise you to use Ocelot.Provider.Polly.AddPolly rather than this one. Polly v7 support will be removed in a future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, IDictionary> errorMapping) - where TProvider : class, IPollyQoSProvider - => AddPollyV7(builder, GetDelegatingHandlerV7, errorMapping); - - /// - /// Adds Polly QoS provider to Ocelot with custom delegate, but default error mapping is used. - /// - /// QoS provider to use (by default use ). - /// Ocelot builder to extend. - /// Your customized delegating handler (to manage QoS behavior by yourself). - /// The reference to the same extended object. - [Obsolete("We advise you to use Ocelot.Provider.Polly.AddPolly rather than this one. Polly v7 support will be removed in a future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler) - where TProvider : class, IPollyQoSProvider - => AddPollyV7(builder, delegatingHandler, DefaultErrorMapping); - - /// - /// Adds Polly QoS provider to Ocelot by defaults. - /// - /// - /// Defaults: - /// - /// - /// - /// - /// - /// QoS provider to use (by default use ). - /// Ocelot builder to extend. - /// The reference to the same extended object. - [Obsolete("Use AddPolly instead, it will be remove in future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder) - where TProvider : class, IPollyQoSProvider - => AddPollyV7(builder, GetDelegatingHandlerV7, DefaultErrorMapping); - - /// - /// Adds Polly QoS provider to Ocelot by defaults with default QoS provider. - /// - /// - /// Defaults: - /// - /// - /// - /// - /// - /// - /// Ocelot builder to extend. - /// The reference to the same extended object. - [Obsolete("We advise you to use Ocelot.Provider.Polly.AddPolly rather than this one. Polly v7 support will be removed in a future version")] - public static IOcelotBuilder AddPollyV7(this IOcelotBuilder builder) - => AddPollyV7(builder, GetDelegatingHandlerV7, DefaultErrorMapping); - - /// - /// Creates default delegating handler based on the type. - /// - /// The downstream route to apply the handler for. - /// The context accessor of the route. - /// The factory of logger. - /// A object, but concrete type is the class. - private static DelegatingHandler GetDelegatingHandlerV7(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) - => new PollyPoliciesDelegatingHandler(route, contextAccessor, loggerFactory); - - #endregion -} diff --git a/src/Ocelot.Provider.Polly/v7/PollyPoliciesDelegatingHandler.cs b/src/Ocelot.Provider.Polly/v7/PollyPoliciesDelegatingHandler.cs deleted file mode 100644 index 162e12337..000000000 --- a/src/Ocelot.Provider.Polly/v7/PollyPoliciesDelegatingHandler.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Diagnostics; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Ocelot.Configuration; -using Ocelot.Logging; -using Polly.CircuitBreaker; - -namespace Ocelot.Provider.Polly.v7; - -/// Delegates sending to downstream. -/// Outdated V7 design! Use the class. -[Obsolete("Due to new v8 policy definition in Polly 8 (use PollyResiliencePipelineDelegatingHandler)")] -public class PollyPoliciesDelegatingHandler : DelegatingHandler -{ - private readonly DownstreamRoute _route; - private readonly IHttpContextAccessor _contextAccessor; - private readonly IOcelotLogger _logger; - - public PollyPoliciesDelegatingHandler( - DownstreamRoute route, - IHttpContextAccessor contextAccessor, - IOcelotLoggerFactory loggerFactory) - { - _route = route; - _contextAccessor = contextAccessor; - _logger = loggerFactory.CreateLogger(); - } - - private IPollyQoSProvider GetQoSProvider() - { - Debug.Assert(_contextAccessor.HttpContext != null, "_contextAccessor.HttpContext != null"); - return _contextAccessor.HttpContext.RequestServices.GetService>(); - } - - /// - /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation. - /// - /// Downstream request. - /// Token to cancel the task. - /// A object of a result. - /// Exception thrown when a circuit is broken. - /// Exception thrown by and classes. - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - var qoSProvider = GetQoSProvider(); - - // At least one policy (timeout) will be returned - // AsyncPollyPolicy can't be null - // AsyncPollyPolicy constructor will throw if no policy is provided - var policy = qoSProvider.GetPollyPolicyWrapper(_route).AsyncPollyPolicy; - - return await policy.ExecuteAsync(async () => await base.SendAsync(request, cancellationToken)); - } -} diff --git a/src/Ocelot.Provider.Polly/v7/PollyPolicyWrapper.cs b/src/Ocelot.Provider.Polly/v7/PollyPolicyWrapper.cs deleted file mode 100644 index 821743433..000000000 --- a/src/Ocelot.Provider.Polly/v7/PollyPolicyWrapper.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Ocelot.Provider.Polly.v7; - -public class PollyPolicyWrapper - where TResult : class -{ - /// - /// Initializes a new instance of the class. - /// We expect at least one policy to be passed in, default can't be null. - /// - /// The policies with at least a policy. - public PollyPolicyWrapper(params IAsyncPolicy[] policies) - { - var allPolicies = policies.Where(p => p != null).ToArray(); - - AsyncPollyPolicy = allPolicies.Length > 1 ? - Policy.WrapAsync(allPolicies) : - allPolicies[0]; - } - - public IAsyncPolicy AsyncPollyPolicy { get; } -} diff --git a/src/Ocelot.Provider.Polly/v7/PollyQoSProvider.cs b/src/Ocelot.Provider.Polly/v7/PollyQoSProvider.cs deleted file mode 100644 index 51c727e80..000000000 --- a/src/Ocelot.Provider.Polly/v7/PollyQoSProvider.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Ocelot.Configuration; -using Ocelot.Logging; -using Polly.CircuitBreaker; -using Polly.Timeout; - -namespace Ocelot.Provider.Polly.v7; - -/// Legacy QoS provider based on Polly v7. -/// Use the as a new QoS provider based on Polly v8. -[Obsolete("Due to new v8 policy definition in Polly 8 (use PollyQoSResiliencePipelineProvider)")] -public class PollyQoSProvider : PollyQoSProviderBase, IPollyQoSProvider -{ - private readonly Dictionary> _policyWrappers = new(); - - private readonly object _lockObject = new(); - private readonly IOcelotLogger _logger; - - // TODO: This should be configurable and available as global config parameter in ocelot.json - public const int DefaultRequestTimeoutSeconds = 90; - - public PollyQoSProvider(IOcelotLoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - } - - [Obsolete("Due to new v8 policy definition in Polly 8 (use GetResiliencePipeline in PollyQoSResiliencePipelineProvider)")] - public PollyPolicyWrapper GetPollyPolicyWrapper(DownstreamRoute route) - { - lock (_lockObject) - { - var currentRouteName = GetRouteName(route); - if (!_policyWrappers.ContainsKey(currentRouteName)) - { - _policyWrappers.Add(currentRouteName, PollyPolicyWrapperFactory(route)); - } - - return _policyWrappers[currentRouteName]; - } - } - - private PollyPolicyWrapper PollyPolicyWrapperFactory(DownstreamRoute route) - { - AsyncCircuitBreakerPolicy exceptionsAllowedBeforeBreakingPolicy = null; - if (route.QosOptions.ExceptionsAllowedBeforeBreaking > 0) - { - var info = $"Route: {GetRouteName(route)}; Breaker logging in {nameof(PollyQoSProvider)}: "; - - exceptionsAllowedBeforeBreakingPolicy = Policy - .HandleResult(r => ServerErrorCodes.Contains(r.StatusCode)) - .Or() - .Or() - .CircuitBreakerAsync(route.QosOptions.ExceptionsAllowedBeforeBreaking, - durationOfBreak: TimeSpan.FromMilliseconds(route.QosOptions.DurationOfBreak), - onBreak: (ex, breakDelay) => - _logger.LogError(info + $"Breaking the circuit for {breakDelay.TotalMilliseconds} ms!", - ex.Exception), - onReset: () => _logger.LogDebug(info + "Call OK! Closed the circuit again."), - onHalfOpen: () => _logger.LogDebug(info + "Half-open; Next call is a trial.")); - } - - // No default set for polly timeout at the minute. - // Since a user could potentially set timeout value = 0, we need to handle this case. - // TODO throw an exception if the user sets timeout value = 0 or at least return a warning - // TODO the design in DelegatingHandlerHandlerFactory should be reviewed - var timeoutPolicy = Policy - .TimeoutAsync( - TimeSpan.FromMilliseconds(route.QosOptions.TimeoutValue), - TimeoutStrategy.Pessimistic); - - return new PollyPolicyWrapper(exceptionsAllowedBeforeBreakingPolicy, timeoutPolicy); - } -} diff --git a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj index b9f107cfc..5cab0b487 100644 --- a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +++ b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj @@ -75,7 +75,7 @@ - + diff --git a/test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs b/test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs deleted file mode 100644 index f968fb5ee..000000000 --- a/test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs +++ /dev/null @@ -1,255 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Logging; -using Ocelot.Provider.Polly; -using Polly; -using Polly.Wrap; -using System.Reflection; -using Ocelot.Provider.Polly.v7; - -namespace Ocelot.UnitTests.Polly; - -public class PollyPoliciesDelegatingHandlerTests -{ - private readonly Mock> _pollyQoSProviderMock; - private readonly Mock _contextAccessorMock; - private readonly PollyPoliciesDelegatingHandler _sut; - - public PollyPoliciesDelegatingHandlerTests() - { - _pollyQoSProviderMock = new Mock>(); - - var loggerFactoryMock = new Mock(); - var loggerMock = new Mock(); - _contextAccessorMock = new Mock(); - - loggerFactoryMock.Setup(x => x.CreateLogger()) - .Returns(loggerMock.Object); - loggerMock.Setup(x => x.LogError(It.IsAny(), It.IsAny())); - - _sut = new PollyPoliciesDelegatingHandler(DownstreamRouteFactory(), _contextAccessorMock.Object, loggerFactoryMock.Object); - } - - [Fact] - public async void SendAsync_OnePolicy_NoWrapping() - { - // Arrange - var fakeResponse = new HttpResponseMessage(HttpStatusCode.NoContent); - fakeResponse.Headers.Add("X-Xunit", nameof(SendAsync_OnePolicy_NoWrapping)); - - MethodInfo method = null; - var onePolicy = new Mock>(); - onePolicy.Setup(x => x.ExecuteAsync(It.IsAny>>())) - .Callback((IInvocation x) => method = x.Method) - .ReturnsAsync(fakeResponse); - - _pollyQoSProviderMock.Setup(x => x.GetPollyPolicyWrapper(It.IsAny())) - .Returns(new PollyPolicyWrapper(onePolicy.Object)); - - var httpContext = new Mock(); - httpContext.Setup(x => x.RequestServices.GetService(typeof(IPollyQoSProvider))) - .Returns(_pollyQoSProviderMock.Object); - - _contextAccessorMock.Setup(x => x.HttpContext).Returns(httpContext.Object); - - // Act - var actual = await InvokeAsync("SendAsync"); - - // Assert - ShouldHaveXunitHeaderWithNoContent(actual, nameof(SendAsync_OnePolicy_NoWrapping)); - method.DeclaringType.Name.ShouldBe("IAsyncPolicy`1"); - method.DeclaringType.ShouldNotBeOfType(); - } - - [Fact] - public async void SendAsync_TwoPolicies_HaveWrapped() - { - // Arrange - var fakeResponse = new HttpResponseMessage(HttpStatusCode.NoContent); - fakeResponse.Headers.Add("X-Xunit", nameof(SendAsync_TwoPolicies_HaveWrapped)); - - var policy1 = new FakeAsyncPolicy("Policy1", fakeResponse); - var policy2 = new FakeAsyncPolicy("Policy2", fakeResponse) - { - IsLast = true, - }; - - _pollyQoSProviderMock.Setup(x => x.GetPollyPolicyWrapper(It.IsAny())) - .Returns(new PollyPolicyWrapper(policy1, policy2)); - - var httpContext = new Mock(); - httpContext.Setup(x => x.RequestServices.GetService(typeof(IPollyQoSProvider))) - .Returns(_pollyQoSProviderMock.Object); - - _contextAccessorMock.Setup(x => x.HttpContext).Returns(httpContext.Object); - - // Act - var actual = await InvokeAsync("SendAsync"); - - // Assert - ShouldHaveXunitHeaderWithNoContent(actual, nameof(SendAsync_TwoPolicies_HaveWrapped)); - ShouldBeWrappedBy(policy1, typeof(AsyncPolicyWrap).FullName); - ShouldBeWrappedBy(policy2, typeof(AsyncPolicy).FullName); - } - - private static DownstreamRoute DownstreamRouteFactory() - { - var options = new QoSOptionsBuilder() - .WithTimeoutValue(100) - .WithExceptionsAllowedBeforeBreaking(2) - .WithDurationOfBreak(200) - .Build(); - - var upstreamPath = new UpstreamPathTemplateBuilder() - .WithTemplate("/") - .WithContainsQueryString(false) - .WithPriority(1) - .WithOriginalValue("/").Build(); - - var route = new DownstreamRouteBuilder() - .WithQosOptions(options) - .WithUpstreamPathTemplate(upstreamPath).Build(); - - return route; - } - - private static void ShouldHaveXunitHeaderWithNoContent(HttpResponseMessage actual, string headerName) - { - actual.ShouldNotBeNull(); - actual.StatusCode.ShouldBe(HttpStatusCode.NoContent); - actual.Headers.GetValues("X-Xunit").ShouldContain(headerName); - } - - private static void ShouldBeWrappedBy(FakeAsyncPolicy policy, string wrapperName) - { - policy.Called.ShouldBeTrue(); - policy.Times.ShouldBe(1); - policy.Method.ShouldNotBeNull(); - policy.Target.ShouldNotBeNull(); - policy.Method.DeclaringType?.DeclaringType.ShouldNotBeNull(); - policy.Method.DeclaringType.DeclaringType.FullName.ShouldContain(wrapperName); - policy.Target.ToString().ShouldContain(wrapperName); - } - - private async Task InvokeAsync(string methodName) - { - var m = typeof(PollyPoliciesDelegatingHandler).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); - var task = (Task)m.Invoke(_sut, new object[] { new HttpRequestMessage(), CancellationToken.None }); - var actual = await task; - return actual; - } - - internal class FakeAsyncPolicy : AsyncPolicy, IAsyncPolicy - where TResult : class - { - public object Result { get; private set; } - public string Name { get; private set; } - - public int Times { get; protected set; } - public bool Called => Times > 0; - public MethodInfo Method { get; protected set; } - public object Target { get; protected set; } - - public bool IsLast { get; set; } - - public FakeAsyncPolicy(string name, object result) - { - Name = name; - Result = result; - } - - protected override async Task ImplementationAsync(Func> action, Context context, CancellationToken cancellationToken, - bool continueOnCapturedContext) - { - Times++; - Method = action.Method; - Target = action.Target; - - if (IsLast) - { - var r = Result?.GetType() == typeof(TResult) - ? (TResult)Result - : Activator.CreateInstance(); - return r; - } - - var result = await action(context, cancellationToken); - return result; - } - - public new IAsyncPolicy WithPolicyKey(string policyKey) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action, IDictionary contextData) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action, Context context) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action, IDictionary contextData, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action, Context context, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action, IDictionary contextData, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action, Context context) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action, IDictionary contextData) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action, IDictionary contextData, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action, Context context, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action, IDictionary contextData, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task ExecuteAsync(Func> action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action, IDictionary contextData) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action, Context context) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action, IDictionary contextData, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action, Context context, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action, IDictionary contextData, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task ExecuteAndCaptureAsync(Func action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action, IDictionary contextData) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action, Context context) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action, IDictionary contextData, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action, Context context, CancellationToken cancellationToken) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action, IDictionary contextData, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - - public Task> ExecuteAndCaptureAsync(Func> action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); - } -} diff --git a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs deleted file mode 100644 index bc56a1a1f..000000000 --- a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs +++ /dev/null @@ -1,242 +0,0 @@ -using Ocelot.Configuration.Builder; -using Ocelot.Logging; -using Ocelot.Provider.Polly.v7; -using Polly; -using Polly.CircuitBreaker; -using Polly.Timeout; -using Polly.Wrap; - -namespace Ocelot.UnitTests.Polly; - -public class PollyQoSProviderTests -{ - [Fact] - public void Should_build() - { - var options = new QoSOptionsBuilder() - .WithTimeoutValue(100) - .WithExceptionsAllowedBeforeBreaking(1) - .WithDurationOfBreak(200) - .Build(); - var route = new DownstreamRouteBuilder().WithQosOptions(options) - .Build(); - var factory = new Mock(); - var pollyQoSProvider = new PollyQoSProvider(factory.Object); - var policy = pollyQoSProvider.GetPollyPolicyWrapper(route).ShouldNotBeNull() - .AsyncPollyPolicy.ShouldNotBeNull(); - policy.ShouldNotBeNull(); - } - - [Fact] - public void Should_build_and_wrap_contains_two_policies() - { - var pollyQosProvider = PollyQoSProviderFactory(); - var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider); - var policy = pollyPolicyWrapper.AsyncPollyPolicy; - - if (policy is AsyncPolicyWrap policyWrap) - { - policyWrap.ShouldNotBeNull(); - var policies = policyWrap.GetPolicies().ToList(); - - policies.Count.ShouldBe(2); - var circuitBreakerFound = false; - var timeoutPolicyFound = false; - - foreach(var currentPolicy in policies) - { - currentPolicy.ShouldNotBeNull(); - var convertedPolicy = (IAsyncPolicy)currentPolicy; - - switch (convertedPolicy) - { - case AsyncCircuitBreakerPolicy: - circuitBreakerFound = true; - continue; - case AsyncTimeoutPolicy: - timeoutPolicyFound = true; - break; - } - } - - Assert.True(circuitBreakerFound); - Assert.True(timeoutPolicyFound); - - return; - } - - Assert.Fail("policy is not AsyncPolicyWrap"); - } - - [Fact] - public void Should_build_and_contains_one_policy_when_with_exceptions_allowed_before_breaking_is_zero() - { - var pollyQosProvider = PollyQoSProviderFactory(); - var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider, true); - var policy = pollyPolicyWrapper.AsyncPollyPolicy; - - if (policy is AsyncTimeoutPolicy convertedPolicy) - { - convertedPolicy.ShouldNotBeNull(); - return; - } - - Assert.Fail("policy is not AsyncTimeoutPolicy"); - } - - [Fact] - public void Should_return_same_circuit_breaker_for_given_route() - { - var pollyQosProvider = PollyQoSProviderFactory(); - var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider); - var pollyPolicyWrapper2 = PolicyWrapperFactory("/", pollyQosProvider); - pollyPolicyWrapper.ShouldBe(pollyPolicyWrapper2); - } - - [Fact] - public void Should_return_different_circuit_breaker_for_two_different_routes() - { - var pollyQosProvider = PollyQoSProviderFactory(); - var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider); - var pollyPolicyWrapper2 = PolicyWrapperFactory("/test", pollyQosProvider); - pollyPolicyWrapper.ShouldNotBe(pollyPolicyWrapper2); - } - - [Theory] - [InlineData(HttpStatusCode.InternalServerError)] - [InlineData(HttpStatusCode.NotImplemented)] - [InlineData(HttpStatusCode.BadGateway)] - [InlineData(HttpStatusCode.ServiceUnavailable)] - [InlineData(HttpStatusCode.GatewayTimeout)] - [InlineData(HttpStatusCode.HttpVersionNotSupported)] - [InlineData(HttpStatusCode.VariantAlsoNegotiates)] - [InlineData(HttpStatusCode.InsufficientStorage)] - [InlineData(HttpStatusCode.LoopDetected)] - public async Task Should_throw_broken_circuit_exception_after_two_exceptions(HttpStatusCode errorCode) - { - var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); - - var response = new HttpResponseMessage(errorCode); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await Assert.ThrowsAsync>(async () => - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - } - - [Fact] - public async Task Should_not_throw_broken_circuit_exception_if_status_code_ok() - { - var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); - - var response = new HttpResponseMessage(HttpStatusCode.OK); - Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); - Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); - Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); - } - - [Fact(Skip = "TODO", DisplayName = "TODO " + nameof(Should_throw_and_before_delay_should_not_allow_requests))] - [Trait("TODO", "Fix after the release")] - public async Task Should_throw_and_before_delay_should_not_allow_requests() - { - var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); - - var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await Assert.ThrowsAsync>(async () => - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - - await Task.Delay(200); - - await Assert.ThrowsAsync>(async () => - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - } - - [Fact] - public async Task Should_throw_but_after_delay_should_allow_one_more_internal_server_error() - { - var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); - - var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await Assert.ThrowsAsync>(async () => - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - - await Task.Delay(600); - - Assert.Equal(HttpStatusCode.InternalServerError, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); - } - - [Fact] - public async Task Should_throw_but_after_delay_should_allow_one_more_internal_server_error_and_throw() - { - var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); - - var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await Assert.ThrowsAsync>(async () => - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - - await Task.Delay(600); - - Assert.Equal(HttpStatusCode.InternalServerError, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); - await Assert.ThrowsAsync>(async () => - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - } - - [Fact] - public async Task Should_throw_but_after_delay_should_allow_one_more_ok_request_and_put_counter_back_to_zero() - { - var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); - - var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await Assert.ThrowsAsync>(async () => - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - - await Task.Delay(600); - - var response2 = new HttpResponseMessage(HttpStatusCode.OK); - Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response2))).StatusCode); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); - await Assert.ThrowsAsync>(async () => - await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - } - - private static PollyQoSProvider PollyQoSProviderFactory() - { - var factory = new Mock(); - factory.Setup(x => x.CreateLogger()) - .Returns(new Mock().Object); - return new PollyQoSProvider(factory.Object); - } - - private static PollyPolicyWrapper PolicyWrapperFactory(string routeTemplate, PollyQoSProvider pollyQoSProvider, bool inactiveExceptionsAllowedBeforeBreaking = false) - { - var options = new QoSOptionsBuilder() - .WithTimeoutValue(5000) - .WithExceptionsAllowedBeforeBreaking(inactiveExceptionsAllowedBeforeBreaking ? 0 : 2) - .WithDurationOfBreak(300) - .Build(); - - var upstreamPath = new UpstreamPathTemplateBuilder() - .WithTemplate(routeTemplate) - .WithContainsQueryString(false) - .WithPriority(1) - .WithOriginalValue(routeTemplate).Build(); - - var route = new DownstreamRouteBuilder() - .WithQosOptions(options) - .WithUpstreamPathTemplate(upstreamPath).Build(); - - var pollyPolicyWrapper = pollyQoSProvider.GetPollyPolicyWrapper(route).ShouldNotBeNull(); - pollyPolicyWrapper.ShouldNotBeNull(); - pollyPolicyWrapper.AsyncPollyPolicy.ShouldNotBeNull(); - - return pollyPolicyWrapper; - } -} From a034e8c1e3fc23a086ad10000c85615b9696a43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9chir=20BEN=20AMEUR?= <32399944+bbenameur@users.noreply.github.com> Date: Mon, 3 Jun 2024 19:48:43 +0200 Subject: [PATCH 13/15] #2002 Early removal of a replaced placeholder parameter in `DownstreamUrlCreatorMiddleware` (#2003) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * issues/2002 : Regression at DownstreamUrlCreatorMiddleware * issues/2002:: add some unit tests and acceptance test * issues/2002: fix test * Apply suggestions from code review Co-authored-by: Raynald Messié * issues/2002: fix build * CS8936 Feature 'collection expressions' is not available in C# 10.0. Please use language version 12.0 or greater. * Code review by @ggnaegi https://github.com/ThreeMammals/Ocelot/pull/2003#pullrequestreview-1975203966 * Original version from develop * The fix by @bbenameur with improved version by @ggnaegi * Don't order parameters, add to the end * AAA pattern in unit tests * Double-check tests * Remove BDDfy * Inherit from `Steps` * DRY in acceptance tests * Tests for #2002 user scenario * Update docs --------- Co-authored-by: Raynald Messié Co-authored-by: Raman Maksimchuk --- docs/features/routing.rst | 104 +- .../UrlMatcher/PlaceholderNameAndValue.cs | 21 +- .../DownstreamUrlCreatorMiddleware.cs | 18 +- .../Routing/RoutingWithQueryStringTests.cs | 663 +++++----- .../DownstreamUrlCreatorMiddlewareTests.cs | 1156 +++++++++-------- 5 files changed, 1000 insertions(+), 962 deletions(-) diff --git a/docs/features/routing.rst b/docs/features/routing.rst index e55b33e5c..825f5d9e1 100644 --- a/docs/features/routing.rst +++ b/docs/features/routing.rst @@ -210,7 +210,7 @@ Priority -------- You can define the order you want your Routes to match the Upstream ``HttpRequest`` by including a **Priority** property in **ocelot.json**. -See `issue 270 `_ for reference. +See issue `270`_ for reference. .. code-block:: json @@ -247,7 +247,7 @@ Query String Placeholders In addition to URL path :ref:`routing-placeholders` Ocelot is able to forward query string parameters with their processing in the form of ``{something}``. Also, the query parameter placeholder needs to be present in both the **DownstreamPathTemplate** and **UpstreamPathTemplate** properties. -Placeholder replacement works bi-directionally between path and query strings, with some `restrictions <#restrictions-on-use>`_ on usage. +Placeholder replacement works bi-directionally between path and query strings, with some restrictions on usage (see :ref:`routing-merging-of-query-parameters`). Path to Query String direction ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -261,7 +261,9 @@ Ocelot allows you to specify a query string as part of the **DownstreamPathTempl "DownstreamPathTemplate": "/api/subscriptions/{subscription}/updates?unitId={unit}", } -In this example Ocelot will use the value from the ``{unit}`` placeholder in the upstream path template and add it to the downstream request as a query string parameter called ``unitId``! Make sure you name the placeholder differently due to `restrictions <#restrictions-on-use>`_ on usage. +In this example Ocelot will use the value from the ``{unit}`` placeholder in the upstream path template and add it to the downstream request as a query string parameter called ``unitId``! + + Note! Make sure you name the placeholder differently due to :ref:`routing-merging-of-query-parameters`. Query String to Path direction @@ -279,7 +281,10 @@ Ocelot will also allow you to put query string parameters in the **UpstreamPathT In this example Ocelot will only match requests that have a matching URL path and the query string starts with ``unitId=something``. You can have other queries after this but you must start with the matching parameter. Also Ocelot will swap the ``{uid}`` parameter from the query string and use it in the downstream request path. -Note, the best practice is giving different placeholder name than the name of query parameter due to `restrictions <#restrictions-on-use>`_ on usage. + + Note, the best practice is giving different placeholder name than the name of query parameter due to :ref:`routing-merging-of-query-parameters`. + +.. _routing-catch-all-query-string: Catch All Query String ^^^^^^^^^^^^^^^^^^^^^^ @@ -295,54 +300,68 @@ The placeholder ``{everything}`` name does not matter, any name will work. } This entire query string routing feature is very useful in cases where the query string should not be transformed but rather routed without any changes, -such as OData filters and etc (see issue `1174 `_). +such as OData filters and etc (see issue `1174`_). -**Note**, the ``{everything}`` placeholder can be empty while catching all query strings, because this is a part of the :ref:`routing-empty-placeholders` feature! [#f1]_ -Thus, upstream paths ``/contracts?`` and ``/contracts`` are routed to downstream path ``/apipath/contracts``, which has no query string at all. + **Note**, the ``{everything}`` placeholder can be empty while catching all query strings, because this is a part of the :ref:`routing-empty-placeholders` feature! [#f1]_ + Thus, upstream paths ``/contracts?`` and ``/contracts`` are routed to downstream path ``/apipath/contracts``, which has no query string at all. -Restrictions on use -^^^^^^^^^^^^^^^^^^^ +.. _routing-merging-of-query-parameters: + +Merging of Query Parameters +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Query string parameters are unsorted and merged to create the final downstream URL. +This process is essential as the ``DownstreamUrlCreatorMiddleware`` requires control over placeholder replacement and the merging of duplicate parameters. +A parameter that appears first in the **UpstreamPathTemplate** may occupy a different position in the final downstream URL. +Furthermore, if the **DownstreamPathTemplate** includes query parameters at the beginning, their position in the **UpstreamPathTemplate** will be indeterminate unless explicitly defined. + +In a typical scenario, the merging algorithm constructs the final downstream URL query string by: -The query string parameters are ordered and merged to produce the final downstream URL. -This is necessary because the ``DownstreamUrlCreatorMiddleware`` needs to have some control when replacing placeholders and merging duplicate parameters. -So, even if your parameter is presented as the first parameter in the upstream, then in the final downstream URL the said query parameter will have a different position. -But this doesn't seem to break anything in the downstream API. +1. Taking the initially defined query parameters in **DownstreamPathTemplate** and placing them at the beginning, with any necessary placeholder replacements. +2. Adding all parameters from the :ref:`routing-catch-all-query-string`, represented by the placeholder ``{everything}``, into the second position (following the explicitly defined parameters from **step 1**). +3. Appending any remaining replaced placeholder values as parameter values to the end of the string, if they are present. -Because of parameters merging, special ASP.NET API `model binding `_ -for arrays is not supported if you use array items representation like ``selectedCourses=1050&selectedCourses=2000``. -This query string will be merged as ``selectedCourses=1050`` in downstream URL. So, array data will be lost! -Make sure upstream clients generate correct query string for array models like ``selectedCourses[0]=1050&selectedCourses[1]=2000``. -To understand array model bidings, see `Bind arrays and string values from headers and query strings `_ docs. +Array parameters in ASP.NET API's model binding +""""""""""""""""""""""""""""""""""""""""""""""" -**Warning!** Query string placeholders have naming restrictions due to ``DownstreamUrlCreatorMiddleware`` implementations. -On the other hand, it gives you the flexibility to control whether the parameter is present in the final downstream URL. -Here are two user scenarios. +Due to parameters merging, ASP.NET API's special `model binding`_ for arrays **is not supported** having the array item representation format of ``selectedCourses=1050&selectedCourses=2000``. +This query string will be merged into ``selectedCourses=1050`` in the downstream URL, resulting in the loss of array data. +It is crucial for upstream clients to generate the correct query string for array models, such as ``selectedCourses[0]=1050&selectedCourses[1]=2000``. +For a comprehensive understanding of array model bindings, refer to the documentation: `Bind arrays and string values from headers and query strings`_. -* User wants to save the parameter after replacing the placeholder (see issue `473 `_). - To do this you need to use the following template definition: +Control over parameter existence +"""""""""""""""""""""""""""""""" - .. code-block:: json +Be aware that query string placeholders are subject to naming restrictions due to the ``DownstreamUrlCreatorMiddleware``'s merging algorithm implementation. +However, this also provides the flexibility to manage the presence of parameters in the final downstream URL by their names. + +Consider the following 2 development scenarios :htm:`→` + +1. A developer wishes **to preserve a parameter** after substituting a placeholder (refer to issue `473`_). + This requires the use of the template definition below: + + .. code-block:: json - { - "UpstreamPathTemplate": "/path/{serverId}/{action}", - "DownstreamPathTemplate": "/path2/{action}?server={serverId}" - } + { + "UpstreamPathTemplate": "/path/{serverId}/{action}", + "DownstreamPathTemplate": "/path2/{action}?server={serverId}" + } - So, ``{serverId}`` placeholder and ``server`` parameter **names are different**! - Finally, the ``server`` parameter is kept. + | Here, the ``{serverId}`` placeholder and the ``server`` parameter **names differ**! Ultimately, the ``server`` parameter is retained. + | It is important to note that due to the case-sensitive comparison of names, the ``server`` parameter will not be preserved with the ``{server}`` placeholder. However, using the ``{Server}`` placeholder is acceptable for retaining the parameter. -* User wants to remove old parameter after replacing placeholder (see issue `952 `_). - To do this you need to use the same names: +2. The developer intends **to remove an outdated parameter** after substituting a placeholder (refer to issue `952`_). + For this action, you must use identical names having the case-sensitive comparison: - .. code-block:: json + .. code-block:: json - { - "UpstreamPathTemplate": "/users?userId={userId}", - "DownstreamPathTemplate": "/persons?personId={userId}" - } + { + "UpstreamPathTemplate": "/users?userId={userId}", + "DownstreamPathTemplate": "/persons?personId={userId}" + } - So, both ``{userId}`` placeholder and ``userId`` parameter **names are the same**! - Finally, the ``userId`` parameter is removed. + | Thus, the ``{userId}`` placeholder and the ``userId`` parameter **have identical names**! Subsequently, the ``userId`` parameter is eliminated. + | Be aware that due to the case sensitive nature of the comparison, if the ``{userid}`` placeholder is used, the ``userId`` parameter will not be removed! .. _routing-security-options: @@ -391,3 +410,10 @@ See the :ref:`sd-dynamic-routing` docs if this sounds interesting to you. .. [#f3] ":ref:`routing-upstream-headers`" feature was proposed in `issue 360 `_, and released in version `24.0 `_. .. [#f4] ":ref:`routing-security-options`" feature was requested as part of `issue 628 `_ (of `12.0.1 `_ version), then redesigned and improved by `issue 1400 `_, and published in version `20.0 `_ docs. .. [#f5] ":ref:`routing-dynamic`" feature was requested as part of `issue 340 `_. Complete reference: :ref:`sd-dynamic-routing`. + +.. _model binding: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-8.0#collections +.. _Bind arrays and string values from headers and query strings: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/parameter-binding?view=aspnetcore-8.0#bind-arrays-and-string-values-from-headers-and-query-strings +.. _270: https://github.com/ThreeMammals/Ocelot/issues/270 +.. _473: https://github.com/ThreeMammals/Ocelot/issues/473 +.. _952: https://github.com/ThreeMammals/Ocelot/issues/952 +.. _1174: https://github.com/ThreeMammals/Ocelot/issues/1174 diff --git a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/PlaceholderNameAndValue.cs b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/PlaceholderNameAndValue.cs index ccaf099cc..7f9163fa3 100644 --- a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/PlaceholderNameAndValue.cs +++ b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/PlaceholderNameAndValue.cs @@ -1,14 +1,15 @@ -namespace Ocelot.DownstreamRouteFinder.UrlMatcher +namespace Ocelot.DownstreamRouteFinder.UrlMatcher; + +public class PlaceholderNameAndValue { - public class PlaceholderNameAndValue + public PlaceholderNameAndValue(string name, string value) { - public PlaceholderNameAndValue(string name, string value) - { - Name = name; - Value = value; - } - - public string Name { get; } - public string Value { get; } + Name = name; + Value = value; } + + public string Name { get; } + public string Value { get; } + + public override string ToString() => $"[{{{Name}}}={Value}]"; } diff --git a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs index a92f6a470..dc7fb312e 100644 --- a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs +++ b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs @@ -99,6 +99,13 @@ private static string MergeQueryStringsWithoutDuplicateValues(string queryString var queries = HttpUtility.ParseQueryString(queryString); var newQueries = HttpUtility.ParseQueryString(newQueryString); + // Remove old replaced query parameters + var placeholderNames = new HashSet(placeholders.Select(p => p.Name.Trim(OpeningBrace, ClosingBrace))); + foreach (var queryKey in queries.AllKeys.Where(placeholderNames.Contains)) + { + queries.Remove(queryKey); + } + var parameters = newQueries.AllKeys .Where(key => !string.IsNullOrEmpty(key)) .ToDictionary(key => key, key => newQueries[key]); @@ -107,16 +114,11 @@ private static string MergeQueryStringsWithoutDuplicateValues(string queryString .Where(key => !string.IsNullOrEmpty(key) && !parameters.ContainsKey(key)) .All(key => parameters.TryAdd(key, queries[key])); - // Remove old replaced query parameters - foreach (var placeholder in placeholders) - { - parameters.Remove(placeholder.Name.Trim(OpeningBrace, ClosingBrace)); - } - - var orderedParams = parameters.OrderBy(x => x.Key).Select(x => $"{x.Key}={x.Value}"); - return QuestionMark + string.Join(Ampersand, orderedParams); + return QuestionMark + string.Join(Ampersand, parameters.Select(MapQueryParameter)); } + private static string MapQueryParameter(KeyValuePair pair) => $"{pair.Key}={pair.Value}"; + private static void RemoveQueryStringParametersThatHaveBeenUsedInTemplate(DownstreamRequest downstreamRequest, List templatePlaceholderNameAndValues) { foreach (var nAndV in templatePlaceholderNameAndValues) diff --git a/test/Ocelot.AcceptanceTests/Routing/RoutingWithQueryStringTests.cs b/test/Ocelot.AcceptanceTests/Routing/RoutingWithQueryStringTests.cs index ad4a16ae5..ed0f8d424 100644 --- a/test/Ocelot.AcceptanceTests/Routing/RoutingWithQueryStringTests.cs +++ b/test/Ocelot.AcceptanceTests/Routing/RoutingWithQueryStringTests.cs @@ -1,379 +1,322 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; -namespace Ocelot.AcceptanceTests.Routing +namespace Ocelot.AcceptanceTests.Routing; + +public sealed class RoutingWithQueryStringTests : Steps, IDisposable { - public class RoutingWithQueryStringTests : IDisposable + private readonly ServiceHandler _serviceHandler; + + public RoutingWithQueryStringTests() { - private readonly Steps _steps; - private readonly ServiceHandler _serviceHandler; + _serviceHandler = new ServiceHandler(); + } - public RoutingWithQueryStringTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } + public override void Dispose() + { + _serviceHandler?.Dispose(); + base.Dispose(); + } - [Fact] - public void Should_return_response_200_with_query_string_template() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); + [Fact] + public void Should_return_response_200_with_query_string_template() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", + "/api/units/{subscriptionId}/{unitId}/updates"); + var configuration = GivenConfiguration(route); - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}", "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/{unitId}/updates")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Theory(DisplayName = "1182: " + nameof(Should_return_200_with_query_string_template_different_keys))] - [InlineData("")] - [InlineData("&x=xxx")] - public void Should_return_200_with_query_string_template_different_keys(string additionalParams) - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); + this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}", "Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/{unitId}/updates")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unit}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/units/{subscriptionId}/updates?unit={unit}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}{additionalParams}", "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/updates?unit={unitId}{additionalParams}")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Theory(DisplayName = "1174: " + nameof(Should_return_200_and_forward_query_parameters_without_duplicates))] - [InlineData("projectNumber=45&startDate=2019-12-12&endDate=2019-12-12", "endDate=2019-12-12&projectNumber=45&startDate=2019-12-12")] - [InlineData("$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z", "$filter=ProjectNumber%20eq%2045%20and%20DateOfSale%20ge%202020-03-01T00:00:00z%20and%20DateOfSale%20le%202020-03-15T00:00:00z")] - public void Should_return_200_and_forward_query_parameters_without_duplicates(string everythingelse, string expectedOrdered) - { - var port = PortFinder.GetRandomPort(); - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/contracts?{everythingelse}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() { Host = "localhost", Port = port }, - }, - UpstreamPathTemplate = "/contracts?{everythingelse}", - UpstreamHttpMethod = new() { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/contracts", $"?{expectedOrdered}", "Hello from @sunilk3")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/contracts?{everythingelse}")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from @sunilk3")) - .BDDfy(); - } - - [Fact] - public void Should_return_response_200_with_odata_query_string() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); + [Theory] + [Trait("Bug", "952")] + [InlineData("")] + [InlineData("&x=xxx")] + public void Should_return_200_with_query_string_template_different_keys(string additionalParams) + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/api/subscriptions/{subscriptionId}/updates?unitId={unit}", + "/api/units/{subscriptionId}/updates?unit={unit}"); + var configuration = GivenConfiguration(route); - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/{everything}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/{everything}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/odata/customers", "?$filter=Name%20eq%20'Sam'", "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/odata/customers?$filter=Name eq 'Sam' ")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void Should_return_response_200_with_query_string_upstream_template() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); + this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}{additionalParams}", "Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/updates?unit={unitId}{additionalParams}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void Should_return_response_404_with_query_string_upstream_template_no_query_string() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); + [Fact] + [Trait("Bug", "952")] + public void Should_map_query_parameters_with_different_names() + { + const string userId = "webley"; + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/persons?personId={userId}", + "/users?userId={userId}"); + var configuration = GivenConfiguration(route); - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - [Fact] - public void Should_return_response_404_with_query_string_upstream_template_different_query_string() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/persons", $"?personId={userId}", "Hello from @webley")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/users?userId={userId}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from @webley")) + .BDDfy(); + } - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?test=1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - [Fact] - public void Should_return_response_200_with_query_string_upstream_template_multiple_params() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = PortFinder.GetRandomPort(); + [Fact] + [Trait("Bug", "952")] + public void Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ() + { + const string uid = "webley"; + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/persons?personId={uid}", + "/users?userId={uid}"); + var configuration = GivenConfiguration(route); - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", "?productId=1", "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}&productId=1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - /// - /// To reproduce 1288: query string should contain the placeholder name and value. - /// - [Fact(DisplayName = "1288: " + nameof(Should_copy_query_string_to_downstream_path))] - public void Should_copy_query_string_to_downstream_path() + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/persons", $"?personId={uid}&userId={uid}", "Hello from @webley")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/users?userId={uid}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from @webley")) + .BDDfy(); + } + + [Fact] + [Trait("Bug", "952")] + public void Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ_case_sensitive() + { + const string userid = "webley"; + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/persons?personId={userid}", + "/users?userId={userid}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/persons", $"?personId={userid}&userId={userid}", "Hello from @webley")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/users?userId={userid}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from @webley")) + .BDDfy(); + } + + [Theory] + [Trait("Bug", "1174")] + [InlineData("projectNumber=45&startDate=2019-12-12&endDate=2019-12-12", "projectNumber=45&startDate=2019-12-12&endDate=2019-12-12")] + [InlineData("$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z", "$filter=ProjectNumber%20eq%2045%20and%20DateOfSale%20ge%202020-03-01T00:00:00z%20and%20DateOfSale%20le%202020-03-15T00:00:00z")] + public void Should_return_200_and_forward_query_parameters_without_duplicates(string everythingelse, string expected) + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/api/contracts?{everythingelse}", + "/contracts?{everythingelse}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/contracts", $"?{expected}", "Hello from @sunilk3")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/contracts?{everythingelse}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from @sunilk3")) + .BDDfy(); + } + + [Fact] + public void Should_return_response_200_with_odata_query_string() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, "/{everything}", "/{everything}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/odata/customers", "?$filter=Name%20eq%20'Sam'", "Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/odata/customers?$filter=Name eq 'Sam' ")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void Should_return_response_200_with_query_string_upstream_template() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/api/units/{subscriptionId}/{unitId}/updates", + "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void Should_return_response_404_with_query_string_upstream_template_no_query_string() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/api/units/{subscriptionId}/{unitId}/updates", + "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_return_response_404_with_query_string_upstream_template_different_query_string() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/api/units/{subscriptionId}/{unitId}/updates", + "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?test=1")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void Should_return_response_200_with_query_string_upstream_template_multiple_params() + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/api/units/{subscriptionId}/{unitId}/updates", + "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/units/{subscriptionId}/{unitId}/updates", "?productId=1", "Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}&productId=1")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + [Trait("Bug", "2002")] + public void Should_map_when_query_parameters_has_same_names_with_placeholder() + { + const string username = "bbenameur"; + const string groupName = "Paris"; + const string roleid = "123456"; + const string everything = "something=9874565"; + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + "/account/{username}/groups/{groupName}/roles?roleId={roleid}&{everything}", + "/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, + $"/account/{username}/groups/{groupName}/roles", + $"?roleId={roleid}&{everything}", + "Hello from Béchir")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Béchir")) + .BDDfy(); + } + + /// + /// To reproduce 1288: query string should contain the placeholder name and value. + /// + [Fact] + [Trait("Bug", "1288")] + public void Should_copy_query_string_to_downstream_path() + { + var idName = "id"; + var idValue = "3"; + var queryName = idName + "1"; + var queryValue = "2" + idValue + "12"; + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, + $"/cpx/t1/{{{idName}}}", + $"/safe/{{{idName}}}"); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/cpx/t1/{idValue}", $"?{queryName}={queryValue}", "Hello from Laura")) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway($"/safe/{idValue}?{queryName}={queryValue}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + private static FileRoute GivenRoute(int port, string downstream, string upstream) => new() + { + DownstreamPathTemplate = downstream, + DownstreamScheme = Uri.UriSchemeHttp, + DownstreamHostAndPorts = new() { - var idName = "id"; - var idValue = "3"; - var queryName = idName + "1"; - var queryValue = "2" + idValue + "12"; - var port = PortFinder.GetRandomPort(); + new("localhost", port), + }, + UpstreamPathTemplate = upstream, + UpstreamHttpMethod = new() { HttpMethods.Get }, + }; - var configuration = new FileConfiguration - { - Routes = new List - { - new FileRoute - { - DownstreamPathTemplate = $"/cpx/t1/{{{idName}}}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() { Host = "localhost", Port = port }, - }, - UpstreamPathTemplate = $"/safe/{{{idName}}}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/cpx/t1/{idValue}", $"?{queryName}={queryValue}", "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/safe/{idValue}?{queryName}={queryValue}")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string queryString, string responseBody) + private void GivenThereIsAServiceRunningOn(int port, string basePath, string queryString, string responseBody) + { + var baseUrl = DownstreamUrl(port); + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + if (context.Request.PathBase.Value != basePath || context.Request.QueryString.Value != queryString) { - if (context.Request.PathBase.Value != basePath || context.Request.QueryString.Value != queryString) - { - context.Response.StatusCode = StatusCodes.Status500InternalServerError; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = StatusCodes.Status200OK; - await context.Response.WriteAsync(responseBody); - } - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - GC.SuppressFinalize(this); - } + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = StatusCodes.Status200OK; + await context.Response.WriteAsync(responseBody); + } + }); } } diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs index 6f2d5333c..8a05f8b13 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs @@ -12,589 +12,655 @@ using Ocelot.Responses; using Ocelot.Values; -namespace Ocelot.UnitTests.DownstreamUrlCreator +namespace Ocelot.UnitTests.DownstreamUrlCreator; + +public sealed class DownstreamUrlCreatorMiddlewareTests : UnitTest { - public class DownstreamUrlCreatorMiddlewareTests : UnitTest + // TODO: Convert to integration tests to use real IDownstreamPathPlaceholderReplacer service (no mocking). There are a lot of failings + // private readonly IDownstreamPathPlaceholderReplacer _downstreamUrlTemplateVariableReplacer; + private readonly Mock _downstreamUrlTemplateVariableReplacer; + + private OkResponse _downstreamPath; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private DownstreamUrlCreatorMiddleware _middleware; + private readonly RequestDelegate _next; + private readonly HttpRequestMessage _request; + private readonly HttpContext _httpContext; + private readonly Mock _repo; + + public DownstreamUrlCreatorMiddlewareTests() { - private readonly Mock _downstreamUrlTemplateVariableReplacer; - private OkResponse _downstreamPath; - private readonly Mock _loggerFactory; - private readonly Mock _logger; - private DownstreamUrlCreatorMiddleware _middleware; - private readonly RequestDelegate _next; - private readonly HttpRequestMessage _request; - private readonly HttpContext _httpContext; - private readonly Mock _repo; - - public DownstreamUrlCreatorMiddlewareTests() - { - _repo = new Mock(); - _httpContext = new DefaultHttpContext(); - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _downstreamUrlTemplateVariableReplacer = new Mock(); - _request = new HttpRequestMessage(HttpMethod.Get, "https://my.url/abc/?q=123"); - _next = context => Task.CompletedTask; - } + _repo = new Mock(); + _httpContext = new DefaultHttpContext(); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _downstreamUrlTemplateVariableReplacer = new Mock(); + _request = new HttpRequestMessage(HttpMethod.Get, "https://my.url/abc/?q=123"); + _next = context => Task.CompletedTask; + } - [Fact] - public void Should_replace_scheme_and_path() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("any old string") + [Fact] + public void Should_replace_scheme_and_path() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithDownstreamScheme("https") + .Build(); + var config = new ServiceProviderConfigurationBuilder() + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List(), + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) - .WithDownstreamScheme("https") - .Build(); - - var config = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List(), - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://my.url/abc?q=123")) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("/api/products/1")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("https://my.url:80/api/products/1?q=123")) - .And(x => ThenTheQueryStringIs("?q=123")) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs("http://my.url/abc?q=123"); + GivenTheServiceProviderConfigIs(config); + GivenTheUrlReplacerWillReturn("/api/products/1"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("https://my.url:80/api/products/1?q=123"); + ThenTheQueryStringIs("?q=123"); + } - [Fact] - public void Should_replace_query_string() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") + [Fact] + public void Should_replace_query_string() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithDownstreamScheme("https") + .Build(); + var config = new ServiceProviderConfigurationBuilder() + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{subscriptionId}", "1"), + new("{unitId}", "2"), + }, + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) - .WithDownstreamScheme("https") - .Build(); - - var config = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new("{subscriptionId}", "1"), - new("{unitId}", "2"), - }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2")) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("api/units/1/2/updates")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates")) - .And(x => ThenTheQueryStringIs(string.Empty)) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2"); + GivenTheServiceProviderConfigIs(config); + GivenTheUrlReplacerWillReturn("api/units/1/2/updates"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates"); + ThenTheQueryStringIs(string.Empty); + } - [Fact] - public void Should_replace_query_string_but_leave_non_placeholder_queries() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") + [Fact] + public void Should_replace_query_string_but_leave_non_placeholder_queries() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithDownstreamScheme("https") + .Build(); + var config = new ServiceProviderConfigurationBuilder() + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{subscriptionId}", "1"), + new("{unitId}", "2"), + }, + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) - .WithDownstreamScheme("https") - .Build(); - - var config = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new("{subscriptionId}", "1"), - new("{unitId}", "2"), - }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2&productId=2")) // unitId is the first - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("api/units/1/2/updates")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates?productId=2")) - .And(x => ThenTheQueryStringIs("?productId=2")) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2&productId=2"); // unitId is the first + GivenTheServiceProviderConfigIs(config); + GivenTheUrlReplacerWillReturn("api/units/1/2/updates"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates?productId=2"); + ThenTheQueryStringIs("?productId=2"); + } - [Fact] - public void Should_replace_query_string_but_leave_non_placeholder_queries_2() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") + [Fact] + public void Should_replace_query_string_but_leave_non_placeholder_queries_2() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithDownstreamScheme("https") + .Build(); + var config = new ServiceProviderConfigurationBuilder() + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{subscriptionId}", "1"), + new("{unitId}", "2"), + }, + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) - .WithDownstreamScheme("https") - .Build(); - - var config = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new PlaceholderNameAndValue("{subscriptionId}", "1"), - new PlaceholderNameAndValue("{unitId}", "2"), - }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?productId=2&unitId=2")) // unitId is the second - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("api/units/1/2/updates")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates?productId=2")) - .And(x => ThenTheQueryStringIs("?productId=2")) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?productId=2&unitId=2"); // unitId is the second + GivenTheServiceProviderConfigIs(config); + GivenTheUrlReplacerWillReturn("api/units/1/2/updates"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates?productId=2"); + ThenTheQueryStringIs("?productId=2"); + } - [Fact] - public void Should_replace_query_string_exact_match() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates/{unitIdIty}") + [Fact] + public void Should_replace_query_string_exact_match() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates/{unitIdIty}") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithDownstreamScheme("https") + .Build(); + var config = new ServiceProviderConfigurationBuilder() + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{subscriptionId}", "1"), + new("{unitId}", "2"), + new("{unitIdIty}", "3"), + }, + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) - .WithDownstreamScheme("https") - .Build(); - - var config = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new("{subscriptionId}", "1"), - new("{unitId}", "2"), - new("{unitIdIty}", "3"), - }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2?unitIdIty=3")) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("api/units/1/2/updates/3")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates/3")) - .And(x => ThenTheQueryStringIs(string.Empty)) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2?unitIdIty=3"); + GivenTheServiceProviderConfigIs(config); + GivenTheUrlReplacerWillReturn("api/units/1/2/updates/3"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates/3"); + ThenTheQueryStringIs(string.Empty); + } - [Fact] - public void Should_not_create_service_fabric_url() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("any old string") + [Fact] + public void Should_not_create_service_fabric_url() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithDownstreamScheme("https") + .Build(); + var config = new ServiceProviderConfigurationBuilder() + .WithType("ServiceFabric") + .WithHost("localhost") + .WithPort(19081) + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List(), + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) - .WithDownstreamScheme("https") - .Build(); - - var config = new ServiceProviderConfigurationBuilder() - .WithType("ServiceFabric") - .WithHost("localhost") - .WithPort(19081) - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List(), - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://my.url/abc?q=123")) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("/api/products/1")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("https://my.url:80/api/products/1?q=123")) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs("http://my.url/abc?q=123"); + GivenTheServiceProviderConfigIs(config); + GivenTheUrlReplacerWillReturn("/api/products/1"); - [Fact] - public void Should_create_service_fabric_url() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamScheme("http") - .WithServiceName("Ocelot/OcelotApp") - .WithUseServiceDiscovery(true) - .Build(); - - var downstreamRouteHolder = new DownstreamRouteHolder( - new List(), - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .Build()); - - var config = new ServiceProviderConfigurationBuilder() - .WithType("ServiceFabric") - .WithHost("localhost") - .WithPort(19081) - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRouteHolder)) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:19081")) - .And(x => x.GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1")) - .BDDfy(); - } + // Act + WhenICallTheMiddleware(); - [Fact] - public void Should_create_service_fabric_url_with_query_string_for_stateless_service() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamScheme("http") - .WithServiceName("Ocelot/OcelotApp") - .WithUseServiceDiscovery(true) - .Build(); - - var downstreamRouteHolder = new DownstreamRouteHolder( - new List(), - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .Build()); - - var config = new ServiceProviderConfigurationBuilder() - .WithType("ServiceFabric") - .WithHost("localhost") - .WithPort(19081) - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRouteHolder)) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:19081?Tom=test&laura=1")) - .And(x => x.GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1?Tom=test&laura=1")) - .BDDfy(); - } + // Assert + ThenTheDownstreamRequestUriIs("https://my.url:80/api/products/1?q=123"); + } - [Fact] - public void Should_create_service_fabric_url_with_query_string_for_stateful_service() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamScheme("http") - .WithServiceName("Ocelot/OcelotApp") - .WithUseServiceDiscovery(true) - .Build(); - - var downstreamRouteHolder = new DownstreamRouteHolder( - new List(), - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .Build()); - - var config = new ServiceProviderConfigurationBuilder() - .WithType("ServiceFabric") - .WithHost("localhost") - .WithPort(19081) - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRouteHolder)) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:19081?PartitionKind=test&PartitionKey=1")) - .And(x => x.GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1?PartitionKind=test&PartitionKey=1")) - .BDDfy(); - } + [Fact] + public void Should_create_service_fabric_url() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamScheme("http") + .WithServiceName("Ocelot/OcelotApp") + .WithUseServiceDiscovery(true) + .Build(); + var downstreamRouteHolder = new DownstreamRouteHolder( + new List(), + new RouteBuilder().WithDownstreamRoute(downstreamRoute).Build()); + var config = new ServiceProviderConfigurationBuilder() + .WithType("ServiceFabric") + .WithHost("localhost") + .WithPort(19081) + .Build(); + GivenTheDownStreamRouteIs(downstreamRouteHolder); + GivenTheServiceProviderConfigIs(config); + GivenTheDownstreamRequestUriIs("http://localhost:19081"); + GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1"); + } - [Fact] - public void Should_create_service_fabric_url_with_version_from_upstream_path_template() - { - var downstreamRoute = new DownstreamRouteHolder( - new List(), - new RouteBuilder().WithDownstreamRoute( - new DownstreamRouteBuilder() - .WithDownstreamScheme("http") - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("/products").Build()) - .WithUseServiceDiscovery(true) - .Build() - ).Build()); - - var config = new ServiceProviderConfigurationBuilder() - .WithType("ServiceFabric") - .WithHost("localhost") - .WithPort(19081) - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:19081?PartitionKind=test&PartitionKey=1")) - .And(x => x.GivenTheUrlReplacerWillReturnSequence("/products", "Service_1.0/Api")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:19081/Service_1.0/Api/products?PartitionKind=test&PartitionKey=1")) - .BDDfy(); - } + [Fact] + public void Should_create_service_fabric_url_with_query_string_for_stateless_service() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamScheme("http") + .WithServiceName("Ocelot/OcelotApp") + .WithUseServiceDiscovery(true) + .Build(); + var downstreamRouteHolder = new DownstreamRouteHolder( + new List(), + new RouteBuilder().WithDownstreamRoute(downstreamRoute).Build()); + var config = new ServiceProviderConfigurationBuilder() + .WithType("ServiceFabric") + .WithHost("localhost") + .WithPort(19081) + .Build(); + GivenTheDownStreamRouteIs(downstreamRouteHolder); + GivenTheServiceProviderConfigIs(config); + GivenTheDownstreamRequestUriIs("http://localhost:19081?Tom=test&laura=1"); + GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1?Tom=test&laura=1"); + } - [Fact(DisplayName = "473: " + nameof(Should_not_remove_additional_query_parameter_when_placeholder_and_parameter_names_are_different))] - public void Should_not_remove_additional_query_parameter_when_placeholder_and_parameter_names_are_different() - { - var methods = new List { "Post", "Get" }; - var downstreamRoute = new DownstreamRouteBuilder() - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() - .WithOriginalValue("/uc/Authorized/{servak}/{action}").Build()) - .WithDownstreamPathTemplate("/Authorized/{action}?server={servak}") - .WithUpstreamHttpMethod(methods) - .WithDownstreamScheme(Uri.UriSchemeHttp) - .Build(); - - var config = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new("{action}", "1"), - new("{servak}", "2"), - }, - new RouteBuilder().WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(methods) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/uc/Authorized/2/1/refresh?refreshToken=123456789")) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("/Authorized/1?server=2")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:5000/Authorized/1?refreshToken=123456789&server=2")) - .And(x => ThenTheQueryStringIs("?refreshToken=123456789&server=2")) - .BDDfy(); - } + [Fact] + public void Should_create_service_fabric_url_with_query_string_for_stateful_service() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamScheme("http") + .WithServiceName("Ocelot/OcelotApp") + .WithUseServiceDiscovery(true) + .Build(); + var downstreamRouteHolder = new DownstreamRouteHolder( + new List(), + new RouteBuilder().WithDownstreamRoute(downstreamRoute).Build()); + var config = new ServiceProviderConfigurationBuilder() + .WithType("ServiceFabric") + .WithHost("localhost") + .WithPort(19081) + .Build(); + GivenTheDownStreamRouteIs(downstreamRouteHolder); + GivenTheServiceProviderConfigIs(config); + GivenTheDownstreamRequestUriIs("http://localhost:19081?PartitionKind=test&PartitionKey=1"); + GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1?PartitionKind=test&PartitionKey=1"); + } - [Fact] - public void Should_not_replace_by_empty_scheme() - { - var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamScheme(string.Empty) - .WithServiceName("Ocelot/OcelotApp") - .WithUseServiceDiscovery(true) - .Build(); - - var downstreamRouteHolder = new DownstreamRouteHolder( - new List(), - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .Build()); - - var config = new ServiceProviderConfigurationBuilder() - .WithType("ServiceFabric") - .WithHost("localhost") - .WithPort(19081) - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRouteHolder)) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheDownstreamRequestUriIs("https://localhost:19081?PartitionKind=test&PartitionKey=1")) - .And(x => x.GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("https://localhost:19081/Ocelot/OcelotApp/api/products/1?PartitionKind=test&PartitionKey=1")) - .BDDfy(); - } + [Fact] + public void Should_create_service_fabric_url_with_version_from_upstream_path_template() + { + // Arrange + var route = new DownstreamRouteBuilder() + .WithDownstreamScheme("http") + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("/products").Build()) + .WithUseServiceDiscovery(true) + .Build(); + var routeHolder = new DownstreamRouteHolder( + new List(), + new RouteBuilder().WithDownstreamRoute(route).Build()); + var config = new ServiceProviderConfigurationBuilder() + .WithType("ServiceFabric") + .WithHost("localhost") + .WithPort(19081) + .Build(); + GivenTheDownStreamRouteIs(routeHolder); + GivenTheServiceProviderConfigIs(config); + GivenTheDownstreamRequestUriIs("http://localhost:19081?PartitionKind=test&PartitionKey=1"); + GivenTheUrlReplacerWillReturnSequence("/products", "Service_1.0/Api"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("http://localhost:19081/Service_1.0/Api/products?PartitionKind=test&PartitionKey=1"); + } - [Fact(DisplayName = "952: " + nameof(Should_map_query_parameters_with_different_names))] - public void Should_map_query_parameters_with_different_names() - { - var methods = new List { "Post", "Get" }; - var downstreamRoute = new DownstreamRouteBuilder() - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() - .WithOriginalValue("/users?userId={userId}").Build()) - .WithDownstreamPathTemplate("/persons?personId={userId}") + [Fact] + [Trait("Bug", "473")] + public void Should_not_remove_additional_query_parameter_when_placeholder_and_parameter_names_are_different() + { + // Arrange + var methods = new List { "Post", "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/uc/Authorized/{servak}/{action}").Build()) + .WithDownstreamPathTemplate("/Authorized/{action}?server={servak}") + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{action}", "1"), + new("{servak}", "2"), + }, + new RouteBuilder().WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(methods) - .WithDownstreamScheme(Uri.UriSchemeHttp) - .Build(); - var config = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new("{userId}", "webley"), - }, - new RouteBuilder().WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(methods) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs($"http://localhost:5000/users?userId=webley")) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("/persons?personId=webley")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs($"http://localhost:5000/persons?personId=webley")) - .And(x => ThenTheQueryStringIs($"?personId=webley")) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs("http://localhost:5000/uc/Authorized/2/1/refresh?refreshToken=123456789"); + GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); + GivenTheUrlReplacerWillReturn("/Authorized/1?server=2"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("http://localhost:5000/Authorized/1?server=2&refreshToken=123456789"); + ThenTheQueryStringIs("?server=2&refreshToken=123456789"); + } - [Fact(DisplayName = "952: " + nameof(Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ))] - public void Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ() - { - var methods = new List { "Post", "Get" }; - var downstreamRoute = new DownstreamRouteBuilder() - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() - .WithOriginalValue("/users?userId={uid}").Build()) - .WithDownstreamPathTemplate("/persons?personId={uid}") + [Fact] + public void Should_not_replace_by_empty_scheme() + { + // Arrange + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamScheme(string.Empty) + .WithServiceName("Ocelot/OcelotApp") + .WithUseServiceDiscovery(true) + .Build(); + var downstreamRouteHolder = new DownstreamRouteHolder( + new List(), + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) + .Build()); + var config = new ServiceProviderConfigurationBuilder() + .WithType("ServiceFabric") + .WithHost("localhost") + .WithPort(19081) + .Build(); + GivenTheDownStreamRouteIs(downstreamRouteHolder); + GivenTheServiceProviderConfigIs(config); + GivenTheDownstreamRequestUriIs("https://localhost:19081?PartitionKind=test&PartitionKey=1"); + GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("https://localhost:19081/Ocelot/OcelotApp/api/products/1?PartitionKind=test&PartitionKey=1"); + } + + [Fact] + [Trait("Bug", "952")] + public void Should_map_query_parameters_with_different_names() + { + // Arrange + var methods = new List { "Post", "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/users?userId={userId}").Build()) + .WithDownstreamPathTemplate("/persons?personId={userId}") + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{userId}", "webley"), + }, + new RouteBuilder().WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(methods) - .WithDownstreamScheme(Uri.UriSchemeHttp) - .Build(); - var config = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new("{uid}", "webley"), - }, - new RouteBuilder().WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(methods) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs($"http://localhost:5000/users?userId=webley")) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn("/persons?personId=webley")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs($"http://localhost:5000/persons?personId=webley&userId=webley")) - .And(x => ThenTheQueryStringIs($"?personId=webley&userId=webley")) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs($"http://localhost:5000/users?userId=webley"); + GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); + GivenTheUrlReplacerWillReturn("/persons?personId=webley"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs($"http://localhost:5000/persons?personId=webley"); + ThenTheQueryStringIs($"?personId=webley"); + } - [Theory(DisplayName = "1174: " + nameof(Should_forward_query_parameters_without_duplicates))] - [InlineData("projectNumber=45&startDate=2019-12-12&endDate=2019-12-12", "endDate=2019-12-12&projectNumber=45&startDate=2019-12-12")] - [InlineData("$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z", "$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z")] - public void Should_forward_query_parameters_without_duplicates(string everythingelse, string expectedOrdered) - { - var methods = new List { "Get" }; - var downstreamRoute = new DownstreamRouteBuilder() - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() - .WithOriginalValue("/contracts?{everythingelse}").Build()) - .WithDownstreamPathTemplate("/api/contracts?{everythingelse}") + [Fact] + [Trait("Bug", "952")] + public void Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ() + { + // Arrange + var methods = new List { "Post", "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/users?userId={uid}").Build()) + .WithDownstreamPathTemplate("/persons?personId={uid}") + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{uid}", "webley"), + }, + new RouteBuilder().WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(methods) - .WithDownstreamScheme(Uri.UriSchemeHttp) - .Build(); - var config = new ServiceProviderConfigurationBuilder().Build(); - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new("{everythingelse}", everythingelse), - }, - new RouteBuilder().WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(methods) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs($"http://localhost:5000//contracts?{everythingelse}")) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn($"/api/contracts?{everythingelse}")) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs($"http://localhost:5000/api/contracts?{expectedOrdered}")) - .And(x => ThenTheQueryStringIs($"?{expectedOrdered}")) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs($"http://localhost:5000/users?userId=webley"); + GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); + GivenTheUrlReplacerWillReturn("/persons?personId=webley"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs($"http://localhost:5000/persons?personId=webley&userId=webley"); + ThenTheQueryStringIs($"?personId=webley&userId=webley"); + } - [Theory] - [Trait("Bug", "748")] - [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/123", "{url}", "123", "/api/v1/test/123", "")] - [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/123?query=1", "{url}", "123", "/api/v1/test/123?query=1", "?query=1")] - [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/?query=1", "{url}", "", "/api/v1/test/?query=1", "?query=1")] - [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1?query=1", "{url}", "", "/api/v1/test?query=1", "?query=1")] - [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/", "{url}", "", "/api/v1/test/", "")] - [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1", "{url}", "", "/api/v1/test", "")] - public void should_fix_issue_748(string upstreamTemplate, string downstreamTemplate, string requestURL, string placeholderName, string placeholderValue, string downstreamURI, string queryString) - { - var methods = new List { "Get" }; - var downstreamRoute = new DownstreamRouteBuilder() - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() - .WithOriginalValue(upstreamTemplate).Build()) - .WithDownstreamPathTemplate(downstreamTemplate) + [Theory] + [Trait("Bug", "1174")] + [InlineData("projectNumber=45&startDate=2019-12-12&endDate=2019-12-12")] + [InlineData("$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z")] + public void Should_forward_query_parameters_without_duplicates(string everythingelse) + { + // Arrange + var methods = new List { "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/contracts?{everythingelse}").Build()) + .WithDownstreamPathTemplate("/api/contracts?{everythingelse}") + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{everythingelse}", everythingelse), + }, + new RouteBuilder().WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(methods) - .WithDownstreamScheme(Uri.UriSchemeHttp) - .Build(); - - var config = new ServiceProviderConfigurationBuilder() - .Build(); - - this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRouteHolder( - new List - { - new(placeholderName, placeholderValue), - new("{version}", "v1"), - }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000" + requestURL)) - .And(x => GivenTheServiceProviderConfigIs(config)) - .And(x => x.GivenTheUrlReplacerWillReturn(downstreamURI)) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:5000" + downstreamURI)) - .And(x => ThenTheQueryStringIs(queryString)) - .BDDfy(); - } + .Build() + )); + GivenTheDownstreamRequestUriIs($"http://localhost:5000//contracts?{everythingelse}"); + GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); + GivenTheUrlReplacerWillReturn($"/api/contracts?{everythingelse}"); + + // Act + WhenICallTheMiddleware(); + + // Assert + var query = everythingelse; + ThenTheDownstreamRequestUriIs($"http://localhost:5000/api/contracts?{query}"); + ThenTheQueryStringIs($"?{query}"); + } - private void GivenTheServiceProviderConfigIs(ServiceProviderConfiguration config) - { - var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null, null); - _httpContext.Items.SetIInternalConfiguration(configuration); - } + [Theory] + [Trait("Bug", "748")] + [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/123", "{url}", "123", "/api/v1/test/123", "")] + [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/123?query=1", "{url}", "123", "/api/v1/test/123?query=1", "?query=1")] + [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/?query=1", "{url}", "", "/api/v1/test/?query=1", "?query=1")] + [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1?query=1", "{url}", "", "/api/v1/test?query=1", "?query=1")] + [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/", "{url}", "", "/api/v1/test/", "")] + [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1", "{url}", "", "/api/v1/test", "")] + public void Should_fix_issue_748(string upstreamTemplate, string downstreamTemplate, string requestURL, string placeholderName, string placeholderValue, string downstreamURI, string queryString) + { + // Arrange + var methods = new List { "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue(upstreamTemplate).Build()) + .WithDownstreamPathTemplate(downstreamTemplate) + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new(placeholderName, placeholderValue), + new("{version}", "v1"), + }, + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build() + )); + GivenTheDownstreamRequestUriIs("http://localhost:5000" + requestURL); + GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); + GivenTheUrlReplacerWillReturn(downstreamURI); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs("http://localhost:5000" + downstreamURI); + ThenTheQueryStringIs(queryString); + } - private void WhenICallTheMiddleware() - { - _middleware = new DownstreamUrlCreatorMiddleware(_next, _loggerFactory.Object, _downstreamUrlTemplateVariableReplacer.Object); - _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); - } + [Fact] + [Trait("Bug", "2002")] + public void Should_map_when_query_parameters_has_same_names_with_placeholder() + { + // Arrange + const string username = "bbenameur"; + const string groupName = "Paris"; + const string roleid = "123456"; + const string everything = "something=9874565"; + var withGetMethod = new List { "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}") + .Build()) + .WithDownstreamPathTemplate("/account/{username}/groups/{groupName}/roles?roleId={roleid}&{everything}") + .WithUpstreamHttpMethod(withGetMethod) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + GivenTheDownStreamRouteIs(new DownstreamRouteHolder( + new List + { + new("{username}", username), + new("{groupName}", groupName), + new("{roleid}", roleid), + new("{everything}", everything), + }, + new RouteBuilder().WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(withGetMethod) + .Build() + )); + GivenTheDownstreamRequestUriIs($"http://localhost:5000/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}"); + GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); + GivenTheUrlReplacerWillReturn($"/account/{username}/groups/{groupName}/roles?roleId={roleid}&{everything}"); + + // Act + WhenICallTheMiddleware(); + + // Assert + ThenTheDownstreamRequestUriIs($"http://localhost:5000/account/{username}/groups/{groupName}/roles?roleId={roleid}&{everything}"); + ThenTheQueryStringIs($"?roleId={roleid}&{everything}"); + } - private void GivenTheDownStreamRouteIs(DownstreamRouteHolder downstreamRoute) - { - _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + private void GivenTheServiceProviderConfigIs(ServiceProviderConfiguration config) + { + var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null, null); + _httpContext.Items.SetIInternalConfiguration(configuration); + } - _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); - } + private void WhenICallTheMiddleware() + { + _middleware = new DownstreamUrlCreatorMiddleware(_next, _loggerFactory.Object, _downstreamUrlTemplateVariableReplacer.Object); + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); + } - private void GivenTheDownstreamRequestUriIs(string uri) - { - _request.RequestUri = new Uri(uri); - _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(_request)); - } + private void GivenTheDownStreamRouteIs(DownstreamRouteHolder downstreamRoute) + { + _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); + } - private void GivenTheUrlReplacerWillReturnSequence(params string[] paths) - { - var setup = _downstreamUrlTemplateVariableReplacer - .SetupSequence(x => x.Replace(It.IsAny(), It.IsAny>())); - foreach (var path in paths) - { - var response = new OkResponse(new DownstreamPath(path)); - setup.Returns(response); - } - } + private void GivenTheDownstreamRequestUriIs(string uri) + { + _request.RequestUri = new Uri(uri); + _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(_request)); + } - private void GivenTheUrlReplacerWillReturn(string path) + private void GivenTheUrlReplacerWillReturnSequence(params string[] paths) + { + var setup = _downstreamUrlTemplateVariableReplacer + .SetupSequence(x => x.Replace(It.IsAny(), It.IsAny>())); + foreach (var path in paths) { - _downstreamPath = new OkResponse(new DownstreamPath(path)); - _downstreamUrlTemplateVariableReplacer - .Setup(x => x.Replace(It.IsAny(), It.IsAny>())) - .Returns(_downstreamPath); + var response = new OkResponse(new DownstreamPath(path)); + setup.Returns(response); } + } - private void ThenTheDownstreamRequestUriIs(string expectedUri) - { - _httpContext.Items.DownstreamRequest().ToHttpRequestMessage().RequestUri.OriginalString.ShouldBe(expectedUri); - } + private void GivenTheUrlReplacerWillReturn(string path) + { + _downstreamPath = new OkResponse(new DownstreamPath(path)); + _downstreamUrlTemplateVariableReplacer + .Setup(x => x.Replace(It.IsAny(), It.IsAny>())) + .Returns(_downstreamPath); + } - private void ThenTheQueryStringIs(string queryString) - { - _httpContext.Items.DownstreamRequest().Query.ShouldBe(queryString); - } + private void ThenTheDownstreamRequestUriIs(string expectedUri) + { + _httpContext.Items.DownstreamRequest().ToHttpRequestMessage().RequestUri.OriginalString.ShouldBe(expectedUri); + } + + private void ThenTheQueryStringIs(string queryString) + { + _httpContext.Items.DownstreamRequest().Query.ShouldBe(queryString); } } From 8c1c61e13cd4fe5628e84651eaba9e83f4fed716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raynald=20Messi=C3=A9?= Date: Thu, 6 Jun 2024 10:41:58 +0200 Subject: [PATCH 14/15] #2085 Enforce Polly v8 Circuit Breaker as the primary strategy (#2086) * #2085 Circuit Breaker behavior polly v7 vs Polly v8 * Code review by @raman-m * Refactor the provider * AAA pattern in unit tests * Convert to file-scoped namespace * Refactor acceptance tests * Code review by @ggnaegi * Rename protected method to `ConfigureStrategies` * Refactor shortcut of the feature * `DurationOfBreak` constraints * Fitness testing of new changes in the provider * Update docs --------- Co-authored-by: Raman Maksimchuk --- docs/features/qualityofservice.rst | 63 ++- .../PollyQoSProviderBase.cs | 25 - .../PollyQoSResiliencePipelineProvider.cs | 114 +++-- src/Ocelot/Configuration/QoSOptions.cs | 16 +- test/Ocelot.AcceptanceTests/PollyQoSTests.cs | 444 ++++++++++-------- ...PollyQoSResiliencePipelineProviderTests.cs | 289 +++++++++--- 6 files changed, 594 insertions(+), 357 deletions(-) delete mode 100644 src/Ocelot.Provider.Polly/PollyQoSProviderBase.cs diff --git a/docs/features/qualityofservice.rst b/docs/features/qualityofservice.rst index 2046ff802..4fa4ecf9c 100644 --- a/docs/features/qualityofservice.rst +++ b/docs/features/qualityofservice.rst @@ -3,22 +3,29 @@ Quality of Service Label: `QoS `_ -Ocelot supports one QoS capability at the current time. You can set on a per Route basis if you want to use a circuit breaker when making requests to a downstream service. -This uses an awesome .NET library called `Polly`_, check them out `in official repository `_. +Ocelot currently supports a single **QoS** capability. +It allows you to configure, on a per-route basis, the use of a circuit breaker when making requests to downstream services. +This feature leverages a superb .NET library known as `Polly`_. For more information, visit their `official repository `_. -The first thing you need to do if you want to use the :doc:`../features/administration` API is bring in the relevant NuGet `package `_: +Installation +------------ + +To use the :doc:`../features/administration` API, the first step is to import the relevant NuGet `package `_: .. code-block:: powershell Install-Package Ocelot.Provider.Polly -Then in your ``ConfigureServices`` method to add `Polly`_ services we must call the ``AddPolly()`` extension of the ``OcelotBuilder`` being returned by ``AddOcelot()`` [#f1]_ like below: +Next, within your ``ConfigureServices`` method, to incorporate `Polly`_ services, invoke the ``AddPolly()`` extension on the ``OcelotBuilder`` returned by ``AddOcelot()`` [#f1]_ as shown below: .. code-block:: csharp services.AddOcelot() .AddPolly(); +Configuration +------------- + Then add the following section to a Route configuration: .. code-block:: json @@ -29,11 +36,36 @@ Then add the following section to a Route configuration: "TimeoutValue": 5000 } -- You must set a number equal or greater than ``2`` against **ExceptionsAllowedBeforeBreaking** for this rule to be implemented. [#f2]_ -- **DurationOfBreak** means the circuit breaker will stay open for 1 second after it is tripped. -- **TimeoutValue** means if a request takes more than 5 seconds, it will automatically be timed out. +- You must set a number equal or greater than ``2`` against ``ExceptionsAllowedBeforeBreaking`` for this rule to be implemented. [#f2]_ +- ``DurationOfBreak`` means the circuit breaker will stay open for 1 second after it is tripped. +- ``TimeoutValue`` means if a request takes more than 5 seconds, it will automatically be timed out. + +Circuit Breaker strategy +------------------------ -You can set the **TimeoutValue** in isolation of the **ExceptionsAllowedBeforeBreaking** and **DurationOfBreak** options: +The options ``ExceptionsAllowedBeforeBreaking`` and ``DurationOfBreak`` can be configured independently of ``TimeoutValue``: + +.. code-block:: json + + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 1000 + } + +Alternatively, you may omit ``DurationOfBreak`` to default to the implicit 5 seconds as per Polly `documentation `_: + +.. code-block:: json + + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3 + } + +This setup activates only the `Circuit breaker `_ strategy. + +Timeout strategy +---------------- + +The ``TimeoutValue`` can be configured independently from the ``ExceptionsAllowedBeforeBreaking`` and ``DurationOfBreak`` settings: .. code-block:: json @@ -41,14 +73,23 @@ You can set the **TimeoutValue** in isolation of the **ExceptionsAllowedBeforeBr "TimeoutValue": 5000 } -There is no point setting the other two in isolation as they affect each other! +This setup activates only the `Timeout `_ strategy. Notes ----- -1. If you do not add a QoS section, QoS will not be used, however Ocelot will default to a **90** seconds timeout on all downstream requests. If someone needs this to be configurable, open an issue. [#f2]_ +1. Without a QoS section, QoS will not be utilized, and Ocelot will impose a default timeout of **90** seconds for all downstream requests. + To request configurability, please open an issue. [#f2]_ + +2. `Polly`_ V7 syntax is no longer supported as of version `23.2`_. [#f3]_ + +3. For `Polly`_ version 8 and above, the following constraints on values are specified in `the documentation `_: + + * The ``ExceptionsAllowedBeforeBreaking`` value must be **2** or higher. + * The ``DurationOfBreak`` value must exceed **500** milliseconds, defaulting to **5000** milliseconds (5 seconds) if unspecified or if the value is **500** milliseconds or less. + * The ``TimeoutValue`` must be over **10** milliseconds. -2. `Polly`_ V7 syntax no longer supported. In version `23.2`_ [#f3]_ with `Polly`_ version 8+, the ``ExceptionsAllowedBeforeBreaking`` value must be equal to or greater than **2**! + Consult the `Resilience strategies `_ documentation for a detailed understanding of each option. .. _qos-extensibility: diff --git a/src/Ocelot.Provider.Polly/PollyQoSProviderBase.cs b/src/Ocelot.Provider.Polly/PollyQoSProviderBase.cs deleted file mode 100644 index 24760c800..000000000 --- a/src/Ocelot.Provider.Polly/PollyQoSProviderBase.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Ocelot.Configuration; -using System.Net; - -namespace Ocelot.Provider.Polly; - -public abstract class PollyQoSProviderBase -{ - protected static readonly HashSet ServerErrorCodes = new() - { - HttpStatusCode.InternalServerError, - HttpStatusCode.NotImplemented, - HttpStatusCode.BadGateway, - HttpStatusCode.ServiceUnavailable, - HttpStatusCode.GatewayTimeout, - HttpStatusCode.HttpVersionNotSupported, - HttpStatusCode.VariantAlsoNegotiates, - HttpStatusCode.InsufficientStorage, - HttpStatusCode.LoopDetected, - }; - - protected static string GetRouteName(DownstreamRoute route) - => string.IsNullOrWhiteSpace(route.ServiceName) - ? route.UpstreamPathTemplate?.Template ?? route.DownstreamPathTemplate?.Value ?? string.Empty - : route.ServiceName; -} diff --git a/src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs b/src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs index be2df16b3..a0000c62d 100644 --- a/src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs +++ b/src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs @@ -4,24 +4,46 @@ using Polly.CircuitBreaker; using Polly.Registry; using Polly.Timeout; +using System.Net; namespace Ocelot.Provider.Polly; /// /// Default provider for Polly V8 pipelines. /// -public class PollyQoSResiliencePipelineProvider : PollyQoSProviderBase, IPollyQoSResiliencePipelineProvider +public class PollyQoSResiliencePipelineProvider : IPollyQoSResiliencePipelineProvider { - private readonly ResiliencePipelineRegistry _resiliencePipelineRegistry; + private readonly ResiliencePipelineRegistry _registry; private readonly IOcelotLogger _logger; - public PollyQoSResiliencePipelineProvider(IOcelotLoggerFactory loggerFactory, - ResiliencePipelineRegistry resiliencePipelineRegistry) + public PollyQoSResiliencePipelineProvider( + IOcelotLoggerFactory loggerFactory, + ResiliencePipelineRegistry registry) { - _resiliencePipelineRegistry = resiliencePipelineRegistry; _logger = loggerFactory.CreateLogger(); + _registry = registry; } + protected static readonly HashSet DefaultServerErrorCodes = new() + { + HttpStatusCode.InternalServerError, + HttpStatusCode.NotImplemented, + HttpStatusCode.BadGateway, + HttpStatusCode.ServiceUnavailable, + HttpStatusCode.GatewayTimeout, + HttpStatusCode.HttpVersionNotSupported, + HttpStatusCode.VariantAlsoNegotiates, + HttpStatusCode.InsufficientStorage, + HttpStatusCode.LoopDetected, + }; + + protected virtual HashSet ServerErrorCodes { get; } = DefaultServerErrorCodes; + + protected virtual string GetRouteName(DownstreamRoute route) + => string.IsNullOrWhiteSpace(route.ServiceName) + ? route.UpstreamPathTemplate?.Template ?? route.DownstreamPathTemplate?.Value ?? string.Empty + : route.ServiceName; + /// /// Gets Polly V8 resilience pipeline (applies QoS feature) for the route. /// @@ -29,56 +51,86 @@ public PollyQoSResiliencePipelineProvider(IOcelotLoggerFactory loggerFactory, /// A object where T is . public ResiliencePipeline GetResiliencePipeline(DownstreamRoute route) { - var options = route.QosOptions; - - // Check if we need pipeline at all before calling GetOrAddPipeline - if (options is null || - (options.ExceptionsAllowedBeforeBreaking == 0 && options.TimeoutValue is int.MaxValue)) + var options = route?.QosOptions; + if (options is null || !options.UseQos) { - return null; // shortcut > no qos + return ResiliencePipeline.Empty; // shortcut -> No QoS } var currentRouteName = GetRouteName(route); - return _resiliencePipelineRegistry.GetOrAddPipeline( - key: new OcelotResiliencePipelineKey(currentRouteName), - configure: (builder) => PollyResiliencePipelineWrapperFactory(builder, route)); + return _registry.GetOrAddPipeline( + key: new OcelotResiliencePipelineKey(currentRouteName), + configure: (builder) => ConfigureStrategies(builder, route)); } - private void PollyResiliencePipelineWrapperFactory(ResiliencePipelineBuilder builder, DownstreamRoute route) + protected virtual void ConfigureStrategies(ResiliencePipelineBuilder builder, DownstreamRoute route) { - var options = route.QosOptions; - - // Add TimeoutStrategy if TimeoutValue is not int.MaxValue and greater than 0 - if (options.TimeoutValue != int.MaxValue && options.TimeoutValue > 0) - { - builder.AddTimeout(TimeSpan.FromMilliseconds(options.TimeoutValue)); - } + ConfigureCircuitBreaker(builder, route); + ConfigureTimeout(builder, route); + } - // Add CircuitBreakerStrategy only if ExceptionsAllowedBeforeBreaking is greater than 0 - if (options.ExceptionsAllowedBeforeBreaking <= 0) + protected virtual ResiliencePipelineBuilder ConfigureCircuitBreaker(ResiliencePipelineBuilder builder, DownstreamRoute route) + { + // Add CircuitBreaker strategy only if ExceptionsAllowedBeforeBreaking is greater/equal than/to 2 + if (route.QosOptions.ExceptionsAllowedBeforeBreaking < 2) { - return; // shortcut > no qos (no timeout, no ExceptionsAllowedBeforeBreaking) + return builder; } + var options = route.QosOptions; var info = $"Circuit Breaker for Route: {GetRouteName(route)}: "; - - var circuitBreakerStrategyOptions = new CircuitBreakerStrategyOptions + var strategyOptions = new CircuitBreakerStrategyOptions { FailureRatio = 0.8, SamplingDuration = TimeSpan.FromSeconds(10), - MinimumThroughput = options.ExceptionsAllowedBeforeBreaking, - BreakDuration = TimeSpan.FromMilliseconds(options.DurationOfBreak), + MinimumThroughput = options.ExceptionsAllowedBeforeBreaking, + BreakDuration = options.DurationOfBreak > QoSOptions.LowBreakDuration + ? TimeSpan.FromMilliseconds(options.DurationOfBreak) + : TimeSpan.FromMilliseconds(QoSOptions.DefaultBreakDuration), ShouldHandle = new PredicateBuilder() .HandleResult(message => ServerErrorCodes.Contains(message.StatusCode)) .Handle() .Handle(), OnOpened = args => { - _logger.LogError(info + $"Breaking for {args.BreakDuration.TotalMilliseconds} ms", args.Outcome.Exception); + _logger.LogError(info + $"Breaking for {args.BreakDuration.TotalMilliseconds} ms", + args.Outcome.Exception); + return ValueTask.CompletedTask; + }, + OnClosed = _ => + { + _logger.LogInformation(info + "Closed"); + return ValueTask.CompletedTask; + }, + OnHalfOpened = _ => + { + _logger.LogInformation(info + "Half Opened"); return ValueTask.CompletedTask; }, }; + return builder.AddCircuitBreaker(strategyOptions); + } + + protected virtual ResiliencePipelineBuilder ConfigureTimeout(ResiliencePipelineBuilder builder, DownstreamRoute route) + { + var options = route.QosOptions; - builder.AddCircuitBreaker(circuitBreakerStrategyOptions); + // Add Timeout strategy if TimeoutValue is not int.MaxValue and greater than 0 + // TimeoutValue must be defined in QosOptions! + if (options.TimeoutValue == int.MaxValue || options.TimeoutValue <= 0) + { + return builder; + } + + var strategyOptions = new TimeoutStrategyOptions + { + Timeout = TimeSpan.FromMilliseconds(options.TimeoutValue), + OnTimeout = _ => + { + _logger.LogInformation($"Timeout for Route: {GetRouteName(route)}"); + return ValueTask.CompletedTask; + }, + }; + return builder.AddTimeout(strategyOptions); } } diff --git a/src/Ocelot/Configuration/QoSOptions.cs b/src/Ocelot/Configuration/QoSOptions.cs index e1897bc58..071bfbf95 100644 --- a/src/Ocelot/Configuration/QoSOptions.cs +++ b/src/Ocelot/Configuration/QoSOptions.cs @@ -32,16 +32,12 @@ public QoSOptions( TimeoutValue = timeoutValue; } - /// - /// How long the circuit should stay open before resetting in milliseconds. - /// - /// - /// If using Polly version 8 or above, this value must be 500 (0.5 sec) or greater. - /// - /// - /// An value (milliseconds). - /// - public int DurationOfBreak { get; } + /// How long the circuit should stay open before resetting in milliseconds. + /// If using Polly version 8 or above, this value must be 500 (0.5 sec) or greater. + /// An value (milliseconds). + public int DurationOfBreak { get; } = DefaultBreakDuration; + public const int LowBreakDuration = 500; // 0.5 seconds + public const int DefaultBreakDuration = 5_000; // 5 seconds /// /// How many times a circuit can fail before being set to open. diff --git a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs index 597731755..5a086019a 100644 --- a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs +++ b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs @@ -4,224 +4,256 @@ using Ocelot.Requester; using System.Reflection; -namespace Ocelot.AcceptanceTests +namespace Ocelot.AcceptanceTests; + +public sealed class PollyQoSTests : Steps, IDisposable { - public sealed class PollyQoSTests : Steps, IDisposable - { - private readonly ServiceHandler _serviceHandler; + private readonly ServiceHandler _serviceHandler; - public PollyQoSTests() - { - _serviceHandler = new ServiceHandler(); - } + public PollyQoSTests() + { + _serviceHandler = new ServiceHandler(); + } - public override void Dispose() - { - _serviceHandler.Dispose(); - base.Dispose(); - } + public override void Dispose() + { + _serviceHandler.Dispose(); + base.Dispose(); + } - private static FileConfiguration FileConfigurationFactory(int port, QoSOptions options, string httpMethod = nameof(HttpMethods.Get)) => new() - { - Routes = new() - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = Uri.UriSchemeHttp, - DownstreamHostAndPorts = new() - { - new("localhost", port), - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new() {httpMethod}, - QoSOptions = new FileQoSOptions(options), - }, - }, - }; - - [Fact] - public void Should_not_timeout() - { - var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(10, 500, 1000, null), HttpMethods.Post); - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, string.Empty, 10)) - .And(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunningWithPolly()) - .And(x => GivenThePostHasContent("postContent")) - .When(x => WhenIPostUrlOnTheApiGateway("/")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void Should_timeout() - { - var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(0, 0, 1000, null), HttpMethods.Post); - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 201, string.Empty, 2100)) - .And(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunningWithPolly()) - .And(x => GivenThePostHasContent("postContent")) - .When(x => WhenIPostUrlOnTheApiGateway("/")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .BDDfy(); - } - - [Fact] - public void Should_open_circuit_breaker_after_two_exceptions() - { - var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(2, 5000, 100000, null)); - - this.Given(x => x.GivenThereIsABrokenServiceRunningOn($"http://localhost:{port}")) - .And(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunningWithPolly()) - .And(x => WhenIGetUrlOnTheApiGateway("/")) - .And(x => WhenIGetUrlOnTheApiGateway("/")) - .And(x => WhenIGetUrlOnTheApiGateway("/")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .BDDfy(); - } - - [Fact] - public void Should_open_circuit_breaker_then_close() - { - var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(2, 500, 1000, null)); - - this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port}", "Hello from Laura")) - .Given(x => GivenThereIsAConfiguration(configuration)) - .Given(x => GivenOcelotIsRunningWithPolly()) - .When(x => WhenIGetUrlOnTheApiGateway("/")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) - .When(x => WhenIGetUrlOnTheApiGateway("/")) // repeat same request because min ExceptionsAllowedBeforeBreaking is 2 - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => WhenIGetUrlOnTheApiGateway("/")) - .Given(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => WhenIGetUrlOnTheApiGateway("/")) - .Given(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => WhenIGetUrlOnTheApiGateway("/")) - .Given(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => GivenIWaitMilliseconds(3000)) - .When(x => WhenIGetUrlOnTheApiGateway("/")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void Open_circuit_should_not_effect_different_route() + private static FileRoute GivenRoute(int port, QoSOptions options, string httpMethod = null, string upstream = null) => new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = Uri.UriSchemeHttp, + DownstreamHostAndPorts = new() { - var port1 = PortFinder.GetRandomPort(); - var port2 = PortFinder.GetRandomPort(); - var qos1 = new QoSOptions(2, 500, 1000, null); - - var configuration = FileConfigurationFactory(port1, qos1); - var route2 = configuration.Routes[0].Clone() as FileRoute; - route2.DownstreamHostAndPorts[0].Port = port2; - route2.UpstreamPathTemplate = "/working"; - route2.QoSOptions = new(); - configuration.Routes.Add(route2); - - this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port1}", "Hello from Laura")) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port2}", 200, "Hello from Tom", 0)) - .And(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunningWithPolly()) - .And(x => WhenIGetUrlOnTheApiGateway("/")) - .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => WhenIGetUrlOnTheApiGateway("/")) // repeat same request because min ExceptionsAllowedBeforeBreaking is 2 - .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => WhenIGetUrlOnTheApiGateway("/")) - .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => WhenIGetUrlOnTheApiGateway("/working")) - .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseBodyShouldBe("Hello from Tom")) - .And(x => WhenIGetUrlOnTheApiGateway("/")) - .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => WhenIGetUrlOnTheApiGateway("/")) - .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => GivenIWaitMilliseconds(3000)) - .When(x => WhenIGetUrlOnTheApiGateway("/")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - [Trait("Bug", "1833")] - public void Should_timeout_per_default_after_90_seconds() - { - // Arrange - var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(new FileQoSOptions()), HttpMethods.Get); - GivenThereIsAServiceRunningOn(DownstreamUrl(port), (int)HttpStatusCode.Created, string.Empty, 3500); // 3.5s > 3s -> ServiceUnavailable - GivenThereIsAConfiguration(configuration); - GivenOcelotIsRunningWithPolly(); - GivenIHackDefaultTimeoutValue(3); // after 3 secs -> Timeout exception aka request cancellation - - // Act - WhenIGetUrlOnTheApiGateway("/"); - - // Assert - ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); - } + new("localhost", port), + }, + UpstreamPathTemplate = upstream ?? "/", + UpstreamHttpMethod = new() { httpMethod ?? HttpMethods.Get }, + QoSOptions = new(options), + }; + + [Fact] + public void Should_not_timeout() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, new QoSOptions(10, 500, 1000, null), HttpMethods.Post); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, string.Empty, 10)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => GivenThePostHasContent("postContent")) + .When(x => WhenIPostUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void Should_timeout() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, new QoSOptions(0, 0, 1000, null), HttpMethods.Post); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, string.Empty, 2100)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => GivenThePostHasContent("postContent")) + .When(x => WhenIPostUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .BDDfy(); + } + + [Fact] + public void Should_open_circuit_breaker_after_two_exceptions() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, new QoSOptions(2, 1000, 100000, null)); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsABrokenServiceRunningOn(port, HttpStatusCode.InternalServerError)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError)) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) // opened + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) // Polly status + .BDDfy(); + } + + [Fact] + [Trait("Bug", "2085")] + public void Should_open_circuit_breaker_for_DefaultBreakDuration() + { + int invalidDuration = QoSOptions.LowBreakDuration; // valid value must be >500ms, exact 500ms is invalid + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, new QoSOptions(2, invalidDuration, 100000, null)); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsABrokenServiceRunningOn(port, HttpStatusCode.InternalServerError)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError)) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) // opened + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) // Polly status + .Given(x => GivenIWaitMilliseconds(QoSOptions.DefaultBreakDuration - 500)) // BreakDuration is not elapsed + .When(x => WhenIGetUrlOnTheApiGateway("/")) // still opened + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) // still opened + .Given(x => GivenThereIsABrokenServiceOnline(HttpStatusCode.NotFound)) + .Given(x => GivenIWaitMilliseconds(500)) // BreakDuration should elapse now + .When(x => WhenIGetUrlOnTheApiGateway("/")) // closed, service online + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) // closed, service online + .And(x => ThenTheResponseBodyShouldBe(nameof(HttpStatusCode.NotFound))) + .BDDfy(); + } + + [Fact] + public void Should_open_circuit_breaker_then_close() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, new QoSOptions(2, 500, 1000, null)); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn(port, "Hello from Laura")) + .Given(x => GivenThereIsAConfiguration(configuration)) + .Given(x => GivenOcelotIsRunningWithPolly()) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(x => WhenIGetUrlOnTheApiGateway("/")) // repeat same request because min ExceptionsAllowedBeforeBreaking is 2 + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => WhenIGetUrlOnTheApiGateway("/")) + .Given(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => WhenIGetUrlOnTheApiGateway("/")) + .Given(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => WhenIGetUrlOnTheApiGateway("/")) + .Given(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => GivenIWaitMilliseconds(3000)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void Open_circuit_should_not_effect_different_route() + { + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var qos1 = new QoSOptions(2, 500, 1000, null); + var route = GivenRoute(port1, qos1); + var route2 = GivenRoute(port2, new(new FileQoSOptions()), null, "/working"); + var configuration = GivenConfiguration(route, route2); + + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn(port1, "Hello from Laura")) + .And(x => x.GivenThereIsAServiceRunningOn(port2, HttpStatusCode.OK, "Hello from Tom", 0)) + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => WhenIGetUrlOnTheApiGateway("/")) // repeat same request because min ExceptionsAllowedBeforeBreaking is 2 + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => WhenIGetUrlOnTheApiGateway("/working")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Tom")) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => WhenIGetUrlOnTheApiGateway("/")) + .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => GivenIWaitMilliseconds(3000)) + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + [Trait("Bug", "1833")] + public void Should_timeout_per_default_after_90_seconds() + { + var port = PortFinder.GetRandomPort(); + var route = GivenRoute(port, new QoSOptions(new FileQoSOptions()), HttpMethods.Get); + var configuration = GivenConfiguration(route); + + this.Given(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, string.Empty, 3500)) // 3.5s > 3s -> ServiceUnavailable + .And(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithPolly()) + .And(x => GivenIHackDefaultTimeoutValue(3)) // after 3 secs -> Timeout exception aka request cancellation + .When(x => WhenIGetUrlOnTheApiGateway("/")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .BDDfy(); + } - private void GivenIHackDefaultTimeoutValue(int defaultTimeoutSeconds) - { - var field = typeof(MessageInvokerPool).GetField("_requestTimeoutSeconds", BindingFlags.NonPublic | BindingFlags.Instance); - var service = _ocelotServer.Services.GetService(typeof(IMessageInvokerPool)); - field.SetValue(service, defaultTimeoutSeconds); // hack the value of default 90 seconds - } + private void GivenIHackDefaultTimeoutValue(int defaultTimeoutSeconds) + { + var field = typeof(MessageInvokerPool).GetField("_requestTimeoutSeconds", BindingFlags.NonPublic | BindingFlags.Instance); + var service = _ocelotServer.Services.GetService(typeof(IMessageInvokerPool)); + field.SetValue(service, defaultTimeoutSeconds); // hack the value of default 90 seconds + } - private static void GivenIWaitMilliseconds(int ms) => Thread.Sleep(ms); + private static void GivenIWaitMilliseconds(int ms) => Thread.Sleep(ms); - private void GivenThereIsABrokenServiceRunningOn(string url) + private HttpStatusCode _brokenServiceStatusCode; + private void GivenThereIsABrokenServiceRunningOn(int port, HttpStatusCode brokenStatusCode) + { + string url = DownstreamUrl(port); + _brokenServiceStatusCode = brokenStatusCode; + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - context.Response.StatusCode = 500; - await context.Response.WriteAsync("this is an exception"); - }); - } + context.Response.StatusCode = (int)_brokenServiceStatusCode; + await context.Response.WriteAsync(_brokenServiceStatusCode.ToString()); + }); + } + + private void GivenThereIsABrokenServiceOnline(HttpStatusCode onlineStatusCode) + { + _brokenServiceStatusCode = onlineStatusCode; + } - private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string responseBody) + private void GivenThereIsAPossiblyBrokenServiceRunningOn(int port, string responseBody) + { + var requestCount = 0; + string url = DownstreamUrl(port); + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => { - var requestCount = 0; - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + if (requestCount == 2) { - if (requestCount == 2) - { - // In Polly v8: - // MinimumThroughput (ExceptionsAllowedBeforeBreaking) must be 2 or more - // BreakDuration (DurationOfBreak) must be 500 or more - // Timeout (TimeoutValue) must be 1000 or more - // So, we wait for 2.1 seconds to make sure the circuit is open - // DurationOfBreak * ExceptionsAllowedBeforeBreaking + Timeout - // 500 * 2 + 1000 = 2000 minimum + 100 milliseconds to exceed the minimum - await Task.Delay(2100); - } - - requestCount++; - context.Response.StatusCode = 200; - await context.Response.WriteAsync(responseBody); - }); - } - - private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, int timeout) + // In Polly v8: + // MinimumThroughput (ExceptionsAllowedBeforeBreaking) must be 2 or more + // BreakDuration (DurationOfBreak) must be 500 or more + // Timeout (TimeoutValue) must be 1000 or more + // So, we wait for 2.1 seconds to make sure the circuit is open + // DurationOfBreak * ExceptionsAllowedBeforeBreaking + Timeout + // 500 * 2 + 1000 = 2000 minimum + 100 milliseconds to exceed the minimum + await Task.Delay(2100); + } + + requestCount++; + context.Response.StatusCode = 200; + await context.Response.WriteAsync(responseBody); + }); + } + + private void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, string responseBody, int timeout) + { + string url = DownstreamUrl(port); + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - Thread.Sleep(timeout); - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - } + Thread.Sleep(timeout); + context.Response.StatusCode = (int)statusCode; + await context.Response.WriteAsync(responseBody); + }); } } diff --git a/test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs b/test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs index 1099a2281..6a80d2bea 100644 --- a/test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs +++ b/test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs @@ -2,6 +2,7 @@ using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Provider.Polly; +using Polly; using Polly.CircuitBreaker; using Polly.Registry; using Polly.Testing; @@ -12,37 +13,109 @@ namespace Ocelot.UnitTests.Polly; public class PollyQoSResiliencePipelineProviderTests { [Fact] - public void Should_build() + public void ShouldBuild() { + // Arrange var options = new QoSOptionsBuilder() - .WithTimeoutValue(1000) // 1s, minimum required by Polly + .WithTimeoutValue(1000) // 10ms, minimum required by Polly .WithExceptionsAllowedBeforeBreaking(2) // 2 is the minimum required by Polly - .WithDurationOfBreak(500) // 0.5s, minimum required by Polly + .WithDurationOfBreak(QoSOptions.LowBreakDuration + 1) // 0.5s, minimum required by Polly .Build(); + var route = new DownstreamRouteBuilder() + .WithQosOptions(options) + .Build(); + var loggerFactoryMock = new Mock(); + var registry = new ResiliencePipelineRegistry(); + var provider = new PollyQoSResiliencePipelineProvider(loggerFactoryMock.Object, registry); + + // Act + var resiliencePipeline = provider.GetResiliencePipeline(route); + // Assert + resiliencePipeline.ShouldNotBeNull(); + resiliencePipeline.ShouldBeOfType>(); + resiliencePipeline.ShouldNotBe(ResiliencePipeline.Empty); + } + + [Fact] + [Trait("Bug", "2085")] + public void ShouldNotBuild_ReturnedEmpty() + { + // Arrange + var options = new QoSOptionsBuilder().Build(); // empty options var route = new DownstreamRouteBuilder() .WithQosOptions(options) .Build(); + var loggerFactoryMock = new Mock(); + var registry = new ResiliencePipelineRegistry(); + var provider = new PollyQoSResiliencePipelineProvider(loggerFactoryMock.Object, registry); + + // Act + var resiliencePipeline = provider.GetResiliencePipeline(route); + + // Assert + resiliencePipeline.ShouldNotBeNull(); + resiliencePipeline.ShouldBeOfType>(); + resiliencePipeline.ShouldBe(ResiliencePipeline.Empty); + } + [Theory] + [Trait("Bug", "2085")] + [InlineData(0, QoSOptions.DefaultBreakDuration)] // default + [InlineData(QoSOptions.LowBreakDuration - 1, QoSOptions.DefaultBreakDuration)] // default + [InlineData(QoSOptions.LowBreakDuration, QoSOptions.DefaultBreakDuration)] // default + [InlineData(QoSOptions.LowBreakDuration + 1, QoSOptions.LowBreakDuration + 1)] // not default, exact + public void ShouldBuild_WithDefaultBreakDuration(int durationOfBreak, int expectedMillisecons) + { + // Arrange + var options = new QoSOptionsBuilder() + .WithTimeoutValue(1000) // 10ms, minimum required by Polly + .WithExceptionsAllowedBeforeBreaking(2) // 2 is the minimum required by Polly + .WithDurationOfBreak(durationOfBreak) // 0.5s, minimum required by Polly + .Build(); + var route = new DownstreamRouteBuilder() + .WithQosOptions(options) + .Build(); var loggerFactoryMock = new Mock(); - var resiliencePipelineRegistry = new ResiliencePipelineRegistry(); - var pollyQoSResiliencePipelineProvider = new PollyQoSResiliencePipelineProvider(loggerFactoryMock.Object, resiliencePipelineRegistry); - var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); + var registry = new ResiliencePipelineRegistry(); + var provider = new PollyQoSResiliencePipelineProvider(loggerFactoryMock.Object, registry); + + // Act + var resiliencePipeline = provider.GetResiliencePipeline(route); + + // Assert resiliencePipeline.ShouldNotBeNull(); + resiliencePipeline.ShouldBeOfType>(); + resiliencePipeline.ShouldNotBe(ResiliencePipeline.Empty); + var descriptor = resiliencePipeline.GetPipelineDescriptor(); + descriptor.ShouldNotBeNull(); + descriptor.Strategies.Count.ShouldBe(2); + descriptor.Strategies[0].Options.ShouldBeOfType>(); + descriptor.Strategies[1].Options.ShouldBeOfType(); + var strategyOptions = descriptor.Strategies[0].Options as CircuitBreakerStrategyOptions; + strategyOptions.ShouldNotBeNull(); + strategyOptions.BreakDuration.ShouldBe(TimeSpan.FromMilliseconds(expectedMillisecons)); } [Fact] public void Should_return_same_circuit_breaker_for_given_route() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); - var route1 = DownstreamRouteFactory("/"); - var route2 = DownstreamRouteFactory("/"); + // Arrange + var provider = GivenProvider(); + var route1 = GivenDownstreamRoute("/"); + var route2 = GivenDownstreamRoute("/"); - var resiliencePipeline1 = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route1); - var resiliencePipeline2 = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route2); + // Act + var resiliencePipeline1 = provider.GetResiliencePipeline(route1); + var resiliencePipeline2 = provider.GetResiliencePipeline(route2); + + // Assert resiliencePipeline1.ShouldBe(resiliencePipeline2); - var resiliencePipeline3 = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route1); + // Act 2 + var resiliencePipeline3 = provider.GetResiliencePipeline(route1); + + // Assert 2 resiliencePipeline3.ShouldBe(resiliencePipeline1); resiliencePipeline3.ShouldBe(resiliencePipeline2); } @@ -50,46 +123,99 @@ public void Should_return_same_circuit_breaker_for_given_route() [Fact] public void Should_return_different_circuit_breaker_for_two_different_routes() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); - var route1 = DownstreamRouteFactory("/"); - var route2 = DownstreamRouteFactory("/test"); + // Arrange + var provider = GivenProvider(); + var route1 = GivenDownstreamRoute("/"); + var route2 = GivenDownstreamRoute("/test"); - var resiliencePipeline1 = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route1); - var resiliencePipeline2 = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route2); + // Act + var resiliencePipeline1 = provider.GetResiliencePipeline(route1); + var resiliencePipeline2 = provider.GetResiliencePipeline(route2); + // Assert resiliencePipeline1.ShouldNotBe(resiliencePipeline2); } [Fact] - public void Should_build_and_wrap_contains_two_policies() + [Trait("Bug", "2085")] + public void ShouldBuild_ContainsTwoStrategies() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); + var pollyQoSResiliencePipelineProvider = GivenProvider(); - var route = DownstreamRouteFactory("/"); + var route = GivenDownstreamRoute("/"); var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); resiliencePipeline.ShouldNotBeNull(); - var resiliencePipelineDescriptor = resiliencePipeline.GetPipelineDescriptor(); - resiliencePipelineDescriptor.ShouldNotBeNull(); - resiliencePipelineDescriptor.Strategies.Count.ShouldBe(2); - resiliencePipelineDescriptor.Strategies[0].Options.ShouldBeOfType(); - resiliencePipelineDescriptor.Strategies[1].Options.ShouldBeOfType>(); + var descriptor = resiliencePipeline.GetPipelineDescriptor(); + descriptor.ShouldNotBeNull(); + descriptor.Strategies.Count.ShouldBe(2); + descriptor.Strategies[0].Options.ShouldBeOfType>(); + descriptor.Strategies[1].Options.ShouldBeOfType(); } [Fact] public void Should_build_and_contains_one_policy_when_with_exceptions_allowed_before_breaking_is_zero() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); + // Arrange + var provider = GivenProvider(); + var route = GivenDownstreamRoute("/", true); // get route with 0 exceptions allowed before breaking - // get route with 0 exceptions allowed before breaking - var route = DownstreamRouteFactory("/", true); - var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); + // Act + var resiliencePipeline = provider.GetResiliencePipeline(route); + var descriptor = resiliencePipeline.GetPipelineDescriptor(); + + // Assert resiliencePipeline.ShouldNotBeNull(); + descriptor.ShouldNotBeNull(); + descriptor.Strategies.Count.ShouldBe(1); + descriptor.Strategies.Single().Options.ShouldBeOfType(); + } - var resiliencePipelineDescriptor = resiliencePipeline.GetPipelineDescriptor(); - resiliencePipelineDescriptor.ShouldNotBeNull(); - resiliencePipelineDescriptor.Strategies.Count.ShouldBe(1); - resiliencePipelineDescriptor.Strategies.Single().Options.ShouldBeOfType(); + [Fact] + [Trait("Bug", "2085")] + public async Task Should_throw_after_timeout() + { + // Arrange + var provider = GivenProvider(); + const int timeOut = 1000; + var route = GivenDownstreamRoute("/", false, timeOut); + var resiliencePipeline = provider.GetResiliencePipeline(route); + var response = new HttpResponseMessage(HttpStatusCode.OK); + var cancellationTokenSource = new CancellationTokenSource(); + + // Assert + await Assert.ThrowsAsync(async () => + + // Act + await resiliencePipeline.ExecuteAsync(async (cancellationToken) => + { + await Task.Delay(timeOut + 500, cancellationToken); // add 500ms to make sure it's timed out + return response; + }, + cancellationTokenSource.Token)); + } + + [Fact] + [Trait("Bug", "2085")] + public async Task Should_not_throw_before_timeout() + { + // Arrange + var provider = GivenProvider(); + const int timeOut = 1000; + var route = GivenDownstreamRoute("/", false, timeOut); + var resiliencePipeline = provider.GetResiliencePipeline(route); + var response = new HttpResponseMessage(HttpStatusCode.OK); + var cancellationTokenSource = new CancellationTokenSource(); + + // Act + await resiliencePipeline.ExecuteAsync(async cancellationToken => + { + await Task.Delay(timeOut - 100, cancellationToken); // subtract 100ms to make sure it's not timed out + return response; + }, cancellationTokenSource.Token); + + // Assert + Assert.True(response.IsSuccessStatusCode); } [Theory] @@ -104,14 +230,17 @@ public void Should_build_and_contains_one_policy_when_with_exceptions_allowed_be [InlineData(HttpStatusCode.LoopDetected)] public async Task Should_throw_broken_circuit_exception_after_two_exceptions(HttpStatusCode errorCode) { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); - - var route = DownstreamRouteFactory("/"); - var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); - + // Arrange + var provider = GivenProvider(); + var route = GivenDownstreamRoute("/"); + var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(errorCode); + + // Act await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + + // Assert await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); } @@ -119,12 +248,13 @@ await Assert.ThrowsAsync(async () => [Fact] public async Task Should_not_throw_broken_circuit_exception_if_status_code_ok() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); - - var route = DownstreamRouteFactory("/"); - var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); - + // Arrange + var provider = GivenProvider(); + var route = GivenDownstreamRoute("/"); + var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.OK); + + // Act, Assert Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))).StatusCode); Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))).StatusCode); Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))).StatusCode); @@ -133,19 +263,21 @@ public async Task Should_not_throw_broken_circuit_exception_if_status_code_ok() [Fact] public async Task Should_throw_and_before_delay_should_not_allow_requests() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); - - var route = DownstreamRouteFactory("/"); - var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); - + // Arrange + var provider = GivenProvider(); + var route = GivenDownstreamRoute("/"); + var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + + // Act, Assert await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); await Task.Delay(200); + // Act, Assert 2 await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); } @@ -153,39 +285,48 @@ await Assert.ThrowsAsync(async () => [Fact] public async Task Should_throw_but_after_delay_should_allow_one_more_internal_server_error() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); - - var route = DownstreamRouteFactory("/"); - var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); - + // Arrange + var provider = GivenProvider(); + var route = GivenDownstreamRoute("/"); + var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + + // Act, Assert await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); await Task.Delay(6000); - Assert.Equal(HttpStatusCode.InternalServerError, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))).StatusCode); + // Act 2 + var response2 = await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + + // Assert 2 + Assert.Equal(HttpStatusCode.InternalServerError, response2.StatusCode); } [Fact] public async Task Should_throw_but_after_delay_should_allow_one_more_internal_server_error_and_throw() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); - - var route = DownstreamRouteFactory("/"); - var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); - + // Arrange + var provider = GivenProvider(); + var route = GivenDownstreamRoute("/"); + var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + + // Act, Assert await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); await Task.Delay(6000); + // Act, Assert 2 Assert.Equal(HttpStatusCode.InternalServerError, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))).StatusCode); + + // Act, Assert 3 await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); } @@ -193,43 +334,45 @@ await Assert.ThrowsAsync(async () => [Fact] public async Task Should_throw_but_after_delay_should_allow_one_more_ok_request_and_put_counter_back_to_zero() { - var pollyQoSResiliencePipelineProvider = PollyQoSResiliencePipelineProviderFactory(); - - var route = DownstreamRouteFactory("/"); - var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); - + // Arrange + var provider = GivenProvider(); + var route = GivenDownstreamRoute("/"); + var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); + + // Act, Assert await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); await Task.Delay(10000); + // Act, Assert 2 var response2 = new HttpResponseMessage(HttpStatusCode.OK); Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response2))).StatusCode); + + // Act, Assert 3 await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response)); await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response))); } - private static PollyQoSResiliencePipelineProvider PollyQoSResiliencePipelineProviderFactory() + private static PollyQoSResiliencePipelineProvider GivenProvider() { var loggerFactoryMock = new Mock(); loggerFactoryMock .Setup(x => x.CreateLogger()) .Returns(new Mock().Object); - var resiliencePipelineRegistry = new ResiliencePipelineRegistry(); - - var pollyQoSResiliencePipelineProvider = new PollyQoSResiliencePipelineProvider(loggerFactoryMock.Object, resiliencePipelineRegistry); - return pollyQoSResiliencePipelineProvider; + var registry = new ResiliencePipelineRegistry(); + return new PollyQoSResiliencePipelineProvider(loggerFactoryMock.Object, registry); } - private static DownstreamRoute DownstreamRouteFactory(string routeTemplate, bool inactiveExceptionsAllowedBeforeBreaking = false) + private static DownstreamRoute GivenDownstreamRoute(string routeTemplate, bool inactiveExceptionsAllowedBeforeBreaking = false, int timeOut = 10000) { var options = new QoSOptionsBuilder() - .WithTimeoutValue(10000) + .WithTimeoutValue(timeOut) .WithExceptionsAllowedBeforeBreaking(inactiveExceptionsAllowedBeforeBreaking ? 0 : 2) .WithDurationOfBreak(5000) .Build(); @@ -241,11 +384,9 @@ private static DownstreamRoute DownstreamRouteFactory(string routeTemplate, bool .WithOriginalValue(routeTemplate) .Build(); - var route = new DownstreamRouteBuilder() + return new DownstreamRouteBuilder() .WithQosOptions(options) .WithUpstreamPathTemplate(upstreamPath) .Build(); - - return route; } } From 696fa0783f17eede2a4b3a9d1c4a64877c2b5e33 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Fri, 7 Jun 2024 21:59:15 +0300 Subject: [PATCH 15/15] Release 23.3 Artifacts | +semver: minor (#2089) * Release notes * Update docs * Twilight Texas codename * Ocelot Core team members should not be in Top 3 Chart --- ReleaseNotes.md | 101 +++++++++++++---- build.cake | 25 +++-- docs/conf.py | 2 +- docs/features/caching.rst | 2 + docs/features/configuration.rst | 2 + docs/features/kubernetes.rst | 2 + docs/features/qualityofservice.rst | 6 + docs/index.rst | 171 +++++++++++++++++++++-------- 8 files changed, 230 insertions(+), 81 deletions(-) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 48db3c3bf..3a0da1bdb 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,31 +1,84 @@ -## Hotfix release (version {0}) for #2031 issue -> Route path template placeholders and their validation rules +## Spring 2024 (version {0}) aka [Twilight Texas](https://www.timeanddate.com/eclipse/solar/2024-april-8) release +> Codenamed: **[Twilight Texas](https://www.timeanddate.com/eclipse/solar/2024-april-8)** +> Read the Docs: [Ocelot 23.3](https://ocelot.readthedocs.io/en/{0}/) -Special thanks to **[Guillaume Gnaegi](https://github.com/ggnaegi)** and [Fabrizio Mancin](https://github.com/Fabman08)! +### What's new? -### About -The bug is related to the [Placeholders](https://ocelot.readthedocs.io/en/latest/features/routing.html#placeholders) feature in [Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html) and [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html). -The bug was introduced in version [23.2.0](https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0) as a part of PR #1927. +- **[Service Discovery](https://github.com/ThreeMammals/Ocelot/blob/main/docs/features/servicediscovery.rst)**: Introducing a new feature for "[Customization of services creation](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/servicediscovery.rst#consul-service-builder-3)" in two primary service discovery providers: [Consul](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/servicediscovery.rst#consul-service-builder-3) and [Kubernetes](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/kubernetes.rst#downstream-scheme-vs-port-names-3), developed by @raman-m. + The customization for both `Consul` and `Kube` providers in service creation is achieved through the overriding of virtual methods in default implementations. The recommendation was to separate the provider's logic and introduce `public virtual` and `protected virtual` methods in concrete classes, enabling: + - The use of `public virtual` methods as dictated by interface definitions. + - The application of `protected virtual` methods to allow developers to customize atomic operations through inheritance from existing concrete classes. + - The injection of new interface objects into the provider's constructor. + - The overriding of the default behavior of classes. -### Breaking Change -The new [validation rules](https://github.com/ThreeMammals/Ocelot/blob/23.2.0/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs#L45-L50) of the `FileConfigurationFluentValidator` class do not allow the Ocelot app to start when implicit [placeholders](https://ocelot.readthedocs.io/en/latest/features/routing.html#placeholders) are defined in custom implementations, such as middlewares, delegating handlers, and replaced services in the dependency injection (DI) container. -These new rules are capable of validating explicit [placeholders](https://ocelot.readthedocs.io/en/latest/features/routing.html#placeholders) only within the `UpstreamPathTemplate` and `DownstreamPathTemplate` properties. Unfortunately, they cannot oversee implicit placeholders in custom implementations, and they do not validate early during the Ocelot app startup process. + Ultimately, customization relies on the virtual methods within the default implementation classes, providing developers the flexibility to override them as necessary for highly tailored Consul/K8s configurations in their specific environments. + For further details, refer to the respective pull requests for both providers: + - `Kube` → PR #2052 + - `Consul` → PR #2067 -Ensure that you avoid using version [23.2.0](https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0). If you are currently on that version, upgrade to version [{0}](https://github.com/ThreeMammals/Ocelot/releases/tag/{0}) by applying this hotfix patch. +- **[Routing](https://github.com/ThreeMammals/Ocelot/blob/main/docs/features/routing.rst)**: Introducing the new "[Routing based on Request Header](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/routing.rst#upstream-headers-3)" feature by @jlukawska. + In addition to routing via `UpstreamPathTemplate`, you can now define an `UpstreamHeaderTemplates` options dictionary. For a route to match, all headers specified in this section are required to be present in the request headers. + For more details, see PR #1312. -### Technical info -With version [23.2.0](https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0), particularly if you have overridden certain service classes or implemented custom logic that manipulates placeholders, you may encounter Ocelot app crashes accompanied by the following errors in the log: -``` -One or more errors occurred. (Unable to start Ocelot, errors are: XXX) -``` -where `XXX` are the following validation error messages: -- `UpstreamPathTemplate 'UUU' doesn't contain the same placeholders in DownstreamPathTemplate 'DDD'` -- `DownstreamPathTemplate 'DDD' doesn't contain the same placeholders in UpstreamPathTemplate 'UUU'` +- **[Configuration](https://github.com/ThreeMammals/Ocelot/blob/main/docs/features/configuration.rst)**: Introducing the "[Custom Default Version Policy](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/configuration.rst#downstreamhttpversionpolicy-3)" feature by @ibnuda. + The configurable `HttpRequestMessage.VersionPolicy` helps avoid HTTP protocol connection errors and stabilizes connections to downstream services, especially when you're not developing those services, documentation is scarce, or the deployed HTTP protocol version is uncertain. + For developers of downstream services, it's possible to `ConfigureKestrel` server and its endpoints with new protocol settings. However, attention to version policy is also required, and this feature provides precise version settings for HTTP connections. + Essentially, this feature promotes the use of HTTP protocols beyond 1.0/1.1, such as HTTP/2 or even HTTP/3. + For additional details, refer to PR #1673. -**Finally**, the [validation rules](https://github.com/ThreeMammals/Ocelot/blob/23.2.0/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs#L45-L50) resulted from the incorrect assumption that placeholders are always explicit and can be validated early. Therefore, custom implementations and feature services in the dependency injection (DI) container, which rely on or manipulate placeholders, should validate the configuration JSON and appropriate options later, directly within their service implementations. +- **[Configuration](https://github.com/ThreeMammals/Ocelot/blob/main/docs/features/configuration.rst)**: Introducing the new "[Route Metadata](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/configuration.rst#route-metadata)" feature by @vantm. Undoubtedly, this is the standout feature of the release! :star: + Route metadata enables Ocelot developers to incorporate custom functions that address specific needs or to create their own plugins/extensions. + In versions of Ocelot prior to [{0}](https://github.com/ThreeMammals/Ocelot/releases/tag/{0}), the configuration was limited to predefined values that Ocelot used internally. This was sufficient for official extensions, but posed challenges for third-party developers who needed to implement configurations not included in the standard `FileConfiguration`. Applying an option to a specific route required knowledge of the array index and other details that might not be readily accessible using the standard `IConfiguration` or `IOptions` models from ASP.NET. Now, metadata can be directly accessed in the `DownstreamRoute` object. Furthermore, metadata can also be retrieved from the global JSON section via the `FileConfiguration.GlobalConfiguration` property. + For more information, see the details in PR #1843 on this remarkable feature. -### Bug Artifacts -- Released in version: [23.2.0](https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0) -- Introduced in: PR #1927 -- Reported bug: #2031 by @ggnaegi and tested by @Fabman08 -- Hotfix PR: #2032 by @raman-m +### Focus On + +
+ Updates of the features: Configuration, Service Discovery, Routing and Quality of Service + + - [Configuration](https://github.com/ThreeMammals/Ocelot/blob/main/docs/features/configuration.rst): New features are "[Custom Default Version Policy](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/configuration.rst#downstreamhttpversionpolicy-3)" by @ibnuda and "[Route Metadata](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/configuration.rst#route-metadata)" by @vantm. + + - [Service Discovery](https://github.com/ThreeMammals/Ocelot/blob/main/docs/features/servicediscovery.rst): New feature is "[Customization of services creation](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/servicediscovery.rst#consul-service-builder-3)" by @raman-m. + + - [Routing](https://github.com/ThreeMammals/Ocelot/blob/main/docs/features/routing.rst): New feature is "[Routing based on Request Header](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/routing.rst#upstream-headers-3)" by @jlukawska. + + - [Quality of Service](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/qualityofservice.rst): The team has decided to remove the Polly V7 policies logic and the corresponding Ocelot `AddPollyV7` extensions (referenced in PR #2079). Furthermore, the Polly V8 Circuit Breaker has been mandated as the primary strategy (as per PR #2086). + See more detaild below in "**Ocelot extra packages**" paragraph. +
+ +
+ Ocelot extra packages + + - **[Ocelot.Provider.Polly](https://www.nuget.org/packages/Ocelot.Provider.Polly)** + + - Our team has resolved to eliminate the Polly V7 policies logic and the corresponding Ocelot `AddPollyV7` extensions entirely (refer to the "[Polly v7 vs v8](https://github.com/ThreeMammals/Ocelot/blob/23.2.0/docs/features/qualityofservice.rst#polly-v7-vs-v8)" documentation). In the previous [23.2](https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0) release, named [Lunar Eclipse](https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0), we included these to maintain the legacy Polly behavior, allowing development teams to transition or retain the old Polly V7 functionality. We are now confident that it is time to progress alongside Polly, shifting our focus to the new Polly V8 [resilience pipelines](https://www.pollydocs.org/pipelines/). For more details, see PR #2079. + + - Additionally, we have implemented Polly v8 Circuit Breaker as the primary strategy. Our Quality of Service (QoS) relies on two main strategies: [Circuit Breaker](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/qualityofservice.rst#circuit-breaker-strategy) and [Timeout](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/qualityofservice.rst#timeout-strategy). If both Circuit Breaker and Timeout are [configured](https://github.com/ThreeMammals/Ocelot/blob/{0}/docs/features/qualityofservice.rst#configuration) with their respective properties in the `QoSOptions` of the route JSON, then the Circuit Breaker strategy will take precedence in the constructed resilience pipeline. For more details, refer to PR #2086. +
+ +
+ Stabilization aka bug fixing + + - Fixed #2034 in PR #2045 by @raman-m + - Fixed #2039 in PR #2050 by @PaulARoy + - Fixed #1590 in PR #1592 by @sergio-str + - Fixed #2054 #2059 in PR #2058 by @thiagoloureiro + - Fixed #954 #957 #1026 in PR #2067 by @raman-m + - Fixed #2002 in PR #2003 by @bbenameur + - Fixed #2085 in PR #2086 by @RaynaldM + - See [all bugs](https://github.com/ThreeMammals/Ocelot/issues?q=is%3Aissue+milestone%3ASpring%2724+is%3Aclosed+label%3Abug) of the [Spring'24](https://github.com/ThreeMammals/Ocelot/milestone/6) milestone +
+ +
+ Documentation for version {0} + + - [Caching](https://ocelot.readthedocs.io/en/{0}/features/caching.html): New [EnableContentHashing option](https://ocelot.readthedocs.io/en/{0}/features/caching.html#enablecontenthashing-option) and [Global Configuration](https://ocelot.readthedocs.io/en/{0}/features/caching.html#global-configuration) sections + - [Configuration](https://ocelot.readthedocs.io/en/{0}/features/configuration.html): New [DownstreamHttpVersionPolicy](https://ocelot.readthedocs.io/en/{0}/features/configuration.html#downstreamhttpversionpolicy-3) and [Route Metadata](https://ocelot.readthedocs.io/en/{0}/features/configuration.html#route-metadata) + - [Kubernetes](https://ocelot.readthedocs.io/en/{0}/features/kubernetes.html): New [Downstream Scheme vs Port Names](https://ocelot.readthedocs.io/en/{0}/features/kubernetes.html#downstream-scheme-vs-port-names-3) section + - [Metadata](https://ocelot.readthedocs.io/en/{0}/features/metadata.html): This is new chapter for [Route Metadata](https://ocelot.readthedocs.io/en/{0}/features/configuration.html#route-metadata) feature. + - [Quality of Service](https://ocelot.readthedocs.io/en/{0}/features/qualityofservice.html) + - [Rate Limiting](https://ocelot.readthedocs.io/en/{0}/features/ratelimiting.html) + - [Request Aggregation](https://ocelot.readthedocs.io/en/{0}/features/requestaggregation.html) + - [Routing](https://ocelot.readthedocs.io/en/{0}/features/routing.html): New [Upstream Headers](https://ocelot.readthedocs.io/en/{0}/features/routing.html#upstream-headers-3) section + - [Service Discovery](https://ocelot.readthedocs.io/en/{0}/features/servicediscovery.html): New [Consul Service Builder](https://ocelot.readthedocs.io/en/{0}/features/servicediscovery.html#consul-service-builder-3) section +
diff --git a/build.cake b/build.cake index a8f7cc362..430a1c804 100644 --- a/build.cake +++ b/build.cake @@ -174,9 +174,10 @@ Task("CreateReleaseNotes") return; } - var shortlogSummary = GitHelper($"shortlog --no-merges --numbered --summary {lastRelease}..HEAD") + var debugUserEmail = false; + var shortlogSummary = GitHelper($"shortlog --no-merges --numbered --summary --email {lastRelease}..HEAD") .ToList(); - var re = new Regex(@"^[\s\t]*(?'commits'\d+)[\s\t]+(?'author'.*)$"); + var re = new Regex(@"^[\s\t]*(?'commits'\d+)[\s\t]+(?'author'.*)[\s\t]+<(?'email'.*)>.*$"); var summary = shortlogSummary .Where(x => re.IsMatch(x)) .Select(x => re.Match(x)) @@ -184,6 +185,7 @@ Task("CreateReleaseNotes") { commits = int.Parse(m.Groups["commits"]?.Value ?? "0"), author = m.Groups["author"]?.Value?.Trim() ?? string.Empty, + email = m.Groups["email"]?.Value?.Trim() ?? string.Empty, }) .ToList(); @@ -192,13 +194,18 @@ Task("CreateReleaseNotes") foreach (var contributor in summary) { var stars = string.Join(string.Empty, Enumerable.Repeat(":star:", contributor.commits)); - starring.Add($"{stars} {contributor.author}"); + var emailInfo = debugUserEmail ? ", " + contributor.email : string.Empty; + starring.Add($"{stars} {contributor.author}{emailInfo}"); } // Honoring aka Top Contributors const int top3 = 3; // going to create Top 3 var topContributors = new List(); + // Ocelot Core team members should not be in Top 3 Chart + var coreTeamNames = new List { "Raman Maksimchuk", "Raynald Messié", "Guillaume Gnaegi" }; + var coreTeamEmails = new List { "dotnet044@gmail.com", "redbird_project@yahoo.fr", "58469901+ggnaegi@users.noreply.github.com" }; var commitsGrouping = summary + .Where(x => !coreTeamNames.Contains(x.author) && !coreTeamEmails.Contains(x.email)) // filter out Ocelot Core team members .GroupBy(x => x.commits) .Select(g => new { @@ -210,7 +217,7 @@ Task("CreateReleaseNotes") .ToList(); // local helpers - string[] places = new[] { "1st", "2nd", "3rd" }; + string[] places = new[] { "1st", "2nd", "3rd", "4", "5", "6", "7", "8", "9", "10", "11" }; static string Plural(int n) => n == 1 ? "" : "s"; static string Honor(string place, string author, int commits, string suffix = null) => $"{place[0]}{place[1..]} :{place}_place_medal: goes to **{author}** for delivering **{commits}** feature{Plural(commits)} {suffix ?? ""}"; @@ -312,11 +319,11 @@ Task("CreateReleaseNotes") } } // END of Top 3 - // releaseNotes.Add("### Honoring :medal_sports: aka Top Contributors :clap:"); - // releaseNotes.AddRange(topContributors); - // releaseNotes.Add(""); - // releaseNotes.Add("### Starring :star: aka Release Influencers :bowtie:"); - // releaseNotes.AddRange(starring); + releaseNotes.Add("### Honoring :medal_sports: aka Top Contributors :clap:"); + releaseNotes.AddRange(topContributors); + releaseNotes.Add(""); + releaseNotes.Add("### Starring :star: aka Release Influencers :bowtie:"); + releaseNotes.AddRange(starring); releaseNotes.Add(""); releaseNotes.Add($"### Features in Release {releaseVersion}"); var commitsHistory = GitHelper($"log --no-merges --date=format:\"%A, %B %d at %H:%M\" --pretty=format:\"%h by **%aN** on %ad →%n%s\" {lastRelease}..HEAD"); diff --git a/docs/conf.py b/docs/conf.py index 6d08568ce..2c31a6b98 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ project = 'Ocelot' copyright = ' 2016-2024 ThreeMammals Ocelot team' author = 'Tom Pallister, Raman Maksimchuk and Ocelot Core team at ThreeMammals' -release = '23.2' +release = '23.3' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/features/caching.rst b/docs/features/caching.rst index 84803060f..3d14b47c9 100644 --- a/docs/features/caching.rst +++ b/docs/features/caching.rst @@ -49,6 +49,8 @@ The **Region** represents a region of caching. Additionally, if a header name is defined in the **Header** property, that header value is looked up by the key (header name) in the ``HttpRequest`` headers, and if the header is found, its value will be included in caching key. This causes the cache to become invalid due to the header value changing. +.. _cch-enablecontenthashing-option: + ``EnableContentHashing`` option ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 5a29a6b48..1be532e08 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -447,6 +447,8 @@ You can utilize these methods in the ``ConfigureAppConfiguration`` method (locat You can find additional details in the dedicated :ref:`di-configuration-overview` section and in subsequent sections related to the :doc:`../features/dependencyinjection` chapter. +.. _config-route-metadata: + Route Metadata -------------- diff --git a/docs/features/kubernetes.rst b/docs/features/kubernetes.rst index 8c6967c87..8bbdb1881 100644 --- a/docs/features/kubernetes.rst +++ b/docs/features/kubernetes.rst @@ -117,6 +117,8 @@ If your downstream service resides in a different namespace, you can override th } ] +.. _k8s-downstream-scheme-vs-port-names: + Downstream Scheme vs Port Names [#f3]_ -------------------------------------- diff --git a/docs/features/qualityofservice.rst b/docs/features/qualityofservice.rst index 4fa4ecf9c..fde699ddf 100644 --- a/docs/features/qualityofservice.rst +++ b/docs/features/qualityofservice.rst @@ -23,6 +23,8 @@ Next, within your ``ConfigureServices`` method, to incorporate `Polly`_ services services.AddOcelot() .AddPolly(); +.. _qos-configuration: + Configuration ------------- @@ -40,6 +42,8 @@ Then add the following section to a Route configuration: - ``DurationOfBreak`` means the circuit breaker will stay open for 1 second after it is tripped. - ``TimeoutValue`` means if a request takes more than 5 seconds, it will automatically be timed out. +.. _qos-circuit-breaker-strategy: + Circuit Breaker strategy ------------------------ @@ -62,6 +66,8 @@ Alternatively, you may omit ``DurationOfBreak`` to default to the implicit 5 sec This setup activates only the `Circuit breaker `_ strategy. +.. _qos-timeout-strategy: + Timeout strategy ---------------- diff --git a/docs/index.rst b/docs/index.rst index c313c5104..d208ae6d3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,13 +1,49 @@ .. _Polly: https://github.com/App-vNext/Polly -.. _@ebjornset: https://github.com/ebjornset -.. _@RaynaldM: https://github.com/RaynaldM -.. _@ArwynFr: https://github.com/ArwynFr -.. _@AlyHKafoury: https://github.com/AlyHKafoury -.. _@FelixBoers: https://github.com/FelixBoers -.. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 +.. _Circuit Breaker: https://www.pollydocs.org/strategies/circuit-breaker.html +.. _Timeout: https://www.pollydocs.org/strategies/timeout.html -Welcome to Ocelot `23.2`_ -====================================================================================== +.. _@raman-m: https://github.com/raman-m +.. _@RaynaldM: https://github.com/RaynaldM +.. _@jlukawska: https://github.com/jlukawska +.. _@ibnuda: https://github.com/ibnuda +.. _@vantm: https://github.com/vantm +.. _@sergio-str: https://github.com/sergio-str +.. _@PaulARoy: https://github.com/PaulARoy +.. _@thiagoloureiro: https://github.com/thiagoloureiro +.. _@bbenameur: https://github.com/bbenameur + +.. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 +.. _23.3.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 +.. _23.2.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 + +.. _954: https://github.com/ThreeMammals/Ocelot/issues/954 +.. _957: https://github.com/ThreeMammals/Ocelot/issues/957 +.. _1026: https://github.com/ThreeMammals/Ocelot/issues/1026 +.. _1312: https://github.com/ThreeMammals/Ocelot/pull/1312 +.. _1590: https://github.com/ThreeMammals/Ocelot/issues/1590 +.. _1592: https://github.com/ThreeMammals/Ocelot/pull/1592 +.. _1673: https://github.com/ThreeMammals/Ocelot/pull/1673 +.. _1843: https://github.com/ThreeMammals/Ocelot/pull/1843 +.. _2002: https://github.com/ThreeMammals/Ocelot/issues/2002 +.. _2003: https://github.com/ThreeMammals/Ocelot/pull/2003 +.. _2034: https://github.com/ThreeMammals/Ocelot/issues/2034 +.. _2039: https://github.com/ThreeMammals/Ocelot/issues/2039 +.. _2045: https://github.com/ThreeMammals/Ocelot/pull/2045 +.. _2050: https://github.com/ThreeMammals/Ocelot/pull/2050 +.. _2052: https://github.com/ThreeMammals/Ocelot/pull/2052 +.. _2054: https://github.com/ThreeMammals/Ocelot/discussions/2054 +.. _2058: https://github.com/ThreeMammals/Ocelot/pull/2058 +.. _2059: https://github.com/ThreeMammals/Ocelot/issues/2059 +.. _2067: https://github.com/ThreeMammals/Ocelot/pull/2067 +.. _2079: https://github.com/ThreeMammals/Ocelot/pull/2079 +.. _2085: https://github.com/ThreeMammals/Ocelot/issues/2085 +.. _2086: https://github.com/ThreeMammals/Ocelot/pull/2086 + +.. role:: htm(raw) + :format: html + +Welcome to Ocelot `23.3`_ +========================= Thanks for taking a look at the Ocelot documentation! Please use the left hand navigation to get around. The team would suggest taking a look at the **Introduction** chapter first. @@ -20,67 +56,108 @@ We **do** follow development process which is described in :doc:`../building/rel Release Notes ------------- - | **Release Tag**: `23.2.0 `_ - | **Release Codename**: `Lunar Eclipse `_ +| Release Tag: `23.3.0`_ +| Release Codename: **Twilight Texas** + :htm:`→` `for men `_ + :htm:`→` `for women `_ + :htm:`→` `for black men `_ What's new? ^^^^^^^^^^^ -- :doc:`../features/configuration`: A brand new :ref:`config-merging-tomemory` by `@ebjornset`_ as a part of the :ref:`config-merging-files` feature. - - The ``AddOcelot`` method merges the **ocelot.*.json** files into a single **ocelot.json** file as the primary configuration file, which is written back to disk and then added to the ``IConfigurationBuilder`` for the well-known ``IConfiguration``. You can now call another ``AddOcelot`` method that adds the merged JSON directly from memory to the ``IConfigurationBuilder``, using ``AddJsonStream`` instead. - - See more details in :ref:`di-configuration-overview` of :doc:`../features/dependencyinjection`. +- :doc:`../features/servicediscovery`: Introducing a new feature for "*Customization of services creation*" in two primary service discovery providers: ``Consul`` (:ref:`sd-consul-service-builder`) and ``Kubernetes`` (:ref:`k8s-downstream-scheme-vs-port-names`), developed by `@raman-m`_. + + The customization for both ``Consul`` and ``Kube`` providers in service creation is achieved through the overriding of virtual methods in default implementations. The recommendation was to separate the provider's logic and introduce ``public virtual`` and ``protected virtual`` methods in concrete classes, enabling: + + - The use of ``public virtual`` methods as dictated by interface definitions. + - The application of ``protected virtual`` methods to allow developers to customize atomic operations through inheritance from existing concrete classes. + - The injection of new interface objects into the provider's constructor. + - The overriding of the default behavior of classes. + + | Ultimately, customization relies on the virtual methods within the default implementation classes, providing developers the flexibility to override them as necessary for highly tailored Consul/K8s configurations in their specific environments. + | For further details, refer to the respective pull requests for both providers: + + - ``Kube`` :htm:`→` PR `2052`_ + - ``Consul`` :htm:`→` PR `2067`_ + +- :doc:`../features/routing`: Introducing the new ":ref:`routing-upstream-headers`" feature by `@jlukawska`_. + + | In addition to routing via ``UpstreamPathTemplate``, you can now define an ``UpstreamHeaderTemplates`` options dictionary. For a route to match, all headers specified in this section are required to be present in the request headers. + | For more details, see PR `1312`_. + +- :doc:`../features/configuration`: Introducing the ":ref:`config-version-policy`" feature by `@ibnuda`_. + + The configurable ``HttpRequestMessage.VersionPolicy`` helps avoid HTTP protocol connection errors and stabilizes connections to downstream services, especially when you're not developing those services, documentation is scarce, or the deployed HTTP protocol version is uncertain. + For developers of downstream services, it's possible to ``ConfigureKestrel`` server and its endpoints with new protocol settings. However, attention to version policy is also required, and this feature provides precise version settings for HTTP connections. + + | Essentially, this feature promotes the use of HTTP protocols beyond 1.0/1.1, such as HTTP/2 or even HTTP/3. + | For additional details, refer to PR `1673`_. -- :doc:`../features/servicefabric`: Published old undocumented :ref:`sf-placeholders` feature of :doc:`../features/servicefabric` `service discovery provider `_. +- :doc:`../features/configuration`: Introducing the new ":ref:`config-route-metadata`" feature by `@vantm`_. - This feature by `@FelixBoers`_ is available starting from version `13.0.0 `_. + Undoubtedly, this is the standout feature of the release! ⭐ -- :doc:`../features/qualityofservice`: A brand new `Polly`_ v8 pipelines :ref:`qos-extensibility` feature by `@RaynaldM`_ + Route metadata enables Ocelot developers to incorporate custom functions that address specific needs or to create their own plugins/extensions. + + In versions of Ocelot prior to `23.3.0`_, the configuration was limited to predefined values that Ocelot used internally. This was sufficient for official extensions, but posed challenges for third-party developers who needed to implement configurations not included in the standard ``FileConfiguration``. + Applying an option to a specific route required knowledge of the array index and other details that might not be readily accessible using the standard ``IConfiguration`` or ``IOptions`` models from ASP.NET. + + | Now, :doc:`../features/metadata` can be directly accessed in the ``DownstreamRoute`` object. Furthermore, metadata can also be retrieved from the global JSON section via the ``FileConfiguration.GlobalConfiguration`` property. + | For more information, see the details in PR `1843`_ on this remarkable feature. Focus On ^^^^^^^^ Updates of the features """"""""""""""""""""""" - - - :doc:`../features/configuration`: New :ref:`config-merging-tomemory` feature by `@ebjornset`_ - - :doc:`../features/dependencyinjection`: Added new overloaded :ref:`di-configuration-addocelot` by `@ebjornset`_ - - :doc:`../features/qualityofservice`: Support of new `Polly`_ v8 syntax and new :ref:`qos-extensibility` feature by `@RaynaldM`_ + +- :doc:`../features/configuration`: New features are ":ref:`config-version-policy`" by `@ibnuda`_ and ":ref:`config-route-metadata`" by `@vantm`_. +- :doc:`../features/servicediscovery`: New feature is "*Customization of services creation*" aka :ref:`sd-consul-service-builder` and :ref:`k8s-downstream-scheme-vs-port-names` by `@raman-m`_. +- :doc:`../features/routing`: New feature is ":ref:`routing-upstream-headers`" by `@jlukawska`_. +- :doc:`../features/qualityofservice`: The team has decided to remove the Polly V7 policies logic and the corresponding Ocelot ``AddPollyV7`` extensions (referenced in PR `2079`_). + + | Furthermore, the Polly V8 Circuit Breaker has been mandated as the primary strategy (as per PR `2086`_). + | See more detaild below in "**Ocelot extra packages**" paragraph. Ocelot extra packages """"""""""""""""""""" - - `Ocelot.Provider.Polly `_: Support of new `Polly`_ v8 syntax. +- `Ocelot.Provider.Polly `_ - | *Polly* `8.0+ `_ versions introduced the concept of `resilience pipelines `_. - | All `AddPolly extensions `_ have been automatically migrated from **v7** to **v8**. - | Please note that older **v7** extensions are marked with the ``[Obsolete]`` attribute and renamed using the ``V7`` suffix. And the old **v7** implementation has been moved to the `v7 namespace `_. - | See more details in :ref:`qos-polly-v7-vs-v8` section of :doc:`../features/qualityofservice` chapter. + - Our team has resolved to eliminate the Polly V7 policies logic and the corresponding Ocelot ``AddPollyV7`` extensions entirely (refer to the "`Polly v7 vs v8 `_" documentation). + In the previous `23.2.0`_ release, named `Lunar Eclipse `_, we included these to maintain the legacy `Polly`_ behavior, allowing development teams to transition or retain the old Polly V7 functionality. + We are now confident that it is time to progress alongside `Polly`_, shifting our focus to the new `Polly V8 `_ `resilience pipelines `_. + For more details, see PR `2079`_. + - Additionally, we have implemented Polly v8 `Circuit Breaker `_ as the primary strategy. + Our :doc:`../features/qualityofservice` (QoS) relies on two main strategies: :ref:`qos-circuit-breaker-strategy` and :ref:`qos-timeout-strategy`. + If both `Circuit Breaker`_ and `Timeout`_ have :ref:`qos-configuration` with their respective properties in the ``QoSOptions`` of the route JSON, then the :ref:`qos-circuit-breaker-strategy` will take precedence in the constructed resilience pipeline. + For more details, refer to PR `2086`_. Stabilization aka bug fixing """""""""""""""""""""""""""" - - `683 `_ by PR `1927 `_. Thanks to `@AlyHKafoury`_! - - | `New rules `_ have been added to Ocelot's configuration validation logic to find duplicate placeholders in path templates. - | See more in the `FileConfigurationFluentValidator `_ class. - - - `1518 `_ hotfix by PR `1986 `_. Thanks to `@ArwynFr`_! - - | Using the default ``IServiceCollection`` `DI extensions `_ to register Ocelot services resulted in the ``ServiceCollection`` provider being forced to be created by calling ``BuildServiceProvider()``. - | This resulted in problems with dependency injection libraries, or worse, causing the Ocelot app to crash! - | See more in the `ServiceCollectionExtensions `_ class. - - - See `all bugs `_ of the `February'24 `_ milestone - -Updated Documentation -""""""""""""""""""""" - - - :doc:`../features/configuration` - - :doc:`../features/dependencyinjection` - - :doc:`../features/qualityofservice` - - :doc:`../features/servicefabric` +- Fixed `2034`_ in PR `2045`_ by `@raman-m`_ +- Fixed `2039`_ in PR `2050`_ by `@PaulARoy`_ +- Fixed `1590`_ in PR `1592`_ by `@sergio-str`_ +- Fixed `2054`_ `2059`_ in PR `2058`_ by `@thiagoloureiro`_ +- Fixed `954`_ `957`_ `1026`_ in PR `2067`_ by `@raman-m`_ +- Fixed `2002`_ in PR `2003`_ by `@bbenameur`_ +- Fixed `2085`_ in PR `2086`_ by `@RaynaldM`_ + +See `all bugs `_ of the `Spring'24 `_ milestone + +Documentation for version `23.3`_ +""""""""""""""""""""""""""""""""" + +- :doc:`../features/caching`: New :ref:`cch-enablecontenthashing-option` and :ref:`cch-global-configuration` sections +- :doc:`../features/configuration`: New :ref:`config-version-policy` and :ref:`config-route-metadata` sections +- :doc:`../features/kubernetes`: New :ref:`k8s-downstream-scheme-vs-port-names` section +- :doc:`../features/metadata`: This is new chapter for :ref:`config-route-metadata` feature +- :doc:`../features/qualityofservice` +- :doc:`../features/ratelimiting` +- :doc:`../features/requestaggregation` +- :doc:`../features/routing`: New :ref:`routing-upstream-headers` section +- :doc:`../features/servicediscovery`: New :ref:`sd-consul-service-builder` and :ref:`k8s-downstream-scheme-vs-port-names` sections .. toctree::