From d24df3642033c1edb4e79899dc5e62b667b68d67 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Wed, 14 Mar 2018 18:49:41 +0000 Subject: [PATCH 1/9] #271 Added some extra logging (#276) --- .../Requester/HttpClientHttpRequester.cs | 3 +- src/Ocelot/Requester/RequestTimedOutError.cs | 2 +- .../Requester/UnableToCompleteRequestError.cs | 2 +- .../Middleware/ResponderMiddleware.cs | 6 +++ test/Ocelot.AcceptanceTests/RoutingTests.cs | 49 +++++++++++++++++++ 5 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/Ocelot/Requester/HttpClientHttpRequester.cs b/src/Ocelot/Requester/HttpClientHttpRequester.cs index c60e3b78..a3b81fbc 100644 --- a/src/Ocelot/Requester/HttpClientHttpRequester.cs +++ b/src/Ocelot/Requester/HttpClientHttpRequester.cs @@ -39,8 +39,7 @@ namespace Ocelot.Requester } catch (TimeoutRejectedException exception) { - return - new ErrorResponse(new RequestTimedOutError(exception)); + return new ErrorResponse(new RequestTimedOutError(exception)); } catch (BrokenCircuitException exception) { diff --git a/src/Ocelot/Requester/RequestTimedOutError.cs b/src/Ocelot/Requester/RequestTimedOutError.cs index 86eab0c6..f99308b3 100644 --- a/src/Ocelot/Requester/RequestTimedOutError.cs +++ b/src/Ocelot/Requester/RequestTimedOutError.cs @@ -6,7 +6,7 @@ namespace Ocelot.Requester public class RequestTimedOutError : Error { public RequestTimedOutError(Exception exception) - : base($"Timeout making http request, exception: {exception.Message}", OcelotErrorCode.RequestTimedOutError) + : base($"Timeout making http request, exception: {exception}", OcelotErrorCode.RequestTimedOutError) { } } diff --git a/src/Ocelot/Requester/UnableToCompleteRequestError.cs b/src/Ocelot/Requester/UnableToCompleteRequestError.cs index 033ca53f..c5eb7b31 100644 --- a/src/Ocelot/Requester/UnableToCompleteRequestError.cs +++ b/src/Ocelot/Requester/UnableToCompleteRequestError.cs @@ -6,7 +6,7 @@ namespace Ocelot.Requester public class UnableToCompleteRequestError : Error { public UnableToCompleteRequestError(Exception exception) - : base($"Error making http request, exception: {exception.Message}", OcelotErrorCode.UnableToCompleteRequestError) + : base($"Error making http request, exception: {exception}", OcelotErrorCode.UnableToCompleteRequestError) { } } diff --git a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs index 4bfbad7a..125280b0 100644 --- a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs +++ b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs @@ -39,6 +39,12 @@ namespace Ocelot.Responder.Middleware { var errors = context.Errors; _logger.LogError($"{errors.Count} pipeline errors found in {MiddlewareName}. Setting error response status code"); + + foreach(var error in errors) + { + _logger.LogError(error.Message); + } + SetErrorResponse(context.HttpContext, errors); } else diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/RoutingTests.cs index 9110af12..6ede8524 100644 --- a/test/Ocelot.AcceptanceTests/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingTests.cs @@ -922,6 +922,55 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } + [Fact] + public void should_fix_issue_271() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/v1/{everything}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/api/v1/{everything}", + UpstreamHttpMethod = new List { "Get", "Put", "Post" }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 54879, + } + }, + }, + new FileReRoute + { + DownstreamPathTemplate = "/connect/token", + DownstreamScheme = "http", + UpstreamPathTemplate = "/connect/token", + UpstreamHttpMethod = new List { "Post" }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 5001, + } + }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:54879/", "/api/v1/modules/Test", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/v1/modules/Test")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) { _builder = new WebHostBuilder() From 4e25f72a97eea95483cfe90c98d83ba44210f588 Mon Sep 17 00:00:00 2001 From: Tom Gardham-Pallister Date: Wed, 14 Mar 2018 19:50:46 +0000 Subject: [PATCH 2/9] #271 Added some extra logging --- src/Ocelot/Request/Mapper/UnmappableRequestError.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ocelot/Request/Mapper/UnmappableRequestError.cs b/src/Ocelot/Request/Mapper/UnmappableRequestError.cs index 79379b6b..81668eef 100644 --- a/src/Ocelot/Request/Mapper/UnmappableRequestError.cs +++ b/src/Ocelot/Request/Mapper/UnmappableRequestError.cs @@ -5,7 +5,7 @@ public class UnmappableRequestError : Error { - public UnmappableRequestError(Exception ex) : base($"Error when parsing incoming request, exception: {ex.Message}", OcelotErrorCode.UnmappableRequestError) + public UnmappableRequestError(Exception exception) : base($"Error when parsing incoming request, exception: {exception}", OcelotErrorCode.UnmappableRequestError) { } } From f7c23d3384ae0b53a1235b224b8bff0cf4b23836 Mon Sep 17 00:00:00 2001 From: Tom Gardham-Pallister Date: Wed, 14 Mar 2018 19:55:24 +0000 Subject: [PATCH 3/9] #271 formatting --- src/Ocelot/Requester/HttpClientHttpRequester.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Ocelot/Requester/HttpClientHttpRequester.cs b/src/Ocelot/Requester/HttpClientHttpRequester.cs index a3b81fbc..29311ead 100644 --- a/src/Ocelot/Requester/HttpClientHttpRequester.cs +++ b/src/Ocelot/Requester/HttpClientHttpRequester.cs @@ -43,8 +43,7 @@ namespace Ocelot.Requester } catch (BrokenCircuitException exception) { - return - new ErrorResponse(new RequestTimedOutError(exception)); + return new ErrorResponse(new RequestTimedOutError(exception)); } catch (Exception exception) { From c1b315173f5be626a8eba98c7a71eebd9e7a10d5 Mon Sep 17 00:00:00 2001 From: Simon Jefford Date: Fri, 16 Mar 2018 17:48:11 +0000 Subject: [PATCH 4/9] Squash some warnings (#278) * squash SA1649 warnings (file/type name mismatch) * squash SA1127 warnings (generic constraint on own line) * squash SA1507 warnings (multiple blank lines) * squash package analysis warnings re: summary text It's not actually possible to provide a summary right now as per https://github.com/NuGet/Home/issues/4587 * squash missing castle.core reference warning * squash obsolete method warnings re: AddOcelotBaseUrl --- src/Ocelot/Ocelot.csproj | 3 ++- src/Ocelot/Requester/HttpClientHttpRequester.cs | 3 ++- ...actory.cs => IDelegatingHandlerHandlerFactory.cs} | 0 .../Ocelot.AcceptanceTests.csproj | 5 ----- test/Ocelot.IntegrationTests/RaftTests.cs | 2 ++ .../ConfigurationBuilderExtensionsTests.cs | 3 ++- .../DependencyInjection/OcelotBuilderTests.cs | 12 ++++++++---- .../Request/Mapper/RequestMapperTests.cs | 1 - 8 files changed, 16 insertions(+), 13 deletions(-) rename src/Ocelot/Requester/{IDelegatingHandlerHandlerProviderFactory.cs => IDelegatingHandlerHandlerFactory.cs} (100%) diff --git a/src/Ocelot/Ocelot.csproj b/src/Ocelot/Ocelot.csproj index 89ada54d..3d9b65c7 100644 --- a/src/Ocelot/Ocelot.csproj +++ b/src/Ocelot/Ocelot.csproj @@ -3,6 +3,7 @@ netcoreapp2.0 2.0.0 2.0.0 + true This 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. We have been unable to find this in my current workplace without having to write our own Javascript middlewares to handle the IdentityServer reference tokens. We would rather use the IdentityServer code that already exists to do this. 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 0.0.0-dev @@ -48,4 +49,4 @@ - \ No newline at end of file + diff --git a/src/Ocelot/Requester/HttpClientHttpRequester.cs b/src/Ocelot/Requester/HttpClientHttpRequester.cs index 29311ead..c6598903 100644 --- a/src/Ocelot/Requester/HttpClientHttpRequester.cs +++ b/src/Ocelot/Requester/HttpClientHttpRequester.cs @@ -75,7 +75,8 @@ namespace Ocelot.Requester } } - public class ReRouteDelegatingHandler where T : DelegatingHandler + public class ReRouteDelegatingHandler + where T : DelegatingHandler { public T DelegatingHandler { get; private set; } } diff --git a/src/Ocelot/Requester/IDelegatingHandlerHandlerProviderFactory.cs b/src/Ocelot/Requester/IDelegatingHandlerHandlerFactory.cs similarity index 100% rename from src/Ocelot/Requester/IDelegatingHandlerHandlerProviderFactory.cs rename to src/Ocelot/Requester/IDelegatingHandlerHandlerFactory.cs diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index ed09ed96..90d5bc64 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -50,9 +50,4 @@ - - - ..\..\..\..\Users\TGP\.nuget\packages\castle.core\4.2.1\lib\netstandard1.3\Castle.Core.dll - - diff --git a/test/Ocelot.IntegrationTests/RaftTests.cs b/test/Ocelot.IntegrationTests/RaftTests.cs index b8c24d5a..3dc01bb9 100644 --- a/test/Ocelot.IntegrationTests/RaftTests.cs +++ b/test/Ocelot.IntegrationTests/RaftTests.cs @@ -342,7 +342,9 @@ namespace Ocelot.IntegrationTests .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); config.AddJsonFile("configuration.json"); config.AddJsonFile("peers.json", optional: true, reloadOnChange: true); + #pragma warning disable CS0618 config.AddOcelotBaseUrl(url); + #pragma warning restore CS0618 config.AddEnvironmentVariables(); }) .ConfigureServices(x => diff --git a/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs index 0fcbf66e..5d32b1ed 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs @@ -22,9 +22,10 @@ namespace Ocelot.UnitTests.DependencyInjection private void GivenTheBaseUrl(string baseUrl) { + #pragma warning disable CS0618 var builder = new ConfigurationBuilder() .AddOcelotBaseUrl(baseUrl); - + #pragma warning restore CS0618 _configuration = builder.Build(); } diff --git a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs index df812f70..1cd22388 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs @@ -223,12 +223,14 @@ namespace Ocelot.UnitTests.DependencyInjection _ocelotBuilder.AddAdministration("/administration", options); } - private void AddTransientGlobalDelegatingHandler() where T : DelegatingHandler + private void AddTransientGlobalDelegatingHandler() + where T : DelegatingHandler { _ocelotBuilder.AddTransientDelegatingHandler(true); } - private void AddSpecificTransientDelegatingHandler() where T : DelegatingHandler + private void AddSpecificTransientDelegatingHandler() + where T : DelegatingHandler { _ocelotBuilder.AddTransientDelegatingHandler(); } @@ -298,12 +300,14 @@ namespace Ocelot.UnitTests.DependencyInjection } } - private void AddGlobalDelegatingHandler() where T : DelegatingHandler + private void AddGlobalDelegatingHandler() + where T : DelegatingHandler { _ocelotBuilder.AddSingletonDelegatingHandler(true); } - private void AddSpecificDelegatingHandler() where T : DelegatingHandler + private void AddSpecificDelegatingHandler() + where T : DelegatingHandler { _ocelotBuilder.AddSingletonDelegatingHandler(); } diff --git a/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs b/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs index ba719a45..22ff9276 100644 --- a/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs +++ b/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs @@ -163,7 +163,6 @@ .BDDfy(); } - [Fact] public void should_not_add_content_headers() { From ed11f3024c3d520a12cb23aa6e75a42bd36471ff Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Sat, 17 Mar 2018 11:35:16 +0000 Subject: [PATCH 5/9] Feature/#274 (#281) * #274 added acceptance tests, need to work out failing unit tests but probably going to be a redesign where we hold a reference to the cookie container and empty it if needed * #274 updated code coverage value * #274 offloaded cache logic to builder in preparation for adding state * #274 hacked something together but this is not right approach * #274 updated defaults and docs * #274 updated code coverage --- build.cake | 2 +- docs/features/configuration.rst | 17 +- .../File/FileHttpHandlerOptions.cs | 4 +- .../Requester/GlobalDelegatingHandler.cs | 14 ++ src/Ocelot/Requester/HttpClientBuilder.cs | 61 ++++++-- .../Requester/HttpClientHttpRequester.cs | 47 +----- src/Ocelot/Requester/HttpClientWrapper.cs | 2 +- src/Ocelot/Requester/IHttpClientBuilder.cs | 10 +- src/Ocelot/Requester/IHttpRequester.cs | 2 +- .../Requester/ReRouteDelegatingHandler.cs | 10 ++ test/Ocelot.AcceptanceTests/HeaderTests.cs | 121 +++++++++++++++ test/Ocelot.AcceptanceTests/Steps.cs | 5 + .../HttpHandlerOptionsCreatorTests.cs | 12 +- .../Requester/HttpClientBuilderTests.cs | 145 ++++++++++++++++-- .../Requester/HttpClientHttpRequesterTest.cs | 5 +- 15 files changed, 372 insertions(+), 85 deletions(-) create mode 100644 src/Ocelot/Requester/GlobalDelegatingHandler.cs create mode 100644 src/Ocelot/Requester/ReRouteDelegatingHandler.cs diff --git a/build.cake b/build.cake index af2f9364..c732864b 100644 --- a/build.cake +++ b/build.cake @@ -17,7 +17,7 @@ var artifactsDir = Directory("artifacts"); // unit testing var artifactsForUnitTestsDir = artifactsDir + Directory("UnitTests"); var unitTestAssemblies = @"./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj"; -var minCodeCoverage = 76.4d; +var minCodeCoverage = 82d; var coverallsRepoToken = "coveralls-repo-token-ocelot"; var coverallsRepo = "https://coveralls.io/github/TomPallister/Ocelot"; diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 93cae138..8a1e61a1 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -73,10 +73,17 @@ Follow Redirects / Use CookieContainer ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use HttpHandlerOptions in ReRoute configuration to set up HttpHandler behavior: -- _AllowAutoRedirect_ is a value that indicates whether the request should follow redirection responses. -Set it true if the request should automatically follow redirection responses from the Downstream resource; otherwise false. The default value is true. -- _UseCookieContainer_ is a value that indicates whether the handler uses the CookieContainer property to store server cookies and uses these cookies when sending requests. -The default value is true. + +1. AllowAutoRedirect is a value that indicates whether the request should follow redirection responses. Set it true if the request should automatically +follow redirection responses from the Downstream resource; otherwise false. The default value is false. +2. UseCookieContainer is a value that indicates whether the handler uses the CookieContainer +property to store server cookies and uses these cookies when sending requests. The default value is false. Please note +that if you are using the CookieContainer Ocelot caches the HttpClient for each downstream service. This means that all requests +to that DownstreamService will share the same cookies. `Issue 274 `_ was created because a user +noticed that the cookies were being shared. I tried to think of a nice way to handle this but I think it is impossible. If you don't cache the clients +that means each request gets a new client and therefore a new cookie container. If you clear the cookies from the cached client container you get race conditions due to inflight +requests. This would also mean that subsequent requests dont use the cookies from the previous response! All in all not a great situation. I would avoid setting +UseCookieContainer to true unless you have a really really good reason. Just look at your response headers and forward the cookies back with your next request! Multiple environments ^^^^^^^^^^^^^^^^^^^^^ @@ -127,4 +134,4 @@ finds your Consul agent and interacts to load and store the configuration from C I 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! I 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 second ttl cache before making a new request to your local consul agent. \ No newline at end of file +This feature has a 3 second ttl cache before making a new request to your local consul agent. diff --git a/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs b/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs index 7f24b572..2934254c 100644 --- a/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs +++ b/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs @@ -4,8 +4,8 @@ { public FileHttpHandlerOptions() { - AllowAutoRedirect = true; - UseCookieContainer = true; + AllowAutoRedirect = false; + UseCookieContainer = false; } public bool AllowAutoRedirect { get; set; } diff --git a/src/Ocelot/Requester/GlobalDelegatingHandler.cs b/src/Ocelot/Requester/GlobalDelegatingHandler.cs new file mode 100644 index 00000000..ba5e1c5f --- /dev/null +++ b/src/Ocelot/Requester/GlobalDelegatingHandler.cs @@ -0,0 +1,14 @@ +using System.Net.Http; + +namespace Ocelot.Requester +{ + public class GlobalDelegatingHandler + { + public GlobalDelegatingHandler(DelegatingHandler delegatingHandler) + { + DelegatingHandler = delegatingHandler; + } + + public DelegatingHandler DelegatingHandler { get; private set; } + } +} diff --git a/src/Ocelot/Requester/HttpClientBuilder.cs b/src/Ocelot/Requester/HttpClientBuilder.cs index b5604081..6cbb3aec 100644 --- a/src/Ocelot/Requester/HttpClientBuilder.cs +++ b/src/Ocelot/Requester/HttpClientBuilder.cs @@ -1,25 +1,61 @@ -using System.Linq; +using System; +using System.Linq; +using System.Net; using System.Net.Http; using Ocelot.Configuration; +using Ocelot.Logging; +using Ocelot.Middleware; namespace Ocelot.Requester { public class HttpClientBuilder : IHttpClientBuilder { private readonly IDelegatingHandlerHandlerFactory _factory; + private readonly IHttpClientCache _cacheHandlers; + private readonly IOcelotLogger _logger; + private string _cacheKey; + private HttpClient _httpClient; + private IHttpClient _client; + private HttpClientHandler _httpclientHandler; - public HttpClientBuilder(IDelegatingHandlerHandlerFactory house) + public HttpClientBuilder( + IDelegatingHandlerHandlerFactory factory, + IHttpClientCache cacheHandlers, + IOcelotLogger logger) { - _factory = house; + _factory = factory; + _cacheHandlers = cacheHandlers; + _logger = logger; } - public IHttpClient Create(DownstreamReRoute request) + public IHttpClient Create(DownstreamContext request) { - var httpclientHandler = new HttpClientHandler { AllowAutoRedirect = request.HttpHandlerOptions.AllowAutoRedirect, UseCookies = request.HttpHandlerOptions.UseCookieContainer}; - - var client = new HttpClient(CreateHttpMessageHandler(httpclientHandler, request)); - - return new HttpClientWrapper(client); + _cacheKey = GetCacheKey(request); + + var httpClient = _cacheHandlers.Get(_cacheKey); + + if (httpClient != null) + { + return httpClient; + } + + _httpclientHandler = new HttpClientHandler + { + AllowAutoRedirect = request.DownstreamReRoute.HttpHandlerOptions.AllowAutoRedirect, + UseCookies = request.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer, + CookieContainer = new CookieContainer() + }; + + _httpClient = new HttpClient(CreateHttpMessageHandler(_httpclientHandler, request.DownstreamReRoute)); + + _client = new HttpClientWrapper(_httpClient); + + return _client; + } + + public void Save() + { + _cacheHandlers.Set(_cacheKey, _client, TimeSpan.FromHours(24)); } private HttpMessageHandler CreateHttpMessageHandler(HttpMessageHandler httpMessageHandler, DownstreamReRoute request) @@ -39,5 +75,12 @@ namespace Ocelot.Requester }); return httpMessageHandler; } + + private string GetCacheKey(DownstreamContext request) + { + var baseUrl = $"{request.DownstreamRequest.RequestUri.Scheme}://{request.DownstreamRequest.RequestUri.Authority}{request.DownstreamRequest.RequestUri.AbsolutePath}"; + + return baseUrl; + } } } diff --git a/src/Ocelot/Requester/HttpClientHttpRequester.cs b/src/Ocelot/Requester/HttpClientHttpRequester.cs index c6598903..4202f611 100644 --- a/src/Ocelot/Requester/HttpClientHttpRequester.cs +++ b/src/Ocelot/Requester/HttpClientHttpRequester.cs @@ -24,17 +24,15 @@ namespace Ocelot.Requester _factory = house; } - public async Task> GetResponse(DownstreamContext request) + public async Task> GetResponse(DownstreamContext context) { - var builder = new HttpClientBuilder(_factory); + var builder = new HttpClientBuilder(_factory, _cacheHandlers, _logger); - var cacheKey = GetCacheKey(request); - - var httpClient = GetHttpClient(cacheKey, builder, request); + var httpClient = builder.Create(context); try { - var response = await httpClient.SendAsync(request.DownstreamRequest); + var response = await httpClient.SendAsync(context.DownstreamRequest); return new OkResponse(response); } catch (TimeoutRejectedException exception) @@ -51,43 +49,8 @@ namespace Ocelot.Requester } finally { - _cacheHandlers.Set(cacheKey, httpClient, TimeSpan.FromHours(24)); + builder.Save(); } } - - private IHttpClient GetHttpClient(string cacheKey, IHttpClientBuilder builder, DownstreamContext request) - { - var httpClient = _cacheHandlers.Get(cacheKey); - - if (httpClient == null) - { - httpClient = builder.Create(request.DownstreamReRoute); - } - - return httpClient; - } - - private string GetCacheKey(DownstreamContext request) - { - var baseUrl = $"{request.DownstreamRequest.RequestUri.Scheme}://{request.DownstreamRequest.RequestUri.Authority}"; - - return baseUrl; - } - } - - public class ReRouteDelegatingHandler - where T : DelegatingHandler - { - public T DelegatingHandler { get; private set; } - } - - public class GlobalDelegatingHandler - { - public GlobalDelegatingHandler(DelegatingHandler delegatingHandler) - { - DelegatingHandler = delegatingHandler; - } - - public DelegatingHandler DelegatingHandler { get; private set; } } } diff --git a/src/Ocelot/Requester/HttpClientWrapper.cs b/src/Ocelot/Requester/HttpClientWrapper.cs index 21e74e48..b1f0345a 100644 --- a/src/Ocelot/Requester/HttpClientWrapper.cs +++ b/src/Ocelot/Requester/HttpClientWrapper.cs @@ -6,7 +6,7 @@ namespace Ocelot.Requester /// /// This class was made to make unit testing easier when HttpClient is used. /// - internal class HttpClientWrapper : IHttpClient + public class HttpClientWrapper : IHttpClient { public HttpClient Client { get; } diff --git a/src/Ocelot/Requester/IHttpClientBuilder.cs b/src/Ocelot/Requester/IHttpClientBuilder.cs index f10e55f5..cc8c6160 100644 --- a/src/Ocelot/Requester/IHttpClientBuilder.cs +++ b/src/Ocelot/Requester/IHttpClientBuilder.cs @@ -1,14 +1,10 @@ -using System.Net.Http; -using Ocelot.Configuration; +using Ocelot.Middleware; namespace Ocelot.Requester { public interface IHttpClientBuilder { - /// - /// Creates the - /// - /// - IHttpClient Create(DownstreamReRoute request); + IHttpClient Create(DownstreamContext request); + void Save(); } } diff --git a/src/Ocelot/Requester/IHttpRequester.cs b/src/Ocelot/Requester/IHttpRequester.cs index 5d9aa5dc..86f6209a 100644 --- a/src/Ocelot/Requester/IHttpRequester.cs +++ b/src/Ocelot/Requester/IHttpRequester.cs @@ -7,6 +7,6 @@ namespace Ocelot.Requester { public interface IHttpRequester { - Task> GetResponse(DownstreamContext request); + Task> GetResponse(DownstreamContext context); } } diff --git a/src/Ocelot/Requester/ReRouteDelegatingHandler.cs b/src/Ocelot/Requester/ReRouteDelegatingHandler.cs new file mode 100644 index 00000000..0a5c5472 --- /dev/null +++ b/src/Ocelot/Requester/ReRouteDelegatingHandler.cs @@ -0,0 +1,10 @@ +using System.Net.Http; + +namespace Ocelot.Requester +{ + public class ReRouteDelegatingHandler + where T : DelegatingHandler + { + public T DelegatingHandler { get; private set; } + } +} diff --git a/test/Ocelot.AcceptanceTests/HeaderTests.cs b/test/Ocelot.AcceptanceTests/HeaderTests.cs index 350e1e52..2bd3c2ab 100644 --- a/test/Ocelot.AcceptanceTests/HeaderTests.cs +++ b/test/Ocelot.AcceptanceTests/HeaderTests.cs @@ -16,6 +16,8 @@ namespace Ocelot.AcceptanceTests public class HeaderTests : IDisposable { private IWebHost _builder; + private string _cookieValue; + private int _count; private readonly Steps _steps; public HeaderTests() @@ -184,6 +186,125 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } + [Fact] + public void request_should_reuse_cookies_with_cookie_container() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/sso/{everything}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 6774, + } + }, + UpstreamPathTemplate = "/sso/{everything}", + UpstreamHttpMethod = new List { "Get", "Post", "Options" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseCookieContainer = true + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6774", "/sso/test", 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseHeaderIs("Set-Cookie", "test=0; path=/")) + .And(x => _steps.GivenIAddCookieToMyRequest("test=1; path=/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void request_should_have_own_cookies_no_cookie_container() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/sso/{everything}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 6775, + } + }, + UpstreamPathTemplate = "/sso/{everything}", + UpstreamHttpMethod = new List { "Get", "Post", "Options" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseCookieContainer = false + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6775", "/sso/test", 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseHeaderIs("Set-Cookie", "test=0; path=/")) + .And(x => _steps.GivenIAddCookieToMyRequest("test=1; path=/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + if(_count == 0) + { + context.Response.Cookies.Append("test", "0"); + _count++; + context.Response.StatusCode = statusCode; + return; + } + + if(context.Request.Cookies.TryGetValue("test", out var cookieValue) || context.Request.Headers.TryGetValue("Set-Cookie", out var headerValue)) + { + if(cookieValue == "0" || headerValue == "test=1; path=/") + { + context.Response.StatusCode = statusCode; + return; + } + } + + context.Response.StatusCode = 500; + }); + }) + .Build(); + + _builder.Start(); + } + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey) { _builder = new WebHostBuilder() diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 0de7113d..2cb0f830 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -274,6 +274,11 @@ namespace Ocelot.AcceptanceTests _ocelotClient = _ocelotServer.CreateClient(); } + internal void GivenIAddCookieToMyRequest(string cookie) + { + _ocelotClient.DefaultRequestHeaders.Add("Set-Cookie", cookie); + } + /// /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. /// diff --git a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs index b0b11bfc..ca22956c 100644 --- a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs @@ -19,10 +19,10 @@ namespace Ocelot.UnitTests.Configuration } [Fact] - public void should_create_options_with_useCookie_and_allowAutoRedirect_true_as_default() + public void should_create_options_with_useCookie_false_and_allowAutoRedirect_true_as_default() { var fileReRoute = new FileReRoute(); - var expectedOptions = new HttpHandlerOptions(true, true, false); + var expectedOptions = new HttpHandlerOptions(false, false, false); this.Given(x => GivenTheFollowing(fileReRoute)) .When(x => WhenICreateHttpHandlerOptions()) @@ -61,12 +61,12 @@ namespace Ocelot.UnitTests.Configuration _httpHandlerOptions = _httpHandlerOptionsCreator.Create(_fileReRoute); } - private void ThenTheFollowingOptionsReturned(HttpHandlerOptions options) + private void ThenTheFollowingOptionsReturned(HttpHandlerOptions expected) { _httpHandlerOptions.ShouldNotBeNull(); - _httpHandlerOptions.AllowAutoRedirect.ShouldBe(options.AllowAutoRedirect); - _httpHandlerOptions.UseCookieContainer.ShouldBe(options.UseCookieContainer); - _httpHandlerOptions.UseTracing.ShouldBe(options.UseTracing); + _httpHandlerOptions.AllowAutoRedirect.ShouldBe(expected.AllowAutoRedirect); + _httpHandlerOptions.UseCookieContainer.ShouldBe(expected.UseCookieContainer); + _httpHandlerOptions.UseTracing.ShouldBe(expected.UseTracing); } } } diff --git a/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs b/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs index c4ab1341..7368fac8 100644 --- a/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs @@ -1,9 +1,19 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Moq; using Ocelot.Configuration; using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Middleware; using Ocelot.Requester; using Ocelot.Responses; using Shouldly; @@ -12,25 +22,37 @@ using Xunit; namespace Ocelot.UnitTests.Requester { - public class HttpClientBuilderTests + public class HttpClientBuilderTests : IDisposable { private readonly HttpClientBuilder _builder; private readonly Mock _factory; private IHttpClient _httpClient; private HttpResponseMessage _response; - private DownstreamReRoute _request; + private DownstreamContext _context; + private readonly Mock _cacheHandlers; + private Mock _logger; + private int _count; + private IWebHost _host; public HttpClientBuilderTests() { + _cacheHandlers = new Mock(); + _logger = new Mock(); _factory = new Mock(); - _builder = new HttpClientBuilder(_factory.Object); + _builder = new HttpClientBuilder(_factory.Object, _cacheHandlers.Object, _logger.Object); } [Fact] public void should_build_http_client() { + var reRoute = new DownstreamReRouteBuilder() + .WithIsQos(false) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) + .WithReRouteKey("") + .Build(); + this.Given(x => GivenTheFactoryReturns()) - .And(x => GivenARequest()) + .And(x => GivenARequest(reRoute)) .When(x => WhenIBuild()) .Then(x => ThenTheHttpClientShouldNotBeNull()) .BDDfy(); @@ -39,6 +61,12 @@ namespace Ocelot.UnitTests.Requester [Fact] public void should_call_delegating_handlers_in_order() { + var reRoute = new DownstreamReRouteBuilder() + .WithIsQos(false) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) + .WithReRouteKey("") + .Build(); + var fakeOne = new FakeDelegatingHandler(); var fakeTwo = new FakeDelegatingHandler(); @@ -49,7 +77,7 @@ namespace Ocelot.UnitTests.Requester }; this.Given(x => GivenTheFactoryReturns(handlers)) - .And(x => GivenARequest()) + .And(x => GivenARequest(reRoute)) .And(x => WhenIBuild()) .When(x => WhenICallTheClient()) .Then(x => ThenTheFakeAreHandledInOrder(fakeOne, fakeTwo)) @@ -57,12 +85,95 @@ namespace Ocelot.UnitTests.Requester .BDDfy(); } - private void GivenARequest() + [Fact] + public void should_re_use_cookies_from_container() { - var reRoute = new DownstreamReRouteBuilder().WithIsQos(false) - .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)).WithReRouteKey("").Build(); + var reRoute = new DownstreamReRouteBuilder() + .WithIsQos(false) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, true, false)) + .WithReRouteKey("") + .Build(); - _request = reRoute; + this.Given(_ => GivenADownstreamService()) + .And(_ => GivenARequest(reRoute)) + .And(_ => GivenTheFactoryReturnsNothing()) + .And(_ => WhenIBuild()) + .And(_ => WhenICallTheClient("http://localhost:5003")) + .And(_ => ThenTheCookieIsSet()) + .And(_ => GivenTheClientIsCached()) + .And(_ => WhenIBuild()) + .When(_ => WhenICallTheClient("http://localhost:5003")) + .Then(_ => ThenTheResponseIsOk()) + .BDDfy(); + } + + private void GivenTheClientIsCached() + { + _cacheHandlers.Setup(x => x.Get(It.IsAny())).Returns(_httpClient); + } + + private void ThenTheCookieIsSet() + { + _response.Headers.TryGetValues("Set-Cookie", out var test).ShouldBeTrue(); + } + + private void WhenICallTheClient(string url) + { + _response = _httpClient + .SendAsync(new HttpRequestMessage(HttpMethod.Get, url)) + .GetAwaiter() + .GetResult(); + } + + private void ThenTheResponseIsOk() + { + _response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + private void GivenADownstreamService() + { + _host = new WebHostBuilder() + .UseUrls("http://localhost:5003") + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.Run(async context => + { + if (_count == 0) + { + context.Response.Cookies.Append("test", "0"); + context.Response.StatusCode = 200; + _count++; + return; + } + if (_count == 1) + { + if (context.Request.Cookies.TryGetValue("test", out var cookieValue) || context.Request.Headers.TryGetValue("Set-Cookie", out var headerValue)) + { + context.Response.StatusCode = 200; + return; + } + + context.Response.StatusCode = 500; + } + }); + }) + .Build(); + + _host.Start(); + } + + private void GivenARequest(DownstreamReRoute downstream) + { + var context = new DownstreamContext(new DefaultHttpContext()) + { + DownstreamReRoute = downstream, + DownstreamRequest = new HttpRequestMessage() { RequestUri = new Uri("http://localhost:5003") }, + }; + + _context = context; } private void ThenSomethingIsReturned() @@ -88,6 +199,14 @@ namespace Ocelot.UnitTests.Requester .Setup(x => x.Get(It.IsAny())) .Returns(new OkResponse>>(handlers)); } + private void GivenTheFactoryReturnsNothing() + { + var handlers = new List>(); + + _factory + .Setup(x => x.Get(It.IsAny())) + .Returns(new OkResponse>>(handlers)); + } private void GivenTheFactoryReturns(List> handlers) { @@ -98,12 +217,18 @@ namespace Ocelot.UnitTests.Requester private void WhenIBuild() { - _httpClient = _builder.Create(_request); + _httpClient = _builder.Create(_context); } private void ThenTheHttpClientShouldNotBeNull() { _httpClient.ShouldNotBeNull(); } + + public void Dispose() + { + _response?.Dispose(); + _host?.Dispose(); + } } } diff --git a/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs b/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs index f2c1f3a0..bbf59692 100644 --- a/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs +++ b/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs @@ -35,7 +35,10 @@ namespace Ocelot.UnitTests.Requester .Setup(x => x.CreateLogger()) .Returns(_logger.Object); _cacheHandlers = new Mock(); - _httpClientRequester = new HttpClientHttpRequester(_loggerFactory.Object, _cacheHandlers.Object, _house.Object); + _httpClientRequester = new HttpClientHttpRequester( + _loggerFactory.Object, + _cacheHandlers.Object, + _house.Object); } [Fact] From 8a2f76d0c5c42d79a3d39045b574245f278877ec Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Sat, 17 Mar 2018 12:54:17 +0000 Subject: [PATCH 6/9] #268 fix flakey acceptance test (#282) * #268 added waiter to test, altho i wasn't able to replicate flakeyness with wait anyway! Hopefully this will be solid now! * #268 fixed a warning * #268 more code coverage --- .../ConsulFileConfigurationPoller.cs | 15 ++- .../Repository/IConsulPollerConfiguration.cs | 8 ++ .../InMemoryConsulPollerConfiguration.cs | 7 ++ .../DependencyInjection/OcelotBuilder.cs | 1 + .../Ocelot/Infrastructure}/Wait.cs | 4 +- .../Ocelot/Infrastructure}/Waiter.cs | 94 +++++++++---------- .../ConfigurationInConsulTests.cs | 30 +++--- .../ConsulFileConfigurationPollerTests.cs | 84 +++++++++++++++-- .../ServerHostedMiddlewareTest.cs | 68 -------------- 9 files changed, 168 insertions(+), 143 deletions(-) create mode 100644 src/Ocelot/Configuration/Repository/IConsulPollerConfiguration.cs create mode 100644 src/Ocelot/Configuration/Repository/InMemoryConsulPollerConfiguration.cs rename {test/Ocelot.UnitTests => src/Ocelot/Infrastructure}/Wait.cs (82%) rename {test/Ocelot.UnitTests => src/Ocelot/Infrastructure}/Waiter.cs (93%) delete mode 100644 test/Ocelot.UnitTests/ServerHostedMiddlewareTest.cs diff --git a/src/Ocelot/Configuration/Repository/ConsulFileConfigurationPoller.cs b/src/Ocelot/Configuration/Repository/ConsulFileConfigurationPoller.cs index a2780062..79efddca 100644 --- a/src/Ocelot/Configuration/Repository/ConsulFileConfigurationPoller.cs +++ b/src/Ocelot/Configuration/Repository/ConsulFileConfigurationPoller.cs @@ -17,10 +17,16 @@ namespace Ocelot.Configuration.Repository private string _previousAsJson; private readonly Timer _timer; private bool _polling; + private readonly IConsulPollerConfiguration _config; - public ConsulFileConfigurationPoller(IOcelotLoggerFactory factory, IFileConfigurationRepository repo, IFileConfigurationSetter setter) + public ConsulFileConfigurationPoller( + IOcelotLoggerFactory factory, + IFileConfigurationRepository repo, + IFileConfigurationSetter setter, + IConsulPollerConfiguration config) { _setter = setter; + _config = config; _logger = factory.CreateLogger(); _repo = repo; _previousAsJson = ""; @@ -30,11 +36,11 @@ namespace Ocelot.Configuration.Repository { return; } - + _polling = true; await Poll(); _polling = false; - }, null, 0, 1000); + }, null, 0, _config.Delay); } private async Task Poll() @@ -63,8 +69,7 @@ namespace Ocelot.Configuration.Repository /// /// We could do object comparison here but performance isnt really a problem. This might be an issue one day! /// - /// - /// + /// hash of the config private string ToJson(FileConfiguration config) { var currentHash = JsonConvert.SerializeObject(config); diff --git a/src/Ocelot/Configuration/Repository/IConsulPollerConfiguration.cs b/src/Ocelot/Configuration/Repository/IConsulPollerConfiguration.cs new file mode 100644 index 00000000..93003087 --- /dev/null +++ b/src/Ocelot/Configuration/Repository/IConsulPollerConfiguration.cs @@ -0,0 +1,8 @@ +namespace Ocelot.Configuration.Repository +{ + public interface IConsulPollerConfiguration + { + int Delay { get; } + } + + } diff --git a/src/Ocelot/Configuration/Repository/InMemoryConsulPollerConfiguration.cs b/src/Ocelot/Configuration/Repository/InMemoryConsulPollerConfiguration.cs new file mode 100644 index 00000000..9e411f76 --- /dev/null +++ b/src/Ocelot/Configuration/Repository/InMemoryConsulPollerConfiguration.cs @@ -0,0 +1,7 @@ +namespace Ocelot.Configuration.Repository +{ + public class InMemoryConsulPollerConfiguration : IConsulPollerConfiguration + { + public int Delay => 1000; + } +} diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 9e2e763b..eccab083 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -148,6 +148,7 @@ namespace Ocelot.DependencyInjection // We add this here so that we can always inject something into the factory for IoC.. _services.AddSingleton(); + _services.TryAddSingleton(); } public IOcelotAdministrationBuilder AddAdministration(string path, string secret) diff --git a/test/Ocelot.UnitTests/Wait.cs b/src/Ocelot/Infrastructure/Wait.cs similarity index 82% rename from test/Ocelot.UnitTests/Wait.cs rename to src/Ocelot/Infrastructure/Wait.cs index d8fd3a88..934e31e5 100644 --- a/test/Ocelot.UnitTests/Wait.cs +++ b/src/Ocelot/Infrastructure/Wait.cs @@ -1,4 +1,4 @@ -namespace Ocelot.UnitTests +namespace Ocelot.Infrastructure { public class Wait { @@ -7,4 +7,4 @@ namespace Ocelot.UnitTests return new Waiter(milliSeconds); } } -} \ No newline at end of file +} diff --git a/test/Ocelot.UnitTests/Waiter.cs b/src/Ocelot/Infrastructure/Waiter.cs similarity index 93% rename from test/Ocelot.UnitTests/Waiter.cs rename to src/Ocelot/Infrastructure/Waiter.cs index 8847c5b9..730bdbff 100644 --- a/test/Ocelot.UnitTests/Waiter.cs +++ b/src/Ocelot/Infrastructure/Waiter.cs @@ -1,47 +1,47 @@ -using System; -using System.Diagnostics; - -namespace Ocelot.UnitTests -{ - public class Waiter - { - private readonly int _milliSeconds; - - public Waiter(int milliSeconds) - { - _milliSeconds = milliSeconds; - } - - public bool Until(Func condition) - { - var stopwatch = Stopwatch.StartNew(); - var passed = false; - while (stopwatch.ElapsedMilliseconds < _milliSeconds) - { - if (condition.Invoke()) - { - passed = true; - break; - } - } - - return passed; - } - - public bool Until(Func condition) - { - var stopwatch = Stopwatch.StartNew(); - var passed = false; - while (stopwatch.ElapsedMilliseconds < _milliSeconds) - { - if (condition.Invoke()) - { - passed = true; - break; - } - } - - return passed; - } - } -} +using System; +using System.Diagnostics; + +namespace Ocelot.Infrastructure +{ + public class Waiter + { + private readonly int _milliSeconds; + + public Waiter(int milliSeconds) + { + _milliSeconds = milliSeconds; + } + + public bool Until(Func condition) + { + var stopwatch = Stopwatch.StartNew(); + var passed = false; + while (stopwatch.ElapsedMilliseconds < _milliSeconds) + { + if (condition.Invoke()) + { + passed = true; + break; + } + } + + return passed; + } + + public bool Until(Func condition) + { + var stopwatch = Stopwatch.StartNew(); + var passed = false; + while (stopwatch.ElapsedMilliseconds < _milliSeconds) + { + if (condition.Invoke()) + { + passed = true; + break; + } + } + + return passed; + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs b/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs index 7c9ac239..797f340c 100644 --- a/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs +++ b/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs @@ -9,8 +9,10 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Ocelot.Configuration.File; +using Shouldly; using TestStack.BDDfy; using Xunit; +using static Ocelot.Infrastructure.Wait; namespace Ocelot.AcceptanceTests { @@ -261,21 +263,27 @@ namespace Ocelot.AcceptanceTests .And(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => GivenTheConsulConfigurationIs(secondConsulConfig)) - .And(x => GivenIWaitForTheConfigToReplicateToOcelot()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status/awesome")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(x => GivenTheConsulConfigurationIs(secondConsulConfig)) + .Then(x => ThenTheConfigIsUpdatedInOcelot()) .BDDfy(); } - private void GivenIWaitForTheConfigToReplicateToOcelot() + private void ThenTheConfigIsUpdatedInOcelot() { - var stopWatch = Stopwatch.StartNew(); - while (stopWatch.ElapsedMilliseconds < 10000) - { - //do nothing! - } + var result = 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) diff --git a/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs b/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs index 4a6faba2..6499c4e0 100644 --- a/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs @@ -1,16 +1,17 @@ using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Threading; using Moq; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.Configuration.Setter; using Ocelot.Logging; using Ocelot.Responses; +using Ocelot.UnitTests.Responder; using TestStack.BDDfy; using Xunit; using Shouldly; -using static Ocelot.UnitTests.Wait; +using static Ocelot.Infrastructure.Wait; namespace Ocelot.UnitTests.Configuration { @@ -21,6 +22,7 @@ namespace Ocelot.UnitTests.Configuration private Mock _repo; private Mock _setter; private FileConfiguration _fileConfig; + private Mock _config; public ConsulFileConfigurationPollerTests() { @@ -30,8 +32,10 @@ namespace Ocelot.UnitTests.Configuration _repo = new Mock(); _setter = new Mock(); _fileConfig = new FileConfiguration(); + _config = new Mock(); _repo.Setup(x => x.Get()).ReturnsAsync(new OkResponse(_fileConfig)); - _poller = new ConsulFileConfigurationPoller(_factory.Object, _repo.Object, _setter.Object); + _config.Setup(x => x.Delay).Returns(10); + _poller = new ConsulFileConfigurationPoller(_factory.Object, _repo.Object, _setter.Object, _config.Object); } public void Dispose() @@ -42,7 +46,7 @@ namespace Ocelot.UnitTests.Configuration [Fact] public void should_start() { - this.Given(x => ThenTheSetterIsCalled(_fileConfig)) + this.Given(x => ThenTheSetterIsCalled(_fileConfig, 1)) .BDDfy(); } @@ -65,22 +69,82 @@ namespace Ocelot.UnitTests.Configuration } }; - this.Given(x => WhenTheConfigIsChangedInConsul(newConfig)) - .Then(x => ThenTheSetterIsCalled(newConfig)) + this.Given(x => WhenTheConfigIsChangedInConsul(newConfig, 0)) + .Then(x => ThenTheSetterIsCalled(newConfig, 1)) .BDDfy(); } - private void WhenTheConfigIsChangedInConsul(FileConfiguration newConfig) + [Fact] + public void should_not_poll_if_already_polling() { - _repo.Setup(x => x.Get()).ReturnsAsync(new OkResponse(newConfig)); + var newConfig = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "test" + } + }, + } + } + }; + + this.Given(x => WhenTheConfigIsChangedInConsul(newConfig, 10)) + .Then(x => ThenTheSetterIsCalled(newConfig, 1)) + .BDDfy(); } - private void ThenTheSetterIsCalled(FileConfiguration fileConfig) + [Fact] + public void should_do_nothing_if_call_to_consul_fails() + { + var newConfig = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "test" + } + }, + } + } + }; + + this.Given(x => WhenConsulErrors()) + .Then(x => ThenTheSetterIsCalled(newConfig, 0)) + .BDDfy(); + } + + private void WhenConsulErrors() + { + _repo + .Setup(x => x.Get()) + .ReturnsAsync(new ErrorResponse(new AnyError())); + } + + private void WhenTheConfigIsChangedInConsul(FileConfiguration newConfig, int delay) + { + _repo + .Setup(x => x.Get()) + .Callback(() => Thread.Sleep(delay)) + .ReturnsAsync(new OkResponse(newConfig)); + } + + private void ThenTheSetterIsCalled(FileConfiguration fileConfig, int times) { var result = WaitFor(2000).Until(() => { try { - _setter.Verify(x => x.Set(fileConfig), Times.Once); + _setter.Verify(x => x.Set(fileConfig), Times.Exactly(times)); return true; } catch(Exception) diff --git a/test/Ocelot.UnitTests/ServerHostedMiddlewareTest.cs b/test/Ocelot.UnitTests/ServerHostedMiddlewareTest.cs deleted file mode 100644 index 15a2d673..00000000 --- a/test/Ocelot.UnitTests/ServerHostedMiddlewareTest.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace Ocelot.UnitTests -{ - using System; - using System.IO; - using System.Net.Http; - using Microsoft.AspNetCore.TestHost; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.AspNetCore.Builder; - using Moq; - using Ocelot.Infrastructure.RequestData; - - public abstract class ServerHostedMiddlewareTest : IDisposable - { - protected TestServer Server { get; private set; } - protected HttpClient Client { get; private set; } - protected string Url { get; private set; } - protected HttpResponseMessage ResponseMessage { get; private set; } - protected Mock ScopedRepository { get; private set; } - - public ServerHostedMiddlewareTest() - { - Url = "http://localhost:51879"; - ScopedRepository = new Mock(); - } - - protected virtual void GivenTheTestServerIsConfigured() - { - var builder = new WebHostBuilder() - .ConfigureServices(x => GivenTheTestServerServicesAreConfigured(x)) - .UseUrls(Url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .Configure(app => GivenTheTestServerPipelineIsConfigured(app)); - - Server = new TestServer(builder); - Client = Server.CreateClient(); - } - - protected virtual void GivenTheTestServerServicesAreConfigured(IServiceCollection services) - { - // override this in your test fixture to set up service dependencies - } - - protected virtual void GivenTheTestServerPipelineIsConfigured(IApplicationBuilder app) - { - // override this in your test fixture to set up the test server pipeline - } - - protected void WhenICallTheMiddleware() - { - ResponseMessage = Client.GetAsync(Url).Result; - } - - protected void WhenICallTheMiddlewareWithTheRequestIdKey(string requestIdKey, string value) - { - Client.DefaultRequestHeaders.Add(requestIdKey, value); - ResponseMessage = Client.GetAsync(Url).Result; - } - - public void Dispose() - { - Client.Dispose(); - Server.Dispose(); - } - } -} From 0ab670a14327aa2d2ee1930173186d786d760f0f Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Sat, 17 Mar 2018 18:07:27 +0000 Subject: [PATCH 7/9] Feature/#52 (#283) * #52 test circle ci * #52 nicked some lads cake script * #52 put the mac build script back * #52 trying another lads circle CI thing doesnt use cake * #52 added test steps * #52 ports for linux build * #52 try travis mac build * #52 dont use build script * #52 dont use build script * #52 acceptance and int tests dont really work on mac...v strange? * #52 unique port for linux tests * #52 increase code coverage * #52 try using cake on linux for travis * #52 try using cake for mac and linux on travis * #52 dont run the acceptance and int tests on mac * #52 build.sh has lf line endings * #52 turns out crlf is OK for cake file..sigh * #52 not sure what return does in cake so wrapped in if just to see * #52 try use travis to work not run on mac * #52 dont need these references * #52 wrong property * #52 remove circle ci for linux and just use travis for all --- .circleci/config.yml | 17 ++++++ .travis.yml | 31 +++++++++++ README.md | 3 +- build.cake | 36 +++++++++++++ build.sh | 2 +- .../LoadBalancerTests.cs | 4 +- test/Ocelot.AcceptanceTests/QoSTests.cs | 4 +- .../OcelotPipelineExtensionsTests.cs | 53 +++++++++++++++++++ 8 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 .circleci/config.yml create mode 100644 .travis.yml create mode 100644 test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..8abd6ffd --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,17 @@ +version: 2 +jobs: + build: + working_directory: /temp + docker: + - image: microsoft/dotnet:sdk + environment: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + steps: + - checkout + - run: dotnet restore + - run: dotnet build + - run: dotnet test ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj + - run: dotnet test ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj + - run: dotnet test ./test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..ef0701aa --- /dev/null +++ b/.travis.yml @@ -0,0 +1,31 @@ +language: csharp +os: + - osx + - linux + +# Ubuntu 14.04 +sudo: required +dist: trusty + +# OS X 10.12 +osx_image: xcode9.2 + +mono: + - 4.4.2 + +dotnet: 2.1.4 + +before_install: + - git fetch --unshallow # Travis always does a shallow clone, but GitVersion needs the full history including branches and tags + - git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" + - git fetch origin + +script: + - ./build.sh + +cache: + directories: + - .packages + - tools/Addins + - tools/gitreleasemanager + - tools/GitVersion.CommandLine \ No newline at end of file diff --git a/README.md b/README.md index 6bbc77e0..ab3d89a5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [](http://threemammals.com/ocelot) -[![Build status](https://ci.appveyor.com/api/projects/status/r6sv51qx36sis1je?svg=true)](https://ci.appveyor.com/project/TomPallister/ocelot-fcfpb) +[![Build status](https://ci.appveyor.com/api/projects/status/r6sv51qx36sis1je?branch=develop&svg=true)](https://ci.appveyor.com/project/TomPallister/ocelot-fcfpb) Windows (AppVeyor) +[![Build Status](https://travis-ci.org/ThreeMammals/Ocelot.svg?branch=develop)](https://travis-ci.org/ThreeMammals/Ocelot) Linux & OSX (Travis) [![Windows Build history](https://buildstats.info/appveyor/chart/TomPallister/ocelot-fcfpb?branch=develop&includeBuildsFromPullRequest=false)](https://ci.appveyor.com/project/TomPallister/ocelot-fcfpb/history?branch=develop) diff --git a/build.cake b/build.cake index c732864b..f757228c 100644 --- a/build.cake +++ b/build.cake @@ -189,6 +189,24 @@ Task("RunAcceptanceTests") .IsDependentOn("Compile") .Does(() => { + if(TravisCI.IsRunningOnTravisCI) + { + Information( + @"Job: + JobId: {0} + JobNumber: {1} + OSName: {2}", + BuildSystem.TravisCI.Environment.Job.JobId, + BuildSystem.TravisCI.Environment.Job.JobNumber, + BuildSystem.TravisCI.Environment.Job.OSName + ); + + if(TravisCI.Environment.Job.OSName.ToLower() == "osx") + { + return; + } + } + var settings = new DotNetCoreTestSettings { Configuration = compileConfig, @@ -205,6 +223,24 @@ Task("RunIntegrationTests") .IsDependentOn("Compile") .Does(() => { + if(TravisCI.IsRunningOnTravisCI) + { + Information( + @"Job: + JobId: {0} + JobNumber: {1} + OSName: {2}", + BuildSystem.TravisCI.Environment.Job.JobId, + BuildSystem.TravisCI.Environment.Job.JobNumber, + BuildSystem.TravisCI.Environment.Job.OSName + ); + + if(TravisCI.Environment.Job.OSName.ToLower() == "osx") + { + return; + } + } + var settings = new DotNetCoreTestSettings { Configuration = compileConfig, diff --git a/build.sh b/build.sh index 04731adf..5b3d6020 100755 --- a/build.sh +++ b/build.sh @@ -98,4 +98,4 @@ if $SHOW_VERSION; then exec mono "$CAKE_EXE" -version else exec mono "$CAKE_EXE" $SCRIPT -verbosity=$VERBOSITY -configuration=$CONFIGURATION -target=$TARGET $DRYRUN "${SCRIPT_ARGUMENTS[@]}" -fi \ No newline at end of file +fi diff --git a/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs b/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs index d67e7e41..f60008bd 100644 --- a/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs +++ b/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs @@ -31,7 +31,7 @@ namespace Ocelot.AcceptanceTests public void should_use_service_discovery_and_load_balance_request() { var downstreamServiceOneUrl = "http://localhost:50881"; - var downstreamServiceTwoUrl = "http://localhost:50882"; + var downstreamServiceTwoUrl = "http://localhost:50892"; var configuration = new FileConfiguration { @@ -54,7 +54,7 @@ namespace Ocelot.AcceptanceTests new FileHostAndPort { Host = "localhost", - Port = 50882 + Port = 50892 } } } diff --git a/test/Ocelot.AcceptanceTests/QoSTests.cs b/test/Ocelot.AcceptanceTests/QoSTests.cs index d11721b8..5e82ba3d 100644 --- a/test/Ocelot.AcceptanceTests/QoSTests.cs +++ b/test/Ocelot.AcceptanceTests/QoSTests.cs @@ -41,7 +41,7 @@ namespace Ocelot.AcceptanceTests new FileHostAndPort { Host = "localhost", - Port = 51872, + Port = 51892, } }, UpstreamPathTemplate = "/", @@ -56,7 +56,7 @@ namespace Ocelot.AcceptanceTests } }; - this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51872", "Hello from Laura")) + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51892", "Hello from Laura")) .Given(x => _steps.GivenThereIsAConfiguration(configuration)) .Given(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) diff --git a/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs new file mode 100644 index 00000000..d61124ba --- /dev/null +++ b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs @@ -0,0 +1,53 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; +using Ocelot.Middleware.Multiplexer; +using Ocelot.Middleware.Pipeline; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Middleware +{ + public class OcelotPipelineExtensionsTests + { + private OcelotPipelineBuilder _builder; + private OcelotRequestDelegate _handlers; + + [Fact] + public void should_set_up_pipeline() + { + this.Given(_ => GivenTheDepedenciesAreSetUp()) + .When(_ => WhenIBuild()) + .Then(_ => ThenThePipelineIsBuilt()) + .BDDfy(); + } + + private void ThenThePipelineIsBuilt() + { + _handlers.ShouldNotBeNull(); + } + + private void WhenIBuild() + { + _handlers = _builder.BuildOcelotPipeline(new OcelotPipelineConfiguration()); + } + + private void GivenTheDepedenciesAreSetUp() + { + IConfigurationBuilder test = new ConfigurationBuilder(); + var root = test.Build(); + var services = new ServiceCollection(); + services.AddSingleton(root); + services.AddOcelot(); + var provider = services.BuildServiceProvider(); + _builder = new OcelotPipelineBuilder(provider); + } + } +} \ No newline at end of file From 978b0a43a08d717f4982c89312a591dd971ac0a8 Mon Sep 17 00:00:00 2001 From: Tom Gardham-Pallister Date: Sat, 17 Mar 2018 18:09:47 +0000 Subject: [PATCH 8/9] #52 remove circle ci script --- .circleci/config.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 8abd6ffd..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: 2 -jobs: - build: - working_directory: /temp - docker: - - image: microsoft/dotnet:sdk - environment: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 - DOTNET_CLI_TELEMETRY_OPTOUT: 1 - steps: - - checkout - - run: dotnet restore - - run: dotnet build - - run: dotnet test ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj - - run: dotnet test ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj - - run: dotnet test ./test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj - From b51df71d7bd2c532323fc4fc1d4fbba6afbb76bf Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Sun, 18 Mar 2018 14:58:39 +0000 Subject: [PATCH 9/9] #280 Add headers to response (#286) * #280 can now add response headers inc trace id, now need to consolidate the header place holder stuff * #280 changed port for linux tests * #280 lots of hacking around to handle errors and consolidate placeholders into one class --- docs/features/headerstransformation.rst | 29 +++- .../Builder/DownstreamReRouteBuilder.cs | 12 +- .../Creator/FileOcelotConfigurationCreator.cs | 1 + .../Creator/HeaderFindAndReplaceCreator.cs | 57 +++++-- .../Creator/HeaderTransformations.cs | 23 ++- src/Ocelot/Configuration/DownstreamReRoute.cs | 6 +- .../DependencyInjection/OcelotBuilder.cs | 3 + src/Ocelot/Errors/OcelotErrorCode.cs | 3 +- src/Ocelot/Headers/AddHeadersToRequest.cs | 1 + src/Ocelot/Headers/AddHeadersToResponse.cs | 44 ++++++ .../Headers/HttpResponseHeaderReplacer.cs | 27 ++-- src/Ocelot/Headers/IAddHeadersToRequest.cs | 2 + src/Ocelot/Headers/IAddHeadersToResponse.cs | 11 ++ .../HttpHeadersTransformationMiddleware.cs | 7 +- .../CouldNotFindPlaceholderError.cs | 12 ++ src/Ocelot/Infrastructure/IPlaceholders.cs | 11 ++ src/Ocelot/Infrastructure/Placeholders.cs | 70 +++++++++ .../Requester/OcelotHttpTracingHandler.cs | 19 ++- src/Ocelot/Requester/TracingHandlerFactory.cs | 9 +- .../ButterflyTracingTests.cs | 58 ++++++- test/Ocelot.AcceptanceTests/Steps.cs | 6 + .../ConsulFileConfigurationPollerTests.cs | 4 +- .../FileConfigurationCreatorTests.cs | 5 +- .../HeaderFindAndReplaceCreatorTests.cs | 83 +++++++++- .../Headers/AddHeadersToResponseTests.cs | 148 ++++++++++++++++++ ...ttpHeadersTransformationMiddlewareTests.cs | 11 +- .../HttpResponseHeaderReplacerTests.cs | 12 +- .../IScopedRequestDataRepository.cs | 0 .../Infrastructure/PlaceholdersTests.cs | 79 ++++++++++ .../Requester/TracingHandlerFactoryTests.cs | 5 +- .../ErrorsToHttpStatusCodeMapperTests.cs | 2 +- 31 files changed, 700 insertions(+), 60 deletions(-) create mode 100644 src/Ocelot/Headers/AddHeadersToResponse.cs create mode 100644 src/Ocelot/Headers/IAddHeadersToResponse.cs create mode 100644 src/Ocelot/Infrastructure/CouldNotFindPlaceholderError.cs create mode 100644 src/Ocelot/Infrastructure/IPlaceholders.cs create mode 100644 src/Ocelot/Infrastructure/Placeholders.cs create mode 100644 test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs create mode 100644 test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs create mode 100644 test/Ocelot.UnitTests/Infrastructure/PlaceholdersTests.cs diff --git a/docs/features/headerstransformation.rst b/docs/features/headerstransformation.rst index 5063c1b7..6a333141 100644 --- a/docs/features/headerstransformation.rst +++ b/docs/features/headerstransformation.rst @@ -3,8 +3,32 @@ Headers Transformation Ocelot allows the user to transform headers pre and post downstream request. At the moment Ocelot only supports find and replace. This feature was requested `GitHub #190 `_ and I decided that it was going to be useful in various ways. -Syntax -^^^^^^ +Add to Response +^^^^^^^^^^^^^^^ + +This feature was requested in `GitHub #280 `_. I have only implemented +for responses but could add for requests in the future. + +If you want to add a header to your downstream response please add the following to a ReRoute in configuration.json.. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Uncle": "Bob" + }, + +In the example above a header with the key Uncle and value Bob would be returned by Ocelot when requesting the specific ReRoute. + +If you want to return the Butterfly APM trace id then do something like the following.. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "AnyKey": "{TraceId}" + }, + +Find and Replace +^^^^^^^^^^^^^^^^ In order to transform a header first we specify the header key and then the type of transform we want e.g. @@ -43,6 +67,7 @@ Ocelot allows placeholders that can be used in header transformation. {BaseUrl} - This will use Ocelot's base url e.g. http://localhost:5000 as its value. {DownstreamBaseUrl} - This will use the downstream services base url e.g. http://localhost:5000 as its value. This only works for DownstreamHeaderTransform at the moment. +{TraceId} - This will use the Butterfly APM Trace Id. This only works for DownstreamHeaderTransform at the moment. Handling 302 Redirects ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs index 0e28aaa7..d6dc0f34 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Net.Http; using Ocelot.Values; using System.Linq; +using Ocelot.Configuration.Creator; namespace Ocelot.Configuration.Builder { @@ -37,11 +38,13 @@ namespace Ocelot.Configuration.Builder private string _upstreamHost; private string _key; private List _delegatingHandlers; + private List _addHeadersToDownstream; public DownstreamReRouteBuilder() { _downstreamAddresses = new List(); _delegatingHandlers = new List(); + _addHeadersToDownstream = new List(); } public DownstreamReRouteBuilder WithDownstreamAddresses(List downstreamAddresses) @@ -224,6 +227,12 @@ namespace Ocelot.Configuration.Builder return this; } + public DownstreamReRouteBuilder WithAddHeadersToDownstream(List addHeadersToDownstream) + { + _addHeadersToDownstream = addHeadersToDownstream; + return this; + } + public DownstreamReRoute Build() { return new DownstreamReRoute( @@ -253,7 +262,8 @@ namespace Ocelot.Configuration.Builder _authenticationOptions, new PathTemplate(_downstreamPathTemplate), _reRouteKey, - _delegatingHandlers); + _delegatingHandlers, + _addHeadersToDownstream); } } } diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index 94cde717..1036f8df 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -213,6 +213,7 @@ namespace Ocelot.Configuration.Creator .WithDownstreamHeaderFindAndReplace(hAndRs.Downstream) .WithUpstreamHost(fileReRoute.UpstreamHost) .WithDelegatingHandlers(fileReRoute.DelegatingHandlers) + .WithAddHeadersToDownstream(hAndRs.AddHeadersToDownstream) .Build(); return reRoute; diff --git a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs index 7ab33e90..4ff87d2f 100644 --- a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs +++ b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs @@ -1,20 +1,22 @@ using System; using System.Collections.Generic; using Ocelot.Configuration.File; +using Ocelot.Infrastructure; +using Ocelot.Logging; using Ocelot.Middleware; +using Ocelot.Responses; namespace Ocelot.Configuration.Creator { public class HeaderFindAndReplaceCreator : IHeaderFindAndReplaceCreator { - private IBaseUrlFinder _finder; - private readonly Dictionary> _placeholders; + private IPlaceholders _placeholders; + private IOcelotLogger _logger; - public HeaderFindAndReplaceCreator(IBaseUrlFinder finder) + public HeaderFindAndReplaceCreator(IPlaceholders placeholders, IOcelotLoggerFactory factory) { - _finder = finder; - _placeholders = new Dictionary>(); - _placeholders.Add("{BaseUrl}", () => _finder.Find()); + _logger = factory.CreateLogger();; + _placeholders = placeholders; } public HeaderTransformations Create(FileReRoute fileReRoute) @@ -24,21 +26,43 @@ namespace Ocelot.Configuration.Creator foreach(var input in fileReRoute.UpstreamHeaderTransform) { var hAndr = Map(input); - upstream.Add(hAndr); + if(!hAndr.IsError) + { + upstream.Add(hAndr.Data); + } + else + { + _logger.LogError($"Unable to add UpstreamHeaderTransform {input.Key}: {input.Value}"); + } } var downstream = new List(); + var addHeadersToDownstream = new List(); foreach(var input in fileReRoute.DownstreamHeaderTransform) { - var hAndr = Map(input); - downstream.Add(hAndr); + if(input.Value.Contains(",")) + { + var hAndr = Map(input); + if(!hAndr.IsError) + { + downstream.Add(hAndr.Data); + } + else + { + _logger.LogError($"Unable to add DownstreamHeaderTransform {input.Key}: {input.Value}"); + } + } + else + { + addHeadersToDownstream.Add(new AddHeader(input.Key, input.Value)); + } } - return new HeaderTransformations(upstream, downstream); + return new HeaderTransformations(upstream, downstream, addHeadersToDownstream); } - private HeaderFindAndReplace Map(KeyValuePair input) + private Response Map(KeyValuePair input) { var findAndReplace = input.Value.Split(","); @@ -51,16 +75,19 @@ namespace Ocelot.Configuration.Creator var placeholder = replace.Substring(startOfPlaceholder, startOfPlaceholder + (endOfPlaceholder + 1)); - if(_placeholders.ContainsKey(placeholder)) + var value = _placeholders.Get(placeholder); + + if(value.IsError) { - var value = _placeholders[placeholder].Invoke(); - replace = replace.Replace(placeholder, value); + return new ErrorResponse(value.Errors); } + + replace = replace.Replace(placeholder, value.Data); } var hAndr = new HeaderFindAndReplace(input.Key, findAndReplace[0], replace, 0); - return hAndr; + return new OkResponse(hAndr); } } } diff --git a/src/Ocelot/Configuration/Creator/HeaderTransformations.cs b/src/Ocelot/Configuration/Creator/HeaderTransformations.cs index 55e1e3b9..72d307e5 100644 --- a/src/Ocelot/Configuration/Creator/HeaderTransformations.cs +++ b/src/Ocelot/Configuration/Creator/HeaderTransformations.cs @@ -4,14 +4,31 @@ namespace Ocelot.Configuration.Creator { public class HeaderTransformations { - public HeaderTransformations(List upstream, List downstream) + public HeaderTransformations( + List upstream, + List downstream, + List addHeader) { + AddHeadersToDownstream = addHeader; Upstream = upstream; Downstream = downstream; } - public List Upstream {get;private set;} + public List Upstream { get; private set; } - public List Downstream {get;private set;} + public List Downstream { get; private set; } + public List AddHeadersToDownstream {get;private set;} + } + + public class AddHeader + { + public AddHeader(string key, string value) + { + this.Key = key; + this.Value = value; + + } + public string Key { get; private set; } + public string Value { get; private set; } } } diff --git a/src/Ocelot/Configuration/DownstreamReRoute.cs b/src/Ocelot/Configuration/DownstreamReRoute.cs index 6c9aa1dd..00accc07 100644 --- a/src/Ocelot/Configuration/DownstreamReRoute.cs +++ b/src/Ocelot/Configuration/DownstreamReRoute.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Ocelot.Configuration.Creator; using Ocelot.Values; namespace Ocelot.Configuration @@ -32,8 +33,10 @@ namespace Ocelot.Configuration AuthenticationOptions authenticationOptions, PathTemplate downstreamPathTemplate, string reRouteKey, - List delegatingHandlers) + List delegatingHandlers, + List addHeadersToDownstream) { + AddHeadersToDownstream = addHeadersToDownstream; DelegatingHandlers = delegatingHandlers; Key = key; UpstreamPathTemplate = upstreamPathTemplate; @@ -90,5 +93,6 @@ namespace Ocelot.Configuration public PathTemplate DownstreamPathTemplate { get; private set; } public string ReRouteKey { get; private set; } public List DelegatingHandlers {get;private set;} + public List AddHeadersToDownstream {get;private set;} } } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index eccab083..8ccb1459 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -52,6 +52,7 @@ namespace Ocelot.DependencyInjection using System.Linq; using System.Net.Http; using Butterfly.Client.AspNetCore; + using Ocelot.Infrastructure; public class OcelotBuilder : IOcelotBuilder { @@ -149,6 +150,8 @@ namespace Ocelot.DependencyInjection // We add this here so that we can always inject something into the factory for IoC.. _services.AddSingleton(); _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); } public IOcelotAdministrationBuilder AddAdministration(string path, string secret) diff --git a/src/Ocelot/Errors/OcelotErrorCode.cs b/src/Ocelot/Errors/OcelotErrorCode.cs index b976e29c..ec44ae23 100644 --- a/src/Ocelot/Errors/OcelotErrorCode.cs +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -34,6 +34,7 @@ RateLimitOptionsError, PathTemplateDoesntStartWithForwardSlash, FileValidationFailedError, - UnableToFindDelegatingHandlerProviderError + UnableToFindDelegatingHandlerProviderError, + CouldNotFindPlaceholderError } } diff --git a/src/Ocelot/Headers/AddHeadersToRequest.cs b/src/Ocelot/Headers/AddHeadersToRequest.cs index c8ec8cf3..f61b1ac3 100644 --- a/src/Ocelot/Headers/AddHeadersToRequest.cs +++ b/src/Ocelot/Headers/AddHeadersToRequest.cs @@ -4,6 +4,7 @@ using Ocelot.Configuration; using Ocelot.Infrastructure.Claims.Parser; using Ocelot.Responses; using System.Net.Http; +using Ocelot.Configuration.Creator; namespace Ocelot.Headers { diff --git a/src/Ocelot/Headers/AddHeadersToResponse.cs b/src/Ocelot/Headers/AddHeadersToResponse.cs new file mode 100644 index 00000000..4c6f48ce --- /dev/null +++ b/src/Ocelot/Headers/AddHeadersToResponse.cs @@ -0,0 +1,44 @@ +namespace Ocelot.Headers +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using Ocelot.Configuration.Creator; + using Ocelot.Infrastructure; + using Ocelot.Infrastructure.RequestData; + using Ocelot.Logging; + + public class AddHeadersToResponse : IAddHeadersToResponse + { + private IPlaceholders _placeholders; + private IOcelotLogger _logger; + + public AddHeadersToResponse(IPlaceholders placeholders, IOcelotLoggerFactory factory) + { + _logger = factory.CreateLogger(); + _placeholders = placeholders; + } + public void Add(List addHeaders, HttpResponseMessage response) + { + foreach(var add in addHeaders) + { + if(add.Value.StartsWith('{') && add.Value.EndsWith('}')) + { + var value = _placeholders.Get(add.Value); + + if(value.IsError) + { + _logger.LogError($"Unable to add header to response {add.Key}: {add.Value}"); + continue; + } + + response.Headers.TryAddWithoutValidation(add.Key, value.Data); + } + else + { + response.Headers.TryAddWithoutValidation(add.Key, add.Value); + } + } + } + } +} diff --git a/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs index cb45ffc3..fc3e8e4c 100644 --- a/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs +++ b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using Ocelot.Configuration; +using Ocelot.Infrastructure; using Ocelot.Infrastructure.Extensions; using Ocelot.Responses; @@ -10,24 +11,14 @@ namespace Ocelot.Headers { public class HttpResponseHeaderReplacer : IHttpResponseHeaderReplacer { - private Dictionary> _placeholders; + private IPlaceholders _placeholders; - public HttpResponseHeaderReplacer() + public HttpResponseHeaderReplacer(IPlaceholders placeholders) { - _placeholders = new Dictionary>(); - _placeholders.Add("{DownstreamBaseUrl}", x => { - var downstreamUrl = $"{x.RequestUri.Scheme}://{x.RequestUri.Host}"; - - if(x.RequestUri.Port != 80 && x.RequestUri.Port != 443) - { - downstreamUrl = $"{downstreamUrl}:{x.RequestUri.Port}"; - } - - return $"{downstreamUrl}/"; - }); + _placeholders = placeholders; } - public Response Replace(HttpResponseMessage response, List fAndRs, HttpRequestMessage httpRequestMessage) + public Response Replace(HttpResponseMessage response, List fAndRs, HttpRequestMessage request) { foreach (var f in fAndRs) { @@ -35,11 +26,13 @@ namespace Ocelot.Headers if(response.Headers.TryGetValues(f.Key, out var values)) { //check to see if it is a placeholder in the find... - if(_placeholders.TryGetValue(f.Find, out var replacePlaceholder)) + var placeholderValue = _placeholders.Get(f.Find, request); + + if(!placeholderValue.IsError) { //if it is we need to get the value of the placeholder - var find = replacePlaceholder(httpRequestMessage); - var replaced = values.ToList()[f.Index].Replace(find, f.Replace.LastCharAsForwardSlash()); + //var find = replacePlaceholder(httpRequestMessage); + var replaced = values.ToList()[f.Index].Replace(placeholderValue.Data, f.Replace.LastCharAsForwardSlash()); response.Headers.Remove(f.Key); response.Headers.Add(f.Key, replaced); } diff --git a/src/Ocelot/Headers/IAddHeadersToRequest.cs b/src/Ocelot/Headers/IAddHeadersToRequest.cs index 387b3f01..fed2407c 100644 --- a/src/Ocelot/Headers/IAddHeadersToRequest.cs +++ b/src/Ocelot/Headers/IAddHeadersToRequest.cs @@ -4,6 +4,8 @@ using System.Net.Http; using Ocelot.Configuration; + using Ocelot.Configuration.Creator; + using Ocelot.Infrastructure.RequestData; using Ocelot.Responses; public interface IAddHeadersToRequest diff --git a/src/Ocelot/Headers/IAddHeadersToResponse.cs b/src/Ocelot/Headers/IAddHeadersToResponse.cs new file mode 100644 index 00000000..51f23758 --- /dev/null +++ b/src/Ocelot/Headers/IAddHeadersToResponse.cs @@ -0,0 +1,11 @@ +namespace Ocelot.Headers +{ + using System.Collections.Generic; + using System.Net.Http; + using Ocelot.Configuration.Creator; + + public interface IAddHeadersToResponse + { + void Add(List addHeaders, HttpResponseMessage response); + } +} diff --git a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs index f4281a23..b09b40f8 100644 --- a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs +++ b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs @@ -13,12 +13,15 @@ namespace Ocelot.Headers.Middleware private readonly IOcelotLogger _logger; private readonly IHttpContextRequestHeaderReplacer _preReplacer; private readonly IHttpResponseHeaderReplacer _postReplacer; + private readonly IAddHeadersToResponse _addHeaders; public HttpHeadersTransformationMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, IHttpContextRequestHeaderReplacer preReplacer, - IHttpResponseHeaderReplacer postReplacer) + IHttpResponseHeaderReplacer postReplacer, + IAddHeadersToResponse addHeaders) { + _addHeaders = addHeaders; _next = next; _postReplacer = postReplacer; _preReplacer = preReplacer; @@ -37,6 +40,8 @@ namespace Ocelot.Headers.Middleware var postFAndRs = context.DownstreamReRoute.DownstreamHeadersFindAndReplace; _postReplacer.Replace(context.DownstreamResponse, postFAndRs, context.DownstreamRequest); + + _addHeaders.Add(context.DownstreamReRoute.AddHeadersToDownstream, context.DownstreamResponse); } } } diff --git a/src/Ocelot/Infrastructure/CouldNotFindPlaceholderError.cs b/src/Ocelot/Infrastructure/CouldNotFindPlaceholderError.cs new file mode 100644 index 00000000..c6214e83 --- /dev/null +++ b/src/Ocelot/Infrastructure/CouldNotFindPlaceholderError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Infrastructure +{ + public class CouldNotFindPlaceholderError : Error + { + public CouldNotFindPlaceholderError(string placeholder) + : base($"Unable to find placeholder called {placeholder}", OcelotErrorCode.CouldNotFindPlaceholderError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Infrastructure/IPlaceholders.cs b/src/Ocelot/Infrastructure/IPlaceholders.cs new file mode 100644 index 00000000..f95fb8b8 --- /dev/null +++ b/src/Ocelot/Infrastructure/IPlaceholders.cs @@ -0,0 +1,11 @@ +using System.Net.Http; +using Ocelot.Responses; + +namespace Ocelot.Infrastructure +{ + public interface IPlaceholders + { + Response Get(string key); + Response Get(string key, HttpRequestMessage request); + } +} \ No newline at end of file diff --git a/src/Ocelot/Infrastructure/Placeholders.cs b/src/Ocelot/Infrastructure/Placeholders.cs new file mode 100644 index 00000000..b00f54fa --- /dev/null +++ b/src/Ocelot/Infrastructure/Placeholders.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Middleware; +using Ocelot.Responses; + +namespace Ocelot.Infrastructure +{ + public class Placeholders : IPlaceholders + { + private Dictionary>> _placeholders; + private Dictionary> _requestPlaceholders; + private readonly IBaseUrlFinder _finder; + private readonly IRequestScopedDataRepository _repo; + + public Placeholders(IBaseUrlFinder finder, IRequestScopedDataRepository repo) + { + _repo = repo; + _finder = finder; + _placeholders = new Dictionary>>(); + _placeholders.Add("{BaseUrl}", () => new OkResponse(_finder.Find())); + _placeholders.Add("{TraceId}", () => { + var traceId = _repo.Get("TraceId"); + if(traceId.IsError) + { + return new ErrorResponse(traceId.Errors); + } + + return new OkResponse(traceId.Data); + }); + + _requestPlaceholders = new Dictionary>(); + _requestPlaceholders.Add("{DownstreamBaseUrl}", x => { + var downstreamUrl = $"{x.RequestUri.Scheme}://{x.RequestUri.Host}"; + + if(x.RequestUri.Port != 80 && x.RequestUri.Port != 443) + { + downstreamUrl = $"{downstreamUrl}:{x.RequestUri.Port}"; + } + + return $"{downstreamUrl}/"; + }); + } + + public Response Get(string key) + { + if(_placeholders.ContainsKey(key)) + { + var response = _placeholders[key].Invoke(); + if(!response.IsError) + { + return new OkResponse(response.Data); + } + } + + return new ErrorResponse(new CouldNotFindPlaceholderError(key)); + } + + public Response Get(string key, HttpRequestMessage request) + { + if(_requestPlaceholders.ContainsKey(key)) + { + return new OkResponse(_requestPlaceholders[key].Invoke(request)); + } + + return new ErrorResponse(new CouldNotFindPlaceholderError(key)); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Requester/OcelotHttpTracingHandler.cs b/src/Ocelot/Requester/OcelotHttpTracingHandler.cs index 33bd4caf..9de364ac 100644 --- a/src/Ocelot/Requester/OcelotHttpTracingHandler.cs +++ b/src/Ocelot/Requester/OcelotHttpTracingHandler.cs @@ -5,26 +5,37 @@ using System.Threading; using System.Threading.Tasks; using Butterfly.Client.Tracing; using Butterfly.OpenTracing; +using Ocelot.Infrastructure.RequestData; namespace Ocelot.Requester { public class OcelotHttpTracingHandler : DelegatingHandler, ITracingHandler { private readonly IServiceTracer _tracer; + private readonly IRequestScopedDataRepository _repo; private const string prefix_spanId = "ot-spanId"; - public OcelotHttpTracingHandler(IServiceTracer tracer, HttpMessageHandler httpMessageHandler = null) + public OcelotHttpTracingHandler( + IServiceTracer tracer, + IRequestScopedDataRepository repo, + HttpMessageHandler httpMessageHandler = null) { + _repo = repo; _tracer = tracer ?? throw new ArgumentNullException(nameof(tracer)); InnerHandler = httpMessageHandler ?? new HttpClientHandler(); } - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) { return _tracer.ChildTraceAsync($"httpclient {request.Method}", DateTimeOffset.UtcNow, span => TracingSendAsync(span, request, cancellationToken)); } - protected virtual async Task TracingSendAsync(ISpan span, HttpRequestMessage request, CancellationToken cancellationToken) + protected virtual async Task TracingSendAsync( + ISpan span, + HttpRequestMessage request, + CancellationToken cancellationToken) { IEnumerable traceIdVals = null; if (request.Headers.TryGetValues(prefix_spanId, out traceIdVals)) @@ -33,6 +44,8 @@ namespace Ocelot.Requester request.Headers.TryAddWithoutValidation(prefix_spanId, span.SpanContext.SpanId); } + _repo.Add("TraceId", span.SpanContext.TraceId); + span.Tags.Client().Component("HttpClient") .HttpMethod(request.Method.Method) .HttpUrl(request.RequestUri.OriginalString) diff --git a/src/Ocelot/Requester/TracingHandlerFactory.cs b/src/Ocelot/Requester/TracingHandlerFactory.cs index 5cb72a79..b514ca18 100644 --- a/src/Ocelot/Requester/TracingHandlerFactory.cs +++ b/src/Ocelot/Requester/TracingHandlerFactory.cs @@ -1,20 +1,25 @@ using Butterfly.Client.Tracing; using Butterfly.OpenTracing; +using Ocelot.Infrastructure.RequestData; namespace Ocelot.Requester { public class TracingHandlerFactory : ITracingHandlerFactory { private readonly IServiceTracer _tracer; + private readonly IRequestScopedDataRepository _repo; - public TracingHandlerFactory(IServiceTracer tracer) + public TracingHandlerFactory( + IServiceTracer tracer, + IRequestScopedDataRepository repo) { + _repo = repo; _tracer = tracer; } public ITracingHandler Get() { - return new OcelotHttpTracingHandler(_tracer); + return new OcelotHttpTracingHandler(_tracer, _repo); } } diff --git a/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs b/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs index a7411ebe..d634e617 100644 --- a/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs +++ b/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs @@ -70,7 +70,7 @@ namespace Ocelot.AcceptanceTests new FileHostAndPort { Host = "localhost", - Port = 51888, + Port = 51388, } }, UpstreamPathTemplate = "/api002/values", @@ -92,7 +92,7 @@ namespace Ocelot.AcceptanceTests var butterflyUrl = "http://localhost:9618"; this.Given(x => GivenServiceOneIsRunning("http://localhost:51887", "/api/values", 200, "Hello from Laura", butterflyUrl)) - .And(x => GivenServiceTwoIsRunning("http://localhost:51888", "/api/values", 200, "Hello from Tom", butterflyUrl)) + .And(x => GivenServiceTwoIsRunning("http://localhost:51388", "/api/values", 200, "Hello from Tom", butterflyUrl)) .And(x => GivenFakeButterfly(butterflyUrl)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningUsingButterfly(butterflyUrl)) @@ -109,6 +109,60 @@ namespace Ocelot.AcceptanceTests commandOnAllStateMachines.ShouldBeTrue(); } + [Fact] + public void should_return_tracing_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51387, + } + }, + UpstreamPathTemplate = "/api001/values", + UpstreamHttpMethod = new List { "Get" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true + }, + QoSOptions = new FileQoSOptions + { + ExceptionsAllowedBeforeBreaking = 3, + DurationOfBreak = 10, + TimeoutValue = 5000 + }, + DownstreamHeaderTransform = new Dictionary() + { + {"Trace-Id", "{TraceId}"}, + {"Tom", "Laura"} + } + } + } + }; + + var butterflyUrl = "http://localhost:9618"; + + this.Given(x => GivenServiceOneIsRunning("http://localhost:51387", "/api/values", 200, "Hello from Laura", butterflyUrl)) + .And(x => GivenFakeButterfly(butterflyUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingButterfly(butterflyUrl)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api001/values")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.ThenTheTraceHeaderIsSet("Trace-Id")) + .And(x => _steps.ThenTheResponseHeaderIs("Tom", "Laura")) + .BDDfy(); + } + private void GivenServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody, string butterflyUrl) { _serviceOneBuilder = new WebHostBuilder() diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 2cb0f830..1ee6f9bc 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -318,6 +318,12 @@ namespace Ocelot.AcceptanceTests header.First().ShouldBe(value); } + public void ThenTheTraceHeaderIsSet(string key) + { + var header = _response.Headers.GetValues(key); + header.First().ShouldNotBeNullOrEmpty(); + } + public void GivenOcelotIsRunningUsingJsonSerializedCache() { _webHostBuilder = new WebHostBuilder(); diff --git a/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs b/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs index 6499c4e0..7393b631 100644 --- a/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs @@ -34,10 +34,10 @@ namespace Ocelot.UnitTests.Configuration _fileConfig = new FileConfiguration(); _config = new Mock(); _repo.Setup(x => x.Get()).ReturnsAsync(new OkResponse(_fileConfig)); - _config.Setup(x => x.Delay).Returns(10); + _config.Setup(x => x.Delay).Returns(100); _poller = new ConsulFileConfigurationPoller(_factory.Object, _repo.Object, _setter.Object, _config.Object); } - + public void Dispose() { _poller.Dispose(); diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index 79aff9ed..ce1d6989 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -826,7 +826,8 @@ namespace Ocelot.UnitTests.Configuration result.DownstreamReRoute[0].ClaimsToHeaders.Count.ShouldBe(expected.DownstreamReRoute[0].ClaimsToHeaders.Count); result.DownstreamReRoute[0].ClaimsToQueries.Count.ShouldBe(expected.DownstreamReRoute[0].ClaimsToQueries.Count); result.DownstreamReRoute[0].RequestIdKey.ShouldBe(expected.DownstreamReRoute[0].RequestIdKey); - result.DownstreamReRoute[0].DelegatingHandlers.ShouldBe(expected.DownstreamReRoute[0].DelegatingHandlers); + result.DownstreamReRoute[0].DelegatingHandlers.ShouldBe(expected.DownstreamReRoute[0].DelegatingHandlers); + result.DownstreamReRoute[0].AddHeadersToDownstream.ShouldBe(expected.DownstreamReRoute[0].AddHeadersToDownstream); } } @@ -909,7 +910,7 @@ namespace Ocelot.UnitTests.Configuration private void GivenTheHeaderFindAndReplaceCreatorReturns() { - _headerFindAndReplaceCreator.Setup(x => x.Create(It.IsAny())).Returns(new HeaderTransformations(new List(), new List())); + _headerFindAndReplaceCreator.Setup(x => x.Create(It.IsAny())).Returns(new HeaderTransformations(new List(), new List(), new List())); } private void GivenTheFollowingIsReturned(ServiceProviderConfiguration serviceProviderConfiguration) diff --git a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs index 37ee8957..aa285e8d 100644 --- a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs @@ -5,7 +5,11 @@ using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; +using Ocelot.Infrastructure; +using Ocelot.Logging; using Ocelot.Middleware; +using Ocelot.Responses; +using Ocelot.UnitTests.Responder; using Shouldly; using TestStack.BDDfy; using Xunit; @@ -17,12 +21,17 @@ namespace Ocelot.UnitTests.Configuration private HeaderFindAndReplaceCreator _creator; private FileReRoute _reRoute; private HeaderTransformations _result; - private Mock _finder; + private Mock _placeholders; + private Mock _factory; + private Mock _logger; public HeaderFindAndReplaceCreatorTests() { - _finder = new Mock(); - _creator = new HeaderFindAndReplaceCreator(_finder.Object); + _logger = new Mock(); + _factory = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _placeholders = new Mock(); + _creator = new HeaderFindAndReplaceCreator(_placeholders.Object, _factory.Object); } [Fact] @@ -84,6 +93,40 @@ namespace Ocelot.UnitTests.Configuration .BDDfy(); } + [Fact] + public void should_log_errors_and_not_add_headers() + { + var reRoute = new FileReRoute + { + DownstreamHeaderTransform = new Dictionary + { + {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, + }, + UpstreamHeaderTransform = new Dictionary + { + {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, + } + }; + + var expected = new List + { + }; + + this.Given(x => GivenTheReRoute(reRoute)) + .And(x => GivenTheBaseUrlErrors()) + .When(x => WhenICreate()) + .Then(x => ThenTheFollowingDownstreamIsReturned(expected)) + .And(x => ThenTheFollowingUpstreamIsReturned(expected)) + .And(x => ThenTheLoggerIsCalledCorrectly("Unable to add DownstreamHeaderTransform Location: http://www.bbc.co.uk/, {BaseUrl}")) + .And(x => ThenTheLoggerIsCalledCorrectly("Unable to add UpstreamHeaderTransform Location: http://www.bbc.co.uk/, {BaseUrl}")) + .BDDfy(); + } + + private void ThenTheLoggerIsCalledCorrectly(string message) + { + _logger.Verify(x => x.LogError(message), Times.Once); + } + [Fact] public void should_use_base_url_partial_placeholder() { @@ -107,9 +150,41 @@ namespace Ocelot.UnitTests.Configuration .BDDfy(); } + + [Fact] + public void should_add_trace_id_header() + { + var reRoute = new FileReRoute + { + DownstreamHeaderTransform = new Dictionary + { + {"Trace-Id", "{TraceId}"}, + } + }; + + var expected = new AddHeader("Trace-Id", "{TraceId}"); + + this.Given(x => GivenTheReRoute(reRoute)) + .And(x => GivenTheBaseUrlIs("http://ocelot.com/")) + .When(x => WhenICreate()) + .Then(x => ThenTheFollowingAddHeaderIsReturned(expected)) + .BDDfy(); + } + private void GivenTheBaseUrlIs(string baseUrl) { - _finder.Setup(x => x.Find()).Returns(baseUrl); + _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse(baseUrl)); + } + + private void GivenTheBaseUrlErrors() + { + _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new ErrorResponse(new AnyError())); + } + + private void ThenTheFollowingAddHeaderIsReturned(AddHeader addHeader) + { + _result.AddHeadersToDownstream[0].Key.ShouldBe(addHeader.Key); + _result.AddHeadersToDownstream[0].Value.ShouldBe(addHeader.Value); } private void ThenTheFollowingDownstreamIsReturned(List downstream) diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs new file mode 100644 index 00000000..a4a19eec --- /dev/null +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs @@ -0,0 +1,148 @@ +using Xunit; +using Shouldly; +using TestStack.BDDfy; +using Ocelot.Headers; +using System.Net.Http; +using System.Collections.Generic; +using Ocelot.Configuration.Creator; +using System.Linq; +using Moq; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Responses; +using Ocelot.Infrastructure; +using Ocelot.UnitTests.Responder; +using System; +using Ocelot.Logging; + +namespace Ocelot.UnitTests.Headers +{ + public class AddHeadersToResponseTests + { + private IAddHeadersToResponse _adder; + private Mock _placeholders; + private HttpResponseMessage _response; + private List _addHeaders; + private Mock _factory; + private Mock _logger; + + public AddHeadersToResponseTests() + { + _factory = new Mock(); + _logger = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _placeholders = new Mock(); + _adder = new AddHeadersToResponse(_placeholders.Object, _factory.Object); + } + + [Fact] + public void should_add_header() + { + var addHeaders = new List + { + new AddHeader("Laura", "Tom") + }; + + this.Given(_ => GivenAResponseMessage()) + .And(_ => GivenTheAddHeaders(addHeaders)) + .When(_ => WhenIAdd()) + .And(_ => ThenTheHeaderIsReturned("Laura", "Tom")) + .BDDfy(); + } + + [Fact] + public void should_add_trace_id_placeholder() + { + var addHeaders = new List + { + new AddHeader("Trace-Id", "{TraceId}") + }; + + var traceId = "123"; + + this.Given(_ => GivenAResponseMessage()) + .And(_ => GivenTheTraceIdIs(traceId)) + .And(_ => GivenTheAddHeaders(addHeaders)) + .When(_ => WhenIAdd()) + .Then(_ => ThenTheHeaderIsReturned("Trace-Id", traceId)) + .BDDfy(); + } + + [Fact] + public void should_add_trace_id_placeholder_and_normal() + { + var addHeaders = new List + { + new AddHeader("Trace-Id", "{TraceId}"), + new AddHeader("Tom", "Laura") + }; + + var traceId = "123"; + + this.Given(_ => GivenAResponseMessage()) + .And(_ => GivenTheTraceIdIs(traceId)) + .And(_ => GivenTheAddHeaders(addHeaders)) + .When(_ => WhenIAdd()) + .Then(_ => ThenTheHeaderIsReturned("Trace-Id", traceId)) + .Then(_ => ThenTheHeaderIsReturned("Tom", "Laura")) + .BDDfy(); + } + + [Fact] + public void should_do_nothing_and_log_error() + { + var addHeaders = new List + { + new AddHeader("Trace-Id", "{TraceId}") + }; + + this.Given(_ => GivenAResponseMessage()) + .And(_ => GivenTheTraceIdErrors()) + .And(_ => GivenTheAddHeaders(addHeaders)) + .When(_ => WhenIAdd()) + .Then(_ => ThenTheHeaderIsNotAdded("Trace-Id")) + .And(_ => ThenTheErrorIsLogged()) + .BDDfy(); + } + + private void ThenTheErrorIsLogged() + { + _logger.Verify(x => x.LogError("Unable to add header to response Trace-Id: {TraceId}"), Times.Once); + } + + private void ThenTheHeaderIsNotAdded(string key) + { + _response.Headers.TryGetValues(key, out var values).ShouldBeFalse(); + } + + private void GivenTheTraceIdIs(string traceId) + { + _placeholders.Setup(x => x.Get("{TraceId}")).Returns(new OkResponse(traceId)); + } + + private void GivenTheTraceIdErrors() + { + _placeholders.Setup(x => x.Get("{TraceId}")).Returns(new ErrorResponse(new AnyError())); + } + + private void ThenTheHeaderIsReturned(string key, string value) + { + var values = _response.Headers.GetValues(key); + values.First().ShouldBe(value); + } + + private void WhenIAdd() + { + _adder.Add(_addHeaders, _response); + } + + private void GivenAResponseMessage() + { + _response = new HttpResponseMessage(); + } + + private void GivenTheAddHeaders(List addHeaders) + { + _addHeaders = addHeaders; + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs index 3085a64d..c49bf172 100644 --- a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs @@ -26,6 +26,7 @@ namespace Ocelot.UnitTests.Headers private HttpHeadersTransformationMiddleware _middleware; private DownstreamContext _downstreamContext; private OcelotRequestDelegate _next; + private Mock _addHeaders; public HttpHeadersTransformationMiddlewareTests() { @@ -36,7 +37,8 @@ namespace Ocelot.UnitTests.Headers _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; - _middleware = new HttpHeadersTransformationMiddleware(_next, _loggerFactory.Object, _preReplacer.Object, _postReplacer.Object); + _addHeaders = new Mock(); + _middleware = new HttpHeadersTransformationMiddleware(_next, _loggerFactory.Object, _preReplacer.Object, _postReplacer.Object, _addHeaders.Object); } [Fact] @@ -49,9 +51,16 @@ namespace Ocelot.UnitTests.Headers .When(x => WhenICallTheMiddleware()) .Then(x => ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly()) .And(x => ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly()) + .And(x => ThenAddHeadersIsCalledCorrectly()) .BDDfy(); } + private void ThenAddHeadersIsCalledCorrectly() + { + _addHeaders + .Verify(x => x.Add(_downstreamContext.DownstreamReRoute.AddHeadersToDownstream, _downstreamContext.DownstreamResponse), Times.Once); + } + private void WhenICallTheMiddleware() { _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); diff --git a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs index 95ae6e6d..ca97de4f 100644 --- a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs @@ -7,20 +7,30 @@ using Ocelot.Configuration; using System.Collections.Generic; using Ocelot.Responses; using System.Linq; +using Moq; +using Ocelot.Infrastructure; +using Ocelot.Middleware; +using Ocelot.Infrastructure.RequestData; namespace Ocelot.UnitTests.Headers { public class HttpResponseHeaderReplacerTests { private HttpResponseMessage _response; + private Placeholders _placeholders; private HttpResponseHeaderReplacer _replacer; private List _headerFindAndReplaces; private Response _result; private HttpRequestMessage _request; + private Mock _finder; + private Mock _repo; public HttpResponseHeaderReplacerTests() { - _replacer = new HttpResponseHeaderReplacer(); + _repo = new Mock(); + _finder = new Mock(); + _placeholders = new Placeholders(_finder.Object, _repo.Object); + _replacer = new HttpResponseHeaderReplacer(_placeholders); } [Fact] diff --git a/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs b/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs new file mode 100644 index 00000000..e69de29b diff --git a/test/Ocelot.UnitTests/Infrastructure/PlaceholdersTests.cs b/test/Ocelot.UnitTests/Infrastructure/PlaceholdersTests.cs new file mode 100644 index 00000000..6acd3655 --- /dev/null +++ b/test/Ocelot.UnitTests/Infrastructure/PlaceholdersTests.cs @@ -0,0 +1,79 @@ +using System; +using System.Net.Http; +using Moq; +using Ocelot.Infrastructure; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Middleware; +using Ocelot.Responses; +using Shouldly; +using Xunit; + +namespace Ocelot.UnitTests.Infrastructure +{ + public class PlaceholdersTests + { + private IPlaceholders _placeholders; + private Mock _finder; + private Mock _repo; + + public PlaceholdersTests() + { + _repo = new Mock(); + _finder = new Mock(); + _placeholders = new Placeholders(_finder.Object, _repo.Object); + } + + [Fact] + public void should_return_base_url() + { + var baseUrl = "http://www.bbc.co.uk"; + _finder.Setup(x => x.Find()).Returns(baseUrl); + var result = _placeholders.Get("{BaseUrl}"); + result.Data.ShouldBe(baseUrl); + } + + [Fact] + public void should_return_key_does_not_exist() + { + var result = _placeholders.Get("{Test}"); + result.IsError.ShouldBeTrue(); + result.Errors[0].Message.ShouldBe("Unable to find placeholder called {Test}"); + } + + [Fact] + public void should_return_downstream_base_url_when_port_is_not_80_or_443() + { + var request = new HttpRequestMessage(); + request.RequestUri = new Uri("http://www.bbc.co.uk"); + var result = _placeholders.Get("{DownstreamBaseUrl}", request); + result.Data.ShouldBe("http://www.bbc.co.uk/"); + } + + + [Fact] + public void should_return_downstream_base_url_when_port_is_80_or_443() + { + var request = new HttpRequestMessage(); + request.RequestUri = new Uri("http://www.bbc.co.uk:123"); + var result = _placeholders.Get("{DownstreamBaseUrl}", request); + result.Data.ShouldBe("http://www.bbc.co.uk:123/"); + } + + [Fact] + public void should_return_key_does_not_exist_for_http_request_message() + { + var result = _placeholders.Get("{Test}", new System.Net.Http.HttpRequestMessage()); + result.IsError.ShouldBeTrue(); + result.Errors[0].Message.ShouldBe("Unable to find placeholder called {Test}"); + } + + [Fact] + public void should_return_trace_id() + { + var traceId = "123"; + _repo.Setup(x => x.Get("TraceId")).Returns(new OkResponse(traceId)); + var result = _placeholders.Get("{TraceId}"); + result.Data.ShouldBe(traceId); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Requester/TracingHandlerFactoryTests.cs b/test/Ocelot.UnitTests/Requester/TracingHandlerFactoryTests.cs index e8196966..bd633cb3 100644 --- a/test/Ocelot.UnitTests/Requester/TracingHandlerFactoryTests.cs +++ b/test/Ocelot.UnitTests/Requester/TracingHandlerFactoryTests.cs @@ -1,5 +1,6 @@ using Butterfly.Client.Tracing; using Moq; +using Ocelot.Infrastructure.RequestData; using Ocelot.Requester; using Shouldly; using Xunit; @@ -10,11 +11,13 @@ namespace Ocelot.UnitTests.Requester { private TracingHandlerFactory _factory; private Mock _tracer; + private Mock _repo; public TracingHandlerFactoryTests() { _tracer = new Mock(); - _factory = new TracingHandlerFactory(_tracer.Object); + _repo = new Mock(); + _factory = new TracingHandlerFactory(_tracer.Object, _repo.Object); } [Fact] diff --git a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs index 9800ffef..8ee2be1f 100644 --- a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs +++ b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs @@ -120,7 +120,7 @@ namespace Ocelot.UnitTests.Responder // If this test fails then it's because the number of error codes has changed. // You should make the appropriate changes to the test cases here to ensure // they cover all the error codes, and then modify this assertion. - Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(33, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); + Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(34, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); } private void ShouldMapErrorToStatusCode(OcelotErrorCode errorCode, HttpStatusCode expectedHttpStatusCode)