diff --git a/docs/features/administration.rst b/docs/features/administration.rst
index 29dc142b..9d907585 100644
--- a/docs/features/administration.rst
+++ b/docs/features/administration.rst
@@ -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
-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..
diff --git a/src/Ocelot/Configuration/File/FileRateLimitRule.cs b/src/Ocelot/Configuration/File/FileRateLimitRule.cs
index 6ebdb2bd..2714be4d 100644
--- a/src/Ocelot/Configuration/File/FileRateLimitRule.cs
+++ b/src/Ocelot/Configuration/File/FileRateLimitRule.cs
@@ -27,7 +27,7 @@ namespace Ocelot.Configuration.File
public string Period { get; set; }
public double PeriodTimespan { get; set; }
-
+
///
/// Maximum number of requests that a client can make in a defined period
///
diff --git a/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs b/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs
index aa21cd26..d8add08a 100644
--- a/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs
+++ b/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs
@@ -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();
}
- public ConfigurationValidationResult(bool isError, Error error)
- {
- IsError = isError;
- Errors = new List { error };
- }
-
public ConfigurationValidationResult(bool isError, List errors)
{
IsError = isError;
diff --git a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs
index ef380a21..3b1588d2 100644
--- a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs
+++ b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs
@@ -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, IConfigurationValidator
{
- private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate;
private readonly List _serviceDiscoveryFinderDelegates;
- public FileConfigurationFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, IServiceProvider provider)
+
+ public FileConfigurationFluentValidator(IServiceProvider provider, ReRouteFluentValidator reRouteFluentValidator, FileGlobalConfigurationFluentValidator fileGlobalConfigurationFluentValidator)
{
- _qosDelegatingHandlerDelegate = provider.GetService();
_serviceDiscoveryFinderDelegates = provider
.GetServices()
.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> IsValid(FileConfiguration configuration)
diff --git a/src/Ocelot/Configuration/Validator/FileGlobalConfigurationFluentValidator.cs b/src/Ocelot/Configuration/Validator/FileGlobalConfigurationFluentValidator.cs
index 7bac74f7..fbad7645 100644
--- a/src/Ocelot/Configuration/Validator/FileGlobalConfigurationFluentValidator.cs
+++ b/src/Ocelot/Configuration/Validator/FileGlobalConfigurationFluentValidator.cs
@@ -1,21 +1,14 @@
-using FluentValidation;
-using Ocelot.Configuration.File;
-
namespace Ocelot.Configuration.Validator
{
- using Requester;
+ using FluentValidation;
+ using File;
public class FileGlobalConfigurationFluentValidator : AbstractValidator
{
- 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);
}
-
}
}
diff --git a/src/Ocelot/Configuration/Validator/FileQoSOptionsFluentValidator.cs b/src/Ocelot/Configuration/Validator/FileQoSOptionsFluentValidator.cs
index 42fc9120..3c0d532b 100644
--- a/src/Ocelot/Configuration/Validator/FileQoSOptionsFluentValidator.cs
+++ b/src/Ocelot/Configuration/Validator/FileQoSOptionsFluentValidator.cs
@@ -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
{
private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate;
- public FileQoSOptionsFluentValidator(QosDelegatingHandlerDelegate qosDelegatingHandlerDelegate)
+ public FileQoSOptionsFluentValidator(IServiceProvider provider)
{
- _qosDelegatingHandlerDelegate = qosDelegatingHandlerDelegate;
-
+ _qosDelegatingHandlerDelegate = provider.GetService();
+
When(qosOptions => qosOptions.TimeoutValue > 0 && qosOptions.ExceptionsAllowedBeforeBreaking > 0, () => {
RuleFor(qosOptions => qosOptions)
.Must(HaveQosHandlerRegistered)
diff --git a/src/Ocelot/Configuration/Validator/FileValidationFailedError.cs b/src/Ocelot/Configuration/Validator/FileValidationFailedError.cs
index e90aa218..937b896b 100644
--- a/src/Ocelot/Configuration/Validator/FileValidationFailedError.cs
+++ b/src/Ocelot/Configuration/Validator/FileValidationFailedError.cs
@@ -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)
{
}
}
diff --git a/src/Ocelot/Configuration/Validator/HostAndPortValidator.cs b/src/Ocelot/Configuration/Validator/HostAndPortValidator.cs
index 5cc1744e..f93a2ccd 100644
--- a/src/Ocelot/Configuration/Validator/HostAndPortValidator.cs
+++ b/src/Ocelot/Configuration/Validator/HostAndPortValidator.cs
@@ -1,13 +1,15 @@
-using FluentValidation;
-using Ocelot.Configuration.File;
-
-namespace Ocelot.Configuration.Validator
-{
- public class HostAndPortValidator : AbstractValidator
- {
- 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
+ {
+ 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!");
+ }
+ }
+}
diff --git a/src/Ocelot/Configuration/Validator/IConfigurationValidator.cs b/src/Ocelot/Configuration/Validator/IConfigurationValidator.cs
index 6f886852..39f90c81 100644
--- a/src/Ocelot/Configuration/Validator/IConfigurationValidator.cs
+++ b/src/Ocelot/Configuration/Validator/IConfigurationValidator.cs
@@ -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> IsValid(FileConfiguration configuration);
diff --git a/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs b/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs
index a6ea82cb..e91c2f00 100644
--- a/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs
+++ b/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs
@@ -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
{
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;
}
}
}
diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs
index 90907465..d2f017c1 100644
--- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs
+++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs
@@ -55,6 +55,10 @@ namespace Ocelot.DependencyInjection
Services.TryAddSingleton();
Services.TryAddSingleton();
Services.TryAddSingleton();
+ Services.AddSingleton();
+ Services.AddSingleton();
+ Services.AddSingleton();
+ Services.AddSingleton();
Services.TryAddSingleton();
Services.TryAddSingleton();
Services.TryAddSingleton();
diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs
similarity index 98%
rename from test/Ocelot.UnitTests/Configuration/FileConfigurationFluentValidatorTests.cs
rename to test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs
index 9b473c6f..c7ffbb05 100644
--- a/test/Ocelot.UnitTests/Configuration/FileConfigurationFluentValidatorTests.cs
+++ b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs
@@ -1,4 +1,4 @@
-namespace Ocelot.UnitTests.Configuration
+namespace Ocelot.UnitTests.Configuration.Validation
{
using System.Collections.Generic;
using System.Security.Claims;
@@ -33,7 +33,8 @@
_authProvider = new Mock();
var provider = new ServiceCollection()
.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]
@@ -766,10 +767,11 @@
.Then(x => x.ThenTheResultIsNotValid())
.Then(x => x.ThenTheErrorIs())
.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(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(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(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 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"))
.BDDfy();
}
@@ -947,7 +949,7 @@
}))
.When(x => x.WhenIValidateTheConfiguration())
.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();
}
@@ -1140,7 +1142,7 @@
}))
.When(x => x.WhenIValidateTheConfiguration())
.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();
}
@@ -1383,7 +1385,7 @@
QosDelegatingHandlerDelegate del = (a,b) => new FakeDelegatingHandler();
collection.AddSingleton(del);
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()
@@ -1392,7 +1394,7 @@
ServiceDiscoveryFinderDelegate del = (a,b,c) => new FakeServiceDiscoveryProvider();
collection.AddSingleton(del);
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
diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs
new file mode 100644
index 00000000..234d37ba
--- /dev/null
+++ b/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs
@@ -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(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();
+ }
+ }
+}
diff --git a/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs
new file mode 100644
index 00000000..4d1c4731
--- /dev/null
+++ b/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs
@@ -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();
+ }
+ }
+}
diff --git a/test/Ocelot.UnitTests/Configuration/Validation/ReRouteFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/ReRouteFluentValidatorTests.cs
new file mode 100644
index 00000000..1b847d93
--- /dev/null
+++ b/test/Ocelot.UnitTests/Configuration/Validation/ReRouteFluentValidatorTests.cs
@@ -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 _authProvider;
+ private QosDelegatingHandlerDelegate _qosDelegatingHandler;
+ private Mock _serviceProvider;
+ private FileReRoute _reRoute;
+ private ValidationResult _result;
+
+ public ReRouteFluentValidatorTests()
+ {
+ _authProvider = new Mock();
+ _serviceProvider = new Mock();
+ // 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
+ {
+ 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
+ {
+ 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
+ {
+ 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 AuthenticateAsync()
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public Task ChallengeAsync(AuthenticationProperties properties)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public Task ForbidAsync(AuthenticationProperties properties)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+ }
+}