diff --git a/docs/features/headerstransformation.rst b/docs/features/headerstransformation.rst new file mode 100644 index 00000000..e6785cec --- /dev/null +++ b/docs/features/headerstransformation.rst @@ -0,0 +1,70 @@ +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 +^^^^^^ + +In order to transform a header first we specify the header key and then the type of transform we want e.g. + +.. code-block:: json + + "Test": "http://www.bbc.co.uk/, http://ocelot.com/" + +The key is "Test" and the value is "http://www.bbc.co.uk/, http://ocelot.com/". The value is saying replace http://www.bbc.co.uk/ with http://ocelot.com/. The syntax is {find}, {replace}. Hopefully pretty simple. There are examples below that explain more. + +Pre Downstream Request +^^^^^^^^^^^^^^^^^^^^^^ + +Add the following to a ReRoute in configuration.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This header will be changed before the request downstream and will be sent to the downstream server. + +.. code-block:: json + + "UpstreamHeaderTransform": { + "Test": "http://www.bbc.co.uk/, http://ocelot.com/" + }, + +Post Downstream Request +^^^^^^^^^^^^^^^^^^^^^^ + +Add the following to a ReRoute in configuration.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This transformation will take place after Ocelot has received the response from the downstream service. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Test": "http://www.bbc.co.uk/, http://ocelot.com/" + }, + +Placeholders +^^^^^^^^^^^^ + +Ocelot allows placeholders that can be used in header transformation. At the moment there is only one placeholder. + +{BaseUrl} - This will use Ocelot's base url e.g. http://localhost:5000 as its value. + +Handling 302 Redirects +^^^^^^^^^^^^^^^^^^^^^^ +Ocelot will by default automatically follow redirects however if you want to return the location header to the client you might want to change the location to be Ocelot not the downstream service. Ocelot allows this with the following configuration. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Location": "http://www.bbc.co.uk/, http://ocelot.com/" + }, + "HttpHandlerOptions": { + "AllowAutoRedirect": false, + }, + +or you could use the BaseUrl placeholder. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Location": "http://localhost:6773, {BaseUrl}" + }, + "HttpHandlerOptions": { + "AllowAutoRedirect": false, + }, + +Ocelot will not try and replace the location header returned by the downstream service with its own URL. diff --git a/docs/index.rst b/docs/index.rst index 0a292bcf..dbd7c93d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -27,6 +27,7 @@ Thanks for taking a look at the Ocelot documentation. Please use the left hand n features/raft features/caching features/qualityofservice + features/headerstransformation features/claimstransformation features/logging features/requestid diff --git a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs index cc5a61aa..d81d3351 100644 --- a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs @@ -36,6 +36,9 @@ namespace Ocelot.Configuration.Builder private bool _useServiceDiscovery; private string _serviceName; + private List _upstreamHeaderFindAndReplace; + private List _downstreamHeaderFindAndReplace; + public ReRouteBuilder WithLoadBalancer(string loadBalancer) { _loadBalancer = loadBalancer; @@ -198,6 +201,18 @@ namespace Ocelot.Configuration.Builder return this; } + public ReRouteBuilder WithUpstreamHeaderFindAndReplace(List upstreamHeaderFindAndReplace) + { + _upstreamHeaderFindAndReplace = upstreamHeaderFindAndReplace; + return this; + } + + public ReRouteBuilder WithDownstreamHeaderFindAndReplace(List downstreamHeaderFindAndReplace) + { + _downstreamHeaderFindAndReplace = downstreamHeaderFindAndReplace; + return this; + } + public ReRoute Build() { return new ReRoute( @@ -226,7 +241,9 @@ namespace Ocelot.Configuration.Builder _rateLimitOptions, _httpHandlerOptions, _useServiceDiscovery, - _serviceName); + _serviceName, + _upstreamHeaderFindAndReplace, + _downstreamHeaderFindAndReplace); } } } diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index a701d9a0..32c5fb47 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -37,6 +37,7 @@ namespace Ocelot.Configuration.Creator private readonly IRegionCreator _regionCreator; private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; private readonly IAdministrationPath _adminPath; + private readonly IHeaderFindAndReplaceCreator _headerFAndRCreator; public FileOcelotConfigurationCreator( @@ -53,9 +54,11 @@ namespace Ocelot.Configuration.Creator IRateLimitOptionsCreator rateLimitOptionsCreator, IRegionCreator regionCreator, IHttpHandlerOptionsCreator httpHandlerOptionsCreator, - IAdministrationPath adminPath + IAdministrationPath adminPath, + IHeaderFindAndReplaceCreator headerFAndRCreator ) { + _headerFAndRCreator = headerFAndRCreator; _adminPath = adminPath; _regionCreator = regionCreator; _rateLimitOptionsCreator = rateLimitOptionsCreator; @@ -128,6 +131,8 @@ namespace Ocelot.Configuration.Creator var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileReRoute); + var hAndRs = _headerFAndRCreator.Create(fileReRoute); + var reRoute = new ReRouteBuilder() .WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate) .WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate) @@ -155,6 +160,8 @@ namespace Ocelot.Configuration.Creator .WithHttpHandlerOptions(httpHandlerOptions) .WithServiceName(fileReRoute.ServiceName) .WithUseServiceDiscovery(fileReRoute.UseServiceDiscovery) + .WithUpstreamHeaderFindAndReplace(hAndRs.Upstream) + .WithDownstreamHeaderFindAndReplace(hAndRs.Downstream) .Build(); return reRoute; diff --git a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs new file mode 100644 index 00000000..e7b61383 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using Ocelot.Configuration.File; +using Ocelot.Middleware; + +namespace Ocelot.Configuration.Creator +{ + public class HeaderFindAndReplaceCreator : IHeaderFindAndReplaceCreator + { + private IBaseUrlFinder _finder; + private Dictionary> _placeholders; + + public HeaderFindAndReplaceCreator(IBaseUrlFinder finder) + { + _finder = finder; + _placeholders = new Dictionary>(); + _placeholders.Add("{BaseUrl}", () => { + return _finder.Find(); + }); + } + + public HeaderTransformations Create(FileReRoute fileReRoute) + { + var upstream = new List(); + + foreach(var input in fileReRoute.UpstreamHeaderTransform) + { + var hAndr = Map(input); + upstream.Add(hAndr); + } + + var downstream = new List(); + + foreach(var input in fileReRoute.DownstreamHeaderTransform) + { + var hAndr = Map(input); + downstream.Add(hAndr); + } + + return new HeaderTransformations(upstream, downstream); + } + + private HeaderFindAndReplace Map(KeyValuePair input) + { + var findAndReplace = input.Value.Split(","); + + var replace = findAndReplace[1].TrimStart(); + + var startOfPlaceholder = replace.IndexOf("{"); + if(startOfPlaceholder > -1) + { + var endOfPlaceholder = replace.IndexOf("}", startOfPlaceholder); + + var placeholder = replace.Substring(startOfPlaceholder, startOfPlaceholder + (endOfPlaceholder + 1)); + + if(_placeholders.ContainsKey(placeholder)) + { + var value = _placeholders[placeholder].Invoke(); + replace = replace.Replace(placeholder, value); + } + } + + var hAndr = new HeaderFindAndReplace(input.Key, findAndReplace[0], replace, 0); + + return hAndr; + } + } +} diff --git a/src/Ocelot/Configuration/Creator/HeaderTransformations.cs b/src/Ocelot/Configuration/Creator/HeaderTransformations.cs new file mode 100644 index 00000000..2fba1c67 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/HeaderTransformations.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Ocelot.Configuration.Creator +{ + public class HeaderTransformations + { + public HeaderTransformations(List upstream, List downstream) + { + Upstream = upstream; + Downstream = downstream; + } + + public List Upstream {get;private set;} + + public List Downstream {get;private set;} + } +} diff --git a/src/Ocelot/Configuration/Creator/IHeaderFindAndReplaceCreator.cs b/src/Ocelot/Configuration/Creator/IHeaderFindAndReplaceCreator.cs new file mode 100644 index 00000000..1423c8ca --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IHeaderFindAndReplaceCreator.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public interface IHeaderFindAndReplaceCreator + { + HeaderTransformations Create(FileReRoute fileReRoute); + } +} diff --git a/src/Ocelot/Configuration/File/FileReRoute.cs b/src/Ocelot/Configuration/File/FileReRoute.cs index d33ba8e4..4a34878e 100644 --- a/src/Ocelot/Configuration/File/FileReRoute.cs +++ b/src/Ocelot/Configuration/File/FileReRoute.cs @@ -11,17 +11,21 @@ namespace Ocelot.Configuration.File AddClaimsToRequest = new Dictionary(); RouteClaimsRequirement = new Dictionary(); AddQueriesToRequest = new Dictionary(); + DownstreamHeaderTransform = new Dictionary(); FileCacheOptions = new FileCacheOptions(); QoSOptions = new FileQoSOptions(); RateLimitOptions = new FileRateLimitRule(); AuthenticationOptions = new FileAuthenticationOptions(); HttpHandlerOptions = new FileHttpHandlerOptions(); + UpstreamHeaderTransform = new Dictionary(); } public string DownstreamPathTemplate { get; set; } public string UpstreamPathTemplate { get; set; } public List UpstreamHttpMethod { get; set; } public Dictionary AddHeadersToRequest { get; set; } + public Dictionary UpstreamHeaderTransform { get; set; } + public Dictionary DownstreamHeaderTransform { get; set; } public Dictionary AddClaimsToRequest { get; set; } public Dictionary RouteClaimsRequirement { get; set; } public Dictionary AddQueriesToRequest { get; set; } diff --git a/src/Ocelot/Configuration/HeaderFindAndReplace.cs b/src/Ocelot/Configuration/HeaderFindAndReplace.cs new file mode 100644 index 00000000..f3835415 --- /dev/null +++ b/src/Ocelot/Configuration/HeaderFindAndReplace.cs @@ -0,0 +1,20 @@ +namespace Ocelot.Configuration +{ + public class HeaderFindAndReplace + { + public HeaderFindAndReplace(string key, string find, string replace, int index) + { + Key = key; + Find = find; + Replace = replace; + Index = index; + } + + public string Key {get;} + public string Find {get;} + public string Replace {get;} + + // only index 0 for now.. + public int Index {get;} + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/ReRoute.cs b/src/Ocelot/Configuration/ReRoute.cs index 18e068aa..9efe6dbc 100644 --- a/src/Ocelot/Configuration/ReRoute.cs +++ b/src/Ocelot/Configuration/ReRoute.cs @@ -32,8 +32,12 @@ namespace Ocelot.Configuration RateLimitOptions ratelimitOptions, HttpHandlerOptions httpHandlerOptions, bool useServiceDiscovery, - string serviceName) + string serviceName, + List upstreamHeadersFindAndReplace, + List downstreamHeadersFindAndReplace) { + DownstreamHeadersFindAndReplace = downstreamHeadersFindAndReplace; + UpstreamHeadersFindAndReplace = upstreamHeadersFindAndReplace; ServiceName = serviceName; UseServiceDiscovery = useServiceDiscovery; ReRouteKey = reRouteKey; @@ -91,5 +95,8 @@ namespace Ocelot.Configuration public HttpHandlerOptions HttpHandlerOptions { get; private set; } public bool UseServiceDiscovery {get;private set;} public string ServiceName {get;private set;} + public List UpstreamHeadersFindAndReplace {get;private set;} + public List DownstreamHeadersFindAndReplace {get;private set;} + } } \ No newline at end of file diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 3ddaf7d3..3b0a008f 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -75,6 +75,9 @@ namespace Ocelot.DependencyInjection //add ocelot services... _services.Configure(configurationRoot); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); diff --git a/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs b/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs index 7a4a66ea..3a3b3b2d 100644 --- a/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs +++ b/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs @@ -13,5 +13,6 @@ namespace Ocelot.DownstreamRouteFinder } public List TemplatePlaceholderNameAndValues { get; private set; } public ReRoute ReRoute { get; private set; } + public object UpstreamHeadersFindAndReplace {get;private set;} } } \ No newline at end of file diff --git a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs index abe9ca41..c2574ff9 100644 --- a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs @@ -37,7 +37,7 @@ namespace Ocelot.Errors.Middleware public async Task Invoke(HttpContext context) { try - { + { await TrySetGlobalRequestId(context); _logger.LogDebug("ocelot pipeline started"); diff --git a/src/Ocelot/Headers/HttpContextRequestHeaderReplacer.cs b/src/Ocelot/Headers/HttpContextRequestHeaderReplacer.cs new file mode 100644 index 00000000..83be9fe7 --- /dev/null +++ b/src/Ocelot/Headers/HttpContextRequestHeaderReplacer.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.Headers +{ + public class HttpContextRequestHeaderReplacer : IHttpContextRequestHeaderReplacer + { + public Response Replace(HttpContext context, List fAndRs) + { + foreach (var f in fAndRs) + { + if(context.Request.Headers.TryGetValue(f.Key, out var values)) + { + var replaced = values[f.Index].Replace(f.Find, f.Replace); + context.Request.Headers.Remove(f.Key); + context.Request.Headers.Add(f.Key, replaced); + } + } + + return new OkResponse(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs new file mode 100644 index 00000000..acd9af30 --- /dev/null +++ b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.Headers +{ + public class HttpResponseHeaderReplacer : IHttpResponseHeaderReplacer + { + public Response Replace(HttpResponseMessage response, List fAndRs) + { + foreach (var f in fAndRs) + { + if(response.Headers.TryGetValues(f.Key, out var values)) + { + var replaced = values.ToList()[f.Index].Replace(f.Find, f.Replace); + response.Headers.Remove(f.Key); + response.Headers.Add(f.Key, replaced); + } + } + + return new OkResponse(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Headers/IHttpContextRequestHeaderReplacer.cs b/src/Ocelot/Headers/IHttpContextRequestHeaderReplacer.cs new file mode 100644 index 00000000..f0e969ec --- /dev/null +++ b/src/Ocelot/Headers/IHttpContextRequestHeaderReplacer.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.Headers +{ + public interface IHttpContextRequestHeaderReplacer + { + Response Replace(HttpContext context, List fAndRs); + } +} \ No newline at end of file diff --git a/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs b/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs new file mode 100644 index 00000000..e3056ca6 --- /dev/null +++ b/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Net.Http; +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.Headers +{ + public interface IHttpResponseHeaderReplacer + { + Response Replace(HttpResponseMessage response, List fAndRs); + } +} \ No newline at end of file diff --git a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs new file mode 100644 index 00000000..302efd18 --- /dev/null +++ b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.Headers.Middleware +{ + public class HttpHeadersTransformationMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IOcelotLogger _logger; + private readonly IHttpContextRequestHeaderReplacer _preReplacer; + private readonly IHttpResponseHeaderReplacer _postReplacer; + + public HttpHeadersTransformationMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IRequestScopedDataRepository requestScopedDataRepository, + IHttpContextRequestHeaderReplacer preReplacer, + IHttpResponseHeaderReplacer postReplacer) + : base(requestScopedDataRepository) + { + _next = next; + _postReplacer = postReplacer; + _preReplacer = preReplacer; + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + var preFAndRs = this.DownstreamRoute.ReRoute.UpstreamHeadersFindAndReplace; + + _preReplacer.Replace(context, preFAndRs); + + await _next.Invoke(context); + + var postFAndRs = this.DownstreamRoute.ReRoute.DownstreamHeadersFindAndReplace; + + _postReplacer.Replace(HttpResponseMessage, postFAndRs); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddlewareExtensions.cs b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddlewareExtensions.cs new file mode 100644 index 00000000..4dc08b4d --- /dev/null +++ b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Headers.Middleware +{ + public static class HttpHeadersTransformationMiddlewareExtensions + { + public static IApplicationBuilder UseHttpHeadersTransformationMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs index 466754b4..02c2394f 100644 --- a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs +++ b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs @@ -86,12 +86,15 @@ namespace Ocelot.Middleware // This is registered first so it can catch any errors and issue an appropriate response builder.UseResponderMiddleware(); - // Initialises downstream request - builder.UseDownstreamRequestInitialiser(); - // Then we get the downstream route information builder.UseDownstreamRouteFinderMiddleware(); + // Now we have the ds route we can transform headers and stuff? + builder.UseHttpHeadersTransformationMiddleware(); + + // Initialises downstream request + builder.UseDownstreamRequestInitialiser(); + // We check whether the request is ratelimit, and if there is no continue processing builder.UseRateLimiting(); diff --git a/src/Ocelot/Request/Builder/HttpRequestCreator.cs b/src/Ocelot/Request/Builder/HttpRequestCreator.cs index 8c3c9218..a2dbb939 100644 --- a/src/Ocelot/Request/Builder/HttpRequestCreator.cs +++ b/src/Ocelot/Request/Builder/HttpRequestCreator.cs @@ -14,7 +14,7 @@ namespace Ocelot.Request.Builder bool useCookieContainer, bool allowAutoRedirect) { - return new OkResponse(new Request(httpRequestMessage, isQos, qosProvider, useCookieContainer, allowAutoRedirect)); + return new OkResponse(new Request(httpRequestMessage, isQos, qosProvider, allowAutoRedirect, useCookieContainer)); } } } \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/HeaderTests.cs b/test/Ocelot.AcceptanceTests/HeaderTests.cs new file mode 100644 index 00000000..99320cd3 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/HeaderTests.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class HeaderTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + private string _downstreamPath; + + public HeaderTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_transform_upstream_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + UpstreamHeaderTransform = new Dictionary + { + {"Laz", "D, GP"} + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 200, "Laz")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenIAddAHeader("Laz", "D")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("GP")) + .BDDfy(); + } + + [Fact] + public void should_transform_downstream_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHeaderTransform = new Dictionary + { + {"Location", "http://www.bbc.co.uk/, http://ocelot.com/"} + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 200, "Location", "http://www.bbc.co.uk/")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://ocelot.com/")) + .BDDfy(); + } + + [Fact] + public void should_fix_issue_190() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 6773, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHeaderTransform = new Dictionary + { + {"Location", "http://localhost:6773, {BaseUrl}"} + }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + AllowAutoRedirect = false + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6773", "/", 302, "Location", "http://localhost:6773/pay/Receive")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect)) + .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://localhost:5000/pay/Receive")) + .BDDfy(); + } + + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + if(context.Request.Headers.TryGetValue(headerKey, out var values)) + { + var result = values.First(); + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(result); + } + }); + }) + .Build(); + + _builder.Start(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey, string headerValue) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + context.Response.OnStarting(() => { + context.Response.Headers.Add(headerKey, headerValue); + context.Response.StatusCode = statusCode; + return Task.CompletedTask; + }); + }); + }) + .Build(); + + _builder.Start(); + } + + internal void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPath) + { + _downstreamPath.ShouldBe(expectedDownstreamPath); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 9cdb8e4e..7f260d5f 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -108,6 +108,12 @@ namespace Ocelot.AcceptanceTests _ocelotClient = _ocelotServer.CreateClient(); } + public void ThenTheResponseHeaderIs(string key, string value) + { + var header = _response.Headers.GetValues(key); + header.First().ShouldBe(value); + } + public void GivenOcelotIsRunningUsingJsonSerializedCache() { _webHostBuilder = new WebHostBuilder(); @@ -326,6 +332,11 @@ namespace Ocelot.AcceptanceTests _response = _ocelotClient.GetAsync(url).Result; } + public void GivenIAddAHeader(string key, string value) + { + _ocelotClient.DefaultRequestHeaders.Add(key, value); + } + public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) { var tasks = new Task[times]; diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index d39fa394..807bb30e 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -39,6 +39,7 @@ namespace Ocelot.UnitTests.Configuration private Mock _regionCreator; private Mock _httpHandlerOptionsCreator; private Mock _adminPath; + private readonly Mock _headerFindAndReplaceCreator; public FileConfigurationCreatorTests() { @@ -56,6 +57,7 @@ namespace Ocelot.UnitTests.Configuration _regionCreator = new Mock(); _httpHandlerOptionsCreator = new Mock(); _adminPath = new Mock(); + _headerFindAndReplaceCreator = new Mock(); _ocelotConfigurationCreator = new FileOcelotConfigurationCreator( _fileConfig.Object, @@ -71,7 +73,8 @@ namespace Ocelot.UnitTests.Configuration _rateLimitOptions.Object, _regionCreator.Object, _httpHandlerOptionsCreator.Object, - _adminPath.Object); + _adminPath.Object, + _headerFindAndReplaceCreator.Object); } [Fact] @@ -91,6 +94,7 @@ namespace Ocelot.UnitTests.Configuration } })) .And(x => x.GivenTheFollowingIsReturned(serviceProviderConfig)) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheConfigIsValid()) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheServiceProviderCreatorIsCalledCorrectly()) @@ -121,10 +125,12 @@ namespace Ocelot.UnitTests.Configuration }, })) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheFollowingRegionIsReturned("region")) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheRegionCreatorIsCalledCorrectly("region")) + .And(x => x.ThenTheHeaderFindAndReplaceCreatorIsCalledCorrectly()) .BDDfy(); } @@ -148,6 +154,7 @@ namespace Ocelot.UnitTests.Configuration }, })) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheRateLimitOptionsCreatorIsCalledCorrectly()) @@ -187,6 +194,7 @@ namespace Ocelot.UnitTests.Configuration }, })) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(serviceOptions)) .And(x => x.GivenTheQosOptionsCreatorReturns(expected)) .When(x => x.WhenICreateTheConfig()) @@ -214,6 +222,7 @@ namespace Ocelot.UnitTests.Configuration }, })) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheReRoutesAre(new List @@ -248,6 +257,7 @@ namespace Ocelot.UnitTests.Configuration }, })) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheReRoutesAre(new List @@ -290,6 +300,7 @@ namespace Ocelot.UnitTests.Configuration } })) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheReRoutesAre(new List @@ -325,6 +336,7 @@ namespace Ocelot.UnitTests.Configuration } })) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheReRoutesAre(new List @@ -359,6 +371,7 @@ namespace Ocelot.UnitTests.Configuration } })) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheUpstreamTemplatePatternCreatorReturns("(?i)/api/products/.*/$")) .When(x => x.WhenICreateTheConfig()) @@ -398,6 +411,7 @@ namespace Ocelot.UnitTests.Configuration } })) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheRequestIdCreatorReturns("blahhhh")) .When(x => x.WhenICreateTheConfig()) @@ -435,6 +449,7 @@ namespace Ocelot.UnitTests.Configuration }, })) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheFollowingHttpHandlerOptionsAreReturned(httpHandlerOptions)) .When(x => x.WhenICreateTheConfig()) @@ -470,6 +485,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheConfigIs(fileConfig)) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheAuthOptionsCreatorReturns(authenticationOptions)) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheClaimsToThingCreatorReturns(new List { new ClaimToThing("CustomerId", "CustomerId", "", 0) })) @@ -504,6 +520,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => x.GivenTheConfigIs(fileConfig)) .And(x => x.GivenTheConfigIsValid()) + .And(x => GivenTheHeaderFindAndReplaceCreatorReturns()) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheAuthOptionsCreatorReturns(authenticationOptions)) .When(x => x.WhenICreateTheConfig()) @@ -661,6 +678,17 @@ namespace Ocelot.UnitTests.Configuration .Verify(x => x.Create(_fileConfiguration.GlobalConfiguration), Times.Once); } + private void ThenTheHeaderFindAndReplaceCreatorIsCalledCorrectly() + { + _headerFindAndReplaceCreator + .Verify(x => x.Create(It.IsAny()), Times.Once); + } + + private void GivenTheHeaderFindAndReplaceCreatorReturns() + { + _headerFindAndReplaceCreator.Setup(x => x.Create(It.IsAny())).Returns(new HeaderTransformations(new List(), new List())); + } + private void GivenTheFollowingIsReturned(ServiceProviderConfiguration serviceProviderConfiguration) { _serviceProviderConfigCreator diff --git a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs new file mode 100644 index 00000000..ceac035e --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Middleware; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class HeaderFindAndReplaceCreatorTests + { + private HeaderFindAndReplaceCreator _creator; + private FileReRoute _reRoute; + private HeaderTransformations _result; + private Mock _finder; + + public HeaderFindAndReplaceCreatorTests() + { + _finder = new Mock(); + _creator = new HeaderFindAndReplaceCreator(_finder.Object); + } + + [Fact] + public void should_create() + { + var reRoute = new FileReRoute + { + UpstreamHeaderTransform = new Dictionary + { + {"Test", "Test, Chicken"}, + + {"Moop", "o, a"} + }, + DownstreamHeaderTransform = new Dictionary + { + {"Pop", "West, East"}, + + {"Bop", "e, r"} + } + }; + + var upstream = new List + { + new HeaderFindAndReplace("Test", "Test", "Chicken", 0), + new HeaderFindAndReplace("Moop", "o", "a", 0) + }; + + var downstream = new List + { + new HeaderFindAndReplace("Pop", "West", "East", 0), + new HeaderFindAndReplace("Bop", "e", "r", 0) + }; + + this.Given(x => GivenTheReRoute(reRoute)) + .When(x => WhenICreate()) + .Then(x => ThenTheFollowingUpstreamIsReturned(upstream)) + .Then(x => ThenTheFollowingDownstreamIsReturned(downstream)) + .BDDfy(); + } + + [Fact] + public void should_use_base_url_placeholder() + { + var reRoute = new FileReRoute + { + DownstreamHeaderTransform = new Dictionary + { + {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, + } + }; + + var downstream = new List + { + new HeaderFindAndReplace("Location", "http://www.bbc.co.uk/", "http://ocelot.com/", 0), + }; + + this.Given(x => GivenTheReRoute(reRoute)) + .And(x => GivenTheBaseUrlIs("http://ocelot.com/")) + .When(x => WhenICreate()) + .Then(x => ThenTheFollowingDownstreamIsReturned(downstream)) + .BDDfy(); + } + + + [Fact] + public void should_use_base_url_partial_placeholder() + { + var reRoute = new FileReRoute + { + DownstreamHeaderTransform = new Dictionary + { + {"Location", "http://www.bbc.co.uk/pay, {BaseUrl}pay"}, + } + }; + + var downstream = new List + { + new HeaderFindAndReplace("Location", "http://www.bbc.co.uk/pay", "http://ocelot.com/pay", 0), + }; + + this.Given(x => GivenTheReRoute(reRoute)) + .And(x => GivenTheBaseUrlIs("http://ocelot.com/")) + .When(x => WhenICreate()) + .Then(x => ThenTheFollowingDownstreamIsReturned(downstream)) + .BDDfy(); + } + + private void GivenTheBaseUrlIs(string baseUrl) + { + _finder.Setup(x => x.Find()).Returns(baseUrl); + } + + private void ThenTheFollowingDownstreamIsReturned(List downstream) + { + _result.Downstream.Count.ShouldBe(downstream.Count); + + for (int i = 0; i < _result.Downstream.Count; i++) + { + var result = _result.Downstream[i]; + var expected = downstream[i]; + result.Find.ShouldBe(expected.Find); + result.Index.ShouldBe(expected.Index); + result.Key.ShouldBe(expected.Key); + result.Replace.ShouldBe(expected.Replace); + } + } + + private void GivenTheReRoute(FileReRoute reRoute) + { + _reRoute = reRoute; + } + + private void WhenICreate() + { + _result = _creator.Create(_reRoute); + } + + private void ThenTheFollowingUpstreamIsReturned(List expecteds) + { + _result.Upstream.Count.ShouldBe(expecteds.Count); + + for (int i = 0; i < _result.Upstream.Count; i++) + { + var result = _result.Upstream[i]; + var expected = expecteds[i]; + result.Find.ShouldBe(expected.Find); + result.Index.ShouldBe(expected.Index); + result.Key.ShouldBe(expected.Key); + result.Replace.ShouldBe(expected.Replace); + } + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs b/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs new file mode 100644 index 00000000..26e0b1e2 --- /dev/null +++ b/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs @@ -0,0 +1,91 @@ +using Xunit; +using Shouldly; +using Ocelot.Headers.Middleware; +using TestStack.BDDfy; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using Ocelot.Responses; +using Ocelot.Configuration; +using Ocelot.Headers; + +namespace Ocelot.UnitTests.Headers +{ + public class HttpContextRequestHeaderReplacerTests + { + private HttpContext _context; + private List _fAndRs; + private HttpContextRequestHeaderReplacer _replacer; + private Response _result; + + public HttpContextRequestHeaderReplacerTests() + { + _replacer = new HttpContextRequestHeaderReplacer(); + } + + [Fact] + public void should_replace_headers() + { + var context = new DefaultHttpContext(); + context.Request.Headers.Add("test", "test"); + + var fAndRs = new List(); + fAndRs.Add(new HeaderFindAndReplace("test", "test", "chiken", 0)); + + this.Given(x => GivenTheFollowingHttpRequest(context)) + .And(x => GivenTheFollowingHeaderReplacements(fAndRs)) + .When(x => WhenICallTheReplacer()) + .Then(x => ThenTheHeadersAreReplaced()) + .BDDfy(); + } + + [Fact] + public void should_not_replace_headers() + { + var context = new DefaultHttpContext(); + context.Request.Headers.Add("test", "test"); + + var fAndRs = new List(); + + this.Given(x => GivenTheFollowingHttpRequest(context)) + .And(x => GivenTheFollowingHeaderReplacements(fAndRs)) + .When(x => WhenICallTheReplacer()) + .Then(x => ThenTheHeadersAreNotReplaced()) + .BDDfy(); + } + + private void ThenTheHeadersAreNotReplaced() + { + _result.ShouldBeOfType(); + foreach (var f in _fAndRs) + { + _context.Request.Headers.TryGetValue(f.Key, out var values); + values[f.Index].ShouldBe("test"); + } + } + + private void GivenTheFollowingHttpRequest(HttpContext context) + { + _context = context; + } + + private void GivenTheFollowingHeaderReplacements(List fAndRs) + { + _fAndRs = fAndRs; + } + + private void WhenICallTheReplacer() + { + _result = _replacer.Replace(_context, _fAndRs); + } + + private void ThenTheHeadersAreReplaced() + { + _result.ShouldBeOfType(); + foreach (var f in _fAndRs) + { + _context.Request.Headers.TryGetValue(f.Key, out var values); + values[f.Index].ShouldBe(f.Replace); + } + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs new file mode 100644 index 00000000..25be507b --- /dev/null +++ b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs @@ -0,0 +1,93 @@ +using Xunit; +using Shouldly; +using Ocelot.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Builder; +using Ocelot.Headers.Middleware; +using TestStack.BDDfy; +using System.Linq; +using System.Threading.Tasks; +using System; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using Moq; +using Ocelot.Configuration; +using Ocelot.DownstreamRouteFinder; +using Ocelot.Responses; +using Ocelot.Configuration.Builder; +using Ocelot.Headers; +using System.Net.Http; + +namespace Ocelot.UnitTests.Headers +{ + public class HttpHeadersTransformationMiddlewareTests : ServerHostedMiddlewareTest + { + private Mock _preReplacer; + private Mock _postReplacer; + + public HttpHeadersTransformationMiddlewareTests() + { + _preReplacer = new Mock(); + _postReplacer = new Mock(); + + GivenTheTestServerIsConfigured(); + } + + [Fact] + public void should_call_pre_and_post_header_transforms() + { + this.Given(x => GivenTheFollowingRequest()) + .And(x => GivenTheReRouteHasPreFindAndReplaceSetUp()) + .And(x => GivenTheHttpResponseMessageIs()) + .When(x => WhenICallTheMiddleware()) + .Then(x => ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly()) + .And(x => ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly()) + .BDDfy(); + } + + private void GivenTheHttpResponseMessageIs() + { + var httpResponseMessage = new HttpResponseMessage(); + var response = new OkResponse(httpResponseMessage); + ScopedRepository.Setup(x => x.Get("HttpResponseMessage")).Returns(response); + } + + private void GivenTheReRouteHasPreFindAndReplaceSetUp() + { + var fAndRs = new List(); + var reRoute = new ReRouteBuilder().WithUpstreamHeaderFindAndReplace(fAndRs).WithDownstreamHeaderFindAndReplace(fAndRs).Build(); + var dR = new DownstreamRoute(null, reRoute); + var response = new OkResponse(dR); + ScopedRepository.Setup(x => x.Get("DownstreamRoute")).Returns(response); + } + + private void ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly() + { + _preReplacer.Verify(x => x.Replace(It.IsAny(), It.IsAny>()), Times.Once); + } + + private void ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly() + { + _postReplacer.Verify(x => x.Replace(It.IsAny(), It.IsAny>()), Times.Once); + } + + private void GivenTheFollowingRequest() + { + Client.DefaultRequestHeaders.Add("test", "test"); + } + + protected override void GivenTheTestServerServicesAreConfigured(IServiceCollection services) + { + services.AddSingleton(); + services.AddLogging(); + services.AddSingleton(ScopedRepository.Object); + services.AddSingleton(_preReplacer.Object); + services.AddSingleton(_postReplacer.Object); + } + + protected override void GivenTheTestServerPipelineIsConfigured(IApplicationBuilder app) + { + app.UseHttpHeadersTransformationMiddleware(); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs new file mode 100644 index 00000000..8d7c5a22 --- /dev/null +++ b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs @@ -0,0 +1,91 @@ +using Xunit; +using Shouldly; +using TestStack.BDDfy; +using System.Net.Http; +using Ocelot.Headers; +using Ocelot.Configuration; +using System.Collections.Generic; +using Ocelot.Responses; +using System.Linq; + +namespace Ocelot.UnitTests.Headers +{ + public class HttpResponseHeaderReplacerTests + { + private HttpResponseMessage _response; + private HttpResponseHeaderReplacer _replacer; + private List _headerFindAndReplaces; + private Response _result; + + public HttpResponseHeaderReplacerTests() + { + _replacer = new HttpResponseHeaderReplacer(); + } + [Fact] + public void should_replace_headers() + { + var response = new HttpResponseMessage(); + response.Headers.Add("test", "test"); + + var fAndRs = new List(); + fAndRs.Add(new HeaderFindAndReplace("test", "test", "chiken", 0)); + + this.Given(x => GivenTheHttpResponse(response)) + .And(x => GivenTheFollowingHeaderReplacements(fAndRs)) + .When(x => WhenICallTheReplacer()) + .Then(x => ThenTheHeadersAreReplaced()) + .BDDfy(); + } + + [Fact] + public void should_not_replace_headers() + { + var response = new HttpResponseMessage(); + response.Headers.Add("test", "test"); + + var fAndRs = new List(); + + this.Given(x => GivenTheHttpResponse(response)) + .And(x => GivenTheFollowingHeaderReplacements(fAndRs)) + .When(x => WhenICallTheReplacer()) + .Then(x => ThenTheHeadersAreNotReplaced()) + .BDDfy(); + } + + + private void ThenTheHeadersAreNotReplaced() + { + _result.ShouldBeOfType(); + foreach (var f in _headerFindAndReplaces) + { + _response.Headers.TryGetValues(f.Key, out var values); + values.ToList()[f.Index].ShouldBe("test"); + } + } + + private void GivenTheFollowingHeaderReplacements(List fAndRs) + { + _headerFindAndReplaces = fAndRs; + } + + private void GivenTheHttpResponse(HttpResponseMessage response) + { + _response = response; + } + + private void WhenICallTheReplacer() + { + _result = _replacer.Replace(_response, _headerFindAndReplaces); + } + + private void ThenTheHeadersAreReplaced() + { + _result.ShouldBeOfType(); + foreach (var f in _headerFindAndReplaces) + { + _response.Headers.TryGetValues(f.Key, out var values); + values.ToList()[f.Index].ShouldBe(f.Replace); + } + } + } +} \ No newline at end of file