Merge branch 'release-5.1.0'

This commit is contained in:
Tom Gardham-Pallister 2018-03-18 15:05:56 +00:00
commit 600002dde4
70 changed files with 1440 additions and 311 deletions

31
.travis.yml Normal file
View File

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

View File

@ -1,6 +1,7 @@
[<img src="http://threemammals.com/images/ocelot_logo.png">](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)

View File

@ -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";
@ -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,

View File

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

View File

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

View File

@ -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<string> _delegatingHandlers;
private List<AddHeader> _addHeadersToDownstream;
public DownstreamReRouteBuilder()
{
_downstreamAddresses = new List<DownstreamHostAndPort>();
_delegatingHandlers = new List<string>();
_addHeadersToDownstream = new List<AddHeader>();
}
public DownstreamReRouteBuilder WithDownstreamAddresses(List<DownstreamHostAndPort> downstreamAddresses)
@ -224,6 +227,12 @@ namespace Ocelot.Configuration.Builder
return this;
}
public DownstreamReRouteBuilder WithAddHeadersToDownstream(List<AddHeader> 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);
}
}
}

View File

@ -213,6 +213,7 @@ namespace Ocelot.Configuration.Creator
.WithDownstreamHeaderFindAndReplace(hAndRs.Downstream)
.WithUpstreamHost(fileReRoute.UpstreamHost)
.WithDelegatingHandlers(fileReRoute.DelegatingHandlers)
.WithAddHeadersToDownstream(hAndRs.AddHeadersToDownstream)
.Build();
return reRoute;

View File

@ -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<string, Func<string>> _placeholders;
private IPlaceholders _placeholders;
private IOcelotLogger _logger;
public HeaderFindAndReplaceCreator(IBaseUrlFinder finder)
public HeaderFindAndReplaceCreator(IPlaceholders placeholders, IOcelotLoggerFactory factory)
{
_finder = finder;
_placeholders = new Dictionary<string, Func<string>>();
_placeholders.Add("{BaseUrl}", () => _finder.Find());
_logger = factory.CreateLogger<HeaderFindAndReplaceCreator>();;
_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<HeaderFindAndReplace>();
var addHeadersToDownstream = new List<AddHeader>();
foreach(var input in fileReRoute.DownstreamHeaderTransform)
{
if(input.Value.Contains(","))
{
var hAndr = Map(input);
downstream.Add(hAndr);
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<string,string> input)
private Response<HeaderFindAndReplace> Map(KeyValuePair<string,string> 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<HeaderFindAndReplace>(value.Errors);
}
replace = replace.Replace(placeholder, value.Data);
}
var hAndr = new HeaderFindAndReplace(input.Key, findAndReplace[0], replace, 0);
return hAndr;
return new OkResponse<HeaderFindAndReplace>(hAndr);
}
}
}

View File

@ -4,14 +4,31 @@ namespace Ocelot.Configuration.Creator
{
public class HeaderTransformations
{
public HeaderTransformations(List<HeaderFindAndReplace> upstream, List<HeaderFindAndReplace> downstream)
public HeaderTransformations(
List<HeaderFindAndReplace> upstream,
List<HeaderFindAndReplace> downstream,
List<AddHeader> addHeader)
{
AddHeadersToDownstream = addHeader;
Upstream = upstream;
Downstream = downstream;
}
public List<HeaderFindAndReplace> Upstream {get;private set;}
public List<HeaderFindAndReplace> Upstream { get; private set; }
public List<HeaderFindAndReplace> Downstream {get;private set;}
public List<HeaderFindAndReplace> Downstream { get; private set; }
public List<AddHeader> 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; }
}
}

View File

@ -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<string> delegatingHandlers)
List<string> delegatingHandlers,
List<AddHeader> 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<string> DelegatingHandlers {get;private set;}
public List<AddHeader> AddHeadersToDownstream {get;private set;}
}
}

View File

@ -4,8 +4,8 @@
{
public FileHttpHandlerOptions()
{
AllowAutoRedirect = true;
UseCookieContainer = true;
AllowAutoRedirect = false;
UseCookieContainer = false;
}
public bool AllowAutoRedirect { get; set; }

View File

@ -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<ConsulFileConfigurationPoller>();
_repo = repo;
_previousAsJson = "";
@ -34,7 +40,7 @@ namespace Ocelot.Configuration.Repository
_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
/// <summary>
/// We could do object comparison here but performance isnt really a problem. This might be an issue one day!
/// </summary>
/// <param name="config"></param>
/// <returns></returns>
/// <returns>hash of the config</returns>
private string ToJson(FileConfiguration config)
{
var currentHash = JsonConvert.SerializeObject(config);

View File

@ -0,0 +1,8 @@
namespace Ocelot.Configuration.Repository
{
public interface IConsulPollerConfiguration
{
int Delay { get; }
}
}

View File

@ -0,0 +1,7 @@
namespace Ocelot.Configuration.Repository
{
public class InMemoryConsulPollerConfiguration : IConsulPollerConfiguration
{
public int Delay => 1000;
}
}

View File

@ -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
{
@ -148,6 +149,9 @@ namespace Ocelot.DependencyInjection
// We add this here so that we can always inject something into the factory for IoC..
_services.AddSingleton<IServiceTracer, FakeServiceTracer>();
_services.TryAddSingleton<IConsulPollerConfiguration, InMemoryConsulPollerConfiguration>();
_services.TryAddSingleton<IAddHeadersToResponse, AddHeadersToResponse>();
_services.TryAddSingleton<IPlaceholders, Placeholders>();
}
public IOcelotAdministrationBuilder AddAdministration(string path, string secret)

View File

@ -34,6 +34,7 @@
RateLimitOptionsError,
PathTemplateDoesntStartWithForwardSlash,
FileValidationFailedError,
UnableToFindDelegatingHandlerProviderError
UnableToFindDelegatingHandlerProviderError,
CouldNotFindPlaceholderError
}
}

View File

@ -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
{

View File

@ -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<AddHeadersToResponse>();
_placeholders = placeholders;
}
public void Add(List<AddHeader> 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);
}
}
}
}
}

View File

@ -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<string, Func<HttpRequestMessage, string>> _placeholders;
private IPlaceholders _placeholders;
public HttpResponseHeaderReplacer()
public HttpResponseHeaderReplacer(IPlaceholders placeholders)
{
_placeholders = new Dictionary<string, Func<HttpRequestMessage, string>>();
_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}";
_placeholders = placeholders;
}
return $"{downstreamUrl}/";
});
}
public Response Replace(HttpResponseMessage response, List<HeaderFindAndReplace> fAndRs, HttpRequestMessage httpRequestMessage)
public Response Replace(HttpResponseMessage response, List<HeaderFindAndReplace> 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);
}

View File

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

View File

@ -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<AddHeader> addHeaders, HttpResponseMessage response);
}
}

View File

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

View File

@ -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)
{
}
}
}

View File

@ -0,0 +1,11 @@
using System.Net.Http;
using Ocelot.Responses;
namespace Ocelot.Infrastructure
{
public interface IPlaceholders
{
Response<string> Get(string key);
Response<string> Get(string key, HttpRequestMessage request);
}
}

View File

@ -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<string, Func<Response<string>>> _placeholders;
private Dictionary<string, Func<HttpRequestMessage, string>> _requestPlaceholders;
private readonly IBaseUrlFinder _finder;
private readonly IRequestScopedDataRepository _repo;
public Placeholders(IBaseUrlFinder finder, IRequestScopedDataRepository repo)
{
_repo = repo;
_finder = finder;
_placeholders = new Dictionary<string, Func<Response<string>>>();
_placeholders.Add("{BaseUrl}", () => new OkResponse<string>(_finder.Find()));
_placeholders.Add("{TraceId}", () => {
var traceId = _repo.Get<string>("TraceId");
if(traceId.IsError)
{
return new ErrorResponse<string>(traceId.Errors);
}
return new OkResponse<string>(traceId.Data);
});
_requestPlaceholders = new Dictionary<string, Func<HttpRequestMessage, string>>();
_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<string> Get(string key)
{
if(_placeholders.ContainsKey(key))
{
var response = _placeholders[key].Invoke();
if(!response.IsError)
{
return new OkResponse<string>(response.Data);
}
}
return new ErrorResponse<string>(new CouldNotFindPlaceholderError(key));
}
public Response<string> Get(string key, HttpRequestMessage request)
{
if(_requestPlaceholders.ContainsKey(key))
{
return new OkResponse<string>(_requestPlaceholders[key].Invoke(request));
}
return new ErrorResponse<string>(new CouldNotFindPlaceholderError(key));
}
}
}

View File

@ -1,4 +1,4 @@
namespace Ocelot.UnitTests
namespace Ocelot.Infrastructure
{
public class Wait
{

View File

@ -1,7 +1,7 @@
using System;
using System.Diagnostics;
namespace Ocelot.UnitTests
namespace Ocelot.Infrastructure
{
public class Waiter
{

View File

@ -3,6 +3,7 @@
<TargetFramework>netcoreapp2.0</TargetFramework>
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
<NETStandardImplicitPackageVersion>2.0.0</NETStandardImplicitPackageVersion>
<NoPackageAnalysis>true</NoPackageAnalysis>
<Description>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.</Description>
<AssemblyTitle>Ocelot</AssemblyTitle>
<VersionPrefix>0.0.0-dev</VersionPrefix>

View File

@ -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)
{
}
}

View File

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

View File

@ -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};
_cacheKey = GetCacheKey(request);
var client = new HttpClient(CreateHttpMessageHandler(httpclientHandler, request));
var httpClient = _cacheHandlers.Get(_cacheKey);
return new HttpClientWrapper(client);
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;
}
}
}

View File

@ -24,28 +24,24 @@ namespace Ocelot.Requester
_factory = house;
}
public async Task<Response<HttpResponseMessage>> GetResponse(DownstreamContext request)
public async Task<Response<HttpResponseMessage>> 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<HttpResponseMessage>(response);
}
catch (TimeoutRejectedException exception)
{
return
new ErrorResponse<HttpResponseMessage>(new RequestTimedOutError(exception));
return new ErrorResponse<HttpResponseMessage>(new RequestTimedOutError(exception));
}
catch (BrokenCircuitException exception)
{
return
new ErrorResponse<HttpResponseMessage>(new RequestTimedOutError(exception));
return new ErrorResponse<HttpResponseMessage>(new RequestTimedOutError(exception));
}
catch (Exception exception)
{
@ -53,42 +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<T> where T : DelegatingHandler
{
public T DelegatingHandler { get; private set; }
}
public class GlobalDelegatingHandler
{
public GlobalDelegatingHandler(DelegatingHandler delegatingHandler)
{
DelegatingHandler = delegatingHandler;
}
public DelegatingHandler DelegatingHandler { get; private set; }
}
}

View File

@ -6,7 +6,7 @@ namespace Ocelot.Requester
/// <summary>
/// This class was made to make unit testing easier when HttpClient is used.
/// </summary>
internal class HttpClientWrapper : IHttpClient
public class HttpClientWrapper : IHttpClient
{
public HttpClient Client { get; }

View File

@ -1,14 +1,10 @@
using System.Net.Http;
using Ocelot.Configuration;
using Ocelot.Middleware;
namespace Ocelot.Requester
{
public interface IHttpClientBuilder
{
/// <summary>
/// Creates the <see cref="HttpClient"/>
/// </summary>
/// <param name="request"></param>
IHttpClient Create(DownstreamReRoute request);
IHttpClient Create(DownstreamContext request);
void Save();
}
}

View File

@ -7,6 +7,6 @@ namespace Ocelot.Requester
{
public interface IHttpRequester
{
Task<Response<HttpResponseMessage>> GetResponse(DownstreamContext request);
Task<Response<HttpResponseMessage>> GetResponse(DownstreamContext context);
}
}

View File

@ -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<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
return _tracer.ChildTraceAsync($"httpclient {request.Method}", DateTimeOffset.UtcNow, span => TracingSendAsync(span, request, cancellationToken));
}
protected virtual async Task<HttpResponseMessage> TracingSendAsync(ISpan span, HttpRequestMessage request, CancellationToken cancellationToken)
protected virtual async Task<HttpResponseMessage> TracingSendAsync(
ISpan span,
HttpRequestMessage request,
CancellationToken cancellationToken)
{
IEnumerable<string> 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)

View File

@ -0,0 +1,10 @@
using System.Net.Http;
namespace Ocelot.Requester
{
public class ReRouteDelegatingHandler<T>
where T : DelegatingHandler
{
public T DelegatingHandler { get; private set; }
}
}

View File

@ -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)
{
}
}

View File

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

View File

@ -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)
{
}
}

View File

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

View File

@ -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<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/api/values",
DownstreamScheme = "http",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 51387,
}
},
UpstreamPathTemplate = "/api001/values",
UpstreamHttpMethod = new List<string> { "Get" },
HttpHandlerOptions = new FileHttpHandlerOptions
{
UseTracing = true
},
QoSOptions = new FileQoSOptions
{
ExceptionsAllowedBeforeBreaking = 3,
DurationOfBreak = 10,
TimeoutValue = 5000
},
DownstreamHeaderTransform = new Dictionary<string, string>()
{
{"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()

View File

@ -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)
var result = WaitFor(20000).Until(() => {
try
{
//do nothing!
_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)

View File

@ -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<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/sso/{everything}",
DownstreamScheme = "http",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 6774,
}
},
UpstreamPathTemplate = "/sso/{everything}",
UpstreamHttpMethod = new List<string> { "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<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/sso/{everything}",
DownstreamScheme = "http",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 6775,
}
},
UpstreamPathTemplate = "/sso/{everything}",
UpstreamHttpMethod = new List<string> { "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()

View File

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

View File

@ -50,9 +50,4 @@
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="Butterfly.Client.AspNetCore" Version="0.0.8" />
</ItemGroup>
<ItemGroup>
<Reference Include="Castle.Core">
<HintPath>..\..\..\..\Users\TGP\.nuget\packages\castle.core\4.2.1\lib\netstandard1.3\Castle.Core.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -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("/"))

View File

@ -922,6 +922,55 @@ namespace Ocelot.AcceptanceTests
.BDDfy();
}
[Fact]
public void should_fix_issue_271()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/api/v1/{everything}",
DownstreamScheme = "http",
UpstreamPathTemplate = "/api/v1/{everything}",
UpstreamHttpMethod = new List<string> { "Get", "Put", "Post" },
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 54879,
}
},
},
new FileReRoute
{
DownstreamPathTemplate = "/connect/token",
DownstreamScheme = "http",
UpstreamPathTemplate = "/connect/token",
UpstreamHttpMethod = new List<string> { "Post" },
DownstreamHostAndPorts = new List<FileHostAndPort>
{
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()

View File

@ -274,6 +274,11 @@ namespace Ocelot.AcceptanceTests
_ocelotClient = _ocelotServer.CreateClient();
}
internal void GivenIAddCookieToMyRequest(string cookie)
{
_ocelotClient.DefaultRequestHeaders.Add("Set-Cookie", cookie);
}
/// <summary>
/// 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.
/// </summary>
@ -313,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();

View File

@ -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 =>

View File

@ -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<IFileConfigurationRepository> _repo;
private Mock<IFileConfigurationSetter> _setter;
private FileConfiguration _fileConfig;
private Mock<IConsulPollerConfiguration> _config;
public ConsulFileConfigurationPollerTests()
{
@ -30,8 +32,10 @@ namespace Ocelot.UnitTests.Configuration
_repo = new Mock<IFileConfigurationRepository>();
_setter = new Mock<IFileConfigurationSetter>();
_fileConfig = new FileConfiguration();
_config = new Mock<IConsulPollerConfiguration>();
_repo.Setup(x => x.Get()).ReturnsAsync(new OkResponse<FileConfiguration>(_fileConfig));
_poller = new ConsulFileConfigurationPoller(_factory.Object, _repo.Object, _setter.Object);
_config.Setup(x => x.Delay).Returns(100);
_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<FileConfiguration>(newConfig));
var newConfig = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamHostAndPorts = new List<FileHostAndPort>
{
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<FileReRoute>
{
new FileReRoute
{
DownstreamHostAndPorts = new List<FileHostAndPort>
{
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<FileConfiguration>(new AnyError()));
}
private void WhenTheConfigIsChangedInConsul(FileConfiguration newConfig, int delay)
{
_repo
.Setup(x => x.Get())
.Callback(() => Thread.Sleep(delay))
.ReturnsAsync(new OkResponse<FileConfiguration>(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)

View File

@ -827,6 +827,7 @@ namespace Ocelot.UnitTests.Configuration
result.DownstreamReRoute[0].ClaimsToQueries.Count.ShouldBe(expected.DownstreamReRoute[0].ClaimsToQueries.Count);
result.DownstreamReRoute[0].RequestIdKey.ShouldBe(expected.DownstreamReRoute[0].RequestIdKey);
result.DownstreamReRoute[0].DelegatingHandlers.ShouldBe(expected.DownstreamReRoute[0].DelegatingHandlers);
result.DownstreamReRoute[0].AddHeadersToDownstream.ShouldBe(expected.DownstreamReRoute[0].AddHeadersToDownstream);
}
}
@ -909,7 +910,7 @@ namespace Ocelot.UnitTests.Configuration
private void GivenTheHeaderFindAndReplaceCreatorReturns()
{
_headerFindAndReplaceCreator.Setup(x => x.Create(It.IsAny<FileReRoute>())).Returns(new HeaderTransformations(new List<HeaderFindAndReplace>(), new List<HeaderFindAndReplace>()));
_headerFindAndReplaceCreator.Setup(x => x.Create(It.IsAny<FileReRoute>())).Returns(new HeaderTransformations(new List<HeaderFindAndReplace>(), new List<HeaderFindAndReplace>(), new List<AddHeader>()));
}
private void GivenTheFollowingIsReturned(ServiceProviderConfiguration serviceProviderConfiguration)

View File

@ -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<IBaseUrlFinder> _finder;
private Mock<IPlaceholders> _placeholders;
private Mock<IOcelotLoggerFactory> _factory;
private Mock<IOcelotLogger> _logger;
public HeaderFindAndReplaceCreatorTests()
{
_finder = new Mock<IBaseUrlFinder>();
_creator = new HeaderFindAndReplaceCreator(_finder.Object);
_logger = new Mock<IOcelotLogger>();
_factory = new Mock<IOcelotLoggerFactory>();
_factory.Setup(x => x.CreateLogger<HeaderFindAndReplaceCreator>()).Returns(_logger.Object);
_placeholders = new Mock<IPlaceholders>();
_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<string, string>
{
{"Location", "http://www.bbc.co.uk/, {BaseUrl}"},
},
UpstreamHeaderTransform = new Dictionary<string, string>
{
{"Location", "http://www.bbc.co.uk/, {BaseUrl}"},
}
};
var expected = new List<HeaderFindAndReplace>
{
};
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<string, string>
{
{"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<string>())).Returns(new OkResponse<string>(baseUrl));
}
private void GivenTheBaseUrlErrors()
{
_placeholders.Setup(x => x.Get(It.IsAny<string>())).Returns(new ErrorResponse<string>(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<HeaderFindAndReplace> downstream)

View File

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

View File

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

View File

@ -223,12 +223,14 @@ namespace Ocelot.UnitTests.DependencyInjection
_ocelotBuilder.AddAdministration("/administration", options);
}
private void AddTransientGlobalDelegatingHandler<T>() where T : DelegatingHandler
private void AddTransientGlobalDelegatingHandler<T>()
where T : DelegatingHandler
{
_ocelotBuilder.AddTransientDelegatingHandler<T>(true);
}
private void AddSpecificTransientDelegatingHandler<T>() where T : DelegatingHandler
private void AddSpecificTransientDelegatingHandler<T>()
where T : DelegatingHandler
{
_ocelotBuilder.AddTransientDelegatingHandler<T>();
}
@ -298,12 +300,14 @@ namespace Ocelot.UnitTests.DependencyInjection
}
}
private void AddGlobalDelegatingHandler<T>() where T : DelegatingHandler
private void AddGlobalDelegatingHandler<T>()
where T : DelegatingHandler
{
_ocelotBuilder.AddSingletonDelegatingHandler<T>(true);
}
private void AddSpecificDelegatingHandler<T>() where T : DelegatingHandler
private void AddSpecificDelegatingHandler<T>()
where T : DelegatingHandler
{
_ocelotBuilder.AddSingletonDelegatingHandler<T>();
}

View File

@ -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<IPlaceholders> _placeholders;
private HttpResponseMessage _response;
private List<AddHeader> _addHeaders;
private Mock<IOcelotLoggerFactory> _factory;
private Mock<IOcelotLogger> _logger;
public AddHeadersToResponseTests()
{
_factory = new Mock<IOcelotLoggerFactory>();
_logger = new Mock<IOcelotLogger>();
_factory.Setup(x => x.CreateLogger<AddHeadersToResponse>()).Returns(_logger.Object);
_placeholders = new Mock<IPlaceholders>();
_adder = new AddHeadersToResponse(_placeholders.Object, _factory.Object);
}
[Fact]
public void should_add_header()
{
var addHeaders = new List<AddHeader>
{
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<AddHeader>
{
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<AddHeader>
{
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<AddHeader>
{
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<string>(traceId));
}
private void GivenTheTraceIdErrors()
{
_placeholders.Setup(x => x.Get("{TraceId}")).Returns(new ErrorResponse<string>(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<AddHeader> addHeaders)
{
_addHeaders = addHeaders;
}
}
}

View File

@ -26,6 +26,7 @@ namespace Ocelot.UnitTests.Headers
private HttpHeadersTransformationMiddleware _middleware;
private DownstreamContext _downstreamContext;
private OcelotRequestDelegate _next;
private Mock<IAddHeadersToResponse> _addHeaders;
public HttpHeadersTransformationMiddlewareTests()
{
@ -36,7 +37,8 @@ namespace Ocelot.UnitTests.Headers
_logger = new Mock<IOcelotLogger>();
_loggerFactory.Setup(x => x.CreateLogger<AuthorisationMiddleware>()).Returns(_logger.Object);
_next = context => Task.CompletedTask;
_middleware = new HttpHeadersTransformationMiddleware(_next, _loggerFactory.Object, _preReplacer.Object, _postReplacer.Object);
_addHeaders = new Mock<IAddHeadersToResponse>();
_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();

View File

@ -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<HeaderFindAndReplace> _headerFindAndReplaces;
private Response _result;
private HttpRequestMessage _request;
private Mock<IBaseUrlFinder> _finder;
private Mock<IRequestScopedDataRepository> _repo;
public HttpResponseHeaderReplacerTests()
{
_replacer = new HttpResponseHeaderReplacer();
_repo = new Mock<IRequestScopedDataRepository>();
_finder = new Mock<IBaseUrlFinder>();
_placeholders = new Placeholders(_finder.Object, _repo.Object);
_replacer = new HttpResponseHeaderReplacer(_placeholders);
}
[Fact]

View File

@ -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<IBaseUrlFinder> _finder;
private Mock<IRequestScopedDataRepository> _repo;
public PlaceholdersTests()
{
_repo = new Mock<IRequestScopedDataRepository>();
_finder = new Mock<IBaseUrlFinder>();
_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<string>("TraceId")).Returns(new OkResponse<string>(traceId));
var result = _placeholders.Get("{TraceId}");
result.Data.ShouldBe(traceId);
}
}
}

View File

@ -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<IConfiguration>(root);
services.AddOcelot();
var provider = services.BuildServiceProvider();
_builder = new OcelotPipelineBuilder(provider);
}
}
}

View File

@ -163,7 +163,6 @@
.BDDfy();
}
[Fact]
public void should_not_add_content_headers()
{

View File

@ -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<IDelegatingHandlerHandlerFactory> _factory;
private IHttpClient _httpClient;
private HttpResponseMessage _response;
private DownstreamReRoute _request;
private DownstreamContext _context;
private readonly Mock<IHttpClientCache> _cacheHandlers;
private Mock<IOcelotLogger> _logger;
private int _count;
private IWebHost _host;
public HttpClientBuilderTests()
{
_cacheHandlers = new Mock<IHttpClientCache>();
_logger = new Mock<IOcelotLogger>();
_factory = new Mock<IDelegatingHandlerHandlerFactory>();
_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<string>())).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<DownstreamReRoute>()))
.Returns(new OkResponse<List<Func<DelegatingHandler>>>(handlers));
}
private void GivenTheFactoryReturnsNothing()
{
var handlers = new List<Func<DelegatingHandler>>();
_factory
.Setup(x => x.Get(It.IsAny<DownstreamReRoute>()))
.Returns(new OkResponse<List<Func<DelegatingHandler>>>(handlers));
}
private void GivenTheFactoryReturns(List<Func<DelegatingHandler>> 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();
}
}
}

View File

@ -35,7 +35,10 @@ namespace Ocelot.UnitTests.Requester
.Setup(x => x.CreateLogger<HttpClientHttpRequester>())
.Returns(_logger.Object);
_cacheHandlers = new Mock<IHttpClientCache>();
_httpClientRequester = new HttpClientHttpRequester(_loggerFactory.Object, _cacheHandlers.Object, _house.Object);
_httpClientRequester = new HttpClientHttpRequester(
_loggerFactory.Object,
_cacheHandlers.Object,
_house.Object);
}
[Fact]

View File

@ -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<IServiceTracer> _tracer;
private Mock<IRequestScopedDataRepository> _repo;
public TracingHandlerFactoryTests()
{
_tracer = new Mock<IServiceTracer>();
_factory = new TracingHandlerFactory(_tracer.Object);
_repo = new Mock<IRequestScopedDataRepository>();
_factory = new TracingHandlerFactory(_tracer.Object, _repo.Object);
}
[Fact]

View File

@ -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)

View File

@ -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<IRequestScopedDataRepository> ScopedRepository { get; private set; }
public ServerHostedMiddlewareTest()
{
Url = "http://localhost:51879";
ScopedRepository = new Mock<IRequestScopedDataRepository>();
}
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();
}
}
}