Merge pull request #51 from ThreeMammals/develop

Feature/adding some re route specific validation tests (#590)
This commit is contained in:
geffzhang 2018-09-11 11:17:47 +08:00 committed by GitHub
commit e799f5792e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 693 additions and 124 deletions

View File

@ -62,7 +62,8 @@ The secret is the client secret that Ocelot's internal IdentityServer will use t
} }
In order for the administration API to work, Ocelot / IdentityServer must be able to call itself for validation. This means that you need to add the base url of Ocelot In order for the administration API to work, Ocelot / IdentityServer must be able to call itself for validation. This means that you need to add the base url of Ocelot
to global configuration if it is not default (http://localhost:5000). This can be done as follows.. to global configuration if it is not default (http://localhost:5000). Please note if you are using something like docker to host Ocelot it might not be able to
call back to localhost etc and you need to know what you are doing with docker networking in this scenario. Anyway this can be done as follows..
If you want to run on a different host and port locally.. If you want to run on a different host and port locally..

View File

@ -1,8 +1,8 @@
using System.Collections.Generic; namespace Ocelot.Configuration.Validator
{
using System.Collections.Generic;
using Ocelot.Errors; using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{
public class ConfigurationValidationResult public class ConfigurationValidationResult
{ {
public ConfigurationValidationResult(bool isError) public ConfigurationValidationResult(bool isError)
@ -11,12 +11,6 @@ namespace Ocelot.Configuration.Validator
Errors = new List<Error>(); Errors = new List<Error>();
} }
public ConfigurationValidationResult(bool isError, Error error)
{
IsError = isError;
Errors = new List<Error> { error };
}
public ConfigurationValidationResult(bool isError, List<Error> errors) public ConfigurationValidationResult(bool isError, List<Error> errors)
{ {
IsError = isError; IsError = isError;

View File

@ -1,46 +1,42 @@
using FluentValidation; namespace Ocelot.Configuration.Validator
using Microsoft.AspNetCore.Authentication; {
using Ocelot.Configuration.File; using FluentValidation;
using Ocelot.Errors; using File;
using Ocelot.Responses; using Errors;
using Responses;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Ocelot.Configuration.Validator
{
using System; using System;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Ocelot.ServiceDiscovery; using ServiceDiscovery;
using Requester;
public class FileConfigurationFluentValidator : AbstractValidator<FileConfiguration>, IConfigurationValidator public class FileConfigurationFluentValidator : AbstractValidator<FileConfiguration>, IConfigurationValidator
{ {
private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate;
private readonly List<ServiceDiscoveryFinderDelegate> _serviceDiscoveryFinderDelegates; private readonly List<ServiceDiscoveryFinderDelegate> _serviceDiscoveryFinderDelegates;
public FileConfigurationFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, IServiceProvider provider)
public FileConfigurationFluentValidator(IServiceProvider provider, ReRouteFluentValidator reRouteFluentValidator, FileGlobalConfigurationFluentValidator fileGlobalConfigurationFluentValidator)
{ {
_qosDelegatingHandlerDelegate = provider.GetService<QosDelegatingHandlerDelegate>();
_serviceDiscoveryFinderDelegates = provider _serviceDiscoveryFinderDelegates = provider
.GetServices<ServiceDiscoveryFinderDelegate>() .GetServices<ServiceDiscoveryFinderDelegate>()
.ToList(); .ToList();
RuleFor(configuration => configuration.ReRoutes) RuleFor(configuration => configuration.ReRoutes)
.SetCollectionValidator(new ReRouteFluentValidator(authenticationSchemeProvider, _qosDelegatingHandlerDelegate)); .SetCollectionValidator(reRouteFluentValidator);
RuleFor(configuration => configuration.GlobalConfiguration) RuleFor(configuration => configuration.GlobalConfiguration)
.SetValidator(new FileGlobalConfigurationFluentValidator(_qosDelegatingHandlerDelegate)); .SetValidator(fileGlobalConfigurationFluentValidator);
RuleForEach(configuration => configuration.ReRoutes) RuleForEach(configuration => configuration.ReRoutes)
.Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.ReRoutes)) .Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.ReRoutes))
.WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicate"); .WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicate");
RuleForEach(configuration => configuration.ReRoutes) RuleForEach(configuration => configuration.ReRoutes)
.Must((config, reRoute) => HaveServiceDiscoveryProviderRegitered(reRoute, config.GlobalConfiguration.ServiceDiscoveryProvider)) .Must((config, reRoute) => HaveServiceDiscoveryProviderRegistered(reRoute, config.GlobalConfiguration.ServiceDiscoveryProvider))
.WithMessage((config, reRoute) => $"Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?"); .WithMessage((config, reRoute) => $"Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?");
RuleFor(configuration => configuration.GlobalConfiguration.ServiceDiscoveryProvider) RuleFor(configuration => configuration.GlobalConfiguration.ServiceDiscoveryProvider)
.Must((config) => HaveServiceDiscoveryProviderRegitered(config)) .Must(HaveServiceDiscoveryProviderRegistered)
.WithMessage((config, reRoute) => $"Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?"); .WithMessage((config, reRoute) => $"Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?");
RuleForEach(configuration => configuration.ReRoutes) RuleForEach(configuration => configuration.ReRoutes)
@ -60,7 +56,7 @@ namespace Ocelot.Configuration.Validator
.WithMessage((config, aggregateReRoute) => $"{nameof(aggregateReRoute)} {aggregateReRoute.UpstreamPathTemplate} contains ReRoute with specific RequestIdKey, this is not possible with Aggregates"); .WithMessage((config, aggregateReRoute) => $"{nameof(aggregateReRoute)} {aggregateReRoute.UpstreamPathTemplate} contains ReRoute with specific RequestIdKey, this is not possible with Aggregates");
} }
private bool HaveServiceDiscoveryProviderRegitered(FileReRoute reRoute, FileServiceDiscoveryProvider serviceDiscoveryProvider) private bool HaveServiceDiscoveryProviderRegistered(FileReRoute reRoute, FileServiceDiscoveryProvider serviceDiscoveryProvider)
{ {
if (string.IsNullOrEmpty(reRoute.ServiceName)) if (string.IsNullOrEmpty(reRoute.ServiceName))
{ {
@ -75,7 +71,7 @@ namespace Ocelot.Configuration.Validator
return _serviceDiscoveryFinderDelegates.Any(); return _serviceDiscoveryFinderDelegates.Any();
} }
private bool HaveServiceDiscoveryProviderRegitered(FileServiceDiscoveryProvider serviceDiscoveryProvider) private bool HaveServiceDiscoveryProviderRegistered(FileServiceDiscoveryProvider serviceDiscoveryProvider)
{ {
if(serviceDiscoveryProvider == null) if(serviceDiscoveryProvider == null)
{ {
@ -87,12 +83,7 @@ namespace Ocelot.Configuration.Validator
return true; return true;
} }
if(string.IsNullOrEmpty(serviceDiscoveryProvider.Type)) return string.IsNullOrEmpty(serviceDiscoveryProvider.Type) || _serviceDiscoveryFinderDelegates.Any();
{
return true;
}
return _serviceDiscoveryFinderDelegates.Any();
} }
public async Task<Response<ConfigurationValidationResult>> IsValid(FileConfiguration configuration) public async Task<Response<ConfigurationValidationResult>> IsValid(FileConfiguration configuration)

View File

@ -1,21 +1,14 @@
using FluentValidation;
using Ocelot.Configuration.File;
namespace Ocelot.Configuration.Validator namespace Ocelot.Configuration.Validator
{ {
using Requester; using FluentValidation;
using File;
public class FileGlobalConfigurationFluentValidator : AbstractValidator<FileGlobalConfiguration> public class FileGlobalConfigurationFluentValidator : AbstractValidator<FileGlobalConfiguration>
{ {
private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate; public FileGlobalConfigurationFluentValidator(FileQoSOptionsFluentValidator fileQoSOptionsFluentValidator)
public FileGlobalConfigurationFluentValidator(QosDelegatingHandlerDelegate qosDelegatingHandlerDelegate)
{ {
_qosDelegatingHandlerDelegate = qosDelegatingHandlerDelegate;
RuleFor(configuration => configuration.QoSOptions) RuleFor(configuration => configuration.QoSOptions)
.SetValidator(new FileQoSOptionsFluentValidator(_qosDelegatingHandlerDelegate)); .SetValidator(fileQoSOptionsFluentValidator);
} }
} }
} }

View File

@ -1,17 +1,18 @@
using FluentValidation;
using Ocelot.Configuration.File;
namespace Ocelot.Configuration.Validator namespace Ocelot.Configuration.Validator
{ {
using System;
using FluentValidation;
using File;
using Microsoft.Extensions.DependencyInjection;
using Requester; using Requester;
public class FileQoSOptionsFluentValidator : AbstractValidator<FileQoSOptions> public class FileQoSOptionsFluentValidator : AbstractValidator<FileQoSOptions>
{ {
private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate; private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate;
public FileQoSOptionsFluentValidator(QosDelegatingHandlerDelegate qosDelegatingHandlerDelegate) public FileQoSOptionsFluentValidator(IServiceProvider provider)
{ {
_qosDelegatingHandlerDelegate = qosDelegatingHandlerDelegate; _qosDelegatingHandlerDelegate = provider.GetService<QosDelegatingHandlerDelegate>();
When(qosOptions => qosOptions.TimeoutValue > 0 && qosOptions.ExceptionsAllowedBeforeBreaking > 0, () => { When(qosOptions => qosOptions.TimeoutValue > 0 && qosOptions.ExceptionsAllowedBeforeBreaking > 0, () => {
RuleFor(qosOptions => qosOptions) RuleFor(qosOptions => qosOptions)

View File

@ -1,13 +1,11 @@
using System; namespace Ocelot.Configuration.Validator
using System.Collections.Generic;
using System.Text;
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{ {
using Errors;
public class FileValidationFailedError : Error public class FileValidationFailedError : Error
{ {
public FileValidationFailedError(string message) : base(message, OcelotErrorCode.FileValidationFailedError) public FileValidationFailedError(string message)
: base(message, OcelotErrorCode.FileValidationFailedError)
{ {
} }
} }

View File

@ -1,13 +1,15 @@
namespace Ocelot.Configuration.Validator
{
using FluentValidation; using FluentValidation;
using Ocelot.Configuration.File; using Ocelot.Configuration.File;
namespace Ocelot.Configuration.Validator
{
public class HostAndPortValidator : AbstractValidator<FileHostAndPort> public class HostAndPortValidator : AbstractValidator<FileHostAndPort>
{ {
public HostAndPortValidator() public HostAndPortValidator()
{ {
RuleFor(r => r.Host).NotEmpty().WithMessage("When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using ReRoute.Host or Ocelot cannot find your service!"); RuleFor(r => r.Host)
.NotEmpty()
.WithMessage("When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using ReRoute.Host or Ocelot cannot find your service!");
} }
} }
} }

View File

@ -1,9 +1,9 @@
using System.Threading.Tasks; namespace Ocelot.Configuration.Validator
{
using System.Threading.Tasks;
using Ocelot.Configuration.File; using Ocelot.Configuration.File;
using Ocelot.Responses; using Ocelot.Responses;
namespace Ocelot.Configuration.Validator
{
public interface IConfigurationValidator public interface IConfigurationValidator
{ {
Task<Response<ConfigurationValidationResult>> IsValid(FileConfiguration configuration); Task<Response<ConfigurationValidationResult>> IsValid(FileConfiguration configuration);

View File

@ -1,37 +1,51 @@
namespace Ocelot.Configuration.Validator namespace Ocelot.Configuration.Validator
{ {
using System;
using FluentValidation; using FluentValidation;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Ocelot.Configuration.File; using File;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System;
using Microsoft.Extensions.DependencyInjection;
using Requester;
public class ReRouteFluentValidator : AbstractValidator<FileReRoute> public class ReRouteFluentValidator : AbstractValidator<FileReRoute>
{ {
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate;
public ReRouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, QosDelegatingHandlerDelegate qosDelegatingHandlerDelegate) public ReRouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, HostAndPortValidator hostAndPortValidator, FileQoSOptionsFluentValidator fileQoSOptionsFluentValidator)
{ {
_authenticationSchemeProvider = authenticationSchemeProvider; _authenticationSchemeProvider = authenticationSchemeProvider;
_qosDelegatingHandlerDelegate = qosDelegatingHandlerDelegate;
RuleFor(reRoute => reRoute.QoSOptions) RuleFor(reRoute => reRoute.QoSOptions)
.SetValidator(new FileQoSOptionsFluentValidator(_qosDelegatingHandlerDelegate)); .SetValidator(fileQoSOptionsFluentValidator);
RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.NotEmpty()
.WithMessage("{PropertyName} cannot be empty");
RuleFor(reRoute => reRoute.UpstreamPathTemplate)
.NotEmpty()
.WithMessage("{PropertyName} cannot be empty");
When(reRoute => !string.IsNullOrEmpty(reRoute.DownstreamPathTemplate), () =>
{
RuleFor(reRoute => reRoute.DownstreamPathTemplate) RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => path.StartsWith("/")) .Must(path => path.StartsWith("/"))
.WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash"); .WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash");
RuleFor(reRoute => reRoute.UpstreamPathTemplate) RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => !path.Contains("//")) .Must(path => !path.Contains("//"))
.WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."); .WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.");
RuleFor(reRoute => reRoute.DownstreamPathTemplate) RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => !path.Contains("https://") && !path.Contains("http://"))
.WithMessage("{PropertyName} {PropertyValue} contains scheme");
});
When(reRoute => !string.IsNullOrEmpty(reRoute.UpstreamPathTemplate), () =>
{
RuleFor(reRoute => reRoute.UpstreamPathTemplate)
.Must(path => !path.Contains("//")) .Must(path => !path.Contains("//"))
.WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."); .WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.");
@ -39,21 +53,25 @@
.Must(path => path.StartsWith("/")) .Must(path => path.StartsWith("/"))
.WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash"); .WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash");
RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => !path.Contains("https://") && !path.Contains("http://"))
.WithMessage("{PropertyName} {PropertyValue} contains scheme");
RuleFor(reRoute => reRoute.UpstreamPathTemplate) RuleFor(reRoute => reRoute.UpstreamPathTemplate)
.Must(path => !path.Contains("https://") && !path.Contains("http://")) .Must(path => !path.Contains("https://") && !path.Contains("http://"))
.WithMessage("{PropertyName} {PropertyValue} contains scheme"); .WithMessage("{PropertyName} {PropertyValue} contains scheme");
});
When(reRoute => reRoute.RateLimitOptions.EnableRateLimiting, () =>
{
RuleFor(reRoute => reRoute.RateLimitOptions.Period)
.NotEmpty()
.WithMessage("RateLimitOptions.Period is empty");
RuleFor(reRoute => reRoute.RateLimitOptions) RuleFor(reRoute => reRoute.RateLimitOptions)
.Must(IsValidPeriod) .Must(IsValidPeriod)
.WithMessage("RateLimitOptions.Period does not contains (s,m,h,d)"); .WithMessage("RateLimitOptions.Period does not contain integer then s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period");
});
RuleFor(reRoute => reRoute.AuthenticationOptions) RuleFor(reRoute => reRoute.AuthenticationOptions)
.MustAsync(IsSupportedAuthenticationProviders) .MustAsync(IsSupportedAuthenticationProviders)
.WithMessage("{PropertyValue} is unsupported authentication provider"); .WithMessage("{PropertyName} {PropertyValue} is unsupported authentication provider");
When(reRoute => string.IsNullOrEmpty(reRoute.ServiceName), () => { When(reRoute => string.IsNullOrEmpty(reRoute.ServiceName), () => {
RuleFor(r => r.DownstreamHostAndPorts).NotEmpty() RuleFor(r => r.DownstreamHostAndPorts).NotEmpty()
@ -62,7 +80,7 @@
When(reRoute => string.IsNullOrEmpty(reRoute.ServiceName), () => { When(reRoute => string.IsNullOrEmpty(reRoute.ServiceName), () => {
RuleFor(reRoute => reRoute.DownstreamHostAndPorts) RuleFor(reRoute => reRoute.DownstreamHostAndPorts)
.SetCollectionValidator(new HostAndPortValidator()); .SetCollectionValidator(hostAndPortValidator);
}); });
} }
@ -82,9 +100,22 @@
private static bool IsValidPeriod(FileRateLimitRule rateLimitOptions) private static bool IsValidPeriod(FileRateLimitRule rateLimitOptions)
{ {
string period = rateLimitOptions.Period; if (string.IsNullOrEmpty(rateLimitOptions.Period))
{
return false;
}
return !rateLimitOptions.EnableRateLimiting || period.Contains("s") || period.Contains("m") || period.Contains("h") || period.Contains("d"); var period = rateLimitOptions.Period;
var secondsRegEx = new Regex("^[0-9]+s");
var minutesRegEx = new Regex("^[0-9]+m");
var hoursRegEx = new Regex("^[0-9]+h");
var daysRegEx = new Regex("^[0-9]+d");
return secondsRegEx.Match(period).Success
|| minutesRegEx.Match(period).Success
|| hoursRegEx.Match(period).Success
|| daysRegEx.Match(period).Success;
} }
} }
} }

View File

@ -55,6 +55,10 @@ namespace Ocelot.DependencyInjection
Services.TryAddSingleton<IInternalConfigurationCreator, FileInternalConfigurationCreator>(); Services.TryAddSingleton<IInternalConfigurationCreator, FileInternalConfigurationCreator>();
Services.TryAddSingleton<IInternalConfigurationRepository, InMemoryInternalConfigurationRepository>(); Services.TryAddSingleton<IInternalConfigurationRepository, InMemoryInternalConfigurationRepository>();
Services.TryAddSingleton<IConfigurationValidator, FileConfigurationFluentValidator>(); Services.TryAddSingleton<IConfigurationValidator, FileConfigurationFluentValidator>();
Services.AddSingleton<HostAndPortValidator>();
Services.AddSingleton<ReRouteFluentValidator>();
Services.AddSingleton<FileGlobalConfigurationFluentValidator>();
Services.AddSingleton<FileQoSOptionsFluentValidator>();
Services.TryAddSingleton<IClaimsToThingCreator, ClaimsToThingCreator>(); Services.TryAddSingleton<IClaimsToThingCreator, ClaimsToThingCreator>();
Services.TryAddSingleton<IAuthenticationOptionsCreator, AuthenticationOptionsCreator>(); Services.TryAddSingleton<IAuthenticationOptionsCreator, AuthenticationOptionsCreator>();
Services.TryAddSingleton<IUpstreamTemplatePatternCreator, UpstreamTemplatePatternCreator>(); Services.TryAddSingleton<IUpstreamTemplatePatternCreator, UpstreamTemplatePatternCreator>();

View File

@ -1,4 +1,4 @@
namespace Ocelot.UnitTests.Configuration namespace Ocelot.UnitTests.Configuration.Validation
{ {
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Claims; using System.Security.Claims;
@ -33,7 +33,8 @@
_authProvider = new Mock<IAuthenticationSchemeProvider>(); _authProvider = new Mock<IAuthenticationSchemeProvider>();
var provider = new ServiceCollection() var provider = new ServiceCollection()
.BuildServiceProvider(); .BuildServiceProvider();
_configurationValidator = new FileConfigurationFluentValidator(_authProvider.Object, provider); // Todo - replace with mocks
_configurationValidator = new FileConfigurationFluentValidator(provider, new ReRouteFluentValidator(_authProvider.Object, new HostAndPortValidator(), new FileQoSOptionsFluentValidator(provider)), new FileGlobalConfigurationFluentValidator(new FileQoSOptionsFluentValidator(provider)));
} }
[Fact] [Fact]
@ -766,10 +767,11 @@
.Then(x => x.ThenTheResultIsNotValid()) .Then(x => x.ThenTheResultIsNotValid())
.Then(x => x.ThenTheErrorIs<FileValidationFailedError>()) .Then(x => x.ThenTheErrorIs<FileValidationFailedError>())
.And(x => x.ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} doesnt start with forward slash")) .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} doesnt start with forward slash"))
.And(x => x.ThenTheErrorMessageAtPositionIs(1, "Upstream Path Template http://asdf.com contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) .And(x => x.ThenTheErrorMessageAtPositionIs(1, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."))
.And(x => x.ThenTheErrorMessageAtPositionIs(2, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) .And(x => x.ThenTheErrorMessageAtPositionIs(2, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} contains scheme"))
.And(x => x.ThenTheErrorMessageAtPositionIs(3, "Upstream Path Template http://asdf.com doesnt start with forward slash"))
.And(x => x.ThenTheErrorMessageAtPositionIs(4, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} contains scheme")) .And(x => x.ThenTheErrorMessageAtPositionIs(3, "Upstream Path Template http://asdf.com contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."))
.And(x => x.ThenTheErrorMessageAtPositionIs(4, "Upstream Path Template http://asdf.com doesnt start with forward slash"))
.And(x => x.ThenTheErrorMessageAtPositionIs(5, "Upstream Path Template http://asdf.com contains scheme")) .And(x => x.ThenTheErrorMessageAtPositionIs(5, "Upstream Path Template http://asdf.com contains scheme"))
.BDDfy(); .BDDfy();
} }
@ -947,7 +949,7 @@
})) }))
.When(x => x.WhenIValidateTheConfiguration()) .When(x => x.WhenIValidateTheConfiguration())
.Then(x => x.ThenTheResultIsNotValid()) .Then(x => x.ThenTheResultIsNotValid())
.And(x => x.ThenTheErrorMessageAtPositionIs(0, "AuthenticationProviderKey:Test,AllowedScopes:[] is unsupported authentication provider")) .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Authentication Options AuthenticationProviderKey:Test,AllowedScopes:[] is unsupported authentication provider"))
.BDDfy(); .BDDfy();
} }
@ -1140,7 +1142,7 @@
})) }))
.When(x => x.WhenIValidateTheConfiguration()) .When(x => x.WhenIValidateTheConfiguration())
.Then(x => x.ThenTheResultIsNotValid()) .Then(x => x.ThenTheResultIsNotValid())
.And(x => x.ThenTheErrorMessageAtPositionIs(0, "RateLimitOptions.Period does not contains (s,m,h,d)")) .And(x => x.ThenTheErrorMessageAtPositionIs(0, "RateLimitOptions.Period does not contain integer then s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period"))
.BDDfy(); .BDDfy();
} }
@ -1383,7 +1385,7 @@
QosDelegatingHandlerDelegate del = (a,b) => new FakeDelegatingHandler(); QosDelegatingHandlerDelegate del = (a,b) => new FakeDelegatingHandler();
collection.AddSingleton<QosDelegatingHandlerDelegate>(del); collection.AddSingleton<QosDelegatingHandlerDelegate>(del);
var provider = collection.BuildServiceProvider(); var provider = collection.BuildServiceProvider();
_configurationValidator = new FileConfigurationFluentValidator(_authProvider.Object, provider); _configurationValidator = new FileConfigurationFluentValidator(provider, new ReRouteFluentValidator(_authProvider.Object, new HostAndPortValidator(), new FileQoSOptionsFluentValidator(provider)), new FileGlobalConfigurationFluentValidator(new FileQoSOptionsFluentValidator(provider)));
} }
private void GivenAServiceDiscoveryHandler() private void GivenAServiceDiscoveryHandler()
@ -1392,7 +1394,7 @@
ServiceDiscoveryFinderDelegate del = (a,b,c) => new FakeServiceDiscoveryProvider(); ServiceDiscoveryFinderDelegate del = (a,b,c) => new FakeServiceDiscoveryProvider();
collection.AddSingleton<ServiceDiscoveryFinderDelegate>(del); collection.AddSingleton<ServiceDiscoveryFinderDelegate>(del);
var provider = collection.BuildServiceProvider(); var provider = collection.BuildServiceProvider();
_configurationValidator = new FileConfigurationFluentValidator(_authProvider.Object, provider); _configurationValidator = new FileConfigurationFluentValidator(provider, new ReRouteFluentValidator(_authProvider.Object, new HostAndPortValidator(), new FileQoSOptionsFluentValidator(provider)), new FileGlobalConfigurationFluentValidator(new FileQoSOptionsFluentValidator(provider)));
} }
private class FakeServiceDiscoveryProvider : IServiceDiscoveryProvider private class FakeServiceDiscoveryProvider : IServiceDiscoveryProvider

View File

@ -0,0 +1,106 @@
using System;
using FluentValidation.Results;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Ocelot.Configuration.File;
using Ocelot.Configuration.Validator;
using Ocelot.Requester;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.Configuration.Validation
{
public class FileQoSOptionsFluentValidatorTests
{
private FileQoSOptionsFluentValidator _validator;
private ServiceCollection _services;
private ValidationResult _result;
private FileQoSOptions _qosOptions;
public FileQoSOptionsFluentValidatorTests()
{
_services = new ServiceCollection();
var provider = _services.BuildServiceProvider();
_validator = new FileQoSOptionsFluentValidator(provider);
}
[Fact]
public void should_be_valid_as_nothing_set()
{
this.Given(_ => GivenThe(new FileQoSOptions()))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsValid())
.BDDfy();
}
[Fact]
public void should_be_valid_as_qos_delegate_set()
{
var qosOptions = new FileQoSOptions
{
TimeoutValue = 1,
ExceptionsAllowedBeforeBreaking = 1
};
this.Given(_ => GivenThe(qosOptions))
.And(_ => GivenAQosDelegate())
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsValid())
.BDDfy();
}
[Fact]
public void should_be_invalid_as_no_qos_delegate()
{
var qosOptions = new FileQoSOptions
{
TimeoutValue = 1,
ExceptionsAllowedBeforeBreaking = 1
};
this.Given(_ => GivenThe(qosOptions))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsInValid())
.And(_ => ThenTheErrorIs())
.BDDfy();
}
private void ThenTheErrorIs()
{
_result.Errors[0].ErrorMessage.ShouldBe("Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?");
}
private void ThenTheResultIsInValid()
{
_result.IsValid.ShouldBeFalse();
}
private void GivenAQosDelegate()
{
QosDelegatingHandlerDelegate fake = (a, b) =>
{
return null;
};
_services.AddSingleton<QosDelegatingHandlerDelegate>(fake);
var provider = _services.BuildServiceProvider();
_validator = new FileQoSOptionsFluentValidator(provider);
}
private void GivenThe(FileQoSOptions qosOptions)
{
_qosOptions = qosOptions;
}
private void WhenIValidate()
{
_result = _validator.Validate(_qosOptions);
}
private void ThenTheResultIsValid()
{
_result.IsValid.ShouldBeTrue();
}
}
}

View File

@ -0,0 +1,78 @@
using System;
using FluentValidation.Results;
using Ocelot.Configuration.File;
using Ocelot.Configuration.Validator;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.Configuration.Validation
{
public class HostAndPortValidatorTests
{
private HostAndPortValidator _validator;
private ValidationResult _result;
private FileHostAndPort _hostAndPort;
public HostAndPortValidatorTests()
{
_validator = new HostAndPortValidator();
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void should_be_invalid_because_host_empty(string host)
{
var fileHostAndPort = new FileHostAndPort
{
Host = host
};
this.Given(_ => GivenThe(fileHostAndPort))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsInValid())
.And(_ => ThenTheErorrIs())
.BDDfy();
}
[Fact]
public void should_be_valid_because_host_set()
{
var fileHostAndPort = new FileHostAndPort
{
Host = "test"
};
this.Given(_ => GivenThe(fileHostAndPort))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsValid())
.BDDfy();
}
private void GivenThe(FileHostAndPort hostAndPort)
{
_hostAndPort = hostAndPort;
}
private void WhenIValidate()
{
_result = _validator.Validate(_hostAndPort);
}
private void ThenTheResultIsValid()
{
_result.IsValid.ShouldBeTrue();
}
private void ThenTheErorrIs()
{
_result.Errors[0].ErrorMessage.ShouldBe("When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using ReRoute.Host or Ocelot cannot find your service!");
}
private void ThenTheResultIsInValid()
{
_result.IsValid.ShouldBeFalse();
}
}
}

View File

@ -0,0 +1,368 @@
namespace Ocelot.UnitTests.Configuration.Validation
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentValidation.Results;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Moq;
using Ocelot.Configuration.File;
using Ocelot.Configuration.Validator;
using Ocelot.Requester;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
public class ReRouteFluentValidatorTests
{
private readonly ReRouteFluentValidator _validator;
private readonly Mock<IAuthenticationSchemeProvider> _authProvider;
private QosDelegatingHandlerDelegate _qosDelegatingHandler;
private Mock<IServiceProvider> _serviceProvider;
private FileReRoute _reRoute;
private ValidationResult _result;
public ReRouteFluentValidatorTests()
{
_authProvider = new Mock<IAuthenticationSchemeProvider>();
_serviceProvider = new Mock<IServiceProvider>();
// Todo - replace with mocks
_validator = new ReRouteFluentValidator(_authProvider.Object, new HostAndPortValidator(), new FileQoSOptionsFluentValidator(_serviceProvider.Object));
}
[Fact]
public void downstream_path_template_should_not_be_empty()
{
var fileReRoute = new FileReRoute();
this.Given(_ => GivenThe(fileReRoute))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsInvalid())
.And(_ => ThenTheErrorsContains("Downstream Path Template cannot be empty"))
.BDDfy();
}
[Fact]
public void upstream_path_template_should_not_be_empty()
{
var fileReRoute = new FileReRoute
{
DownstreamPathTemplate = "test"
};
this.Given(_ => GivenThe(fileReRoute))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsInvalid())
.And(_ => ThenTheErrorsContains("Upstream Path Template cannot be empty"))
.BDDfy();
}
[Fact]
public void downstream_path_template_should_start_with_forward_slash()
{
var fileReRoute = new FileReRoute
{
DownstreamPathTemplate = "test"
};
this.Given(_ => GivenThe(fileReRoute))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsInvalid())
.And(_ => ThenTheErrorsContains("Downstream Path Template test doesnt start with forward slash"))
.BDDfy();
}
[Fact]
public void downstream_path_template_should_not_contain_double_forward_slash()
{
var fileReRoute = new FileReRoute
{
DownstreamPathTemplate = "//test"
};
this.Given(_ => GivenThe(fileReRoute))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsInvalid())
.And(_ => ThenTheErrorsContains("Downstream Path Template //test contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."))
.BDDfy();
}
[Theory]
[InlineData("https://test")]
[InlineData("http://test")]
[InlineData("/test/http://")]
[InlineData("/test/https://")]
public void downstream_path_template_should_not_contain_scheme(string downstreamPathTemplate)
{
var fileReRoute = new FileReRoute
{
DownstreamPathTemplate = downstreamPathTemplate
};
this.Given(_ => GivenThe(fileReRoute))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsInvalid())
.And(_ => ThenTheErrorsContains($"Downstream Path Template {downstreamPathTemplate} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."))
.BDDfy();
}
[Fact]
public void upstream_path_template_should_start_with_forward_slash()
{
var fileReRoute = new FileReRoute
{
DownstreamPathTemplate = "/test",
UpstreamPathTemplate = "test"
};
this.Given(_ => GivenThe(fileReRoute))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsInvalid())
.And(_ => ThenTheErrorsContains("Upstream Path Template test doesnt start with forward slash"))
.BDDfy();
}
[Fact]
public void upstream_path_template_should_not_contain_double_forward_slash()
{
var fileReRoute = new FileReRoute
{
DownstreamPathTemplate = "/test",
UpstreamPathTemplate = "//test"
};
this.Given(_ => GivenThe(fileReRoute))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsInvalid())
.And(_ => ThenTheErrorsContains("Upstream Path Template //test contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."))
.BDDfy();
}
[Theory]
[InlineData("https://test")]
[InlineData("http://test")]
[InlineData("/test/http://")]
[InlineData("/test/https://")]
public void upstream_path_template_should_not_contain_scheme(string upstreamPathTemplate)
{
var fileReRoute = new FileReRoute
{
DownstreamPathTemplate = "/test",
UpstreamPathTemplate = upstreamPathTemplate
};
this.Given(_ => GivenThe(fileReRoute))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsInvalid())
.And(_ => ThenTheErrorsContains($"Upstream Path Template {upstreamPathTemplate} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."))
.BDDfy();
}
[Fact]
public void should_not_be_valid_if_enable_rate_limiting_true_and_period_is_empty()
{
var fileReRoute = new FileReRoute
{
DownstreamPathTemplate = "/test",
UpstreamPathTemplate = "/test",
RateLimitOptions = new FileRateLimitRule
{
EnableRateLimiting = true
}
};
this.Given(_ => GivenThe(fileReRoute))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsInvalid())
.And(_ => ThenTheErrorsContains("RateLimitOptions.Period is empty"))
.BDDfy();
}
[Fact]
public void should_not_be_valid_if_enable_rate_limiting_true_and_period_has_value()
{
var fileReRoute = new FileReRoute
{
DownstreamPathTemplate = "/test",
UpstreamPathTemplate = "/test",
RateLimitOptions = new FileRateLimitRule
{
EnableRateLimiting = true,
Period = "test"
}
};
this.Given(_ => GivenThe(fileReRoute))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsInvalid())
.And(_ => ThenTheErrorsContains("RateLimitOptions.Period does not contain integer then s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period"))
.BDDfy();
}
[Fact]
public void should_not_be_valid_if_specified_authentication_provider_isnt_registered()
{
var fileReRoute = new FileReRoute
{
DownstreamPathTemplate = "/test",
UpstreamPathTemplate = "/test",
AuthenticationOptions = new FileAuthenticationOptions
{
AuthenticationProviderKey = "JwtLads"
}
};
this.Given(_ => GivenThe(fileReRoute))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsInvalid())
.And(_ => ThenTheErrorsContains($"Authentication Options AuthenticationProviderKey:JwtLads,AllowedScopes:[] is unsupported authentication provider"))
.BDDfy();
}
[Fact]
public void should_not_be_valid_if_not_using_service_discovery_and_no_host_and_ports()
{
var fileReRoute = new FileReRoute
{
DownstreamPathTemplate = "/test",
UpstreamPathTemplate = "/test",
};
this.Given(_ => GivenThe(fileReRoute))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsInvalid())
.And(_ => ThenTheErrorsContains("When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!"))
.BDDfy();
}
[Fact]
public void should_be_valid_if_using_service_discovery_and_no_host_and_ports()
{
var fileReRoute = new FileReRoute
{
DownstreamPathTemplate = "/test",
UpstreamPathTemplate = "/test",
ServiceName = "Lads"
};
this.Given(_ => GivenThe(fileReRoute))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsValid())
.BDDfy();
}
[Fact]
public void should_be_valid_re_route_using_host_and_port_and_paths()
{
var fileReRoute = new FileReRoute
{
DownstreamPathTemplate = "/test",
UpstreamPathTemplate = "/test",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 5000
}
}
};
this.Given(_ => GivenThe(fileReRoute))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsValid())
.BDDfy();
}
[Fact]
public void should_be_valid_if_specified_authentication_provider_is_registered()
{
const string key = "JwtLads";
var fileReRoute = new FileReRoute
{
DownstreamPathTemplate = "/test",
UpstreamPathTemplate = "/test",
AuthenticationOptions = new FileAuthenticationOptions
{
AuthenticationProviderKey = key
},
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 5000
}
}
};
this.Given(_ => GivenThe(fileReRoute))
.And(_ => GivenAnAuthProvider(key))
.When(_ => WhenIValidate())
.Then(_ => ThenTheResultIsValid())
.BDDfy();
}
private void GivenAnAuthProvider(string key)
{
var schemes = new List<AuthenticationScheme>
{
new AuthenticationScheme(key, key, typeof(FakeAutheHandler))
};
_authProvider
.Setup(x => x.GetAllSchemesAsync())
.ReturnsAsync(schemes);
}
private void ThenTheResultIsValid()
{
_result.IsValid.ShouldBeTrue();
}
private void GivenThe(FileReRoute reRoute)
{
_reRoute = reRoute;
}
private void WhenIValidate()
{
_result = _validator.Validate(_reRoute);
}
private void ThenTheResultIsInvalid()
{
_result.IsValid.ShouldBeFalse();
}
private void ThenTheErrorsContains(string expected)
{
_result.Errors.ShouldContain(x => x.ErrorMessage == expected);
}
class FakeAutheHandler : IAuthenticationHandler
{
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
throw new System.NotImplementedException();
}
public Task<AuthenticateResult> AuthenticateAsync()
{
throw new System.NotImplementedException();
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
throw new System.NotImplementedException();
}
public Task ForbidAsync(AuthenticationProperties properties)
{
throw new System.NotImplementedException();
}
}
}
}