#568 worked out how to check if qos handler present and kill Ocelot i… (#578)

* #568 worked out how to check if qos handler present and kill Ocelot if options specified but no handler, need to refactor this into fluent validation style

* #568 acceptance tests to make sure Ocelot won't start if the user specifies QoSOptions but doesnt have a QoSHandler registered
This commit is contained in:
Tom Pallister 2018-08-28 18:57:21 +01:00 committed by GitHub
parent 8db5570840
commit 29a7af9486
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1694 additions and 1377 deletions

View File

@ -11,6 +11,12 @@ 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

@ -15,13 +15,17 @@ namespace Ocelot.Configuration.Validator
public class FileConfigurationFluentValidator : AbstractValidator<FileConfiguration>, IConfigurationValidator public class FileConfigurationFluentValidator : AbstractValidator<FileConfiguration>, IConfigurationValidator
{ {
private readonly IServiceProvider _provider; private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate;
public FileConfigurationFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, IServiceProvider provider) public FileConfigurationFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, IServiceProvider provider)
{ {
_provider = provider; _qosDelegatingHandlerDelegate = provider.GetService<QosDelegatingHandlerDelegate>();
RuleFor(configuration => configuration.ReRoutes) RuleFor(configuration => configuration.ReRoutes)
.SetCollectionValidator(new ReRouteFluentValidator(authenticationSchemeProvider, provider)); .SetCollectionValidator(new ReRouteFluentValidator(authenticationSchemeProvider, _qosDelegatingHandlerDelegate));
RuleFor(configuration => configuration.GlobalConfiguration)
.SetValidator(new FileGlobalConfigurationFluentValidator(_qosDelegatingHandlerDelegate));
RuleForEach(configuration => configuration.ReRoutes) RuleForEach(configuration => configuration.ReRoutes)
.Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.ReRoutes)) .Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.ReRoutes))
@ -44,13 +48,6 @@ 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 AllReRoutesForAggregateExist(FileAggregateReRoute fileAggregateReRoute, List<FileReRoute> reRoutes)
{
var reRoutesForAggregate = reRoutes.Where(r => fileAggregateReRoute.ReRouteKeys.Contains(r.Key));
return reRoutesForAggregate.Count() == fileAggregateReRoute.ReRouteKeys.Count;
}
public async Task<Response<ConfigurationValidationResult>> IsValid(FileConfiguration configuration) public async Task<Response<ConfigurationValidationResult>> IsValid(FileConfiguration configuration)
{ {
var validateResult = await ValidateAsync(configuration); var validateResult = await ValidateAsync(configuration);
@ -67,6 +64,13 @@ namespace Ocelot.Configuration.Validator
return new OkResponse<ConfigurationValidationResult>(result); return new OkResponse<ConfigurationValidationResult>(result);
} }
private bool AllReRoutesForAggregateExist(FileAggregateReRoute fileAggregateReRoute, List<FileReRoute> reRoutes)
{
var reRoutesForAggregate = reRoutes.Where(r => fileAggregateReRoute.ReRouteKeys.Contains(r.Key));
return reRoutesForAggregate.Count() == fileAggregateReRoute.ReRouteKeys.Count;
}
private static bool DoesNotContainReRoutesWithSpecificRequestIdKeys(FileAggregateReRoute fileAggregateReRoute, private static bool DoesNotContainReRoutesWithSpecificRequestIdKeys(FileAggregateReRoute fileAggregateReRoute,
List<FileReRoute> reRoutes) List<FileReRoute> reRoutes)
{ {
@ -83,7 +87,7 @@ namespace Ocelot.Configuration.Validator
&& (r.UpstreamHost != reRoute.UpstreamHost || reRoute.UpstreamHost == null)) && (r.UpstreamHost != reRoute.UpstreamHost || reRoute.UpstreamHost == null))
.ToList(); .ToList();
if(matchingReRoutes.Count == 1) if (matchingReRoutes.Count == 1)
{ {
return true; return true;
} }

View File

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

View File

@ -0,0 +1,28 @@
using FluentValidation;
using Ocelot.Configuration.File;
namespace Ocelot.Configuration.Validator
{
using Requester;
public class FileQoSOptionsFluentValidator : AbstractValidator<FileQoSOptions>
{
private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate;
public FileQoSOptionsFluentValidator(QosDelegatingHandlerDelegate qosDelegatingHandlerDelegate)
{
_qosDelegatingHandlerDelegate = qosDelegatingHandlerDelegate;
When(qosOptions => qosOptions.TimeoutValue > 0 && qosOptions.ExceptionsAllowedBeforeBreaking > 0, () => {
RuleFor(qosOptions => qosOptions)
.Must(HaveQosHandlerRegistered)
.WithMessage("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 bool HaveQosHandlerRegistered(FileQoSOptions arg)
{
return _qosDelegatingHandlerDelegate != null;
}
}
}

View File

@ -13,12 +13,15 @@
public class ReRouteFluentValidator : AbstractValidator<FileReRoute> public class ReRouteFluentValidator : AbstractValidator<FileReRoute>
{ {
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
private readonly IServiceProvider _serviceProvider; private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate;
public ReRouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, IServiceProvider serviceProvider) public ReRouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, QosDelegatingHandlerDelegate qosDelegatingHandlerDelegate)
{ {
_authenticationSchemeProvider = authenticationSchemeProvider; _authenticationSchemeProvider = authenticationSchemeProvider;
_serviceProvider = serviceProvider; _qosDelegatingHandlerDelegate = qosDelegatingHandlerDelegate;
RuleFor(reRoute => reRoute.QoSOptions)
.SetValidator(new FileQoSOptionsFluentValidator(_qosDelegatingHandlerDelegate));
RuleFor(reRoute => reRoute.DownstreamPathTemplate) RuleFor(reRoute => reRoute.DownstreamPathTemplate)
.Must(path => path.StartsWith("/")) .Must(path => path.StartsWith("/"))

View File

@ -15,6 +15,101 @@ namespace Ocelot.AcceptanceTests
_steps = new Steps(); _steps = new Steps();
} }
[Fact]
public void should_throw_exception_if_cannot_start_because_no_qos_delegate_registered_globally()
{
var invalidConfig = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 51878,
}
},
UpstreamPathTemplate = "/laura",
UpstreamHttpMethod = new List<string> { "Get" },
Key = "Laura",
}
},
GlobalConfiguration = new FileGlobalConfiguration
{
QoSOptions = new FileQoSOptions
{
TimeoutValue = 1,
ExceptionsAllowedBeforeBreaking = 1
}
}
};
Exception exception = null;
_steps.GivenThereIsAConfiguration(invalidConfig);
try
{
_steps.GivenOcelotIsRunning();
}
catch (Exception ex)
{
exception = ex;
}
exception.ShouldNotBeNull();
exception.Message.ShouldBe("One or more errors occurred. (Unable to start Ocelot, errors are: 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()?)");
}
[Fact]
public void should_throw_exception_if_cannot_start_because_no_qos_delegate_registered_for_re_route()
{
var invalidConfig = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 51878,
}
},
UpstreamPathTemplate = "/laura",
UpstreamHttpMethod = new List<string> { "Get" },
Key = "Laura",
QoSOptions = new FileQoSOptions
{
TimeoutValue = 1,
ExceptionsAllowedBeforeBreaking = 1
}
}
}
};
Exception exception = null;
_steps.GivenThereIsAConfiguration(invalidConfig);
try
{
_steps.GivenOcelotIsRunning();
}
catch (Exception ex)
{
exception = ex;
}
exception.ShouldNotBeNull();
exception.Message.ShouldBe("One or more errors occurred. (Unable to start Ocelot, errors are: 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()?)");
}
[Fact] [Fact]
public void should_throw_exception_if_cannot_start() public void should_throw_exception_if_cannot_start()
{ {
@ -36,7 +131,7 @@ namespace Ocelot.AcceptanceTests
{ {
_steps.GivenOcelotIsRunning(); _steps.GivenOcelotIsRunning();
} }
catch(Exception ex) catch (Exception ex)
{ {
exception = ex; exception = ex;
} }

View File

@ -18,19 +18,179 @@
using Ocelot.Requester; using Ocelot.Requester;
using Requester; using Requester;
public class ConfigurationFluentValidationTests public class FileConfigurationFluentValidatorTests
{ {
private IConfigurationValidator _configurationValidator; private IConfigurationValidator _configurationValidator;
private FileConfiguration _fileConfiguration; private FileConfiguration _fileConfiguration;
private Response<ConfigurationValidationResult> _result; private Response<ConfigurationValidationResult> _result;
private readonly Mock<IAuthenticationSchemeProvider> _provider; private readonly Mock<IAuthenticationSchemeProvider> _authProvider;
public ConfigurationFluentValidationTests() public FileConfigurationFluentValidatorTests()
{ {
_provider = new Mock<IAuthenticationSchemeProvider>(); _authProvider = new Mock<IAuthenticationSchemeProvider>();
var provider = new ServiceCollection() var provider = new ServiceCollection()
.BuildServiceProvider(); .BuildServiceProvider();
_configurationValidator = new FileConfigurationFluentValidator(_provider.Object, provider); _configurationValidator = new FileConfigurationFluentValidator(_authProvider.Object, provider);
}
[Fact]
public void configuration_is_valid_if_qos_options_specified_and_has_qos_handler()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 51878,
}
},
UpstreamPathTemplate = "/laura",
UpstreamHttpMethod = new List<string> { "Get" },
Key = "Laura",
QoSOptions = new FileQoSOptions
{
TimeoutValue = 1,
ExceptionsAllowedBeforeBreaking = 1
}
}
}
};
this.Given(x => x.GivenAConfiguration(configuration))
.And(x => x.GivenAQoSHandler())
.When(x => x.WhenIValidateTheConfiguration())
.Then(x => x.ThenTheResultIsValid())
.BDDfy();
}
[Fact]
public void configuration_is_valid_if_qos_options_specified_globally_and_has_qos_handler()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 51878,
}
},
UpstreamPathTemplate = "/laura",
UpstreamHttpMethod = new List<string> { "Get" },
Key = "Laura",
}
},
GlobalConfiguration = new FileGlobalConfiguration
{
QoSOptions = new FileQoSOptions
{
TimeoutValue = 1,
ExceptionsAllowedBeforeBreaking = 1
}
}
};
this.Given(x => x.GivenAConfiguration(configuration))
.And(x => x.GivenAQoSHandler())
.When(x => x.WhenIValidateTheConfiguration())
.Then(x => x.ThenTheResultIsValid())
.BDDfy();
}
[Fact]
public void configuration_is_invalid_if_qos_options_specified_but_no_qos_handler()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 51878,
}
},
UpstreamPathTemplate = "/laura",
UpstreamHttpMethod = new List<string> { "Get" },
Key = "Laura",
QoSOptions = new FileQoSOptions
{
TimeoutValue = 1,
ExceptionsAllowedBeforeBreaking = 1
}
}
}
};
this.Given(x => x.GivenAConfiguration(configuration))
.When(x => x.WhenIValidateTheConfiguration())
.Then(x => x.ThenTheResultIsNotValid())
.And(x => x.ThenTheErrorIs<FileValidationFailedError>())
.And(x => x.ThenTheErrorMessageAtPositionIs(0, "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()?"))
.BDDfy();
}
[Fact]
public void configuration_is_invalid_if_qos_options_specified_globally_but_no_qos_handler()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 51878,
}
},
UpstreamPathTemplate = "/laura",
UpstreamHttpMethod = new List<string> { "Get" },
Key = "Laura",
}
},
GlobalConfiguration = new FileGlobalConfiguration
{
QoSOptions = new FileQoSOptions
{
TimeoutValue = 1,
ExceptionsAllowedBeforeBreaking = 1
}
}
};
this.Given(x => x.GivenAConfiguration(configuration))
.When(x => x.WhenIValidateTheConfiguration())
.Then(x => x.ThenTheResultIsNotValid())
.And(x => x.ThenTheErrorIs<FileValidationFailedError>())
.And(x => x.ThenTheErrorMessageAtPositionIs(0, "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()?"))
.BDDfy();
} }
[Fact] [Fact]
@ -646,8 +806,8 @@
public void configuration_is_valid_with_duplicate_reroutes_all_verbs_but_different_hosts() public void configuration_is_valid_with_duplicate_reroutes_all_verbs_but_different_hosts()
{ {
this.Given(x => x.GivenAConfiguration(new FileConfiguration this.Given(x => x.GivenAConfiguration(new FileConfiguration
{ {
ReRoutes = new List<FileReRoute> ReRoutes = new List<FileReRoute>
{ {
new FileReRoute new FileReRoute
{ {
@ -676,7 +836,7 @@
UpstreamHost = "host1" UpstreamHost = "host1"
} }
} }
})) }))
.When(x => x.WhenIValidateTheConfiguration()) .When(x => x.WhenIValidateTheConfiguration())
.Then(x => x.ThenTheResultIsValid()) .Then(x => x.ThenTheResultIsValid())
.BDDfy(); .BDDfy();
@ -1045,19 +1205,19 @@
private void GivenTheAuthSchemeExists(string name) private void GivenTheAuthSchemeExists(string name)
{ {
_provider.Setup(x => x.GetAllSchemesAsync()).ReturnsAsync(new List<AuthenticationScheme> _authProvider.Setup(x => x.GetAllSchemesAsync()).ReturnsAsync(new List<AuthenticationScheme>
{ {
new AuthenticationScheme(name, name, typeof(TestHandler)) new AuthenticationScheme(name, name, typeof(TestHandler))
}); });
} }
private void GivenAQosDelegate() private void GivenAQoSHandler()
{ {
var services = new ServiceCollection(); var collection = new ServiceCollection();
QosDelegatingHandlerDelegate del = (a, b) => new FakeDelegatingHandler(); QosDelegatingHandlerDelegate del = (a,b) => new FakeDelegatingHandler();
services.AddSingleton<QosDelegatingHandlerDelegate>(del); collection.AddSingleton<QosDelegatingHandlerDelegate>(del);
var provider = services.BuildServiceProvider(); var provider = collection.BuildServiceProvider();
_configurationValidator = new FileConfigurationFluentValidator(_provider.Object, provider); _configurationValidator = new FileConfigurationFluentValidator(_authProvider.Object, provider);
} }
private class TestOptions : AuthenticationSchemeOptions private class TestOptions : AuthenticationSchemeOptions