Feature/transform headers (#204)

* New feature that lets a user do find and replace on an upstream header

* can transform downstream and upstream headers, not sure if interface is good

* can replace location header with placeholder

* added some syntax
This commit is contained in:
Tom Pallister 2018-01-22 20:21:29 +00:00 committed by GitHub
parent 9c048ba615
commit d0eee70c46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1028 additions and 9 deletions

View File

@ -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 <https://github.com/TomPallister/Ocelot/issues/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.

View File

@ -27,6 +27,7 @@ Thanks for taking a look at the Ocelot documentation. Please use the left hand n
features/raft features/raft
features/caching features/caching
features/qualityofservice features/qualityofservice
features/headerstransformation
features/claimstransformation features/claimstransformation
features/logging features/logging
features/requestid features/requestid

View File

@ -36,6 +36,9 @@ namespace Ocelot.Configuration.Builder
private bool _useServiceDiscovery; private bool _useServiceDiscovery;
private string _serviceName; private string _serviceName;
private List<HeaderFindAndReplace> _upstreamHeaderFindAndReplace;
private List<HeaderFindAndReplace> _downstreamHeaderFindAndReplace;
public ReRouteBuilder WithLoadBalancer(string loadBalancer) public ReRouteBuilder WithLoadBalancer(string loadBalancer)
{ {
_loadBalancer = loadBalancer; _loadBalancer = loadBalancer;
@ -198,6 +201,18 @@ namespace Ocelot.Configuration.Builder
return this; return this;
} }
public ReRouteBuilder WithUpstreamHeaderFindAndReplace(List<HeaderFindAndReplace> upstreamHeaderFindAndReplace)
{
_upstreamHeaderFindAndReplace = upstreamHeaderFindAndReplace;
return this;
}
public ReRouteBuilder WithDownstreamHeaderFindAndReplace(List<HeaderFindAndReplace> downstreamHeaderFindAndReplace)
{
_downstreamHeaderFindAndReplace = downstreamHeaderFindAndReplace;
return this;
}
public ReRoute Build() public ReRoute Build()
{ {
return new ReRoute( return new ReRoute(
@ -226,7 +241,9 @@ namespace Ocelot.Configuration.Builder
_rateLimitOptions, _rateLimitOptions,
_httpHandlerOptions, _httpHandlerOptions,
_useServiceDiscovery, _useServiceDiscovery,
_serviceName); _serviceName,
_upstreamHeaderFindAndReplace,
_downstreamHeaderFindAndReplace);
} }
} }
} }

View File

@ -37,6 +37,7 @@ namespace Ocelot.Configuration.Creator
private readonly IRegionCreator _regionCreator; private readonly IRegionCreator _regionCreator;
private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator;
private readonly IAdministrationPath _adminPath; private readonly IAdministrationPath _adminPath;
private readonly IHeaderFindAndReplaceCreator _headerFAndRCreator;
public FileOcelotConfigurationCreator( public FileOcelotConfigurationCreator(
@ -53,9 +54,11 @@ namespace Ocelot.Configuration.Creator
IRateLimitOptionsCreator rateLimitOptionsCreator, IRateLimitOptionsCreator rateLimitOptionsCreator,
IRegionCreator regionCreator, IRegionCreator regionCreator,
IHttpHandlerOptionsCreator httpHandlerOptionsCreator, IHttpHandlerOptionsCreator httpHandlerOptionsCreator,
IAdministrationPath adminPath IAdministrationPath adminPath,
IHeaderFindAndReplaceCreator headerFAndRCreator
) )
{ {
_headerFAndRCreator = headerFAndRCreator;
_adminPath = adminPath; _adminPath = adminPath;
_regionCreator = regionCreator; _regionCreator = regionCreator;
_rateLimitOptionsCreator = rateLimitOptionsCreator; _rateLimitOptionsCreator = rateLimitOptionsCreator;
@ -128,6 +131,8 @@ namespace Ocelot.Configuration.Creator
var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileReRoute); var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileReRoute);
var hAndRs = _headerFAndRCreator.Create(fileReRoute);
var reRoute = new ReRouteBuilder() var reRoute = new ReRouteBuilder()
.WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate) .WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate)
.WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate) .WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate)
@ -155,6 +160,8 @@ namespace Ocelot.Configuration.Creator
.WithHttpHandlerOptions(httpHandlerOptions) .WithHttpHandlerOptions(httpHandlerOptions)
.WithServiceName(fileReRoute.ServiceName) .WithServiceName(fileReRoute.ServiceName)
.WithUseServiceDiscovery(fileReRoute.UseServiceDiscovery) .WithUseServiceDiscovery(fileReRoute.UseServiceDiscovery)
.WithUpstreamHeaderFindAndReplace(hAndRs.Upstream)
.WithDownstreamHeaderFindAndReplace(hAndRs.Downstream)
.Build(); .Build();
return reRoute; return reRoute;

View File

@ -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<string, Func<string>> _placeholders;
public HeaderFindAndReplaceCreator(IBaseUrlFinder finder)
{
_finder = finder;
_placeholders = new Dictionary<string, Func<string>>();
_placeholders.Add("{BaseUrl}", () => {
return _finder.Find();
});
}
public HeaderTransformations Create(FileReRoute fileReRoute)
{
var upstream = new List<HeaderFindAndReplace>();
foreach(var input in fileReRoute.UpstreamHeaderTransform)
{
var hAndr = Map(input);
upstream.Add(hAndr);
}
var downstream = new List<HeaderFindAndReplace>();
foreach(var input in fileReRoute.DownstreamHeaderTransform)
{
var hAndr = Map(input);
downstream.Add(hAndr);
}
return new HeaderTransformations(upstream, downstream);
}
private HeaderFindAndReplace Map(KeyValuePair<string,string> 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;
}
}
}

View File

@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace Ocelot.Configuration.Creator
{
public class HeaderTransformations
{
public HeaderTransformations(List<HeaderFindAndReplace> upstream, List<HeaderFindAndReplace> downstream)
{
Upstream = upstream;
Downstream = downstream;
}
public List<HeaderFindAndReplace> Upstream {get;private set;}
public List<HeaderFindAndReplace> Downstream {get;private set;}
}
}

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
using Ocelot.Configuration.File;
namespace Ocelot.Configuration.Creator
{
public interface IHeaderFindAndReplaceCreator
{
HeaderTransformations Create(FileReRoute fileReRoute);
}
}

View File

@ -11,17 +11,21 @@ namespace Ocelot.Configuration.File
AddClaimsToRequest = new Dictionary<string, string>(); AddClaimsToRequest = new Dictionary<string, string>();
RouteClaimsRequirement = new Dictionary<string, string>(); RouteClaimsRequirement = new Dictionary<string, string>();
AddQueriesToRequest = new Dictionary<string, string>(); AddQueriesToRequest = new Dictionary<string, string>();
DownstreamHeaderTransform = new Dictionary<string, string>();
FileCacheOptions = new FileCacheOptions(); FileCacheOptions = new FileCacheOptions();
QoSOptions = new FileQoSOptions(); QoSOptions = new FileQoSOptions();
RateLimitOptions = new FileRateLimitRule(); RateLimitOptions = new FileRateLimitRule();
AuthenticationOptions = new FileAuthenticationOptions(); AuthenticationOptions = new FileAuthenticationOptions();
HttpHandlerOptions = new FileHttpHandlerOptions(); HttpHandlerOptions = new FileHttpHandlerOptions();
UpstreamHeaderTransform = new Dictionary<string, string>();
} }
public string DownstreamPathTemplate { get; set; } public string DownstreamPathTemplate { get; set; }
public string UpstreamPathTemplate { get; set; } public string UpstreamPathTemplate { get; set; }
public List<string> UpstreamHttpMethod { get; set; } public List<string> UpstreamHttpMethod { get; set; }
public Dictionary<string, string> AddHeadersToRequest { get; set; } public Dictionary<string, string> AddHeadersToRequest { get; set; }
public Dictionary<string, string> UpstreamHeaderTransform { get; set; }
public Dictionary<string, string> DownstreamHeaderTransform { get; set; }
public Dictionary<string, string> AddClaimsToRequest { get; set; } public Dictionary<string, string> AddClaimsToRequest { get; set; }
public Dictionary<string, string> RouteClaimsRequirement { get; set; } public Dictionary<string, string> RouteClaimsRequirement { get; set; }
public Dictionary<string, string> AddQueriesToRequest { get; set; } public Dictionary<string, string> AddQueriesToRequest { get; set; }

View File

@ -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;}
}
}

View File

@ -32,8 +32,12 @@ namespace Ocelot.Configuration
RateLimitOptions ratelimitOptions, RateLimitOptions ratelimitOptions,
HttpHandlerOptions httpHandlerOptions, HttpHandlerOptions httpHandlerOptions,
bool useServiceDiscovery, bool useServiceDiscovery,
string serviceName) string serviceName,
List<HeaderFindAndReplace> upstreamHeadersFindAndReplace,
List<HeaderFindAndReplace> downstreamHeadersFindAndReplace)
{ {
DownstreamHeadersFindAndReplace = downstreamHeadersFindAndReplace;
UpstreamHeadersFindAndReplace = upstreamHeadersFindAndReplace;
ServiceName = serviceName; ServiceName = serviceName;
UseServiceDiscovery = useServiceDiscovery; UseServiceDiscovery = useServiceDiscovery;
ReRouteKey = reRouteKey; ReRouteKey = reRouteKey;
@ -91,5 +95,8 @@ namespace Ocelot.Configuration
public HttpHandlerOptions HttpHandlerOptions { get; private set; } public HttpHandlerOptions HttpHandlerOptions { get; private set; }
public bool UseServiceDiscovery {get;private set;} public bool UseServiceDiscovery {get;private set;}
public string ServiceName {get;private set;} public string ServiceName {get;private set;}
public List<HeaderFindAndReplace> UpstreamHeadersFindAndReplace {get;private set;}
public List<HeaderFindAndReplace> DownstreamHeadersFindAndReplace {get;private set;}
} }
} }

View File

@ -75,6 +75,9 @@ namespace Ocelot.DependencyInjection
//add ocelot services... //add ocelot services...
_services.Configure<FileConfiguration>(configurationRoot); _services.Configure<FileConfiguration>(configurationRoot);
_services.TryAddSingleton<IHttpResponseHeaderReplacer, HttpResponseHeaderReplacer>();
_services.TryAddSingleton<IHttpContextRequestHeaderReplacer, HttpContextRequestHeaderReplacer>();
_services.TryAddSingleton<IHeaderFindAndReplaceCreator, HeaderFindAndReplaceCreator>();
_services.TryAddSingleton<IOcelotConfigurationCreator, FileOcelotConfigurationCreator>(); _services.TryAddSingleton<IOcelotConfigurationCreator, FileOcelotConfigurationCreator>();
_services.TryAddSingleton<IOcelotConfigurationRepository, InMemoryOcelotConfigurationRepository>(); _services.TryAddSingleton<IOcelotConfigurationRepository, InMemoryOcelotConfigurationRepository>();
_services.TryAddSingleton<IConfigurationValidator, FileConfigurationFluentValidator>(); _services.TryAddSingleton<IConfigurationValidator, FileConfigurationFluentValidator>();

View File

@ -13,5 +13,6 @@ namespace Ocelot.DownstreamRouteFinder
} }
public List<PlaceholderNameAndValue> TemplatePlaceholderNameAndValues { get; private set; } public List<PlaceholderNameAndValue> TemplatePlaceholderNameAndValues { get; private set; }
public ReRoute ReRoute { get; private set; } public ReRoute ReRoute { get; private set; }
public object UpstreamHeadersFindAndReplace {get;private set;}
} }
} }

View File

@ -37,7 +37,7 @@ namespace Ocelot.Errors.Middleware
public async Task Invoke(HttpContext context) public async Task Invoke(HttpContext context)
{ {
try try
{ {
await TrySetGlobalRequestId(context); await TrySetGlobalRequestId(context);
_logger.LogDebug("ocelot pipeline started"); _logger.LogDebug("ocelot pipeline started");

View File

@ -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<HeaderFindAndReplace> 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();
}
}
}

View File

@ -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<HeaderFindAndReplace> 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();
}
}
}

View File

@ -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<HeaderFindAndReplace> fAndRs);
}
}

View File

@ -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<HeaderFindAndReplace> fAndRs);
}
}

View File

@ -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<HttpHeadersTransformationMiddleware>();
}
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);
}
}
}

View File

@ -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<HttpHeadersTransformationMiddleware>();
}
}
}

View File

@ -86,12 +86,15 @@ namespace Ocelot.Middleware
// This is registered first so it can catch any errors and issue an appropriate response // This is registered first so it can catch any errors and issue an appropriate response
builder.UseResponderMiddleware(); builder.UseResponderMiddleware();
// Initialises downstream request
builder.UseDownstreamRequestInitialiser();
// Then we get the downstream route information // Then we get the downstream route information
builder.UseDownstreamRouteFinderMiddleware(); 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 // We check whether the request is ratelimit, and if there is no continue processing
builder.UseRateLimiting(); builder.UseRateLimiting();

View File

@ -14,7 +14,7 @@ namespace Ocelot.Request.Builder
bool useCookieContainer, bool useCookieContainer,
bool allowAutoRedirect) bool allowAutoRedirect)
{ {
return new OkResponse<Request>(new Request(httpRequestMessage, isQos, qosProvider, useCookieContainer, allowAutoRedirect)); return new OkResponse<Request>(new Request(httpRequestMessage, isQos, qosProvider, allowAutoRedirect, useCookieContainer));
} }
} }
} }

View File

@ -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<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
DownstreamHost = "localhost",
DownstreamPort = 51879,
UpstreamPathTemplate = "/",
UpstreamHttpMethod = new List<string> { "Get" },
UpstreamHeaderTransform = new Dictionary<string,string>
{
{"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<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
DownstreamHost = "localhost",
DownstreamPort = 51879,
UpstreamPathTemplate = "/",
UpstreamHttpMethod = new List<string> { "Get" },
DownstreamHeaderTransform = new Dictionary<string,string>
{
{"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<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
DownstreamHost = "localhost",
DownstreamPort = 6773,
UpstreamPathTemplate = "/",
UpstreamHttpMethod = new List<string> { "Get" },
DownstreamHeaderTransform = new Dictionary<string,string>
{
{"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();
}
}
}

View File

@ -108,6 +108,12 @@ namespace Ocelot.AcceptanceTests
_ocelotClient = _ocelotServer.CreateClient(); _ocelotClient = _ocelotServer.CreateClient();
} }
public void ThenTheResponseHeaderIs(string key, string value)
{
var header = _response.Headers.GetValues(key);
header.First().ShouldBe(value);
}
public void GivenOcelotIsRunningUsingJsonSerializedCache() public void GivenOcelotIsRunningUsingJsonSerializedCache()
{ {
_webHostBuilder = new WebHostBuilder(); _webHostBuilder = new WebHostBuilder();
@ -326,6 +332,11 @@ namespace Ocelot.AcceptanceTests
_response = _ocelotClient.GetAsync(url).Result; _response = _ocelotClient.GetAsync(url).Result;
} }
public void GivenIAddAHeader(string key, string value)
{
_ocelotClient.DefaultRequestHeaders.Add(key, value);
}
public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times)
{ {
var tasks = new Task[times]; var tasks = new Task[times];

View File

@ -39,6 +39,7 @@ namespace Ocelot.UnitTests.Configuration
private Mock<IRegionCreator> _regionCreator; private Mock<IRegionCreator> _regionCreator;
private Mock<IHttpHandlerOptionsCreator> _httpHandlerOptionsCreator; private Mock<IHttpHandlerOptionsCreator> _httpHandlerOptionsCreator;
private Mock<IAdministrationPath> _adminPath; private Mock<IAdministrationPath> _adminPath;
private readonly Mock<IHeaderFindAndReplaceCreator> _headerFindAndReplaceCreator;
public FileConfigurationCreatorTests() public FileConfigurationCreatorTests()
{ {
@ -56,6 +57,7 @@ namespace Ocelot.UnitTests.Configuration
_regionCreator = new Mock<IRegionCreator>(); _regionCreator = new Mock<IRegionCreator>();
_httpHandlerOptionsCreator = new Mock<IHttpHandlerOptionsCreator>(); _httpHandlerOptionsCreator = new Mock<IHttpHandlerOptionsCreator>();
_adminPath = new Mock<IAdministrationPath>(); _adminPath = new Mock<IAdministrationPath>();
_headerFindAndReplaceCreator = new Mock<IHeaderFindAndReplaceCreator>();
_ocelotConfigurationCreator = new FileOcelotConfigurationCreator( _ocelotConfigurationCreator = new FileOcelotConfigurationCreator(
_fileConfig.Object, _fileConfig.Object,
@ -71,7 +73,8 @@ namespace Ocelot.UnitTests.Configuration
_rateLimitOptions.Object, _rateLimitOptions.Object,
_regionCreator.Object, _regionCreator.Object,
_httpHandlerOptionsCreator.Object, _httpHandlerOptionsCreator.Object,
_adminPath.Object); _adminPath.Object,
_headerFindAndReplaceCreator.Object);
} }
[Fact] [Fact]
@ -91,6 +94,7 @@ namespace Ocelot.UnitTests.Configuration
} }
})) }))
.And(x => x.GivenTheFollowingIsReturned(serviceProviderConfig)) .And(x => x.GivenTheFollowingIsReturned(serviceProviderConfig))
.And(x => GivenTheHeaderFindAndReplaceCreatorReturns())
.And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigIsValid())
.When(x => x.WhenICreateTheConfig()) .When(x => x.WhenICreateTheConfig())
.Then(x => x.ThenTheServiceProviderCreatorIsCalledCorrectly()) .Then(x => x.ThenTheServiceProviderCreatorIsCalledCorrectly())
@ -121,10 +125,12 @@ namespace Ocelot.UnitTests.Configuration
}, },
})) }))
.And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions))
.And(x => GivenTheHeaderFindAndReplaceCreatorReturns())
.And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigIsValid())
.And(x => x.GivenTheFollowingRegionIsReturned("region")) .And(x => x.GivenTheFollowingRegionIsReturned("region"))
.When(x => x.WhenICreateTheConfig()) .When(x => x.WhenICreateTheConfig())
.Then(x => x.ThenTheRegionCreatorIsCalledCorrectly("region")) .Then(x => x.ThenTheRegionCreatorIsCalledCorrectly("region"))
.And(x => x.ThenTheHeaderFindAndReplaceCreatorIsCalledCorrectly())
.BDDfy(); .BDDfy();
} }
@ -148,6 +154,7 @@ namespace Ocelot.UnitTests.Configuration
}, },
})) }))
.And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigIsValid())
.And(x => GivenTheHeaderFindAndReplaceCreatorReturns())
.And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions))
.When(x => x.WhenICreateTheConfig()) .When(x => x.WhenICreateTheConfig())
.Then(x => x.ThenTheRateLimitOptionsCreatorIsCalledCorrectly()) .Then(x => x.ThenTheRateLimitOptionsCreatorIsCalledCorrectly())
@ -187,6 +194,7 @@ namespace Ocelot.UnitTests.Configuration
}, },
})) }))
.And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigIsValid())
.And(x => GivenTheHeaderFindAndReplaceCreatorReturns())
.And(x => x.GivenTheFollowingOptionsAreReturned(serviceOptions)) .And(x => x.GivenTheFollowingOptionsAreReturned(serviceOptions))
.And(x => x.GivenTheQosOptionsCreatorReturns(expected)) .And(x => x.GivenTheQosOptionsCreatorReturns(expected))
.When(x => x.WhenICreateTheConfig()) .When(x => x.WhenICreateTheConfig())
@ -214,6 +222,7 @@ namespace Ocelot.UnitTests.Configuration
}, },
})) }))
.And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigIsValid())
.And(x => GivenTheHeaderFindAndReplaceCreatorReturns())
.And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions))
.When(x => x.WhenICreateTheConfig()) .When(x => x.WhenICreateTheConfig())
.Then(x => x.ThenTheReRoutesAre(new List<ReRoute> .Then(x => x.ThenTheReRoutesAre(new List<ReRoute>
@ -248,6 +257,7 @@ namespace Ocelot.UnitTests.Configuration
}, },
})) }))
.And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigIsValid())
.And(x => GivenTheHeaderFindAndReplaceCreatorReturns())
.And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions))
.When(x => x.WhenICreateTheConfig()) .When(x => x.WhenICreateTheConfig())
.Then(x => x.ThenTheReRoutesAre(new List<ReRoute> .Then(x => x.ThenTheReRoutesAre(new List<ReRoute>
@ -290,6 +300,7 @@ namespace Ocelot.UnitTests.Configuration
} }
})) }))
.And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigIsValid())
.And(x => GivenTheHeaderFindAndReplaceCreatorReturns())
.And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions))
.When(x => x.WhenICreateTheConfig()) .When(x => x.WhenICreateTheConfig())
.Then(x => x.ThenTheReRoutesAre(new List<ReRoute> .Then(x => x.ThenTheReRoutesAre(new List<ReRoute>
@ -325,6 +336,7 @@ namespace Ocelot.UnitTests.Configuration
} }
})) }))
.And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigIsValid())
.And(x => GivenTheHeaderFindAndReplaceCreatorReturns())
.And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions))
.When(x => x.WhenICreateTheConfig()) .When(x => x.WhenICreateTheConfig())
.Then(x => x.ThenTheReRoutesAre(new List<ReRoute> .Then(x => x.ThenTheReRoutesAre(new List<ReRoute>
@ -359,6 +371,7 @@ namespace Ocelot.UnitTests.Configuration
} }
})) }))
.And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigIsValid())
.And(x => GivenTheHeaderFindAndReplaceCreatorReturns())
.And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions))
.And(x => x.GivenTheUpstreamTemplatePatternCreatorReturns("(?i)/api/products/.*/$")) .And(x => x.GivenTheUpstreamTemplatePatternCreatorReturns("(?i)/api/products/.*/$"))
.When(x => x.WhenICreateTheConfig()) .When(x => x.WhenICreateTheConfig())
@ -398,6 +411,7 @@ namespace Ocelot.UnitTests.Configuration
} }
})) }))
.And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigIsValid())
.And(x => GivenTheHeaderFindAndReplaceCreatorReturns())
.And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions))
.And(x => x.GivenTheRequestIdCreatorReturns("blahhhh")) .And(x => x.GivenTheRequestIdCreatorReturns("blahhhh"))
.When(x => x.WhenICreateTheConfig()) .When(x => x.WhenICreateTheConfig())
@ -435,6 +449,7 @@ namespace Ocelot.UnitTests.Configuration
}, },
})) }))
.And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions))
.And(x => GivenTheHeaderFindAndReplaceCreatorReturns())
.And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigIsValid())
.And(x => x.GivenTheFollowingHttpHandlerOptionsAreReturned(httpHandlerOptions)) .And(x => x.GivenTheFollowingHttpHandlerOptionsAreReturned(httpHandlerOptions))
.When(x => x.WhenICreateTheConfig()) .When(x => x.WhenICreateTheConfig())
@ -470,6 +485,7 @@ namespace Ocelot.UnitTests.Configuration
this.Given(x => x.GivenTheConfigIs(fileConfig)) this.Given(x => x.GivenTheConfigIs(fileConfig))
.And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigIsValid())
.And(x => GivenTheHeaderFindAndReplaceCreatorReturns())
.And(x => x.GivenTheAuthOptionsCreatorReturns(authenticationOptions)) .And(x => x.GivenTheAuthOptionsCreatorReturns(authenticationOptions))
.And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions))
.And(x => x.GivenTheClaimsToThingCreatorReturns(new List<ClaimToThing> { new ClaimToThing("CustomerId", "CustomerId", "", 0) })) .And(x => x.GivenTheClaimsToThingCreatorReturns(new List<ClaimToThing> { new ClaimToThing("CustomerId", "CustomerId", "", 0) }))
@ -504,6 +520,7 @@ namespace Ocelot.UnitTests.Configuration
this.Given(x => x.GivenTheConfigIs(fileConfig)) this.Given(x => x.GivenTheConfigIs(fileConfig))
.And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigIsValid())
.And(x => GivenTheHeaderFindAndReplaceCreatorReturns())
.And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions)) .And(x => x.GivenTheFollowingOptionsAreReturned(reRouteOptions))
.And(x => x.GivenTheAuthOptionsCreatorReturns(authenticationOptions)) .And(x => x.GivenTheAuthOptionsCreatorReturns(authenticationOptions))
.When(x => x.WhenICreateTheConfig()) .When(x => x.WhenICreateTheConfig())
@ -661,6 +678,17 @@ namespace Ocelot.UnitTests.Configuration
.Verify(x => x.Create(_fileConfiguration.GlobalConfiguration), Times.Once); .Verify(x => x.Create(_fileConfiguration.GlobalConfiguration), Times.Once);
} }
private void ThenTheHeaderFindAndReplaceCreatorIsCalledCorrectly()
{
_headerFindAndReplaceCreator
.Verify(x => x.Create(It.IsAny<FileReRoute>()), Times.Once);
}
private void GivenTheHeaderFindAndReplaceCreatorReturns()
{
_headerFindAndReplaceCreator.Setup(x => x.Create(It.IsAny<FileReRoute>())).Returns(new HeaderTransformations(new List<HeaderFindAndReplace>(), new List<HeaderFindAndReplace>()));
}
private void GivenTheFollowingIsReturned(ServiceProviderConfiguration serviceProviderConfiguration) private void GivenTheFollowingIsReturned(ServiceProviderConfiguration serviceProviderConfiguration)
{ {
_serviceProviderConfigCreator _serviceProviderConfigCreator

View File

@ -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<IBaseUrlFinder> _finder;
public HeaderFindAndReplaceCreatorTests()
{
_finder = new Mock<IBaseUrlFinder>();
_creator = new HeaderFindAndReplaceCreator(_finder.Object);
}
[Fact]
public void should_create()
{
var reRoute = new FileReRoute
{
UpstreamHeaderTransform = new Dictionary<string, string>
{
{"Test", "Test, Chicken"},
{"Moop", "o, a"}
},
DownstreamHeaderTransform = new Dictionary<string, string>
{
{"Pop", "West, East"},
{"Bop", "e, r"}
}
};
var upstream = new List<HeaderFindAndReplace>
{
new HeaderFindAndReplace("Test", "Test", "Chicken", 0),
new HeaderFindAndReplace("Moop", "o", "a", 0)
};
var downstream = new List<HeaderFindAndReplace>
{
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<string, string>
{
{"Location", "http://www.bbc.co.uk/, {BaseUrl}"},
}
};
var downstream = new List<HeaderFindAndReplace>
{
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<string, string>
{
{"Location", "http://www.bbc.co.uk/pay, {BaseUrl}pay"},
}
};
var downstream = new List<HeaderFindAndReplace>
{
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<HeaderFindAndReplace> 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<HeaderFindAndReplace> 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);
}
}
}
}

View File

@ -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<HeaderFindAndReplace> _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<HeaderFindAndReplace>();
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<HeaderFindAndReplace>();
this.Given(x => GivenTheFollowingHttpRequest(context))
.And(x => GivenTheFollowingHeaderReplacements(fAndRs))
.When(x => WhenICallTheReplacer())
.Then(x => ThenTheHeadersAreNotReplaced())
.BDDfy();
}
private void ThenTheHeadersAreNotReplaced()
{
_result.ShouldBeOfType<OkResponse>();
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<HeaderFindAndReplace> fAndRs)
{
_fAndRs = fAndRs;
}
private void WhenICallTheReplacer()
{
_result = _replacer.Replace(_context, _fAndRs);
}
private void ThenTheHeadersAreReplaced()
{
_result.ShouldBeOfType<OkResponse>();
foreach (var f in _fAndRs)
{
_context.Request.Headers.TryGetValue(f.Key, out var values);
values[f.Index].ShouldBe(f.Replace);
}
}
}
}

View File

@ -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<IHttpContextRequestHeaderReplacer> _preReplacer;
private Mock<IHttpResponseHeaderReplacer> _postReplacer;
public HttpHeadersTransformationMiddlewareTests()
{
_preReplacer = new Mock<IHttpContextRequestHeaderReplacer>();
_postReplacer = new Mock<IHttpResponseHeaderReplacer>();
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>(httpResponseMessage);
ScopedRepository.Setup(x => x.Get<HttpResponseMessage>("HttpResponseMessage")).Returns(response);
}
private void GivenTheReRouteHasPreFindAndReplaceSetUp()
{
var fAndRs = new List<HeaderFindAndReplace>();
var reRoute = new ReRouteBuilder().WithUpstreamHeaderFindAndReplace(fAndRs).WithDownstreamHeaderFindAndReplace(fAndRs).Build();
var dR = new DownstreamRoute(null, reRoute);
var response = new OkResponse<DownstreamRoute>(dR);
ScopedRepository.Setup(x => x.Get<DownstreamRoute>("DownstreamRoute")).Returns(response);
}
private void ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly()
{
_preReplacer.Verify(x => x.Replace(It.IsAny<HttpContext>(), It.IsAny<List<HeaderFindAndReplace>>()), Times.Once);
}
private void ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly()
{
_postReplacer.Verify(x => x.Replace(It.IsAny<HttpResponseMessage>(), It.IsAny<List<HeaderFindAndReplace>>()), Times.Once);
}
private void GivenTheFollowingRequest()
{
Client.DefaultRequestHeaders.Add("test", "test");
}
protected override void GivenTheTestServerServicesAreConfigured(IServiceCollection services)
{
services.AddSingleton<IOcelotLoggerFactory, AspDotNetLoggerFactory>();
services.AddLogging();
services.AddSingleton(ScopedRepository.Object);
services.AddSingleton(_preReplacer.Object);
services.AddSingleton(_postReplacer.Object);
}
protected override void GivenTheTestServerPipelineIsConfigured(IApplicationBuilder app)
{
app.UseHttpHeadersTransformationMiddleware();
}
}
}

View File

@ -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<HeaderFindAndReplace> _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<HeaderFindAndReplace>();
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<HeaderFindAndReplace>();
this.Given(x => GivenTheHttpResponse(response))
.And(x => GivenTheFollowingHeaderReplacements(fAndRs))
.When(x => WhenICallTheReplacer())
.Then(x => ThenTheHeadersAreNotReplaced())
.BDDfy();
}
private void ThenTheHeadersAreNotReplaced()
{
_result.ShouldBeOfType<OkResponse>();
foreach (var f in _headerFindAndReplaces)
{
_response.Headers.TryGetValues(f.Key, out var values);
values.ToList()[f.Index].ShouldBe("test");
}
}
private void GivenTheFollowingHeaderReplacements(List<HeaderFindAndReplace> fAndRs)
{
_headerFindAndReplaces = fAndRs;
}
private void GivenTheHttpResponse(HttpResponseMessage response)
{
_response = response;
}
private void WhenICallTheReplacer()
{
_result = _replacer.Replace(_response, _headerFindAndReplaces);
}
private void ThenTheHeadersAreReplaced()
{
_result.ShouldBeOfType<OkResponse>();
foreach (var f in _headerFindAndReplaces)
{
_response.Headers.TryGetValues(f.Key, out var values);
values.ToList()[f.Index].ShouldBe(f.Replace);
}
}
}
}