Feature/adding some re route specific validation tests (#590)

* started refactoring json config validation stack because ive made it crap

* inject validators from DI rather than instanciating, next step mock them

* added some unit tests for specific validators on the train yey
This commit is contained in:
Tom Pallister
2018-09-04 22:27:54 +01:00
committed by GitHub
parent ef6db657b7
commit a5c1839ed7
15 changed files with 693 additions and 124 deletions

View File

@ -27,7 +27,7 @@ namespace Ocelot.Configuration.File
public string Period { get; set; }
public double PeriodTimespan { get; set; }
/// <summary>
/// Maximum number of requests that a client can make in a defined period
/// </summary>

View File

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

View File

@ -1,46 +1,42 @@
using FluentValidation;
using Microsoft.AspNetCore.Authentication;
using Ocelot.Configuration.File;
using Ocelot.Errors;
using Ocelot.Responses;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Ocelot.Configuration.Validator
namespace Ocelot.Configuration.Validator
{
using FluentValidation;
using File;
using Errors;
using Responses;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.ServiceDiscovery;
using Requester;
using ServiceDiscovery;
public class FileConfigurationFluentValidator : AbstractValidator<FileConfiguration>, IConfigurationValidator
{
private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate;
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
.GetServices<ServiceDiscoveryFinderDelegate>()
.ToList();
RuleFor(configuration => configuration.ReRoutes)
.SetCollectionValidator(new ReRouteFluentValidator(authenticationSchemeProvider, _qosDelegatingHandlerDelegate));
.SetCollectionValidator(reRouteFluentValidator);
RuleFor(configuration => configuration.GlobalConfiguration)
.SetValidator(new FileGlobalConfigurationFluentValidator(_qosDelegatingHandlerDelegate));
.SetValidator(fileGlobalConfigurationFluentValidator);
RuleForEach(configuration => configuration.ReRoutes)
.Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.ReRoutes))
.WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicate");
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()?");
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()?");
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");
}
private bool HaveServiceDiscoveryProviderRegitered(FileReRoute reRoute, FileServiceDiscoveryProvider serviceDiscoveryProvider)
private bool HaveServiceDiscoveryProviderRegistered(FileReRoute reRoute, FileServiceDiscoveryProvider serviceDiscoveryProvider)
{
if (string.IsNullOrEmpty(reRoute.ServiceName))
{
@ -75,7 +71,7 @@ namespace Ocelot.Configuration.Validator
return _serviceDiscoveryFinderDelegates.Any();
}
private bool HaveServiceDiscoveryProviderRegitered(FileServiceDiscoveryProvider serviceDiscoveryProvider)
private bool HaveServiceDiscoveryProviderRegistered(FileServiceDiscoveryProvider serviceDiscoveryProvider)
{
if(serviceDiscoveryProvider == null)
{
@ -87,12 +83,7 @@ namespace Ocelot.Configuration.Validator
return true;
}
if(string.IsNullOrEmpty(serviceDiscoveryProvider.Type))
{
return true;
}
return _serviceDiscoveryFinderDelegates.Any();
return string.IsNullOrEmpty(serviceDiscoveryProvider.Type) || _serviceDiscoveryFinderDelegates.Any();
}
public async Task<Response<ConfigurationValidationResult>> IsValid(FileConfiguration configuration)

View File

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

View File

@ -1,18 +1,19 @@
using FluentValidation;
using Ocelot.Configuration.File;
namespace Ocelot.Configuration.Validator
{
using System;
using FluentValidation;
using File;
using Microsoft.Extensions.DependencyInjection;
using Requester;
public class FileQoSOptionsFluentValidator : AbstractValidator<FileQoSOptions>
{
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, () => {
RuleFor(qosOptions => qosOptions)
.Must(HaveQosHandlerRegistered)

View File

@ -1,13 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
namespace Ocelot.Configuration.Validator
{
using Errors;
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 @@
using FluentValidation;
using Ocelot.Configuration.File;
namespace Ocelot.Configuration.Validator
{
public class HostAndPortValidator : AbstractValidator<FileHostAndPort>
{
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!");
}
}
}
namespace Ocelot.Configuration.Validator
{
using FluentValidation;
using Ocelot.Configuration.File;
public class HostAndPortValidator : AbstractValidator<FileHostAndPort>
{
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!");
}
}
}

View File

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

View File

@ -1,59 +1,77 @@
namespace Ocelot.Configuration.Validator
{
using System;
using FluentValidation;
using Microsoft.AspNetCore.Authentication;
using Ocelot.Configuration.File;
using File;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System;
using Microsoft.Extensions.DependencyInjection;
using Requester;
public class ReRouteFluentValidator : AbstractValidator<FileReRoute>
{
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate;
public ReRouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, QosDelegatingHandlerDelegate qosDelegatingHandlerDelegate)
public ReRouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, HostAndPortValidator hostAndPortValidator, FileQoSOptionsFluentValidator fileQoSOptionsFluentValidator)
{
_authenticationSchemeProvider = authenticationSchemeProvider;
_qosDelegatingHandlerDelegate = qosDelegatingHandlerDelegate;
RuleFor(reRoute => reRoute.QoSOptions)
.SetValidator(new FileQoSOptionsFluentValidator(_qosDelegatingHandlerDelegate));
.SetValidator(fileQoSOptionsFluentValidator);
RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => path.StartsWith("/"))
.WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash");
.NotEmpty()
.WithMessage("{PropertyName} cannot be empty");
RuleFor(reRoute => reRoute.UpstreamPathTemplate)
.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.");
.NotEmpty()
.WithMessage("{PropertyName} cannot be empty");
RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.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.");
When(reRoute => !string.IsNullOrEmpty(reRoute.DownstreamPathTemplate), () =>
{
RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => path.StartsWith("/"))
.WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash");
RuleFor(reRoute => reRoute.UpstreamPathTemplate)
.Must(path => path.StartsWith("/"))
.WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash");
RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.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.");
RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => !path.Contains("https://") && !path.Contains("http://"))
.WithMessage("{PropertyName} {PropertyValue} contains scheme");
RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => !path.Contains("https://") && !path.Contains("http://"))
.WithMessage("{PropertyName} {PropertyValue} contains scheme");
});
RuleFor(reRoute => reRoute.UpstreamPathTemplate)
.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("//"))
.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.UpstreamPathTemplate)
.Must(path => path.StartsWith("/"))
.WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash");
RuleFor(reRoute => reRoute.UpstreamPathTemplate)
.Must(path => !path.Contains("https://") && !path.Contains("http://"))
.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)
.Must(IsValidPeriod)
.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.RateLimitOptions)
.Must(IsValidPeriod)
.WithMessage("RateLimitOptions.Period does not contains (s,m,h,d)");
RuleFor(reRoute => reRoute.AuthenticationOptions)
.MustAsync(IsSupportedAuthenticationProviders)
.WithMessage("{PropertyValue} is unsupported authentication provider");
.WithMessage("{PropertyName} {PropertyValue} is unsupported authentication provider");
When(reRoute => string.IsNullOrEmpty(reRoute.ServiceName), () => {
RuleFor(r => r.DownstreamHostAndPorts).NotEmpty()
@ -62,7 +80,7 @@
When(reRoute => string.IsNullOrEmpty(reRoute.ServiceName), () => {
RuleFor(reRoute => reRoute.DownstreamHostAndPorts)
.SetCollectionValidator(new HostAndPortValidator());
.SetCollectionValidator(hostAndPortValidator);
});
}
@ -82,9 +100,22 @@
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<IInternalConfigurationRepository, InMemoryInternalConfigurationRepository>();
Services.TryAddSingleton<IConfigurationValidator, FileConfigurationFluentValidator>();
Services.AddSingleton<HostAndPortValidator>();
Services.AddSingleton<ReRouteFluentValidator>();
Services.AddSingleton<FileGlobalConfigurationFluentValidator>();
Services.AddSingleton<FileQoSOptionsFluentValidator>();
Services.TryAddSingleton<IClaimsToThingCreator, ClaimsToThingCreator>();
Services.TryAddSingleton<IAuthenticationOptionsCreator, AuthenticationOptionsCreator>();
Services.TryAddSingleton<IUpstreamTemplatePatternCreator, UpstreamTemplatePatternCreator>();