Added ability to strip claims and forward to downstream service as headers

This commit is contained in:
TomPallister 2016-10-18 15:51:56 +01:00
parent 279aae3151
commit 84256e7bac
32 changed files with 1467 additions and 94 deletions

View File

@ -38,12 +38,25 @@ Priorities
TBC really but example configuration for a route below. TBC really but example configuration for a route below.
ReRoutes: ReRoutes:
- DownstreamTemplate: http://localhost:51876/ # the url we are forwarding the request to
- DownstreamTemplate: http://localhost:52876/
# the path we are listening on for this re route
UpstreamTemplate: / UpstreamTemplate: /
UpstreamHttpMethod: Post # the method we are listening for on this re route
UpstreamHttpMethod: Get
# only support identity server at the moment
AuthenticationOptions: AuthenticationOptions:
Provider: IdentityServer Provider: IdentityServer
ProviderRootUrl: http://localhost:51888 ProviderRootUrl: http://localhost:52888
ScopeName: api ScopeName: api
AdditionalScopes: [] AdditionalScopes:
- openid
- offline_access
#require if using reference tokens
ScopeSecret: secret ScopeSecret: secret
# WARNING - will overwrite any headers already in the request with these values
AddHeadersToRequest:
CustomerId: Claims[CustomerId] > value
LocationId: Claims[LocationId] > value
UserType: Claims[sub] > value[0] > |
UserId: Claims[sub] > value[1] > |

View File

@ -1,4 +1,6 @@
namespace Ocelot.Library.Builder using Ocelot.Library.RequestBuilder;
namespace Ocelot.Library.Builder
{ {
using System.Collections.Generic; using System.Collections.Generic;
using Configuration; using Configuration;
@ -16,6 +18,7 @@
private List<string> _additionalScopes; private List<string> _additionalScopes;
private bool _requireHttps; private bool _requireHttps;
private string _scopeSecret; private string _scopeSecret;
private List<ConfigurationHeaderExtractorProperties> _configHeaderExtractorProperties;
public ReRouteBuilder() public ReRouteBuilder()
{ {
@ -86,9 +89,15 @@
return this; return this;
} }
public ReRouteBuilder WithConfigurationHeaderExtractorProperties(List<ConfigurationHeaderExtractorProperties> input)
{
_configHeaderExtractorProperties = input;
return this;
}
public ReRoute Build() public ReRoute Build()
{ {
return new ReRoute(_downstreamTemplate, _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern, _isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName, _requireHttps, _additionalScopes, _scopeSecret)); return new ReRoute(_downstreamTemplate, _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern, _isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName, _requireHttps, _additionalScopes, _scopeSecret), _configHeaderExtractorProperties);
} }
} }
} }

View File

@ -1,3 +1,8 @@
using System;
using System.Linq;
using Microsoft.Extensions.Logging;
using Ocelot.Library.RequestBuilder;
namespace Ocelot.Library.Configuration namespace Ocelot.Library.Configuration
{ {
using System.Collections.Generic; using System.Collections.Generic;
@ -11,11 +16,18 @@ namespace Ocelot.Library.Configuration
private readonly List<ReRoute> _reRoutes; private readonly List<ReRoute> _reRoutes;
private const string RegExMatchEverything = ".*"; private const string RegExMatchEverything = ".*";
private const string RegExMatchEndString = "$"; private const string RegExMatchEndString = "$";
private readonly IConfigurationHeaderExtrator _configurationHeaderExtrator;
private readonly ILogger<OcelotConfiguration> _logger;
public OcelotConfiguration(IOptions<YamlConfiguration> options, IConfigurationValidator configurationValidator) public OcelotConfiguration(IOptions<YamlConfiguration> options,
IConfigurationValidator configurationValidator,
IConfigurationHeaderExtrator configurationHeaderExtrator,
ILogger<OcelotConfiguration> logger)
{ {
_options = options; _options = options;
_configurationValidator = configurationValidator; _configurationValidator = configurationValidator;
_configurationHeaderExtrator = configurationHeaderExtrator;
_logger = logger;
_reRoutes = new List<ReRoute>(); _reRoutes = new List<ReRoute>();
SetUpConfiguration(); SetUpConfiguration();
} }
@ -43,7 +55,7 @@ namespace Ocelot.Library.Configuration
var placeholders = new List<string>(); var placeholders = new List<string>();
for (int i = 0; i < upstreamTemplate.Length; i++) for (var i = 0; i < upstreamTemplate.Length; i++)
{ {
if (IsPlaceHolder(upstreamTemplate, i)) if (IsPlaceHolder(upstreamTemplate, i))
{ {
@ -70,17 +82,41 @@ namespace Ocelot.Library.Configuration
reRoute.AuthenticationOptions.RequireHttps, reRoute.AuthenticationOptions.AdditionalScopes, reRoute.AuthenticationOptions.RequireHttps, reRoute.AuthenticationOptions.AdditionalScopes,
reRoute.AuthenticationOptions.ScopeSecret); reRoute.AuthenticationOptions.ScopeSecret);
var configHeaders = GetHeadersToAddToRequest(reRoute);
_reRoutes.Add(new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate, _reRoutes.Add(new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate,
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, authOptionsForRoute reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated,
authOptionsForRoute, configHeaders
)); ));
} }
else else
{ {
_reRoutes.Add(new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate, reRoute.UpstreamHttpMethod, _reRoutes.Add(new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate, reRoute.UpstreamHttpMethod,
upstreamTemplate, isAuthenticated, null)); upstreamTemplate, isAuthenticated, null, new List<ConfigurationHeaderExtractorProperties>()));
} }
} }
private List<ConfigurationHeaderExtractorProperties> GetHeadersToAddToRequest(YamlReRoute reRoute)
{
var configHeaders = new List<ConfigurationHeaderExtractorProperties>();
foreach (var add in reRoute.AddHeadersToRequest)
{
var configurationHeader = _configurationHeaderExtrator.Extract(add.Key, add.Value);
if (configurationHeader.IsError)
{
_logger.LogCritical(new EventId(1, "Application Failed to start"),
$"Unable to extract configuration for key: {add.Key} and value: {add.Value} your configuration file is incorrect");
throw new Exception(configurationHeader.Errors[0].Message);
}
configHeaders.Add(configurationHeader.Data);
}
return configHeaders;
}
private bool IsPlaceHolder(string upstreamTemplate, int i) private bool IsPlaceHolder(string upstreamTemplate, int i)
{ {
return upstreamTemplate[i] == '{'; return upstreamTemplate[i] == '{';

View File

@ -1,8 +1,11 @@
namespace Ocelot.Library.Configuration using System.Collections.Generic;
using Ocelot.Library.RequestBuilder;
namespace Ocelot.Library.Configuration
{ {
public class ReRoute public class ReRoute
{ {
public ReRoute(string downstreamTemplate, string upstreamTemplate, string upstreamHttpMethod, string upstreamTemplatePattern, bool isAuthenticated, AuthenticationOptions authenticationOptions) public ReRoute(string downstreamTemplate, string upstreamTemplate, string upstreamHttpMethod, string upstreamTemplatePattern, bool isAuthenticated, AuthenticationOptions authenticationOptions, List<ConfigurationHeaderExtractorProperties> configurationHeaderExtractorProperties)
{ {
DownstreamTemplate = downstreamTemplate; DownstreamTemplate = downstreamTemplate;
UpstreamTemplate = upstreamTemplate; UpstreamTemplate = upstreamTemplate;
@ -10,6 +13,8 @@
UpstreamTemplatePattern = upstreamTemplatePattern; UpstreamTemplatePattern = upstreamTemplatePattern;
IsAuthenticated = isAuthenticated; IsAuthenticated = isAuthenticated;
AuthenticationOptions = authenticationOptions; AuthenticationOptions = authenticationOptions;
ConfigurationHeaderExtractorProperties = configurationHeaderExtractorProperties
?? new List<ConfigurationHeaderExtractorProperties>();
} }
public string DownstreamTemplate { get; private set; } public string DownstreamTemplate { get; private set; }
@ -18,5 +23,6 @@
public string UpstreamHttpMethod { get; private set; } public string UpstreamHttpMethod { get; private set; }
public bool IsAuthenticated { get; private set; } public bool IsAuthenticated { get; private set; }
public AuthenticationOptions AuthenticationOptions { get; private set; } public AuthenticationOptions AuthenticationOptions { get; private set; }
public List<ConfigurationHeaderExtractorProperties> ConfigurationHeaderExtractorProperties { get; private set; }
} }
} }

View File

@ -21,6 +21,9 @@
services.Configure<YamlConfiguration>(configurationRoot); services.Configure<YamlConfiguration>(configurationRoot);
// Add framework services. // Add framework services.
services.AddSingleton<IAddHeadersToRequest, AddHeadersToRequest>();
services.AddSingleton<IClaimsParser, ClaimsParser>();
services.AddSingleton<IConfigurationHeaderExtrator, ConfigurationHeaderExtrator>();
services.AddSingleton<IConfigurationValidator, ConfigurationValidator>(); services.AddSingleton<IConfigurationValidator, ConfigurationValidator>();
services.AddSingleton<IOcelotConfiguration, OcelotConfiguration>(); services.AddSingleton<IOcelotConfiguration, OcelotConfiguration>();
services.AddSingleton<IUrlPathToUrlTemplateMatcher, RegExUrlMatcher>(); services.AddSingleton<IUrlPathToUrlTemplateMatcher, RegExUrlMatcher>();

View File

@ -10,6 +10,10 @@
CannotFindDataError, CannotFindDataError,
UnableToCompleteRequestError, UnableToCompleteRequestError,
UnableToCreateAuthenticationHandlerError, UnableToCreateAuthenticationHandlerError,
UnsupportedAuthenticationProviderError UnsupportedAuthenticationProviderError,
CannotFindClaimError,
ParsingConfigurationHeaderError,
NoInstructionsError,
InstructionNotForClaimsError
} }
} }

View File

@ -1,23 +0,0 @@
namespace Ocelot.Library.Middleware
{
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Repository;
public class ClaimsParserMiddleware : OcelotMiddleware
{
private readonly RequestDelegate _next;
public ClaimsParserMiddleware(RequestDelegate next, IScopedRequestDataRepository scopedRequestDataRepository)
: base(scopedRequestDataRepository)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
await _next.Invoke(context);
}
}
}

View File

@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Primitives;
using Ocelot.Library.DownstreamRouteFinder;
using Ocelot.Library.RequestBuilder;
namespace Ocelot.Library.Middleware
{
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Repository;
public class HttpRequestHeadersBuilderMiddleware : OcelotMiddleware
{
private readonly RequestDelegate _next;
private readonly IAddHeadersToRequest _addHeadersToRequest;
private readonly IScopedRequestDataRepository _scopedRequestDataRepository;
public HttpRequestHeadersBuilderMiddleware(RequestDelegate next,
IScopedRequestDataRepository scopedRequestDataRepository,
IAddHeadersToRequest addHeadersToRequest)
: base(scopedRequestDataRepository)
{
_next = next;
_addHeadersToRequest = addHeadersToRequest;
_scopedRequestDataRepository = scopedRequestDataRepository;
}
public async Task Invoke(HttpContext context)
{
var downstreamRoute = _scopedRequestDataRepository.Get<DownstreamRoute>("DownstreamRoute");
if (downstreamRoute.Data.ReRoute.ConfigurationHeaderExtractorProperties.Any())
{
_addHeadersToRequest.SetHeadersOnContext(downstreamRoute.Data.ReRoute.ConfigurationHeaderExtractorProperties, context);
}
await _next.Invoke(context);
}
}
}

View File

@ -0,0 +1,12 @@
namespace Ocelot.Library.Middleware
{
using Microsoft.AspNetCore.Builder;
public static class HttpRequestHeadersBuilderMiddlewareExtensions
{
public static IApplicationBuilder UseHttpRequestHeadersBuilderMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<HttpRequestHeadersBuilderMiddleware>();
}
}
}

View File

@ -12,6 +12,8 @@
builder.UseAuthenticationMiddleware(); builder.UseAuthenticationMiddleware();
builder.UseHttpRequestHeadersBuilderMiddleware();
builder.UseDownstreamUrlCreatorMiddleware(); builder.UseDownstreamUrlCreatorMiddleware();
builder.UseHttpRequestBuilderMiddleware(); builder.UseHttpRequestBuilderMiddleware();

View File

@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Ocelot.Library.Responses;
namespace Ocelot.Library.RequestBuilder
{
public class AddHeadersToRequest : IAddHeadersToRequest
{
private readonly IClaimsParser _claimsParser;
public AddHeadersToRequest(IClaimsParser claimsParser)
{
_claimsParser = claimsParser;
}
public Response SetHeadersOnContext(List<ConfigurationHeaderExtractorProperties> configurationHeaderExtractorProperties, HttpContext context)
{
foreach (var config in configurationHeaderExtractorProperties)
{
var value = _claimsParser.GetValue(context.User.Claims, config.ClaimKey, config.Delimiter, config.Index);
if (value.IsError)
{
return new ErrorResponse(value.Errors);
}
var exists = context.Request.Headers.FirstOrDefault(x => x.Key == config.HeaderKey);
if (!string.IsNullOrEmpty(exists.Key))
{
context.Request.Headers.Remove(exists);
}
context.Request.Headers.Add(config.HeaderKey, new StringValues(value.Data));
}
return new OkResponse();
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Library.Errors;
namespace Ocelot.Library.RequestBuilder
{
public class CannotFindClaimError : Error
{
public CannotFindClaimError(string message)
: base(message, OcelotErrorCode.CannotFindClaimError)
{
}
}
}

View File

@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Ocelot.Library.Errors;
using Ocelot.Library.Responses;
namespace Ocelot.Library.RequestBuilder
{
public class ClaimsParser : IClaimsParser
{
public Response<string> GetValue(IEnumerable<Claim> claims, string key, string delimiter, int index)
{
var claimResponse = GetValue(claims, key);
if (claimResponse.IsError)
{
return claimResponse;
}
if (string.IsNullOrEmpty(delimiter))
{
return claimResponse;
}
var splits = claimResponse.Data.Split(delimiter.ToCharArray());
if (splits.Length < index || index < 0)
{
return new ErrorResponse<string>(new List<Error>
{
new CannotFindClaimError($"Cannot find claim for key: {key}, delimiter: {delimiter}, index: {index}")
});
}
var value = splits[index];
return new OkResponse<string>(value);
}
private Response<string> GetValue(IEnumerable<Claim> claims, string key)
{
var claim = claims.FirstOrDefault(c => c.Type == key);
if (claim != null)
{
return new OkResponse<string>(claim.Value);
}
return new ErrorResponse<string>(new List<Error>
{
new CannotFindClaimError($"Cannot find claim for key: {key}")
});
}
}
}

View File

@ -0,0 +1,18 @@
namespace Ocelot.Library.RequestBuilder
{
public class ConfigurationHeaderExtractorProperties
{
public ConfigurationHeaderExtractorProperties(string headerKey, string claimKey, string delimiter, int index)
{
ClaimKey = claimKey;
Delimiter = delimiter;
Index = index;
HeaderKey = headerKey;
}
public string HeaderKey { get; private set; }
public string ClaimKey { get; private set; }
public string Delimiter { get; private set; }
public int Index { get; private set; }
}
}

View File

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Ocelot.Library.Errors;
using Ocelot.Library.Responses;
namespace Ocelot.Library.RequestBuilder
{
public class ConfigurationHeaderExtrator : IConfigurationHeaderExtrator
{
private readonly Regex _claimRegex = new Regex("Claims\\[.*\\]");
private readonly Regex _indexRegex = new Regex("value\\[.*\\]");
private const string SplitToken = ">";
public Response<ConfigurationHeaderExtractorProperties> Extract(string headerKey, string value)
{
try
{
var instructions = value.Split(SplitToken.ToCharArray());
if (instructions.Length <= 1)
{
return new ErrorResponse<ConfigurationHeaderExtractorProperties>(
new List<Error>
{
new NoInstructionsError(SplitToken)
});
}
var claimMatch = _claimRegex.IsMatch(instructions[0]);
if (!claimMatch)
{
return new ErrorResponse<ConfigurationHeaderExtractorProperties>(
new List<Error>
{
new InstructionNotForClaimsError()
});
}
var claimKey = GetIndexValue(instructions[0]);
var index = 0;
var delimiter = string.Empty;
if (instructions.Length > 2 && _indexRegex.IsMatch(instructions[1]))
{
index = int.Parse(GetIndexValue(instructions[1]));
delimiter = instructions[2].Trim();
}
return new OkResponse<ConfigurationHeaderExtractorProperties>(
new ConfigurationHeaderExtractorProperties(headerKey, claimKey, delimiter, index));
}
catch (Exception exception)
{
return new ErrorResponse<ConfigurationHeaderExtractorProperties>(
new List<Error>
{
new ParsingConfigurationHeaderError(exception)
});
}
}
private string GetIndexValue(string instruction)
{
var firstIndexer = instruction.IndexOf("[", StringComparison.Ordinal);
var lastIndexer = instruction.IndexOf("]", StringComparison.Ordinal);
var length = lastIndexer - firstIndexer;
var claimKey = instruction.Substring(firstIndexer + 1, length - 1);
return claimKey;
}
}
}

View File

@ -0,0 +1,12 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Ocelot.Library.Responses;
namespace Ocelot.Library.RequestBuilder
{
public interface IAddHeadersToRequest
{
Response SetHeadersOnContext(List<ConfigurationHeaderExtractorProperties> configurationHeaderExtractorProperties,
HttpContext context);
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Security.Claims;
using Ocelot.Library.Responses;
namespace Ocelot.Library.RequestBuilder
{
public interface IClaimsParser
{
Response<string> GetValue(IEnumerable<Claim> claims, string key, string delimiter, int index);
}
}

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
using Ocelot.Library.Responses;
namespace Ocelot.Library.RequestBuilder
{
public interface IConfigurationHeaderExtrator
{
Response<ConfigurationHeaderExtractorProperties> Extract(string headerKey, string value);
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Library.Errors;
namespace Ocelot.Library.RequestBuilder
{
public class InstructionNotForClaimsError : Error
{
public InstructionNotForClaimsError()
: base("instructions did not contain claims, at the moment we only support claims extraction", OcelotErrorCode.InstructionNotForClaimsError)
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Library.Errors;
namespace Ocelot.Library.RequestBuilder
{
public class NoInstructionsError : Error
{
public NoInstructionsError(string splitToken)
: base($"There we no instructions splitting on {splitToken}", OcelotErrorCode.NoInstructionsError)
{
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using Ocelot.Library.Errors;
namespace Ocelot.Library.RequestBuilder
{
public class ParsingConfigurationHeaderError : Error
{
public ParsingConfigurationHeaderError(Exception exception)
: base($"error parsing configuration eception is {exception.Message}", OcelotErrorCode.ParsingConfigurationHeaderError)
{
}
}
}

View File

@ -11,8 +11,6 @@
public class HttpContextResponder : IHttpResponder public class HttpContextResponder : IHttpResponder
{ {
public async Task<HttpContext> CreateResponse(HttpContext context, HttpResponseMessage response) public async Task<HttpContext> CreateResponse(HttpContext context, HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
{ {
context.Response.OnStarting(x => context.Response.OnStarting(x =>
{ {
@ -23,8 +21,6 @@
await context.Response.WriteAsync(await response.Content.ReadAsStringAsync()); await context.Response.WriteAsync(await response.Content.ReadAsStringAsync());
return context; return context;
} }
return context;
}
public async Task<HttpContext> CreateErrorResponse(HttpContext context, int statusCode) public async Task<HttpContext> CreateErrorResponse(HttpContext context, int statusCode)
{ {

View File

@ -144,49 +144,6 @@ namespace Ocelot.AcceptanceTests
.BDDfy(); .BDDfy();
} }
[Fact]
public void should_return_response_200_and_foward_claim_as_header()
{
this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:51888", "api", AccessTokenType.Jwt))
.And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51876", 200, "Hello from Laura"))
.And(x => x.GivenIHaveAToken("http://localhost:51888"))
.And(x => x.GivenThereIsAConfiguration(new YamlConfiguration
{
ReRoutes = new List<YamlReRoute>
{
new YamlReRoute
{
DownstreamTemplate = "http://localhost:51876/",
UpstreamTemplate = "/",
UpstreamHttpMethod = "Get",
AuthenticationOptions = new YamlAuthenticationOptions
{
AdditionalScopes = new List<string>(),
Provider = "IdentityServer",
ProviderRootUrl = "http://localhost:51888",
RequireHttps = false,
ScopeName = "api",
ScopeSecret = "secret"
},
AddHeadersToRequest =
{
{ "CustomerId", "Claims[CustomerId] -> value" },
{ "LocationId", "Claims[LocationId] -> value"},
{ "UserId", "Claims[Subject] -> delimiter(|) -> value[0]" },
{ "UserId", "Claims[Subject] -> delimiter(|) -> value[1]" }
}
}
}
}))
.And(x => x.GivenTheApiGatewayIsRunning())
.And(x => x.GivenIHaveAddedATokenToMyRequest())
.When(x => x.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => x.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => x.ThenTheResponseBodyShouldBe("Hello from Laura"))
.BDDfy();
}
[Fact] [Fact]
public void should_return_201_using_identity_server_access_token() public void should_return_201_using_identity_server_access_token()
{ {

View File

@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using IdentityServer4.Models;
using IdentityServer4.Services.InMemory;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Ocelot.Library.Configuration.Yaml;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
using YamlDotNet.Serialization;
[assembly: CollectionBehavior(DisableTestParallelization = true)]
namespace Ocelot.AcceptanceTests
{
public class ClaimsToHeadersForwardingTests : IDisposable
{
private TestServer _ocelotServer;
private HttpClient _ocelotClient;
private HttpResponseMessage _response;
private readonly string _configurationPath;
private IWebHost _servicebuilder;
// Sadly we need to change this when we update the netcoreapp version to make the test update the config correctly
private double _netCoreAppVersion = 1.4;
private BearerToken _token;
private IWebHost _identityServerBuilder;
public ClaimsToHeadersForwardingTests()
{
_configurationPath = $"./bin/Debug/netcoreapp{_netCoreAppVersion}/configuration.yaml";
}
[Fact]
public void should_return_response_200_and_foward_claim_as_header()
{
var user = new InMemoryUser
{
Username = "test",
Password = "test",
Enabled = true,
Subject = "registered|1231231",
Claims = new List<Claim>
{
new Claim("CustomerId", "123"),
new Claim("LocationId", "1")
}
};
this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:52888", "api", AccessTokenType.Jwt, user))
.And(x => x.GivenThereIsAServiceRunningOn("http://localhost:52876", 200))
.And(x => x.GivenIHaveAToken("http://localhost:52888"))
.And(x => x.GivenThereIsAConfiguration(new YamlConfiguration
{
ReRoutes = new List<YamlReRoute>
{
new YamlReRoute
{
DownstreamTemplate = "http://localhost:52876/",
UpstreamTemplate = "/",
UpstreamHttpMethod = "Get",
AuthenticationOptions = new YamlAuthenticationOptions
{
AdditionalScopes = new List<string>
{
"openid", "offline_access"
},
Provider = "IdentityServer",
ProviderRootUrl = "http://localhost:52888",
RequireHttps = false,
ScopeName = "api",
ScopeSecret = "secret",
},
AddHeadersToRequest =
{
{"CustomerId", "Claims[CustomerId] > value"},
{"LocationId", "Claims[LocationId] > value"},
{"UserType", "Claims[sub] > value[0] > |"},
{"UserId", "Claims[sub] > value[1] > |"}
}
}
}
}))
.And(x => x.GivenTheApiGatewayIsRunning())
.And(x => x.GivenIHaveAddedATokenToMyRequest())
.When(x => x.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => x.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => x.ThenTheResponseBodyShouldBe("CustomerId: 123 LocationId: 1 UserType: registered UserId: 1231231"))
.BDDfy();
}
private void WhenIGetUrlOnTheApiGateway(string url)
{
_response = _ocelotClient.GetAsync(url).Result;
}
private void ThenTheResponseBodyShouldBe(string expectedBody)
{
_response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody);
}
/// <summary>
/// This is annoying cos it should be in the constructor but we need to set up the yaml file before calling startup so its a step.
/// </summary>
private void GivenTheApiGatewayIsRunning()
{
_ocelotServer = new TestServer(new WebHostBuilder()
.UseStartup<Startup>());
_ocelotClient = _ocelotServer.CreateClient();
}
private void GivenThereIsAConfiguration(YamlConfiguration yamlConfiguration)
{
var serializer = new Serializer();
if (File.Exists(_configurationPath))
{
File.Delete(_configurationPath);
}
using (TextWriter writer = File.CreateText(_configurationPath))
{
serializer.Serialize(writer, yamlConfiguration);
}
}
private void GivenThereIsAServiceRunningOn(string url, int statusCode)
{
_servicebuilder = new WebHostBuilder()
.UseUrls(url)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseUrls(url)
.Configure(app =>
{
app.Run(async context =>
{
var customerId = context.Request.Headers.First(x => x.Key == "CustomerId").Value.First();
var locationId = context.Request.Headers.First(x => x.Key == "LocationId").Value.First();
var userType = context.Request.Headers.First(x => x.Key == "UserType").Value.First();
var userId = context.Request.Headers.First(x => x.Key == "UserId").Value.First();
var responseBody = $"CustomerId: {customerId} LocationId: {locationId} UserType: {userType} UserId: {userId}";
context.Response.StatusCode = statusCode;
await context.Response.WriteAsync(responseBody);
});
})
.Build();
_servicebuilder.Start();
}
private void GivenThereIsAnIdentityServerOn(string url, string scopeName, AccessTokenType tokenType, InMemoryUser user)
{
_identityServerBuilder = new WebHostBuilder()
.UseUrls(url)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseUrls(url)
.ConfigureServices(services =>
{
services.AddLogging();
services.AddDeveloperIdentityServer()
.AddInMemoryScopes(new List<Scope>
{
new Scope
{
Name = scopeName,
Description = "My API",
Enabled = true,
AllowUnrestrictedIntrospection = true,
ScopeSecrets = new List<Secret>()
{
new Secret
{
Value = "secret".Sha256()
}
},
IncludeAllClaimsForUser = true
},
StandardScopes.OpenId,
StandardScopes.OfflineAccess
})
.AddInMemoryClients(new List<Client>
{
new Client
{
ClientId = "client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets = new List<Secret> {new Secret("secret".Sha256())},
AllowedScopes = new List<string> { scopeName, "openid", "offline_access" },
AccessTokenType = tokenType,
Enabled = true,
RequireClientSecret = false
}
})
.AddInMemoryUsers(new List<InMemoryUser>
{
user
});
})
.Configure(app =>
{
app.UseIdentityServer();
})
.Build();
_identityServerBuilder.Start();
VerifyIdentiryServerStarted(url);
}
private void VerifyIdentiryServerStarted(string url)
{
using (var httpClient = new HttpClient())
{
var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result;
response.EnsureSuccessStatusCode();
}
}
private void GivenIHaveAToken(string url)
{
var tokenUrl = $"{url}/connect/token";
var formData = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("client_id", "client"),
new KeyValuePair<string, string>("client_secret", "secret"),
new KeyValuePair<string, string>("scope", "api"),
new KeyValuePair<string, string>("username", "test"),
new KeyValuePair<string, string>("password", "test"),
new KeyValuePair<string, string>("grant_type", "password")
};
var content = new FormUrlEncodedContent(formData);
using (var httpClient = new HttpClient())
{
var response = httpClient.PostAsync(tokenUrl, content).Result;
response.EnsureSuccessStatusCode();
var responseContent = response.Content.ReadAsStringAsync().Result;
_token = JsonConvert.DeserializeObject<BearerToken>(responseContent);
}
}
private void GivenIHaveAddedATokenToMyRequest()
{
_ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);
}
private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode)
{
_response.StatusCode.ShouldBe(expectedHttpStatusCode);
}
public void Dispose()
{
_servicebuilder?.Dispose();
_ocelotClient?.Dispose();
_ocelotServer?.Dispose();
_identityServerBuilder?.Dispose();
}
// ReSharper disable once ClassNeverInstantiated.Local
class BearerToken
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }
[JsonProperty("token_type")]
public string TokenType { get; set; }
}
}
}

View File

@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Ocelot.Library.Configuration.Yaml;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
using YamlDotNet.Serialization;
namespace Ocelot.AcceptanceTests
{
public class ReturnsErrorTests : IDisposable
{
private TestServer _ocelotServer;
private HttpClient _ocelotClient;
private HttpResponseMessage _response;
private readonly string _configurationPath;
private IWebHost _servicebuilder;
// Sadly we need to change this when we update the netcoreapp version to make the test update the config correctly
private double _netCoreAppVersion = 1.4;
public ReturnsErrorTests()
{
_configurationPath = $"./bin/Debug/netcoreapp{_netCoreAppVersion}/configuration.yaml";
}
[Fact]
public void should_return_response_200_and_foward_claim_as_header()
{
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:53876"))
.And(x => x.GivenThereIsAConfiguration(new YamlConfiguration
{
ReRoutes = new List<YamlReRoute>
{
new YamlReRoute
{
DownstreamTemplate = "http://localhost:53876/",
UpstreamTemplate = "/",
UpstreamHttpMethod = "Get",
}
}
}))
.And(x => x.GivenTheApiGatewayIsRunning())
.When(x => x.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => x.ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError))
.BDDfy();
}
private void WhenIGetUrlOnTheApiGateway(string url)
{
_response = _ocelotClient.GetAsync(url).Result;
}
private void ThenTheResponseBodyShouldBe(string expectedBody)
{
_response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody);
}
/// <summary>
/// This is annoying cos it should be in the constructor but we need to set up the yaml file before calling startup so its a step.
/// </summary>
private void GivenTheApiGatewayIsRunning()
{
_ocelotServer = new TestServer(new WebHostBuilder()
.UseStartup<Startup>());
_ocelotClient = _ocelotServer.CreateClient();
}
private void GivenThereIsAConfiguration(YamlConfiguration yamlConfiguration)
{
var serializer = new Serializer();
if (File.Exists(_configurationPath))
{
File.Delete(_configurationPath);
}
using (TextWriter writer = File.CreateText(_configurationPath))
{
serializer.Serialize(writer, yamlConfiguration);
}
}
private void GivenThereIsAServiceRunningOn(string url)
{
_servicebuilder = new WebHostBuilder()
.UseUrls(url)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseUrls(url)
.Configure(app =>
{
app.Run(context =>
{
throw new Exception("BLAMMMM");
});
})
.Build();
_servicebuilder.Start();
}
private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode)
{
_response.StatusCode.ShouldBe(expectedHttpStatusCode);
}
public void Dispose()
{
_servicebuilder?.Dispose();
_ocelotClient?.Dispose();
_ocelotServer?.Dispose();
}
}
}

View File

@ -1,6 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moq; using Moq;
using Ocelot.Library.RequestBuilder;
using Shouldly; using Shouldly;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
@ -18,9 +20,13 @@ namespace Ocelot.UnitTests.Configuration
private readonly Mock<IConfigurationValidator> _validator; private readonly Mock<IConfigurationValidator> _validator;
private OcelotConfiguration _config; private OcelotConfiguration _config;
private YamlConfiguration _yamlConfiguration; private YamlConfiguration _yamlConfiguration;
private readonly Mock<IConfigurationHeaderExtrator> _configExtractor;
private readonly Mock<ILogger<OcelotConfiguration>> _logger;
public OcelotConfigurationTests() public OcelotConfigurationTests()
{ {
_logger = new Mock<ILogger<OcelotConfiguration>>();
_configExtractor = new Mock<IConfigurationHeaderExtrator>();
_validator = new Mock<IConfigurationValidator>(); _validator = new Mock<IConfigurationValidator>();
_yamlConfig = new Mock<IOptions<YamlConfiguration>>(); _yamlConfig = new Mock<IOptions<YamlConfiguration>>();
} }
@ -54,6 +60,114 @@ namespace Ocelot.UnitTests.Configuration
.BDDfy(); .BDDfy();
} }
[Fact]
public void should_create_with_headers_to_extract()
{
var expected = new List<ReRoute>
{
new ReRouteBuilder()
.WithDownstreamTemplate("/products/{productId}")
.WithUpstreamTemplate("/api/products/{productId}")
.WithUpstreamHttpMethod("Get")
.WithUpstreamTemplatePattern("/api/products/.*$")
.WithAuthenticationProvider("IdentityServer")
.WithAuthenticationProviderUrl("http://localhost:51888")
.WithRequireHttps(false)
.WithScopeSecret("secret")
.WithAuthenticationProviderScopeName("api")
.WithConfigurationHeaderExtractorProperties(new List<ConfigurationHeaderExtractorProperties>
{
new ConfigurationHeaderExtractorProperties("CustomerId", "CustomerId", "", 0),
})
.Build()
};
this.Given(x => x.GivenTheYamlConfigIs(new YamlConfiguration
{
ReRoutes = new List<YamlReRoute>
{
new YamlReRoute
{
UpstreamTemplate = "/api/products/{productId}",
DownstreamTemplate = "/products/{productId}",
UpstreamHttpMethod = "Get",
AuthenticationOptions = new YamlAuthenticationOptions
{
AdditionalScopes = new List<string>(),
Provider = "IdentityServer",
ProviderRootUrl = "http://localhost:51888",
RequireHttps = false,
ScopeName = "api",
ScopeSecret = "secret"
},
AddHeadersToRequest =
{
{"CustomerId", "Claims[CustomerId] > value"},
}
}
}
}))
.And(x => x.GivenTheYamlConfigIsValid())
.And(x => x.GivenTheConfigHeaderExtractorReturns(new ConfigurationHeaderExtractorProperties("CustomerId", "CustomerId", "", 0)))
.When(x => x.WhenIInstanciateTheOcelotConfig())
.Then(x => x.ThenTheReRoutesAre(expected))
.And(x => x.ThenTheAuthenticationOptionsAre(expected))
.BDDfy();
}
private void GivenTheConfigHeaderExtractorReturns(ConfigurationHeaderExtractorProperties expected)
{
_configExtractor
.Setup(x => x.Extract(It.IsAny<string>(), It.IsAny<string>()))
.Returns(new OkResponse<ConfigurationHeaderExtractorProperties>(expected));
}
[Fact]
public void should_create_with_authentication_properties()
{
var expected = new List<ReRoute>
{
new ReRouteBuilder()
.WithDownstreamTemplate("/products/{productId}")
.WithUpstreamTemplate("/api/products/{productId}")
.WithUpstreamHttpMethod("Get")
.WithUpstreamTemplatePattern("/api/products/.*$")
.WithAuthenticationProvider("IdentityServer")
.WithAuthenticationProviderUrl("http://localhost:51888")
.WithRequireHttps(false)
.WithScopeSecret("secret")
.WithAuthenticationProviderScopeName("api")
.Build()
};
this.Given(x => x.GivenTheYamlConfigIs(new YamlConfiguration
{
ReRoutes = new List<YamlReRoute>
{
new YamlReRoute
{
UpstreamTemplate = "/api/products/{productId}",
DownstreamTemplate = "/products/{productId}",
UpstreamHttpMethod = "Get",
AuthenticationOptions = new YamlAuthenticationOptions
{
AdditionalScopes = new List<string>(),
Provider = "IdentityServer",
ProviderRootUrl = "http://localhost:51888",
RequireHttps = false,
ScopeName = "api",
ScopeSecret = "secret"
}
}
}
}))
.And(x => x.GivenTheYamlConfigIsValid())
.When(x => x.WhenIInstanciateTheOcelotConfig())
.Then(x => x.ThenTheReRoutesAre(expected))
.And(x => x.ThenTheAuthenticationOptionsAre(expected))
.BDDfy();
}
[Fact] [Fact]
public void should_create_template_pattern_that_matches_more_than_one_placeholder() public void should_create_template_pattern_that_matches_more_than_one_placeholder()
{ {
@ -158,7 +272,8 @@ namespace Ocelot.UnitTests.Configuration
private void WhenIInstanciateTheOcelotConfig() private void WhenIInstanciateTheOcelotConfig()
{ {
_config = new OcelotConfiguration(_yamlConfig.Object, _validator.Object); _config = new OcelotConfiguration(_yamlConfig.Object, _validator.Object,
_configExtractor.Object, _logger.Object);
} }
private void ThenTheReRoutesAre(List<ReRoute> expectedReRoutes) private void ThenTheReRoutesAre(List<ReRoute> expectedReRoutes)
@ -174,5 +289,22 @@ namespace Ocelot.UnitTests.Configuration
result.UpstreamTemplatePattern.ShouldBe(expected.UpstreamTemplatePattern); result.UpstreamTemplatePattern.ShouldBe(expected.UpstreamTemplatePattern);
} }
} }
private void ThenTheAuthenticationOptionsAre(List<ReRoute> expectedReRoutes)
{
for (int i = 0; i < _config.ReRoutes.Count; i++)
{
var result = _config.ReRoutes[i].AuthenticationOptions;
var expected = expectedReRoutes[i].AuthenticationOptions;
result.AdditionalScopes.ShouldBe(expected.AdditionalScopes);
result.Provider.ShouldBe(expected.Provider);
result.ProviderRootUrl.ShouldBe(expected.ProviderRootUrl);
result.RequireHttps.ShouldBe(expected.RequireHttps);
result.ScopeName.ShouldBe(expected.ScopeName);
result.ScopeSecret.ShouldBe(expected.ScopeSecret);
}
}
} }
} }

View File

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Ocelot.Library.Builder;
using Ocelot.Library.DownstreamRouteFinder;
using Ocelot.Library.Middleware;
using Ocelot.Library.Repository;
using Ocelot.Library.RequestBuilder;
using Ocelot.Library.Responses;
using Ocelot.Library.UrlMatcher;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.Middleware
{
public class HttpRequestHeadersBuilderMiddlewareTests : IDisposable
{
private readonly Mock<IScopedRequestDataRepository> _scopedRepository;
private readonly Mock<IAddHeadersToRequest> _addHeaders;
private readonly string _url;
private readonly TestServer _server;
private readonly HttpClient _client;
private Response<DownstreamRoute> _downstreamRoute;
private HttpResponseMessage _result;
public HttpRequestHeadersBuilderMiddlewareTests()
{
_url = "http://localhost:51879";
_scopedRepository = new Mock<IScopedRequestDataRepository>();
_addHeaders = new Mock<IAddHeadersToRequest>();
var builder = new WebHostBuilder()
.ConfigureServices(x =>
{
x.AddSingleton(_addHeaders.Object);
x.AddSingleton(_scopedRepository.Object);
})
.UseUrls(_url)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseUrls(_url)
.Configure(app =>
{
app.UseHttpRequestHeadersBuilderMiddleware();
});
_server = new TestServer(builder);
_client = _server.CreateClient();
}
[Fact]
public void happy_path()
{
var downstreamRoute = new DownstreamRoute(new List<TemplateVariableNameAndValue>(),
new ReRouteBuilder()
.WithDownstreamTemplate("any old string")
.WithConfigurationHeaderExtractorProperties(new List<ConfigurationHeaderExtractorProperties>
{
new ConfigurationHeaderExtractorProperties("UserId", "Subject", "", 0)
})
.Build());
this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute))
.And(x => x.GivenTheAddHeadersToRequestReturns("123"))
.When(x => x.WhenICallTheMiddleware())
.Then(x => x.ThenTheAddHeadersToRequestIsCalledCorrectly())
.BDDfy();
}
private void GivenTheAddHeadersToRequestReturns(string claimValue)
{
_addHeaders
.Setup(x => x.SetHeadersOnContext(It.IsAny<List<ConfigurationHeaderExtractorProperties>>(),
It.IsAny<HttpContext>()))
.Returns(new OkResponse());
}
private void ThenTheAddHeadersToRequestIsCalledCorrectly()
{
_addHeaders
.Verify(x => x.SetHeadersOnContext(It.IsAny<List<ConfigurationHeaderExtractorProperties>>(),
It.IsAny<HttpContext>()), Times.Once);
}
private void WhenICallTheMiddleware()
{
_result = _client.GetAsync(_url).Result;
}
private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute)
{
_downstreamRoute = new OkResponse<DownstreamRoute>(downstreamRoute);
_scopedRepository
.Setup(x => x.Get<DownstreamRoute>(It.IsAny<string>()))
.Returns(_downstreamRoute);
}
public void Dispose()
{
_client.Dispose();
_server.Dispose();
}
}
}

View File

@ -0,0 +1,152 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Moq;
using Ocelot.Library.Errors;
using Ocelot.Library.RequestBuilder;
using Ocelot.Library.Responses;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.RequestBuilder
{
public class AddHeadersToRequestTests
{
private readonly AddHeadersToRequest _addHeadersToRequest;
private readonly Mock<IClaimsParser> _parser;
private List<ConfigurationHeaderExtractorProperties> _configuration;
private HttpContext _context;
private Response _result;
private Response<string> _claimValue;
public AddHeadersToRequestTests()
{
_parser = new Mock<IClaimsParser>();
_addHeadersToRequest = new AddHeadersToRequest(_parser.Object);
}
[Fact]
public void should_add_headers_to_context()
{
var context = new DefaultHttpContext
{
User = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
{
new Claim("test", "data")
}))
};
this.Given(
x => x.GivenConfigurationHeaderExtractorProperties(new List<ConfigurationHeaderExtractorProperties>
{
new ConfigurationHeaderExtractorProperties("header-key", "", "", 0)
}))
.Given(x => x.GivenHttpContext(context))
.And(x => x.GivenTheClaimParserReturns(new OkResponse<string>("value")))
.When(x => x.WhenIAddHeadersToTheRequest())
.Then(x => x.ThenTheResultIsSuccess())
.And(x => x.ThenTheHeaderIsAdded())
.BDDfy();
}
[Fact]
public void if_header_exists_should_replace_it()
{
var context = new DefaultHttpContext
{
User = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
{
new Claim("test", "data")
})),
};
context.Request.Headers.Add("header-key", new StringValues("initial"));
this.Given(
x => x.GivenConfigurationHeaderExtractorProperties(new List<ConfigurationHeaderExtractorProperties>
{
new ConfigurationHeaderExtractorProperties("header-key", "", "", 0)
}))
.Given(x => x.GivenHttpContext(context))
.And(x => x.GivenTheClaimParserReturns(new OkResponse<string>("value")))
.When(x => x.WhenIAddHeadersToTheRequest())
.Then(x => x.ThenTheResultIsSuccess())
.And(x => x.ThenTheHeaderIsAdded())
.BDDfy();
}
[Fact]
public void should_return_error()
{
this.Given(
x => x.GivenConfigurationHeaderExtractorProperties(new List<ConfigurationHeaderExtractorProperties>
{
new ConfigurationHeaderExtractorProperties("", "", "", 0)
}))
.Given(x => x.GivenHttpContext(new DefaultHttpContext()))
.And(x => x.GivenTheClaimParserReturns(new ErrorResponse<string>(new List<Error>
{
new AnyError()
})))
.When(x => x.WhenIAddHeadersToTheRequest())
.Then(x => x.ThenTheResultIsError())
.BDDfy();
}
private void ThenTheHeaderIsAdded()
{
var header = _context.Request.Headers.First(x => x.Key == "header-key");
header.Value.First().ShouldBe(_claimValue.Data);
}
private void GivenConfigurationHeaderExtractorProperties(List<ConfigurationHeaderExtractorProperties> configuration)
{
_configuration = configuration;
}
private void GivenHttpContext(HttpContext context)
{
_context = context;
}
private void GivenTheClaimParserReturns(Response<string> claimValue)
{
_claimValue = claimValue;
_parser
.Setup(
x =>
x.GetValue(It.IsAny<IEnumerable<Claim>>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<int>()))
.Returns(_claimValue);
}
private void WhenIAddHeadersToTheRequest()
{
_result = _addHeadersToRequest.SetHeadersOnContext(_configuration, _context);
}
private void ThenTheResultIsSuccess()
{
_result.IsError.ShouldBe(false);
}
private void ThenTheResultIsError()
{
_result.IsError.ShouldBe(true);
}
class AnyError : Error
{
public AnyError()
: base("blahh", OcelotErrorCode.UnknownError)
{
}
}
}
}

View File

@ -0,0 +1,123 @@
using System.Collections.Generic;
using System.Security.Claims;
using Ocelot.Library.Errors;
using Ocelot.Library.RequestBuilder;
using Ocelot.Library.Responses;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.RequestBuilder
{
public class ClaimParserTests
{
private readonly IClaimsParser _claimsParser;
private readonly List<Claim> _claims;
private string _key;
private Response<string> _result;
private string _delimiter;
private int _index;
public ClaimParserTests()
{
_claims = new List<Claim>();
_claimsParser = new Library.RequestBuilder.ClaimsParser();
}
[Fact]
public void can_parse_claims_dictionary_access_string_returning_value_to_function()
{
this.Given(x => x.GivenAClaimOf(new Claim("CustomerId", "1234")))
.And(x => x.GivenTheKeyIs("CustomerId"))
.When(x => x.WhenICallTheParser())
.Then(x => x.ThenTheResultIs(new OkResponse<string>("1234")))
.BDDfy();
}
[Fact]
public void should_return_error_response_when_cannot_find_requested_claim()
{
this.Given(x => x.GivenAClaimOf(new Claim("BallsId", "1234")))
.And(x => x.GivenTheKeyIs("CustomerId"))
.When(x => x.WhenICallTheParser())
.Then(x => x.ThenTheResultIs(new ErrorResponse<string>(new List<Error>
{
new CannotFindClaimError($"Cannot find claim for key: {_key}")
})))
.BDDfy();
}
[Fact]
public void can_parse_claims_dictionary_access_string_using_delimiter_and_retuning_at_correct_index()
{
this.Given(x => x.GivenAClaimOf(new Claim("Subject", "registered|4321")))
.And(x => x.GivenTheDelimiterIs("|"))
.And(x => x.GivenTheIndexIs(1))
.And(x => x.GivenTheKeyIs("Subject"))
.When(x => x.WhenICallTheParser())
.Then(x => x.ThenTheResultIs(new OkResponse<string>("4321")))
.BDDfy();
}
[Fact]
public void should_return_error_response_if_index_too_large()
{
this.Given(x => x.GivenAClaimOf(new Claim("Subject", "registered|4321")))
.And(x => x.GivenTheDelimiterIs("|"))
.And(x => x.GivenTheIndexIs(24))
.And(x => x.GivenTheKeyIs("Subject"))
.When(x => x.WhenICallTheParser())
.Then(x => x.ThenTheResultIs(new ErrorResponse<string>(new List<Error>
{
new CannotFindClaimError($"Cannot find claim for key: {_key}, delimiter: {_delimiter}, index: {_index}")
})))
.BDDfy();
}
[Fact]
public void should_return_error_response_if_index_too_small()
{
this.Given(x => x.GivenAClaimOf(new Claim("Subject", "registered|4321")))
.And(x => x.GivenTheDelimiterIs("|"))
.And(x => x.GivenTheIndexIs(-1))
.And(x => x.GivenTheKeyIs("Subject"))
.When(x => x.WhenICallTheParser())
.Then(x => x.ThenTheResultIs(new ErrorResponse<string>(new List<Error>
{
new CannotFindClaimError($"Cannot find claim for key: {_key}, delimiter: {_delimiter}, index: {_index}")
})))
.BDDfy();
}
private void GivenTheIndexIs(int index)
{
_index = index;
}
private void GivenTheDelimiterIs(string delimiter)
{
_delimiter = delimiter;
}
private void GivenAClaimOf(Claim claim)
{
_claims.Add(claim);
}
private void GivenTheKeyIs(string key)
{
_key = key;
}
private void WhenICallTheParser()
{
_result = _claimsParser.GetValue(_claims, _key, _delimiter, _index);
}
private void ThenTheResultIs(Response<string> expected)
{
_result.Data.ShouldBe(expected.Data);
_result.IsError.ShouldBe(expected.IsError);
}
}
}

View File

@ -0,0 +1,116 @@
using System.Collections.Generic;
using System.Linq;
using Ocelot.Library.Errors;
using Ocelot.Library.RequestBuilder;
using Ocelot.Library.Responses;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.RequestBuilder
{
public class ConfigurationHeadersExtractorTests
{
private Dictionary<string, string> _dictionary;
private readonly IConfigurationHeaderExtrator _configurationHeaderExtrator;
private Response<ConfigurationHeaderExtractorProperties> _result;
public ConfigurationHeadersExtractorTests()
{
_configurationHeaderExtrator = new ConfigurationHeaderExtrator();
}
[Fact]
public void returns_no_instructions_error()
{
this.Given(x => x.GivenTheDictionaryIs(new Dictionary<string, string>()
{
{"CustomerId", ""},
}))
.When(x => x.WhenICallTheExtractor())
.Then(
x =>
x.ThenAnErrorIsReturned(new ErrorResponse<ConfigurationHeaderExtractorProperties>(
new List<Error>
{
new NoInstructionsError(">")
})))
.BDDfy();
}
[Fact]
public void returns_no_instructions_not_for_claims_error()
{
this.Given(x => x.GivenTheDictionaryIs(new Dictionary<string, string>()
{
{"CustomerId", "Cheese[CustomerId] > value"},
}))
.When(x => x.WhenICallTheExtractor())
.Then(
x =>
x.ThenAnErrorIsReturned(new ErrorResponse<ConfigurationHeaderExtractorProperties>(
new List<Error>
{
new InstructionNotForClaimsError()
})))
.BDDfy();
}
[Fact]
public void can_parse_entry_to_work_out_properties_with_key()
{
this.Given(x => x.GivenTheDictionaryIs(new Dictionary<string, string>()
{
{"CustomerId", "Claims[CustomerId] > value"},
}))
.When(x => x.WhenICallTheExtractor())
.Then(
x =>
x.ThenTheClaimParserPropertiesAreReturned(
new OkResponse<ConfigurationHeaderExtractorProperties>(
new ConfigurationHeaderExtractorProperties("CustomerId", "CustomerId", "", 0))))
.BDDfy();
}
[Fact]
public void can_parse_entry_to_work_out_properties_with_key_delimiter_and_index()
{
this.Given(x => x.GivenTheDictionaryIs(new Dictionary<string, string>()
{
{"UserId", "Claims[Subject] > value[0] > |"},
}))
.When(x => x.WhenICallTheExtractor())
.Then(
x =>
x.ThenTheClaimParserPropertiesAreReturned(
new OkResponse<ConfigurationHeaderExtractorProperties>(
new ConfigurationHeaderExtractorProperties("UserId", "Subject", "|", 0))))
.BDDfy();
}
private void ThenAnErrorIsReturned(Response<ConfigurationHeaderExtractorProperties> expected)
{
_result.IsError.ShouldBe(expected.IsError);
_result.Errors[0].ShouldBeOfType(expected.Errors[0].GetType());
}
private void ThenTheClaimParserPropertiesAreReturned(Response<ConfigurationHeaderExtractorProperties> expected)
{
_result.Data.ClaimKey.ShouldBe(expected.Data.ClaimKey);
_result.Data.Delimiter.ShouldBe(expected.Data.Delimiter);
_result.Data.Index.ShouldBe(expected.Data.Index);
_result.IsError.ShouldBe(expected.IsError);
}
private void WhenICallTheExtractor()
{
var first = _dictionary.First();
_result = _configurationHeaderExtrator.Extract(first.Key, first.Value);
}
private void GivenTheDictionaryIs(Dictionary<string, string> dictionary)
{
_dictionary = dictionary;
}
}
}