diff --git a/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs b/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs index 7e06cb6b..aa21cd26 100644 --- a/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs +++ b/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs @@ -1,24 +1,30 @@ -using System.Collections.Generic; -using Ocelot.Errors; - -namespace Ocelot.Configuration.Validator -{ - public class ConfigurationValidationResult - { - public ConfigurationValidationResult(bool isError) - { - IsError = isError; - Errors = new List(); - } - - public ConfigurationValidationResult(bool isError, List errors) - { - IsError = isError; - Errors = errors; - } - - public bool IsError { get; } - - public List Errors { get; } - } -} +using System.Collections.Generic; +using Ocelot.Errors; + +namespace Ocelot.Configuration.Validator +{ + public class ConfigurationValidationResult + { + public ConfigurationValidationResult(bool isError) + { + IsError = isError; + Errors = new List(); + } + + public ConfigurationValidationResult(bool isError, Error error) + { + IsError = isError; + Errors = new List { error }; + } + + public ConfigurationValidationResult(bool isError, List errors) + { + IsError = isError; + Errors = errors; + } + + public bool IsError { get; } + + public List Errors { get; } + } +} diff --git a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs index ebac0a30..5e6ac61b 100644 --- a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs +++ b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs @@ -1,129 +1,133 @@ -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 -{ - using System; - using Microsoft.Extensions.DependencyInjection; - using Requester; - - public class FileConfigurationFluentValidator : AbstractValidator, IConfigurationValidator - { - private readonly IServiceProvider _provider; - - public FileConfigurationFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, IServiceProvider provider) - { - _provider = provider; - RuleFor(configuration => configuration.ReRoutes) - .SetCollectionValidator(new ReRouteFluentValidator(authenticationSchemeProvider, provider)); - - 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) => IsNotDuplicateIn(reRoute, config.Aggregates)) - .WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicate aggregate"); - - RuleForEach(configuration => configuration.Aggregates) - .Must((config, aggregateReRoute) => IsNotDuplicateIn(aggregateReRoute, config.Aggregates)) - .WithMessage((config, aggregate) => $"{nameof(aggregate)} {aggregate.UpstreamPathTemplate} has duplicate aggregate"); - - RuleForEach(configuration => configuration.Aggregates) - .Must((config, aggregateReRoute) => AllReRoutesForAggregateExist(aggregateReRoute, config.ReRoutes)) - .WithMessage((config, aggregateReRoute) => $"ReRoutes for {nameof(aggregateReRoute)} {aggregateReRoute.UpstreamPathTemplate} either do not exist or do not have correct ServiceName property"); - - RuleForEach(configuration => configuration.Aggregates) - .Must((config, aggregateReRoute) => DoesNotContainReRoutesWithSpecificRequestIdKeys(aggregateReRoute, config.ReRoutes)) - .WithMessage((config, aggregateReRoute) => $"{nameof(aggregateReRoute)} {aggregateReRoute.UpstreamPathTemplate} contains ReRoute with specific RequestIdKey, this is not possible with Aggregates"); - } - - private bool AllReRoutesForAggregateExist(FileAggregateReRoute fileAggregateReRoute, List reRoutes) - { - var reRoutesForAggregate = reRoutes.Where(r => fileAggregateReRoute.ReRouteKeys.Contains(r.Key)); - - return reRoutesForAggregate.Count() == fileAggregateReRoute.ReRouteKeys.Count; - } - - public async Task> IsValid(FileConfiguration configuration) - { - var validateResult = await ValidateAsync(configuration); - - if (validateResult.IsValid) - { - return new OkResponse(new ConfigurationValidationResult(false)); - } - - var errors = validateResult.Errors.Select(failure => new FileValidationFailedError(failure.ErrorMessage)); - - var result = new ConfigurationValidationResult(true, errors.Cast().ToList()); - - return new OkResponse(result); - } - - private static bool DoesNotContainReRoutesWithSpecificRequestIdKeys(FileAggregateReRoute fileAggregateReRoute, - List reRoutes) - { - var reRoutesForAggregate = reRoutes.Where(r => fileAggregateReRoute.ReRouteKeys.Contains(r.Key)); - - return reRoutesForAggregate.All(r => string.IsNullOrEmpty(r.RequestIdKey)); - } - - private static bool IsNotDuplicateIn(FileReRoute reRoute, - List reRoutes) - { - var matchingReRoutes = reRoutes - .Where(r => r.UpstreamPathTemplate == reRoute.UpstreamPathTemplate - && (r.UpstreamHost != reRoute.UpstreamHost || reRoute.UpstreamHost == null)) - .ToList(); - - if(matchingReRoutes.Count == 1) - { - return true; - } - - var allowAllVerbs = matchingReRoutes.Any(x => x.UpstreamHttpMethod.Count == 0); - - var duplicateAllowAllVerbs = matchingReRoutes.Count(x => x.UpstreamHttpMethod.Count == 0) > 1; - - var specificVerbs = matchingReRoutes.Any(x => x.UpstreamHttpMethod.Count != 0); - - var duplicateSpecificVerbs = matchingReRoutes.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any(); - - if (duplicateAllowAllVerbs || duplicateSpecificVerbs || (allowAllVerbs && specificVerbs)) - { - return false; - } - - return true; - } - - private static bool IsNotDuplicateIn(FileReRoute reRoute, - List aggregateReRoutes) - { - var duplicate = aggregateReRoutes - .Any(a => a.UpstreamPathTemplate == reRoute.UpstreamPathTemplate - && a.UpstreamHost == reRoute.UpstreamHost - && reRoute.UpstreamHttpMethod.Select(x => x.ToLower()).Contains("get")); - - return !duplicate; - } - - private static bool IsNotDuplicateIn(FileAggregateReRoute reRoute, - List aggregateReRoutes) - { - var matchingReRoutes = aggregateReRoutes - .Where(r => r.UpstreamPathTemplate == reRoute.UpstreamPathTemplate - && r.UpstreamHost == reRoute.UpstreamHost) - .ToList(); - - return matchingReRoutes.Count <= 1; - } - } -} +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 +{ + using System; + using Microsoft.Extensions.DependencyInjection; + using Requester; + + public class FileConfigurationFluentValidator : AbstractValidator, IConfigurationValidator + { + private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate; + + public FileConfigurationFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, IServiceProvider provider) + { + _qosDelegatingHandlerDelegate = provider.GetService(); + + RuleFor(configuration => configuration.ReRoutes) + .SetCollectionValidator(new ReRouteFluentValidator(authenticationSchemeProvider, _qosDelegatingHandlerDelegate)); + + RuleFor(configuration => configuration.GlobalConfiguration) + .SetValidator(new FileGlobalConfigurationFluentValidator(_qosDelegatingHandlerDelegate)); + + 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) => IsNotDuplicateIn(reRoute, config.Aggregates)) + .WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicate aggregate"); + + RuleForEach(configuration => configuration.Aggregates) + .Must((config, aggregateReRoute) => IsNotDuplicateIn(aggregateReRoute, config.Aggregates)) + .WithMessage((config, aggregate) => $"{nameof(aggregate)} {aggregate.UpstreamPathTemplate} has duplicate aggregate"); + + RuleForEach(configuration => configuration.Aggregates) + .Must((config, aggregateReRoute) => AllReRoutesForAggregateExist(aggregateReRoute, config.ReRoutes)) + .WithMessage((config, aggregateReRoute) => $"ReRoutes for {nameof(aggregateReRoute)} {aggregateReRoute.UpstreamPathTemplate} either do not exist or do not have correct ServiceName property"); + + RuleForEach(configuration => configuration.Aggregates) + .Must((config, aggregateReRoute) => DoesNotContainReRoutesWithSpecificRequestIdKeys(aggregateReRoute, config.ReRoutes)) + .WithMessage((config, aggregateReRoute) => $"{nameof(aggregateReRoute)} {aggregateReRoute.UpstreamPathTemplate} contains ReRoute with specific RequestIdKey, this is not possible with Aggregates"); + } + + public async Task> IsValid(FileConfiguration configuration) + { + var validateResult = await ValidateAsync(configuration); + + if (validateResult.IsValid) + { + return new OkResponse(new ConfigurationValidationResult(false)); + } + + var errors = validateResult.Errors.Select(failure => new FileValidationFailedError(failure.ErrorMessage)); + + var result = new ConfigurationValidationResult(true, errors.Cast().ToList()); + + return new OkResponse(result); + } + + private bool AllReRoutesForAggregateExist(FileAggregateReRoute fileAggregateReRoute, List reRoutes) + { + var reRoutesForAggregate = reRoutes.Where(r => fileAggregateReRoute.ReRouteKeys.Contains(r.Key)); + + return reRoutesForAggregate.Count() == fileAggregateReRoute.ReRouteKeys.Count; + } + + private static bool DoesNotContainReRoutesWithSpecificRequestIdKeys(FileAggregateReRoute fileAggregateReRoute, + List reRoutes) + { + var reRoutesForAggregate = reRoutes.Where(r => fileAggregateReRoute.ReRouteKeys.Contains(r.Key)); + + return reRoutesForAggregate.All(r => string.IsNullOrEmpty(r.RequestIdKey)); + } + + private static bool IsNotDuplicateIn(FileReRoute reRoute, + List reRoutes) + { + var matchingReRoutes = reRoutes + .Where(r => r.UpstreamPathTemplate == reRoute.UpstreamPathTemplate + && (r.UpstreamHost != reRoute.UpstreamHost || reRoute.UpstreamHost == null)) + .ToList(); + + if (matchingReRoutes.Count == 1) + { + return true; + } + + var allowAllVerbs = matchingReRoutes.Any(x => x.UpstreamHttpMethod.Count == 0); + + var duplicateAllowAllVerbs = matchingReRoutes.Count(x => x.UpstreamHttpMethod.Count == 0) > 1; + + var specificVerbs = matchingReRoutes.Any(x => x.UpstreamHttpMethod.Count != 0); + + var duplicateSpecificVerbs = matchingReRoutes.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any(); + + if (duplicateAllowAllVerbs || duplicateSpecificVerbs || (allowAllVerbs && specificVerbs)) + { + return false; + } + + return true; + } + + private static bool IsNotDuplicateIn(FileReRoute reRoute, + List aggregateReRoutes) + { + var duplicate = aggregateReRoutes + .Any(a => a.UpstreamPathTemplate == reRoute.UpstreamPathTemplate + && a.UpstreamHost == reRoute.UpstreamHost + && reRoute.UpstreamHttpMethod.Select(x => x.ToLower()).Contains("get")); + + return !duplicate; + } + + private static bool IsNotDuplicateIn(FileAggregateReRoute reRoute, + List aggregateReRoutes) + { + var matchingReRoutes = aggregateReRoutes + .Where(r => r.UpstreamPathTemplate == reRoute.UpstreamPathTemplate + && r.UpstreamHost == reRoute.UpstreamHost) + .ToList(); + + return matchingReRoutes.Count <= 1; + } + } +} diff --git a/src/Ocelot/Configuration/Validator/FileGlobalConfigurationFluentValidator.cs b/src/Ocelot/Configuration/Validator/FileGlobalConfigurationFluentValidator.cs new file mode 100644 index 00000000..7bac74f7 --- /dev/null +++ b/src/Ocelot/Configuration/Validator/FileGlobalConfigurationFluentValidator.cs @@ -0,0 +1,21 @@ +using FluentValidation; +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Validator +{ + using Requester; + + public class FileGlobalConfigurationFluentValidator : AbstractValidator + { + private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate; + + public FileGlobalConfigurationFluentValidator(QosDelegatingHandlerDelegate qosDelegatingHandlerDelegate) + { + _qosDelegatingHandlerDelegate = qosDelegatingHandlerDelegate; + + RuleFor(configuration => configuration.QoSOptions) + .SetValidator(new FileQoSOptionsFluentValidator(_qosDelegatingHandlerDelegate)); + } + + } +} diff --git a/src/Ocelot/Configuration/Validator/FileQoSOptionsFluentValidator.cs b/src/Ocelot/Configuration/Validator/FileQoSOptionsFluentValidator.cs new file mode 100644 index 00000000..42fc9120 --- /dev/null +++ b/src/Ocelot/Configuration/Validator/FileQoSOptionsFluentValidator.cs @@ -0,0 +1,28 @@ +using FluentValidation; +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Validator +{ + using Requester; + + public class FileQoSOptionsFluentValidator : AbstractValidator + { + 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; + } + } +} diff --git a/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs b/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs index c54fc2aa..74b0934e 100644 --- a/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs +++ b/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs @@ -1,92 +1,95 @@ -namespace Ocelot.Configuration.Validator -{ - using FluentValidation; - using Microsoft.AspNetCore.Authentication; - using Ocelot.Configuration.File; - using System.Linq; - 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 IServiceProvider _serviceProvider; - - public ReRouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, IServiceProvider serviceProvider) - { - _authenticationSchemeProvider = authenticationSchemeProvider; - _serviceProvider = serviceProvider; - - RuleFor(reRoute => reRoute.DownstreamPathTemplate) - .Must(path => path.StartsWith("/")) - .WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash"); - - 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.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.UpstreamPathTemplate) - .Must(path => path.StartsWith("/")) - .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) - .Must(path => !path.Contains("https://") && !path.Contains("http://")) - .WithMessage("{PropertyName} {PropertyValue} contains scheme"); - - 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"); - - When(reRoute => reRoute.UseServiceDiscovery, () => { - RuleFor(r => r.ServiceName).NotEmpty() - .WithMessage("ServiceName cannot be empty or null when using service discovery or Ocelot cannot look up your service!"); - }); - - When(reRoute => !reRoute.UseServiceDiscovery, () => { - RuleFor(r => r.DownstreamHostAndPorts).NotEmpty() - .WithMessage("When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!"); - }); - - When(reRoute => !reRoute.UseServiceDiscovery, () => { - RuleFor(reRoute => reRoute.DownstreamHostAndPorts) - .SetCollectionValidator(new HostAndPortValidator()); - }); - } - - private async Task IsSupportedAuthenticationProviders(FileAuthenticationOptions authenticationOptions, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(authenticationOptions.AuthenticationProviderKey)) - { - return true; - } - - var schemes = await _authenticationSchemeProvider.GetAllSchemesAsync(); - - var supportedSchemes = schemes.Select(scheme => scheme.Name).ToList(); - - return supportedSchemes.Contains(authenticationOptions.AuthenticationProviderKey); - } - - private static bool IsValidPeriod(FileRateLimitRule rateLimitOptions) - { - string period = rateLimitOptions.Period; - - return !rateLimitOptions.EnableRateLimiting || period.Contains("s") || period.Contains("m") || period.Contains("h") || period.Contains("d"); - } - } -} +namespace Ocelot.Configuration.Validator +{ + using FluentValidation; + using Microsoft.AspNetCore.Authentication; + using Ocelot.Configuration.File; + using System.Linq; + 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) + { + _authenticationSchemeProvider = authenticationSchemeProvider; + _qosDelegatingHandlerDelegate = qosDelegatingHandlerDelegate; + + RuleFor(reRoute => reRoute.QoSOptions) + .SetValidator(new FileQoSOptionsFluentValidator(_qosDelegatingHandlerDelegate)); + + RuleFor(reRoute => reRoute.DownstreamPathTemplate) + .Must(path => path.StartsWith("/")) + .WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash"); + + 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.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.UpstreamPathTemplate) + .Must(path => path.StartsWith("/")) + .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) + .Must(path => !path.Contains("https://") && !path.Contains("http://")) + .WithMessage("{PropertyName} {PropertyValue} contains scheme"); + + 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"); + + When(reRoute => reRoute.UseServiceDiscovery, () => { + RuleFor(r => r.ServiceName).NotEmpty() + .WithMessage("ServiceName cannot be empty or null when using service discovery or Ocelot cannot look up your service!"); + }); + + When(reRoute => !reRoute.UseServiceDiscovery, () => { + RuleFor(r => r.DownstreamHostAndPorts).NotEmpty() + .WithMessage("When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!"); + }); + + When(reRoute => !reRoute.UseServiceDiscovery, () => { + RuleFor(reRoute => reRoute.DownstreamHostAndPorts) + .SetCollectionValidator(new HostAndPortValidator()); + }); + } + + private async Task IsSupportedAuthenticationProviders(FileAuthenticationOptions authenticationOptions, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(authenticationOptions.AuthenticationProviderKey)) + { + return true; + } + + var schemes = await _authenticationSchemeProvider.GetAllSchemesAsync(); + + var supportedSchemes = schemes.Select(scheme => scheme.Name).ToList(); + + return supportedSchemes.Contains(authenticationOptions.AuthenticationProviderKey); + } + + private static bool IsValidPeriod(FileRateLimitRule rateLimitOptions) + { + string period = rateLimitOptions.Period; + + return !rateLimitOptions.EnableRateLimiting || period.Contains("s") || period.Contains("m") || period.Contains("h") || period.Contains("d"); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/CannotStartOcelotTests.cs b/test/Ocelot.AcceptanceTests/CannotStartOcelotTests.cs index 5ecedd1a..35029a13 100644 --- a/test/Ocelot.AcceptanceTests/CannotStartOcelotTests.cs +++ b/test/Ocelot.AcceptanceTests/CannotStartOcelotTests.cs @@ -1,53 +1,148 @@ -namespace Ocelot.AcceptanceTests -{ - using System; - using System.Collections.Generic; - using Ocelot.Configuration.File; - using Shouldly; - using Xunit; - - public class CannotStartOcelotTests : IDisposable - { - private readonly Steps _steps; - - public CannotStartOcelotTests() - { - _steps = new Steps(); - } - - [Fact] - public void should_throw_exception_if_cannot_start() - { - var invalidConfig = new FileConfiguration() - { - ReRoutes = new List - { - new FileReRoute - { - UpstreamPathTemplate = "api", - DownstreamPathTemplate = "test" - } - } - }; - - 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: Downstream Path Template test doesnt start with forward slash,Upstream Path Template api doesnt start with forward slash,When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!)"); - } - - public void Dispose() - { - _steps.Dispose(); - } - } -} +namespace Ocelot.AcceptanceTests +{ + using System; + using System.Collections.Generic; + using Ocelot.Configuration.File; + using Shouldly; + using Xunit; + + public class CannotStartOcelotTests : IDisposable + { + private readonly Steps _steps; + + public CannotStartOcelotTests() + { + _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 + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51878, + } + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "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 + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51878, + } + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "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] + public void should_throw_exception_if_cannot_start() + { + var invalidConfig = new FileConfiguration() + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamPathTemplate = "api", + DownstreamPathTemplate = "test" + } + } + }; + + 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: Downstream Path Template test doesnt start with forward slash,Upstream Path Template api doesnt start with forward slash,When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!)"); + } + + public void Dispose() + { + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/ConfigurationFluentValidationTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationFluentValidatorTests.cs similarity index 85% rename from test/Ocelot.UnitTests/Configuration/ConfigurationFluentValidationTests.cs rename to test/Ocelot.UnitTests/Configuration/FileConfigurationFluentValidatorTests.cs index da8d0e75..9fec1700 100644 --- a/test/Ocelot.UnitTests/Configuration/ConfigurationFluentValidationTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationFluentValidatorTests.cs @@ -1,1080 +1,1240 @@ -namespace Ocelot.UnitTests.Configuration -{ - using System.Collections.Generic; - using System.Security.Claims; - using System.Text.Encodings.Web; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Authentication; - using Microsoft.Extensions.Logging; - using Microsoft.Extensions.Options; - using Moq; - using Ocelot.Configuration.File; - using Ocelot.Configuration.Validator; - using Ocelot.Responses; - using Shouldly; - using TestStack.BDDfy; - using Xunit; - using Microsoft.Extensions.DependencyInjection; - using Ocelot.Requester; - using Requester; - - public class ConfigurationFluentValidationTests - { - private IConfigurationValidator _configurationValidator; - private FileConfiguration _fileConfiguration; - private Response _result; - private readonly Mock _provider; - - public ConfigurationFluentValidationTests() - { - _provider = new Mock(); - var provider = new ServiceCollection() - .BuildServiceProvider(); - _configurationValidator = new FileConfigurationFluentValidator(_provider.Object, provider); +namespace Ocelot.UnitTests.Configuration +{ + using System.Collections.Generic; + using System.Security.Claims; + using System.Text.Encodings.Web; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Authentication; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + using Moq; + using Ocelot.Configuration.File; + using Ocelot.Configuration.Validator; + using Ocelot.Responses; + using Shouldly; + using TestStack.BDDfy; + using Xunit; + using Microsoft.Extensions.DependencyInjection; + using Ocelot.Requester; + using Requester; + + public class FileConfigurationFluentValidatorTests + { + private IConfigurationValidator _configurationValidator; + private FileConfiguration _fileConfiguration; + private Response _result; + private readonly Mock _authProvider; + + public FileConfigurationFluentValidatorTests() + { + _authProvider = new Mock(); + var provider = new ServiceCollection() + .BuildServiceProvider(); + _configurationValidator = new FileConfigurationFluentValidator(_authProvider.Object, provider); } - - [Fact] - public void configuration_is_valid_if_aggregates_are_valid() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Tom", - "Laura" - } - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_aggregates_are_duplicate_of_re_routes() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom", - UpstreamHost = "localhost" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/tom", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Tom", - "Laura" - }, - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "reRoute /tom has duplicate aggregate")) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_if_aggregates_are_not_duplicate_of_re_routes() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Post" }, - Key = "Tom", - UpstreamHost = "localhost" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/tom", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Tom", - "Laura" - }, - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_aggregates_are_duplicate_of_aggregates() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/lol", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/tom", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Tom", - "Laura" - } - }, - new FileAggregateReRoute - { - UpstreamPathTemplate = "/tom", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Tom", - "Laura" - } - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "aggregate /tom has duplicate aggregate")) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_re_routes_dont_exist_for_aggregate() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Tom", - "Laura" - } - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "ReRoutes for aggregateReRoute / either do not exist or do not have correct ServiceName property")) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_aggregate_has_re_routes_with_specific_request_id_keys() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = "should_fail", - Key = "Tom" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Tom", - "Laura" - } - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "aggregateReRoute / contains ReRoute with specific RequestIdKey, this is not possible with Aggregates")) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_scheme_in_downstream_or_upstream_template() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "http://www.bbc.co.uk/api/products/{productId}", - UpstreamPathTemplate = "http://asdf.com" - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .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(5, "Upstream Path Template http://asdf.com contains scheme")) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_with_one_reroute() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk" - } - }, - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_without_slash_prefix_downstream_path_template() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "api/products/", - UpstreamPathTemplate = "/asdf/" - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template api/products/ doesnt start with forward slash")) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_without_slash_prefix_upstream_path_template() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "api/prod/", - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Upstream Path Template api/prod/ doesnt start with forward slash")) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_upstream_url_contains_forward_slash_then_another_forward_slash() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "//api/prod/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - Port = 80 - } - }, - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Upstream Path Template //api/prod/ 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 configuration_is_invalid_if_downstream_url_contains_forward_slash_then_another_forward_slash() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "//api/products/", - UpstreamPathTemplate = "/api/prod/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - Port = 80 - } - }, - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template //api/products/ 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 configuration_is_valid_with_valid_authentication_provider() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - } - }, - AuthenticationOptions = new FileAuthenticationOptions() - { - AuthenticationProviderKey = "Test" - } - } - } - })) - .And(x => x.GivenTheAuthSchemeExists("Test")) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_with_invalid_authentication_provider() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - AuthenticationOptions = new FileAuthenticationOptions() - { - AuthenticationProviderKey = "Test" - } - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "AuthenticationProviderKey:Test,AllowedScopes:[] is unsupported authentication provider")) - .BDDfy(); - } - - [Fact] - public void configuration_is_not_valid_with_duplicate_reroutes_all_verbs() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bb.co.uk" - } - }, - }, - new FileReRoute - { - DownstreamPathTemplate = "/www/test/", - UpstreamPathTemplate = "/asdf/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bb.co.uk" - } - }, - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "reRoute /asdf/ has duplicate")) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_with_duplicate_reroutes_all_verbs_but_different_hosts() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bb.co.uk" - } - }, - UpstreamHost = "host1" - }, - new FileReRoute - { - DownstreamPathTemplate = "/www/test/", - UpstreamPathTemplate = "/asdf/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bb.co.uk" - } - }, - UpstreamHost = "host1" - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_not_valid_with_duplicate_reroutes_specific_verbs() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - } - }, - UpstreamHttpMethod = new List {"Get"} - }, - new FileReRoute - { - DownstreamPathTemplate = "/www/test/", - UpstreamPathTemplate = "/asdf/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - } - }, - UpstreamHttpMethod = new List {"Get"} - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "reRoute /asdf/ has duplicate")) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_with_duplicate_reroutes_different_verbs() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - } - }, - }, - new FileReRoute - { - DownstreamPathTemplate = "/www/test/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Post"}, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - } - }, - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_with_invalid_rate_limit_configuration() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - } - }, - RateLimitOptions = new FileRateLimitRule - { - Period = "1x", - EnableRateLimiting = true - } - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "RateLimitOptions.Period does not contains (s,m,h,d)")) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_with_valid_rate_limit_configuration() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - } - }, - RateLimitOptions = new FileRateLimitRule - { - Period = "1d", - EnableRateLimiting = true - } - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - public void configuration_is_invalid_with_using_service_discovery_and_no_service_name(string serviceName) - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - UseServiceDiscovery = true, - ServiceName = serviceName - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "ServiceName cannot be empty or null when using service discovery or Ocelot cannot look up your service!")) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_with_using_service_discovery_and_service_name() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - UseServiceDiscovery = true, - ServiceName = "Test" - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - public void configuration_is_invalid_when_not_using_service_discovery_and_host(string downstreamHost) - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - UseServiceDiscovery = false, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = downstreamHost, - } - }, - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using ReRoute.Host or Ocelot cannot find your service!")) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_when_not_using_service_discovery_and_host_is_set() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - UseServiceDiscovery = false, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk" - } - }, - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_when_no_downstream_but_has_host_and_port() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - UseServiceDiscovery = false, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "test" - } - } - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_not_valid_when_no_host_and_port() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - UseServiceDiscovery = false, - DownstreamHostAndPorts = new List - { - } - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!")) - .BDDfy(); - } - - [Fact] - public void configuration_is_not_valid_when_host_and_port_is_empty() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - UseServiceDiscovery = false, - DownstreamHostAndPorts = new List - { - new FileHostAndPort() - } - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using ReRoute.Host or Ocelot cannot find your service!")) - .BDDfy(); - } - - private void GivenAConfiguration(FileConfiguration fileConfiguration) - { - _fileConfiguration = fileConfiguration; - } - - private void WhenIValidateTheConfiguration() - { - _result = _configurationValidator.IsValid(_fileConfiguration).Result; - } - - private void ThenTheResultIsValid() - { - _result.Data.IsError.ShouldBeFalse(); - } - - private void ThenTheResultIsNotValid() - { - _result.Data.IsError.ShouldBeTrue(); - } - - private void ThenTheErrorIs() - { - _result.Data.Errors[0].ShouldBeOfType(); - } - - private void ThenTheErrorMessageAtPositionIs(int index, string expected) - { - _result.Data.Errors[index].Message.ShouldBe(expected); - } - - private void GivenTheAuthSchemeExists(string name) - { - _provider.Setup(x => x.GetAllSchemesAsync()).ReturnsAsync(new List - { - new AuthenticationScheme(name, name, typeof(TestHandler)) - }); - } - - private void GivenAQosDelegate() - { - var services = new ServiceCollection(); - QosDelegatingHandlerDelegate del = (a, b) => new FakeDelegatingHandler(); - services.AddSingleton(del); - var provider = services.BuildServiceProvider(); - _configurationValidator = new FileConfigurationFluentValidator(_provider.Object, provider); - } - - private class TestOptions : AuthenticationSchemeOptions - { - } - - private class TestHandler : AuthenticationHandler - { - public TestHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) - { - } - - protected override Task HandleAuthenticateAsync() - { - var principal = new ClaimsPrincipal(); - return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); - } - } - } -} + + [Fact] + public void configuration_is_valid_if_qos_options_specified_and_has_qos_handler() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51878, + } + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "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 + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51878, + } + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "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 + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51878, + } + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "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()) + .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 + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51878, + } + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "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()) + .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_valid_if_aggregates_are_valid() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51878, + } + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura" + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51880, + } + }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new List { "Get" }, + Key = "Tom" + } + }, + Aggregates = new List + { + new FileAggregateReRoute + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + ReRouteKeys = new List + { + "Tom", + "Laura" + } + } + } + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_aggregates_are_duplicate_of_re_routes() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51878, + } + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura" + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51880, + } + }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new List { "Get" }, + Key = "Tom", + UpstreamHost = "localhost" + } + }, + Aggregates = new List + { + new FileAggregateReRoute + { + UpstreamPathTemplate = "/tom", + UpstreamHost = "localhost", + ReRouteKeys = new List + { + "Tom", + "Laura" + }, + } + } + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "reRoute /tom has duplicate aggregate")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_if_aggregates_are_not_duplicate_of_re_routes() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51878, + } + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura" + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51880, + } + }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new List { "Post" }, + Key = "Tom", + UpstreamHost = "localhost" + } + }, + Aggregates = new List + { + new FileAggregateReRoute + { + UpstreamPathTemplate = "/tom", + UpstreamHost = "localhost", + ReRouteKeys = new List + { + "Tom", + "Laura" + }, + } + } + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_aggregates_are_duplicate_of_aggregates() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51878, + } + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura" + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51880, + } + }, + UpstreamPathTemplate = "/lol", + UpstreamHttpMethod = new List { "Get" }, + Key = "Tom" + } + }, + Aggregates = new List + { + new FileAggregateReRoute + { + UpstreamPathTemplate = "/tom", + UpstreamHost = "localhost", + ReRouteKeys = new List + { + "Tom", + "Laura" + } + }, + new FileAggregateReRoute + { + UpstreamPathTemplate = "/tom", + UpstreamHost = "localhost", + ReRouteKeys = new List + { + "Tom", + "Laura" + } + } + } + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "aggregate /tom has duplicate aggregate")) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_re_routes_dont_exist_for_aggregate() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51878, + } + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura" + } + }, + Aggregates = new List + { + new FileAggregateReRoute + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + ReRouteKeys = new List + { + "Tom", + "Laura" + } + } + } + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "ReRoutes for aggregateReRoute / either do not exist or do not have correct ServiceName property")) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_aggregate_has_re_routes_with_specific_request_id_keys() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51878, + } + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura" + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51880, + } + }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new List { "Get" }, + RequestIdKey = "should_fail", + Key = "Tom" + } + }, + Aggregates = new List + { + new FileAggregateReRoute + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + ReRouteKeys = new List + { + "Tom", + "Laura" + } + } + } + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "aggregateReRoute / contains ReRoute with specific RequestIdKey, this is not possible with Aggregates")) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_scheme_in_downstream_or_upstream_template() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "http://www.bbc.co.uk/api/products/{productId}", + UpstreamPathTemplate = "http://asdf.com" + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .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(5, "Upstream Path Template http://asdf.com contains scheme")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_one_reroute() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "bbc.co.uk" + } + }, + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_without_slash_prefix_downstream_path_template() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "api/products/", + UpstreamPathTemplate = "/asdf/" + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template api/products/ doesnt start with forward slash")) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_without_slash_prefix_upstream_path_template() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "api/prod/", + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Upstream Path Template api/prod/ doesnt start with forward slash")) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_upstream_url_contains_forward_slash_then_another_forward_slash() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "//api/prod/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "bbc.co.uk", + Port = 80 + } + }, + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Upstream Path Template //api/prod/ 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 configuration_is_invalid_if_downstream_url_contains_forward_slash_then_another_forward_slash() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "//api/products/", + UpstreamPathTemplate = "/api/prod/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "bbc.co.uk", + Port = 80 + } + }, + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template //api/products/ 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 configuration_is_valid_with_valid_authentication_provider() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "bbc.co.uk", + } + }, + AuthenticationOptions = new FileAuthenticationOptions() + { + AuthenticationProviderKey = "Test" + } + } + } + })) + .And(x => x.GivenTheAuthSchemeExists("Test")) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_with_invalid_authentication_provider() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + AuthenticationOptions = new FileAuthenticationOptions() + { + AuthenticationProviderKey = "Test" + } + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "AuthenticationProviderKey:Test,AllowedScopes:[] is unsupported authentication provider")) + .BDDfy(); + } + + [Fact] + public void configuration_is_not_valid_with_duplicate_reroutes_all_verbs() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "bb.co.uk" + } + }, + }, + new FileReRoute + { + DownstreamPathTemplate = "/www/test/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "bb.co.uk" + } + }, + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "reRoute /asdf/ has duplicate")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_duplicate_reroutes_all_verbs_but_different_hosts() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "bb.co.uk" + } + }, + UpstreamHost = "host1" + }, + new FileReRoute + { + DownstreamPathTemplate = "/www/test/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "bb.co.uk" + } + }, + UpstreamHost = "host1" + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_not_valid_with_duplicate_reroutes_specific_verbs() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "bbc.co.uk", + } + }, + UpstreamHttpMethod = new List {"Get"} + }, + new FileReRoute + { + DownstreamPathTemplate = "/www/test/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "bbc.co.uk", + } + }, + UpstreamHttpMethod = new List {"Get"} + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "reRoute /asdf/ has duplicate")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_duplicate_reroutes_different_verbs() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "bbc.co.uk", + } + }, + }, + new FileReRoute + { + DownstreamPathTemplate = "/www/test/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Post"}, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "bbc.co.uk", + } + }, + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_with_invalid_rate_limit_configuration() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "bbc.co.uk", + } + }, + RateLimitOptions = new FileRateLimitRule + { + Period = "1x", + EnableRateLimiting = true + } + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "RateLimitOptions.Period does not contains (s,m,h,d)")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_valid_rate_limit_configuration() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "bbc.co.uk", + } + }, + RateLimitOptions = new FileRateLimitRule + { + Period = "1d", + EnableRateLimiting = true + } + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void configuration_is_invalid_with_using_service_discovery_and_no_service_name(string serviceName) + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + UseServiceDiscovery = true, + ServiceName = serviceName + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "ServiceName cannot be empty or null when using service discovery or Ocelot cannot look up your service!")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_using_service_discovery_and_service_name() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + UseServiceDiscovery = true, + ServiceName = "Test" + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void configuration_is_invalid_when_not_using_service_discovery_and_host(string downstreamHost) + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + UseServiceDiscovery = false, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = downstreamHost, + } + }, + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using ReRoute.Host or Ocelot cannot find your service!")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_when_not_using_service_discovery_and_host_is_set() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + UseServiceDiscovery = false, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "bbc.co.uk" + } + }, + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_when_no_downstream_but_has_host_and_port() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + UseServiceDiscovery = false, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "test" + } + } + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_not_valid_when_no_host_and_port() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + UseServiceDiscovery = false, + DownstreamHostAndPorts = new List + { + } + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!")) + .BDDfy(); + } + + [Fact] + public void configuration_is_not_valid_when_host_and_port_is_empty() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + UseServiceDiscovery = false, + DownstreamHostAndPorts = new List + { + new FileHostAndPort() + } + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using ReRoute.Host or Ocelot cannot find your service!")) + .BDDfy(); + } + + private void GivenAConfiguration(FileConfiguration fileConfiguration) + { + _fileConfiguration = fileConfiguration; + } + + private void WhenIValidateTheConfiguration() + { + _result = _configurationValidator.IsValid(_fileConfiguration).Result; + } + + private void ThenTheResultIsValid() + { + _result.Data.IsError.ShouldBeFalse(); + } + + private void ThenTheResultIsNotValid() + { + _result.Data.IsError.ShouldBeTrue(); + } + + private void ThenTheErrorIs() + { + _result.Data.Errors[0].ShouldBeOfType(); + } + + private void ThenTheErrorMessageAtPositionIs(int index, string expected) + { + _result.Data.Errors[index].Message.ShouldBe(expected); + } + + private void GivenTheAuthSchemeExists(string name) + { + _authProvider.Setup(x => x.GetAllSchemesAsync()).ReturnsAsync(new List + { + new AuthenticationScheme(name, name, typeof(TestHandler)) + }); + } + + private void GivenAQoSHandler() + { + var collection = new ServiceCollection(); + QosDelegatingHandlerDelegate del = (a,b) => new FakeDelegatingHandler(); + collection.AddSingleton(del); + var provider = collection.BuildServiceProvider(); + _configurationValidator = new FileConfigurationFluentValidator(_authProvider.Object, provider); + } + + private class TestOptions : AuthenticationSchemeOptions + { + } + + private class TestHandler : AuthenticationHandler + { + public TestHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { + } + + protected override Task HandleAuthenticateAsync() + { + var principal = new ClaimsPrincipal(); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); + } + } + } +}