diff --git a/docs/features/delegatinghandlers.rst b/docs/features/delegatinghandlers.rst new file mode 100644 index 00000000..a6f2e4e9 --- /dev/null +++ b/docs/features/delegatinghandlers.rst @@ -0,0 +1,43 @@ +Delegating Handers +================== + +Ocelot allows the user to add delegating handlers to the HttpClient transport. This feature was requested `GitHub #208 `_ and I decided that it was going to be useful in various ways. + +Usage +^^^^^^ + +In order to add delegating handlers to the HttpClient transport you need to do the following. + +.. code-block:: csharp + + services.AddOcelot() + .AddDelegatingHandler(() => new FakeHandler()) + .AddDelegatingHandler(() => new FakeHandler()); + +Or for singleton like behaviour.. + +.. code-block:: csharp + + var handlerOne = new FakeHandler(); + var handlerTwo = new FakeHandler(); + + services.AddOcelot() + .AddDelegatingHandler(() => handlerOne) + .AddDelegatingHandler(() => handlerTwo); + +You can have as many DelegatingHandlers as you want and they are run in a first in first out order. If you are using Ocelot's QoS functionality then that will always be run after your last delegating handler. + +In order to create a class that can be used a delegating handler it must look as follows + +.. code-block:: csharp + + public class FakeHandler : DelegatingHandler + { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + //do stuff and optionally call the base handler.. + return await base.SendAsync(request, cancellationToken); + } + } + +Hopefully other people will find this feature useful! \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index c2bc7041..16ce9f19 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -33,6 +33,8 @@ Thanks for taking a look at the Ocelot documentation. Please use the left hand n features/requestid features/middlewareinjection features/loadbalancer + features/delegatinghandlers + .. toctree:: :maxdepth: 2 diff --git a/global.json b/global.json index 38106d45..7281f931 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "projects": [ "src", "test" ], "sdk": { - "version": "2.0.2" + "version": "2.1.4" } } \ No newline at end of file diff --git a/src/Ocelot/DependencyInjection/IOcelotAdministrationBuilder.cs b/src/Ocelot/DependencyInjection/IOcelotAdministrationBuilder.cs new file mode 100644 index 00000000..859f4d98 --- /dev/null +++ b/src/Ocelot/DependencyInjection/IOcelotAdministrationBuilder.cs @@ -0,0 +1,7 @@ +namespace Ocelot.DependencyInjection +{ + public interface IOcelotAdministrationBuilder + { + IOcelotAdministrationBuilder AddRafty(); + } +} diff --git a/src/Ocelot/DependencyInjection/IOcelotBuilder.cs b/src/Ocelot/DependencyInjection/IOcelotBuilder.cs index 4519364b..d2f92101 100644 --- a/src/Ocelot/DependencyInjection/IOcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/IOcelotBuilder.cs @@ -1,6 +1,7 @@ using Butterfly.Client.AspNetCore; using CacheManager.Core; using System; +using System.Net.Http; namespace Ocelot.DependencyInjection { @@ -10,5 +11,6 @@ namespace Ocelot.DependencyInjection IOcelotBuilder AddCacheManager(Action settings); IOcelotBuilder AddOpenTracing(Action settings); IOcelotAdministrationBuilder AddAdministration(string path, string secret); + IOcelotBuilder AddDelegatingHandler(Func delegatingHandler); } } diff --git a/src/Ocelot/DependencyInjection/OcelotAdministrationBuilder.cs b/src/Ocelot/DependencyInjection/OcelotAdministrationBuilder.cs new file mode 100644 index 00000000..580839c3 --- /dev/null +++ b/src/Ocelot/DependencyInjection/OcelotAdministrationBuilder.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Raft; +using Rafty.Concensus; +using Rafty.FiniteStateMachine; +using Rafty.Infrastructure; +using Rafty.Log; + +namespace Ocelot.DependencyInjection +{ + public class OcelotAdministrationBuilder : IOcelotAdministrationBuilder + { + private readonly IServiceCollection _services; + private readonly IConfiguration _configurationRoot; + + public OcelotAdministrationBuilder(IServiceCollection services, IConfiguration configurationRoot) + { + _configurationRoot = configurationRoot; + _services = services; + } + + public IOcelotAdministrationBuilder AddRafty() + { + var settings = new InMemorySettings(4000, 5000, 100, 5000); + _services.AddSingleton(); + _services.AddSingleton(); + _services.AddSingleton(settings); + _services.AddSingleton(); + _services.AddSingleton(); + _services.Configure(_configurationRoot); + return this; + } + } +} diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 308cea14..10f9d082 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -1,19 +1,12 @@ -using Butterfly.Client.AspNetCore; using CacheManager.Core; -using IdentityServer4.AccessTokenValidation; using IdentityServer4.Models; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Ocelot.Authorisation; using Ocelot.Cache; using Ocelot.Claims; -using Ocelot.Configuration; using Ocelot.Configuration.Authentication; -using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Configuration.Parser; @@ -32,7 +25,6 @@ using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.QueryStrings; -using Ocelot.Raft; using Ocelot.RateLimit; using Ocelot.Request.Builder; using Ocelot.Request.Mapper; @@ -40,24 +32,29 @@ using Ocelot.Requester; using Ocelot.Requester.QoS; using Ocelot.Responder; using Ocelot.ServiceDiscovery; -using Rafty.Concensus; -using Rafty.FiniteStateMachine; -using Rafty.Infrastructure; -using Rafty.Log; using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; -using System.Linq; using System.Reflection; using System.Security.Cryptography.X509Certificates; +using IdentityServer4.AccessTokenValidation; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; using FileConfigurationProvider = Ocelot.Configuration.Provider.FileConfigurationProvider; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System.Linq; +using System.Net.Http; +using Butterfly.Client.AspNetCore; namespace Ocelot.DependencyInjection { public class OcelotBuilder : IOcelotBuilder { - private IServiceCollection _services; - private IConfiguration _configurationRoot; + private readonly IServiceCollection _services; + private readonly IConfiguration _configurationRoot; + private IDelegatingHandlerHandlerProvider _provider; public OcelotBuilder(IServiceCollection services, IConfiguration configurationRoot) { @@ -122,6 +119,9 @@ namespace Ocelot.DependencyInjection _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc // could maybe use a scoped data repository _services.TryAddSingleton(); @@ -142,6 +142,11 @@ namespace Ocelot.DependencyInjection _services.AddMiddlewareAnalysis(); _services.AddWebEncoders(); _services.AddSingleton(new NullAdministrationPath()); + + //these get picked out later and added to http request + _provider = new DelegatingHandlerHandlerProvider(); + _services.TryAddSingleton(_provider); + _services.AddTransient(); } public IOcelotAdministrationBuilder AddAdministration(string path, string secret) @@ -161,6 +166,19 @@ namespace Ocelot.DependencyInjection return new OcelotAdministrationBuilder(_services, _configurationRoot); } + public IOcelotBuilder AddDelegatingHandler(Func delegatingHandler) + { + _provider.Add(delegatingHandler); + return this; + } + + public IOcelotBuilder AddOpenTracing(Action settings) + { + _services.AddTransient(); + _services.AddButterfly(settings); + return this; + } + public IOcelotBuilder AddStoreOcelotConfigurationInConsul() { var serviceDiscoveryPort = _configurationRoot.GetValue("GlobalConfiguration:ServiceDiscoveryProvider:Port", 0); @@ -203,13 +221,6 @@ namespace Ocelot.DependencyInjection return this; } - public IOcelotBuilder AddOpenTracing(Action settings) - { - _services.AddTransient(); - _services.AddButterfly(settings); - return this; - } - private void AddIdentityServer(IIdentityServerConfiguration identityServerConfiguration, IAdministrationPath adminPath) { _services.TryAddSingleton(identityServerConfiguration); @@ -281,33 +292,4 @@ namespace Ocelot.DependencyInjection }; } } - - public interface IOcelotAdministrationBuilder - { - IOcelotAdministrationBuilder AddRafty(); - } - - public class OcelotAdministrationBuilder : IOcelotAdministrationBuilder - { - private IServiceCollection _services; - private IConfiguration _configurationRoot; - - public OcelotAdministrationBuilder(IServiceCollection services, IConfiguration configurationRoot) - { - _configurationRoot = configurationRoot; - _services = services; - } - - public IOcelotAdministrationBuilder AddRafty() - { - var settings = new InMemorySettings(4000, 5000, 100, 5000); - _services.AddSingleton(); - _services.AddSingleton(); - _services.AddSingleton(settings); - _services.AddSingleton(); - _services.AddSingleton(); - _services.Configure(_configurationRoot); - return this; - } - } } diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs index 4befa3f9..bcfde4b0 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Ocelot.Configuration; -using Ocelot.Configuration.Provider; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Errors; using Ocelot.Responses; diff --git a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs index c2574ff9..54461bbd 100644 --- a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs @@ -62,7 +62,7 @@ namespace Ocelot.Errors.Middleware private async Task TrySetGlobalRequestId(HttpContext context) { //try and get the global request id and set it for logs... - //shoudl this basically be immutable per request...i guess it should! + //should this basically be immutable per request...i guess it should! //first thing is get config var configuration = await _configProvider.Get(); diff --git a/src/Ocelot/Errors/OcelotErrorCode.cs b/src/Ocelot/Errors/OcelotErrorCode.cs index e4fba055..b976e29c 100644 --- a/src/Ocelot/Errors/OcelotErrorCode.cs +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -33,6 +33,7 @@ UnmappableRequestError, RateLimitOptionsError, PathTemplateDoesntStartWithForwardSlash, - FileValidationFailedError + FileValidationFailedError, + UnableToFindDelegatingHandlerProviderError } } diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs b/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs index 2daf74ff..8ce7bcd4 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs @@ -9,4 +9,4 @@ namespace Ocelot.LoadBalancer.LoadBalancers { } } -} \ No newline at end of file +} diff --git a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs index 29347c88..aa37196f 100644 --- a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs +++ b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.Provider; using Ocelot.Infrastructure.RequestData; using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.Logging; @@ -46,11 +45,14 @@ namespace Ocelot.LoadBalancer.Middleware } var uriBuilder = new UriBuilder(DownstreamRequest.RequestUri); + uriBuilder.Host = hostAndPort.Data.DownstreamHost; + if (hostAndPort.Data.DownstreamPort > 0) { uriBuilder.Port = hostAndPort.Data.DownstreamPort; } + DownstreamRequest.RequestUri = uriBuilder.Uri; try diff --git a/src/Ocelot/Request/Builder/HttpRequestCreator.cs b/src/Ocelot/Request/Builder/HttpRequestCreator.cs index 8e76adb3..5b8ef968 100644 --- a/src/Ocelot/Request/Builder/HttpRequestCreator.cs +++ b/src/Ocelot/Request/Builder/HttpRequestCreator.cs @@ -13,9 +13,10 @@ namespace Ocelot.Request.Builder IQoSProvider qosProvider, bool useCookieContainer, bool allowAutoRedirect, + string reRouteKey, bool isTracing) { - return new OkResponse(new Request(httpRequestMessage, isQos, qosProvider, allowAutoRedirect, useCookieContainer, isTracing)); + return new OkResponse(new Request(httpRequestMessage, isQos, qosProvider, allowAutoRedirect, useCookieContainer, reRouteKey, isTracing)); } } } diff --git a/src/Ocelot/Request/Builder/IRequestCreator.cs b/src/Ocelot/Request/Builder/IRequestCreator.cs index 8bfc3f2f..abde4a6b 100644 --- a/src/Ocelot/Request/Builder/IRequestCreator.cs +++ b/src/Ocelot/Request/Builder/IRequestCreator.cs @@ -14,6 +14,7 @@ IQoSProvider qosProvider, bool useCookieContainer, bool allowAutoRedirect, + string reRouteKe, bool isTracing); } } diff --git a/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs b/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs index 25b674e4..4160db31 100644 --- a/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs +++ b/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs @@ -47,7 +47,9 @@ namespace Ocelot.Request.Middleware qosProvider.Data, DownstreamRoute.ReRoute.HttpHandlerOptions.UseCookieContainer, DownstreamRoute.ReRoute.HttpHandlerOptions.AllowAutoRedirect, + DownstreamRoute.ReRoute.ReRouteKey, DownstreamRoute.ReRoute.HttpHandlerOptions.UseTracing); + if (buildResult.IsError) { _logger.LogDebug("IRequestCreator returned an error, setting pipeline error"); diff --git a/src/Ocelot/Request/Request.cs b/src/Ocelot/Request/Request.cs index 3b003e57..3bb670f6 100644 --- a/src/Ocelot/Request/Request.cs +++ b/src/Ocelot/Request/Request.cs @@ -11,6 +11,7 @@ namespace Ocelot.Request IQoSProvider qosProvider, bool allowAutoRedirect, bool useCookieContainer, + string reRouteKey, bool isTracing ) { @@ -19,6 +20,7 @@ namespace Ocelot.Request QosProvider = qosProvider; AllowAutoRedirect = allowAutoRedirect; UseCookieContainer = useCookieContainer; + ReRouteKey = reRouteKey; IsTracing = isTracing; } @@ -28,5 +30,6 @@ namespace Ocelot.Request public IQoSProvider QosProvider { get; private set; } public bool AllowAutoRedirect { get; private set; } public bool UseCookieContainer { get; private set; } + public string ReRouteKey { get; private set; } } -} +} \ No newline at end of file diff --git a/src/Ocelot/Requester/DelegatingHandlerHandlerHouse.cs b/src/Ocelot/Requester/DelegatingHandlerHandlerHouse.cs new file mode 100644 index 00000000..0a7d6947 --- /dev/null +++ b/src/Ocelot/Requester/DelegatingHandlerHandlerHouse.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Ocelot.Errors; +using Ocelot.Responses; + +namespace Ocelot.Requester +{ + public class DelegatingHandlerHandlerHouse : IDelegatingHandlerHandlerHouse + { + private readonly IDelegatingHandlerHandlerProviderFactory _factory; + private readonly ConcurrentDictionary _housed; + + public DelegatingHandlerHandlerHouse(IDelegatingHandlerHandlerProviderFactory factory) + { + _factory = factory; + _housed = new ConcurrentDictionary(); + } + + public Response Get(Request.Request request) + { + try + { + if (_housed.TryGetValue(request.ReRouteKey, out var provider)) + { + //todo once day we might need a check here to see if we need to create a new provider + provider = _housed[request.ReRouteKey]; + return new OkResponse(provider); + } + + provider = _factory.Get(request); + AddHoused(request.ReRouteKey, provider); + return new OkResponse(provider); + } + catch (Exception ex) + { + return new ErrorResponse(new List() + { + new UnableToFindDelegatingHandlerProviderError($"unabe to find delegating handler provider for {request.ReRouteKey} exception is {ex}") + }); + } + } + + private void AddHoused(string key, IDelegatingHandlerHandlerProvider provider) + { + _housed.AddOrUpdate(key, provider, (k, v) => provider); + } + } +} diff --git a/src/Ocelot/Requester/DelegatingHandlerHandlerProvider.cs b/src/Ocelot/Requester/DelegatingHandlerHandlerProvider.cs new file mode 100644 index 00000000..71684c52 --- /dev/null +++ b/src/Ocelot/Requester/DelegatingHandlerHandlerProvider.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; + +namespace Ocelot.Requester +{ + public class DelegatingHandlerHandlerProvider : IDelegatingHandlerHandlerProvider + { + private readonly Dictionary> _handlers; + + public DelegatingHandlerHandlerProvider() + { + _handlers = new Dictionary>(); + } + + public void Add(Func handler) + { + var key = _handlers.Count == 0 ? 0 : _handlers.Count + 1; + _handlers[key] = handler; + } + + public List> Get() + { + return _handlers.Count > 0 ? _handlers.OrderBy(x => x.Key).Select(x => x.Value).ToList() : new List>(); + } + } +} diff --git a/src/Ocelot/Requester/DelegatingHandlerHandlerProviderFactory.cs b/src/Ocelot/Requester/DelegatingHandlerHandlerProviderFactory.cs new file mode 100644 index 00000000..1e6cb4c4 --- /dev/null +++ b/src/Ocelot/Requester/DelegatingHandlerHandlerProviderFactory.cs @@ -0,0 +1,43 @@ +using System.Net.Http; +using Ocelot.Logging; + +namespace Ocelot.Requester +{ + public class DelegatingHandlerHandlerProviderFactory : IDelegatingHandlerHandlerProviderFactory + { + private readonly ITracingHandler _tracingHandler; + private readonly IOcelotLoggerFactory _loggerFactory; + private readonly IDelegatingHandlerHandlerProvider _allRoutesProvider; + + public DelegatingHandlerHandlerProviderFactory(IOcelotLoggerFactory loggerFactory, IDelegatingHandlerHandlerProvider allRoutesProvider, ITracingHandler tracingHandler) + { + _tracingHandler = tracingHandler; + _loggerFactory = loggerFactory; + _allRoutesProvider = allRoutesProvider; + } + + public IDelegatingHandlerHandlerProvider Get(Request.Request request) + { + var handlersAppliedToAll = _allRoutesProvider.Get(); + + var provider = new DelegatingHandlerHandlerProvider(); + + foreach (var handler in handlersAppliedToAll) + { + provider.Add(handler); + } + + if (request.IsTracing) + { + provider.Add(() => (DelegatingHandler)_tracingHandler); + } + + if (request.IsQos) + { + provider.Add(() => new PollyCircuitBreakingDelegatingHandler(request.QosProvider, _loggerFactory)); + } + + return provider; + } + } +} diff --git a/src/Ocelot/Requester/HttpClientBuilder.cs b/src/Ocelot/Requester/HttpClientBuilder.cs index 5f64ad6a..561b5205 100644 --- a/src/Ocelot/Requester/HttpClientBuilder.cs +++ b/src/Ocelot/Requester/HttpClientBuilder.cs @@ -1,48 +1,33 @@ -using Microsoft.Extensions.DependencyInjection; -using Ocelot.Logging; -using Ocelot.Requester.QoS; -using System; -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Net.Http; -using System.Threading.Tasks; namespace Ocelot.Requester { - internal class HttpClientBuilder : IHttpClientBuilder + public class HttpClientBuilder : IHttpClientBuilder { - private readonly Dictionary> _handlers = new Dictionary>(); + private readonly IDelegatingHandlerHandlerHouse _house; - public IHttpClientBuilder WithQos(IQoSProvider qosProvider, IOcelotLogger logger) + public HttpClientBuilder(IDelegatingHandlerHandlerHouse house) { - _handlers.Add(5000, () => new PollyCircuitBreakingDelegatingHandler(qosProvider, logger)); - - return this; + _house = house; } - private IHttpClientBuilder WithTracing(IServiceProvider provider) + public IHttpClient Create(Request.Request request) { - _handlers.Add(6000, () => provider.GetService()); - return this; - } - - public IHttpClient Create(bool useCookies, bool allowAutoRedirect, bool isTracing, IServiceProvider provider) - { - var httpclientHandler = new HttpClientHandler { AllowAutoRedirect = allowAutoRedirect, UseCookies = useCookies }; - if (isTracing) - { - WithTracing(provider); - } - var client = new HttpClient(CreateHttpMessageHandler(httpclientHandler)); + var httpclientHandler = new HttpClientHandler { AllowAutoRedirect = request.AllowAutoRedirect, UseCookies = request.UseCookieContainer}; + + var client = new HttpClient(CreateHttpMessageHandler(httpclientHandler, request)); return new HttpClientWrapper(client); } - private HttpMessageHandler CreateHttpMessageHandler(HttpMessageHandler httpMessageHandler) - { - _handlers - .OrderByDescending(handler => handler.Key) - .Select(handler => handler.Value) + private HttpMessageHandler CreateHttpMessageHandler(HttpMessageHandler httpMessageHandler, Request.Request request) + { + var provider = _house.Get(request); + + //todo handle error + provider.Data.Get() + .Select(handler => handler) .Reverse() .ToList() .ForEach(handler => @@ -54,22 +39,4 @@ namespace Ocelot.Requester return httpMessageHandler; } } - - /// - /// This class was made to make unit testing easier when HttpClient is used. - /// - internal class HttpClientWrapper : IHttpClient - { - public HttpClient Client { get; } - - public HttpClientWrapper(HttpClient client) - { - Client = client; - } - - public Task SendAsync(HttpRequestMessage request) - { - return Client.SendAsync(request); - } - } -} +} \ No newline at end of file diff --git a/src/Ocelot/Requester/HttpClientHttpRequester.cs b/src/Ocelot/Requester/HttpClientHttpRequester.cs index 1fdfef23..9fa3b271 100644 --- a/src/Ocelot/Requester/HttpClientHttpRequester.cs +++ b/src/Ocelot/Requester/HttpClientHttpRequester.cs @@ -1,84 +1,84 @@ -using System; -using System.Collections.Concurrent; -using System.Net.Http; -using System.Threading.Tasks; -using Ocelot.Configuration; -using Ocelot.Logging; -using Ocelot.Responses; -using Polly.CircuitBreaker; -using Polly.Timeout; - -namespace Ocelot.Requester -{ - public class HttpClientHttpRequester : IHttpRequester - { - private readonly IHttpClientCache _cacheHandlers; - private readonly IOcelotLogger _logger; - private readonly IServiceProvider _serviceProvider; - - public HttpClientHttpRequester(IOcelotLoggerFactory loggerFactory, IHttpClientCache cacheHandlers, IServiceProvider provider) - { - _logger = loggerFactory.CreateLogger(); - _cacheHandlers = cacheHandlers; - _serviceProvider = provider; - } - - public async Task> GetResponse(Request.Request request) - { - var builder = new HttpClientBuilder(); - - var cacheKey = GetCacheKey(request, builder); - - var httpClient = GetHttpClient(cacheKey, builder, request.UseCookieContainer, request.AllowAutoRedirect, request.IsTracing); - - try - { - var response = await httpClient.SendAsync(request.HttpRequestMessage); - return new OkResponse(response); - } - catch (TimeoutRejectedException exception) - { - return - new ErrorResponse(new RequestTimedOutError(exception)); - } - catch (BrokenCircuitException exception) - { - return - new ErrorResponse(new RequestTimedOutError(exception)); - } - catch (Exception exception) - { - return new ErrorResponse(new UnableToCompleteRequestError(exception)); - } - finally - { - _cacheHandlers.Set(cacheKey, httpClient, TimeSpan.FromHours(24)); - } - - } - - private IHttpClient GetHttpClient(string cacheKey, IHttpClientBuilder builder, bool useCookieContainer, bool allowAutoRedirect,bool isTracing) - { - var httpClient = _cacheHandlers.Get(cacheKey); - - if (httpClient == null) - { - httpClient = builder.Create(useCookieContainer, allowAutoRedirect, isTracing, _serviceProvider); - } - return httpClient; - } - - private string GetCacheKey(Request.Request request, IHttpClientBuilder builder) - { - string baseUrl = $"{request.HttpRequestMessage.RequestUri.Scheme}://{request.HttpRequestMessage.RequestUri.Authority}"; - - if (request.IsQos) - { - builder.WithQos(request.QosProvider, _logger); - baseUrl = $"{baseUrl}{request.QosProvider.CircuitBreaker.CircuitBreakerPolicy.PolicyKey}"; - } - - return baseUrl; - } - } -} +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Ocelot.Logging; +using Ocelot.Responses; +using Polly.CircuitBreaker; +using Polly.Timeout; + +namespace Ocelot.Requester +{ + public class HttpClientHttpRequester : IHttpRequester + { + private readonly IHttpClientCache _cacheHandlers; + private readonly IOcelotLogger _logger; + private readonly IDelegatingHandlerHandlerHouse _house; + + public HttpClientHttpRequester(IOcelotLoggerFactory loggerFactory, + IHttpClientCache cacheHandlers, + IDelegatingHandlerHandlerHouse house) + { + _logger = loggerFactory.CreateLogger(); + _cacheHandlers = cacheHandlers; + _house = house; + } + + public async Task> GetResponse(Request.Request request) + { + var builder = new HttpClientBuilder(_house); + + var cacheKey = GetCacheKey(request); + + var httpClient = GetHttpClient(cacheKey, builder, request); + + try + { + var response = await httpClient.SendAsync(request.HttpRequestMessage); + return new OkResponse(response); + } + catch (TimeoutRejectedException exception) + { + return + new ErrorResponse(new RequestTimedOutError(exception)); + } + catch (BrokenCircuitException exception) + { + return + new ErrorResponse(new RequestTimedOutError(exception)); + } + catch (Exception exception) + { + return new ErrorResponse(new UnableToCompleteRequestError(exception)); + } + finally + { + _cacheHandlers.Set(cacheKey, httpClient, TimeSpan.FromHours(24)); + } + + } + + private IHttpClient GetHttpClient(string cacheKey, IHttpClientBuilder builder, Request.Request request) + { + var httpClient = _cacheHandlers.Get(cacheKey); + + if (httpClient == null) + { + httpClient = builder.Create(request); + } + + return httpClient; + } + + private string GetCacheKey(Request.Request request) + { + var baseUrl = $"{request.HttpRequestMessage.RequestUri.Scheme}://{request.HttpRequestMessage.RequestUri.Authority}"; + + if (request.IsQos) + { + baseUrl = $"{baseUrl}{request.QosProvider.CircuitBreaker.CircuitBreakerPolicy.PolicyKey}"; + } + + return baseUrl; + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Requester/HttpClientWrapper.cs b/src/Ocelot/Requester/HttpClientWrapper.cs new file mode 100644 index 00000000..21e74e48 --- /dev/null +++ b/src/Ocelot/Requester/HttpClientWrapper.cs @@ -0,0 +1,23 @@ +using System.Net.Http; +using System.Threading.Tasks; + +namespace Ocelot.Requester +{ + /// + /// This class was made to make unit testing easier when HttpClient is used. + /// + internal class HttpClientWrapper : IHttpClient + { + public HttpClient Client { get; } + + public HttpClientWrapper(HttpClient client) + { + Client = client; + } + + public Task SendAsync(HttpRequestMessage request) + { + return Client.SendAsync(request); + } + } +} diff --git a/src/Ocelot/Requester/IDelegatingHandlerHandlerHouse.cs b/src/Ocelot/Requester/IDelegatingHandlerHandlerHouse.cs new file mode 100644 index 00000000..78dd1fc1 --- /dev/null +++ b/src/Ocelot/Requester/IDelegatingHandlerHandlerHouse.cs @@ -0,0 +1,9 @@ +using Ocelot.Responses; + +namespace Ocelot.Requester +{ + public interface IDelegatingHandlerHandlerHouse + { + Response Get(Request.Request request); + } +} diff --git a/src/Ocelot/Requester/IDelegatingHandlerHandlerProvider.cs b/src/Ocelot/Requester/IDelegatingHandlerHandlerProvider.cs new file mode 100644 index 00000000..addaaeb7 --- /dev/null +++ b/src/Ocelot/Requester/IDelegatingHandlerHandlerProvider.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; + +namespace Ocelot.Requester +{ + public interface IDelegatingHandlerHandlerProvider + { + void Add(Func handler); + List> Get(); + } +} diff --git a/src/Ocelot/Requester/IDelegatingHandlerHandlerProviderFactory.cs b/src/Ocelot/Requester/IDelegatingHandlerHandlerProviderFactory.cs new file mode 100644 index 00000000..dcf007d4 --- /dev/null +++ b/src/Ocelot/Requester/IDelegatingHandlerHandlerProviderFactory.cs @@ -0,0 +1,7 @@ +namespace Ocelot.Requester +{ + public interface IDelegatingHandlerHandlerProviderFactory + { + IDelegatingHandlerHandlerProvider Get(Request.Request request); + } +} diff --git a/src/Ocelot/Requester/IHttpClientBuilder.cs b/src/Ocelot/Requester/IHttpClientBuilder.cs index 5ed30ef5..d6b43501 100644 --- a/src/Ocelot/Requester/IHttpClientBuilder.cs +++ b/src/Ocelot/Requester/IHttpClientBuilder.cs @@ -1,27 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Ocelot.Logging; -using Ocelot.Requester.QoS; -using System.Net; +using System.Net.Http; +using Ocelot.Configuration; namespace Ocelot.Requester { public interface IHttpClientBuilder { - /// - /// Sets a PollyCircuitBreakingDelegatingHandler . - /// - IHttpClientBuilder WithQos(IQoSProvider qosProvider, IOcelotLogger logger); - - /// /// Creates the - /// - /// Defines if http client should use cookie container - /// Defines if http client should allow auto redirect - IHttpClient Create(bool useCookies, bool allowAutoRedirect, bool isTracing, IServiceProvider provider); + /// + /// + IHttpClient Create(Request.Request request); } } diff --git a/src/Ocelot/Requester/OcelotHttpTracingHandler.cs b/src/Ocelot/Requester/OcelotHttpTracingHandler.cs index 6435a113..ff588fe8 100644 --- a/src/Ocelot/Requester/OcelotHttpTracingHandler.cs +++ b/src/Ocelot/Requester/OcelotHttpTracingHandler.cs @@ -8,7 +8,16 @@ using Butterfly.OpenTracing; namespace Ocelot.Requester { - public class OcelotHttpTracingHandler : DelegatingHandler + public interface ITracingHandler + { + } + + public class NoTracingHandler : DelegatingHandler, ITracingHandler + { + + } + + public class OcelotHttpTracingHandler : DelegatingHandler, ITracingHandler { private readonly IServiceTracer _tracer; private const string prefix_spanId = "ot-spanId"; diff --git a/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs b/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs index ce8eec1a..45b0ac12 100644 --- a/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs +++ b/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs @@ -16,10 +16,10 @@ namespace Ocelot.Requester public PollyCircuitBreakingDelegatingHandler( IQoSProvider qoSProvider, - IOcelotLogger logger) + IOcelotLoggerFactory loggerFactory) { _qoSProvider = qoSProvider; - _logger = logger; + _logger = loggerFactory.CreateLogger(); } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) diff --git a/src/Ocelot/Requester/UnableToFindDelegatingHandlerProviderError.cs b/src/Ocelot/Requester/UnableToFindDelegatingHandlerProviderError.cs new file mode 100644 index 00000000..fd8aeb9e --- /dev/null +++ b/src/Ocelot/Requester/UnableToFindDelegatingHandlerProviderError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Requester +{ + public class UnableToFindDelegatingHandlerProviderError : Error + { + public UnableToFindDelegatingHandlerProviderError(string message) + : base(message, OcelotErrorCode.UnableToFindDelegatingHandlerProviderError) + { + } + } +} diff --git a/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs b/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs index c21650f1..ef882a50 100644 --- a/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs +++ b/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Consul; using Ocelot.Infrastructure.Extensions; +using Ocelot.Logging; using Ocelot.Values; namespace Ocelot.ServiceDiscovery @@ -11,13 +12,18 @@ namespace Ocelot.ServiceDiscovery public class ConsulServiceDiscoveryProvider : IServiceDiscoveryProvider { private readonly ConsulRegistryConfiguration _consulConfig; + private readonly IOcelotLogger _logger; private readonly ConsulClient _consul; private const string VersionPrefix = "version-"; - public ConsulServiceDiscoveryProvider(ConsulRegistryConfiguration consulRegistryConfiguration) - { + public ConsulServiceDiscoveryProvider(ConsulRegistryConfiguration consulRegistryConfiguration, IOcelotLoggerFactory factory) + {; + _logger = factory.CreateLogger(); + var consulHost = string.IsNullOrEmpty(consulRegistryConfiguration?.HostName) ? "localhost" : consulRegistryConfiguration.HostName; + var consulPort = consulRegistryConfiguration?.Port ?? 8500; + _consulConfig = new ConsulRegistryConfiguration(consulHost, consulPort, consulRegistryConfiguration?.KeyOfServiceInConsul); _consul = new ConsulClient(config => @@ -30,7 +36,19 @@ namespace Ocelot.ServiceDiscovery { var queryResult = await _consul.Health.Service(_consulConfig.KeyOfServiceInConsul, string.Empty, true); - var services = queryResult.Response.Select(BuildService); + var services = new List(); + + foreach (var serviceEntry in queryResult.Response) + { + if (IsValid(serviceEntry)) + { + services.Add(BuildService(serviceEntry)); + } + else + { + _logger.LogError($"Unable to use service Address: {serviceEntry.Service.Address} and Port: {serviceEntry.Service.Port} as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"); + } + } return services.ToList(); } @@ -45,6 +63,16 @@ namespace Ocelot.ServiceDiscovery serviceEntry.Service.Tags ?? Enumerable.Empty()); } + private bool IsValid(ServiceEntry serviceEntry) + { + if (serviceEntry.Service.Address.Contains("http://") || serviceEntry.Service.Address.Contains("https://") || serviceEntry.Service.Port <= 0) + { + return false; + } + + return true; + } + private string GetVersionFromStrings(IEnumerable strings) { return strings diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs index eb73dae0..e8c97bd4 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs @@ -1,11 +1,19 @@ using System.Collections.Generic; using Ocelot.Configuration; +using Ocelot.Logging; using Ocelot.Values; namespace Ocelot.ServiceDiscovery { public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory { + private readonly IOcelotLoggerFactory _factory; + + public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory) + { + _factory = factory; + } + public IServiceDiscoveryProvider Get(ServiceProviderConfiguration serviceConfig, ReRoute reRoute) { if (reRoute.UseServiceDiscovery) @@ -28,7 +36,7 @@ namespace Ocelot.ServiceDiscovery private IServiceDiscoveryProvider GetServiceDiscoveryProvider(string keyOfServiceInConsul, string providerHostName, int providerPort) { var consulRegistryConfiguration = new ConsulRegistryConfiguration(providerHostName, providerPort, keyOfServiceInConsul); - return new ConsulServiceDiscoveryProvider(consulRegistryConfiguration); + return new ConsulServiceDiscoveryProvider(consulRegistryConfiguration, _factory); } } } diff --git a/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs new file mode 100644 index 00000000..bb2a43df --- /dev/null +++ b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class HttpDelegatingHandlersTests + { + private IWebHost _builder; + private readonly Steps _steps; + private string _downstreamPath; + + public HttpDelegatingHandlersTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_call_handlers() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 61879, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + var handlerOne = new FakeHandler(); + var handlerTwo = new FakeHandler(); + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:61879", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithHandlers(handlerOne, handlerTwo)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => ThenTheHandlersAreCalledCorrectly(handlerOne, handlerTwo)) + .BDDfy(); + } + + private void ThenTheHandlersAreCalledCorrectly(FakeHandler one, FakeHandler two) + { + one.TimeCalled.ShouldBeLessThan(two.TimeCalled); + } + + class FakeHandler : DelegatingHandler + { + + public DateTime TimeCalled { get; private set; } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + TimeCalled = DateTime.Now; + return await base.SendAsync(request, cancellationToken); + } + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + }) + .Build(); + + _builder.Start(); + } + + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index bd41f007..bcd017b4 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -88,6 +88,38 @@ namespace Ocelot.AcceptanceTests _ocelotClient = _ocelotServer.CreateClient(); } + /// + /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. + /// + public void GivenOcelotIsRunningWithHandlers(DelegatingHandler handlerOne, DelegatingHandler handlerTwo) + { + _webHostBuilder = new WebHostBuilder(); + + _webHostBuilder.ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddOcelot() + .AddDelegatingHandler(() => handlerOne) + .AddDelegatingHandler(() => handlerTwo); + }); + _webHostBuilder.ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddJsonFile("configuration.json"); + config.AddEnvironmentVariables(); + }).Configure(a => + { + a.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + /// /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. /// diff --git a/test/Ocelot.ManualTest/ManualTestStartup.cs b/test/Ocelot.ManualTest/ManualTestStartup.cs index 0a39154d..dd5cf979 100644 --- a/test/Ocelot.ManualTest/ManualTestStartup.cs +++ b/test/Ocelot.ManualTest/ManualTestStartup.cs @@ -1,13 +1,7 @@ -using System; -using CacheManager.Core; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Ocelot.DependencyInjection; using Ocelot.Middleware; -using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; namespace Ocelot.ManualTest { @@ -15,11 +9,6 @@ namespace Ocelot.ManualTest { public void ConfigureServices(IServiceCollection services) { - Action settings = (x) => - { - x.WithDictionaryHandle(); - }; - services.AddAuthentication() .AddJwtBearer("TestKey", x => { @@ -28,7 +17,10 @@ namespace Ocelot.ManualTest }); services.AddOcelot() - .AddCacheManager(settings) + .AddCacheManager(x => + { + x.WithDictionaryHandle(); + }) .AddOpenTracing(option => { option.CollectorUrl = "http://localhost:9618"; @@ -42,4 +34,4 @@ namespace Ocelot.ManualTest app.UseOcelot().Wait(); } } -} +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs index b12f8d6d..e5839856 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs @@ -1,4 +1,8 @@ -using CacheManager.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using CacheManager.Core; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Internal; using Microsoft.Extensions.Configuration; @@ -9,10 +13,9 @@ using Ocelot.Configuration.File; using Ocelot.Configuration.Setter; using Ocelot.DependencyInjection; using Ocelot.Requester; +using Ocelot.UnitTests.Requester; using Shouldly; using System; -using System.Collections.Generic; -using System.Linq; using TestStack.BDDfy; using Xunit; @@ -20,11 +23,11 @@ namespace Ocelot.UnitTests.DependencyInjection { public class OcelotBuilderTests { - private IServiceCollection _services; + private readonly IServiceCollection _services; private IServiceProvider _serviceProvider; - private IConfiguration _configRoot; + private readonly IConfiguration _configRoot; private IOcelotBuilder _ocelotBuilder; - private int _maxRetries; + private readonly int _maxRetries; public OcelotBuilderTests() { @@ -38,6 +41,19 @@ namespace Ocelot.UnitTests.DependencyInjection } private Exception _ex; + [Fact] + public void should_add_delegating_handlers() + { + var fakeOne = new FakeDelegatingHandler(0); + var fakeTwo = new FakeDelegatingHandler(1); + + this.Given(x => WhenISetUpOcelotServices()) + .When(x => AddDelegate(fakeOne)) + .And(x => AddDelegate(fakeTwo)) + .Then(x => ThenTheProviderIsRegisteredAndReturnsHandlers()) + .BDDfy(); + } + [Fact] public void should_set_up_services() { @@ -54,7 +70,7 @@ namespace Ocelot.UnitTests.DependencyInjection .BDDfy(); } - + [Fact] public void should_set_up_cache_manager() { @@ -74,7 +90,7 @@ namespace Ocelot.UnitTests.DependencyInjection .BDDfy(); } - [Fact] + [Fact] public void should_set_up_rafty() { this.Given(x => WhenISetUpOcelotServices()) @@ -119,6 +135,17 @@ namespace Ocelot.UnitTests.DependencyInjection path.Path.ShouldBe("/administration"); } + private void ThenTheProviderIsRegisteredAndReturnsHandlers() + { + _serviceProvider = _services.BuildServiceProvider(); + var provider = _serviceProvider.GetService(); + var handlers = provider.Get(); + var handler = (FakeDelegatingHandler)handlers[0].Invoke(); + handler.Order.ShouldBe(0); + handler = (FakeDelegatingHandler)handlers[1].Invoke(); + handler.Order.ShouldBe(1); + } + private void OnlyOneVersionOfEachCacheIsRegistered() { var outputCache = _services.Single(x => x.ServiceType == typeof(IOcelotCache)); @@ -157,6 +184,11 @@ namespace Ocelot.UnitTests.DependencyInjection } } + private void AddDelegate(DelegatingHandler handler) + { + _ocelotBuilder.AddDelegatingHandler(() => handler); + } + private void ThenAnOcelotBuilderIsReturned() { _ocelotBuilder.ShouldBeOfType(); diff --git a/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs index 0027e90b..b4d6acdc 100644 --- a/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs @@ -58,7 +58,7 @@ this.Given(x => x.GivenTheDownStreamUrlIs("any old string")) .And(x => x.GivenTheQosProviderHouseReturns(new OkResponse(new NoQoSProvider()))) .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) - .And(x => x.GivenTheRequestBuilderReturns(new Ocelot.Request.Request(new HttpRequestMessage(), true, new NoQoSProvider(), false, false,false))) + .And(x => x.GivenTheRequestBuilderReturns(new Ocelot.Request.Request(new HttpRequestMessage(), true, new NoQoSProvider(), false, false, "", false))) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheScopedDataRepositoryIsCalledCorrectly()) .BDDfy(); @@ -78,7 +78,7 @@ this.Given(x => x.GivenTheDownStreamUrlIs("any old string")) .And(x => x.GivenTheQosProviderHouseReturns(new ErrorResponse(It.IsAny()))) .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) - .And(x => x.GivenTheRequestBuilderReturns(new Ocelot.Request.Request(new HttpRequestMessage(), true, new NoQoSProvider(), false, false, false))) + .And(x => x.GivenTheRequestBuilderReturns(new Ocelot.Request.Request(new HttpRequestMessage(), true, new NoQoSProvider(), false, false, "", false))) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheScopedDataRepositoryQosProviderError()) .BDDfy(); @@ -132,8 +132,8 @@ It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny() - )) + It.IsAny(), + It.IsAny())) .ReturnsAsync(_request); } diff --git a/test/Ocelot.UnitTests/Request/HttpRequestCreatorTests.cs b/test/Ocelot.UnitTests/Request/HttpRequestCreatorTests.cs index a7f2650a..dbfc8643 100644 --- a/test/Ocelot.UnitTests/Request/HttpRequestCreatorTests.cs +++ b/test/Ocelot.UnitTests/Request/HttpRequestCreatorTests.cs @@ -1,4 +1,4 @@ -namespace Ocelot.UnitTests.Request +namespace Ocelot.UnitTests.Request { using System.Net.Http; @@ -17,9 +17,9 @@ private readonly HttpRequestMessage _requestMessage; private readonly bool _useCookieContainer; private readonly bool _allowAutoRedirect; - private readonly bool _useTracing; - private Response _response; + private string _reRouteKey; + private readonly bool _useTracing; public HttpRequestCreatorTests() { @@ -28,7 +28,7 @@ _qoSProvider = new NoQoSProvider(); _useCookieContainer = false; _allowAutoRedirect = false; - _useTracing = false; + _requestMessage = new HttpRequestMessage(); } @@ -48,7 +48,7 @@ private void WhenIBuildARequest() { _response = _requestCreator.Build(_requestMessage, - _isQos, _qoSProvider, _useCookieContainer, _allowAutoRedirect, _useTracing) + _isQos, _qoSProvider, _useCookieContainer, _allowAutoRedirect, _reRouteKey, _useTracing) .GetAwaiter() .GetResult(); } diff --git a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerHouseTests.cs b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerHouseTests.cs new file mode 100644 index 00000000..f78917d4 --- /dev/null +++ b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerHouseTests.cs @@ -0,0 +1,108 @@ +using System; +using System.Net.Http; +using Moq; +using Ocelot.Requester; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class DelegatingHandlerHandlerHouseTests + { + private readonly DelegatingHandlerHandlerHouse _house; + private Mock _factory; + private readonly Mock _provider; + private Ocelot.Request.Request _request; + private Response _result; + + public DelegatingHandlerHandlerHouseTests() + { + _provider = new Mock(); + _factory = new Mock(); + _house = new DelegatingHandlerHandlerHouse(_factory.Object); + } + + [Fact] + public void should_create_and_store_provider() + { + var request = new Ocelot.Request.Request(new HttpRequestMessage(), true, null, true, true, "key", false); + + this.Given(x => GivenTheRequest(request)) + .And(x => GivenTheProviderReturns()) + .When(x => WhenIGet()) + .Then(x => ThenTheFactoryIsCalled(1)) + .And(x => ThenTheProviderIsNotNull()) + .BDDfy(); + } + + [Fact] + public void should_get_provider() + { + var request = new Ocelot.Request.Request(new HttpRequestMessage(), true, null, true, true, "key", false); + + this.Given(x => GivenTheRequest(request)) + .And(x => GivenTheProviderReturns()) + .And(x => WhenIGet()) + .And(x => GivenTheFactoryIsCleared()) + .When(x => WhenIGet()) + .Then(x => ThenTheFactoryIsCalled(0)) + .And(x => ThenTheProviderIsNotNull()) + .BDDfy(); + } + + [Fact] + public void should_return_error() + { + var request = new Ocelot.Request.Request(new HttpRequestMessage(), true, null, true, true, "key", false); + + this.Given(x => GivenTheRequest(request)) + .And(x => GivenTheProviderThrows()) + .When(x => WhenIGet()) + .And(x => ThenAnErrorIsReturned()) + .BDDfy(); + } + + private void ThenAnErrorIsReturned() + { + _result.IsError.ShouldBeTrue(); + _result.Errors[0].ShouldBeOfType(); + } + + private void GivenTheProviderThrows() + { + _factory.Setup(x => x.Get(It.IsAny())).Throws(); + } + + private void GivenTheFactoryIsCleared() + { + _factory = new Mock(); + } + + private void ThenTheProviderIsNotNull() + { + _result.Data.ShouldBe(_provider.Object); + } + + private void WhenIGet() + { + _result = _house.Get(_request); + } + + private void GivenTheRequest(Ocelot.Request.Request request) + { + _request = request; + } + + private void GivenTheProviderReturns() + { + _factory.Setup(x => x.Get(It.IsAny())).Returns(_provider.Object); + } + + private void ThenTheFactoryIsCalled(int times) + { + _factory.Verify(x => x.Get(_request), Times.Exactly(times)); + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs new file mode 100644 index 00000000..e64df1b9 --- /dev/null +++ b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Moq; +using Ocelot.Logging; +using Ocelot.Requester; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class DelegatingHandlerHandlerProviderFactoryTests + { + private readonly DelegatingHandlerHandlerProviderFactory _factory; + private Mock _loggerFactory; + private Ocelot.Request.Request _request; + private IDelegatingHandlerHandlerProvider _provider; + private readonly Mock _allRoutesProvider; + + public DelegatingHandlerHandlerProviderFactoryTests() + { + _allRoutesProvider = new Mock(); + _loggerFactory = new Mock(); + _factory = new DelegatingHandlerHandlerProviderFactory(_loggerFactory.Object, _allRoutesProvider.Object, null); + } + + [Fact] + public void should_all_from_all_routes_provider_and_qos() + { + var handlers = new List> + { + () => new FakeDelegatingHandler(0), + () => new FakeDelegatingHandler(1) + }; + + var request = new Ocelot.Request.Request(new HttpRequestMessage(), true, null, true, true, "", false); + + this.Given(x => GivenTheFollowingRequest(request)) + .And(x => GivenTheAllRoutesProviderReturns(handlers)) + .When(x => WhenIGet()) + .Then(x => ThenThereIsDelegatesInProvider(3)) + .And(x => ThenTheDelegatesAreAddedCorrectly()) + .And(x => ThenItIsPolly(2)) + .BDDfy(); + } + + [Fact] + public void should_return_provider_with_no_delegates() + { + var request = new Ocelot.Request.Request(new HttpRequestMessage(), false, null, true, true, "", false); + + this.Given(x => GivenTheFollowingRequest(request)) + .And(x => GivenTheAllRoutesProviderReturns()) + .When(x => WhenIGet()) + .Then(x => ThenNoDelegatesAreInTheProvider()) + .BDDfy(); + } + + [Fact] + public void should_return_provider_with_qos_delegate() + { + var request = new Ocelot.Request.Request(new HttpRequestMessage(), true, null, true, true, "", false); + + this.Given(x => GivenTheFollowingRequest(request)) + .And(x => GivenTheAllRoutesProviderReturns()) + .When(x => WhenIGet()) + .Then(x => ThenThereIsDelegatesInProvider(1)) + .And(x => ThenItIsPolly(0)) + .BDDfy(); + } + + private void ThenTheDelegatesAreAddedCorrectly() + { + var delegates = _provider.Get(); + var del = delegates[0].Invoke(); + var handler = (FakeDelegatingHandler) del; + handler.Order.ShouldBe(0); + + del = delegates[1].Invoke(); + handler = (FakeDelegatingHandler)del; + handler.Order.ShouldBe(1); + } + + private void GivenTheAllRoutesProviderReturns() + { + _allRoutesProvider.Setup(x => x.Get()).Returns(new List>()); + } + + private void GivenTheAllRoutesProviderReturns(List> handlers) + { + _allRoutesProvider.Setup(x => x.Get()).Returns(handlers); + } + + private void ThenItIsPolly(int i) + { + var delegates = _provider.Get(); + var del = delegates[i].Invoke(); + del.ShouldBeOfType(); + } + + private void ThenThereIsDelegatesInProvider(int count) + { + _provider.ShouldNotBeNull(); + _provider.Get().Count.ShouldBe(count); + } + + private void GivenTheFollowingRequest(Ocelot.Request.Request request) + { + _request = request; + } + + private void WhenIGet() + { + _provider = _factory.Get(_request); + } + + private void ThenNoDelegatesAreInTheProvider() + { + _provider.ShouldNotBeNull(); + _provider.Get().Count.ShouldBe(0); + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderTests.cs b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderTests.cs new file mode 100644 index 00000000..d93e291a --- /dev/null +++ b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderTests.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Ocelot.Requester; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class DelegatingHandlerHandlerProviderTests + { + private readonly DelegatingHandlerHandlerProvider _provider; + private List> _handlers; + + public DelegatingHandlerHandlerProviderTests() + { + _provider = new DelegatingHandlerHandlerProvider(); + } + + [Fact] + public void should_return_empty_list() + { + this.When(x => WhenIGet()) + .Then(x => ThenAnEmptyListIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_get_delegating_handlers_in_order_first_in_first_out() + { + this.Given(x => GivenTheHandlers()) + .When(x => WhenIGet()) + .Then(x => ThenTheHandlersAreReturnedInOrder()) + .BDDfy(); + } + + private void ThenAnEmptyListIsReturned() + { + _handlers.Count.ShouldBe(0); + } + + private void ThenTheHandlersAreReturnedInOrder() + { + var handler = (FakeDelegatingHandler)_handlers[0].Invoke(); + handler.Order.ShouldBe(0); + handler = (FakeDelegatingHandler)_handlers[1].Invoke(); + handler.Order.ShouldBe(1); + } + + private void WhenIGet() + { + _handlers = _provider.Get(); + } + + private void GivenTheHandlers() + { + _provider.Add(() => new FakeDelegatingHandler(0)); + _provider.Add(() => new FakeDelegatingHandler(1)); + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/FakeDelegatingHandler.cs b/test/Ocelot.UnitTests/Requester/FakeDelegatingHandler.cs new file mode 100644 index 00000000..545e956e --- /dev/null +++ b/test/Ocelot.UnitTests/Requester/FakeDelegatingHandler.cs @@ -0,0 +1,28 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Ocelot.UnitTests.Requester +{ + public class FakeDelegatingHandler : DelegatingHandler + { + public FakeDelegatingHandler() + { + + } + + public FakeDelegatingHandler(int order) + { + Order = order; + } + public int Order {get;private set;} + public DateTime TimeCalled {get;private set;} + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + TimeCalled = DateTime.Now; + return new HttpResponseMessage(); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs b/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs new file mode 100644 index 00000000..6081590a --- /dev/null +++ b/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Moq; +using Ocelot.Requester; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class HttpClientBuilderTests + { + private readonly HttpClientBuilder _builder; + private readonly Mock _house; + private readonly Mock _provider; + private IHttpClientBuilder _builderResult; + private IHttpClient _httpClient; + private HttpResponseMessage _response; + private Ocelot.Request.Request _request; + + public HttpClientBuilderTests() + { + _provider = new Mock(); + _house = new Mock(); + _builder = new HttpClientBuilder(_house.Object); + } + + [Fact] + public void should_build_http_client() + { + this.Given(x => GivenTheProviderReturns()) + .And(x => GivenARequest()) + .And(x => GivenTheHouseReturns()) + .When(x => WhenIBuild()) + .Then(x => ThenTheHttpClientShouldNotBeNull()) + .BDDfy(); + } + + [Fact] + public void should_call_delegating_handlers_in_order() + { + var fakeOne = new FakeDelegatingHandler(); + var fakeTwo = new FakeDelegatingHandler(); + + var handlers = new List>() + { + () => fakeOne, + () => fakeTwo + }; + + this.Given(x => GivenTheProviderReturns(handlers)) + .And(x => GivenARequest()) + .And(x => GivenTheHouseReturns()) + .And(x => WhenIBuild()) + .When(x => WhenICallTheClient()) + .Then(x => ThenTheFakeAreHandledInOrder(fakeOne, fakeTwo)) + .And(x => ThenSomethingIsReturned()) + .BDDfy(); + } + + private void GivenARequest() + { + _request = new Ocelot.Request.Request(null, false, null, false, false, "", false); + } + + private void GivenTheHouseReturns() + { + _house + .Setup(x => x.Get(It.IsAny())) + .Returns(new OkResponse(_provider.Object)); + } + + private void ThenSomethingIsReturned() + { + _response.ShouldNotBeNull(); + } + + private void WhenICallTheClient() + { + _response = _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "http://test.com")).GetAwaiter().GetResult(); + } + + private void ThenTheFakeAreHandledInOrder(FakeDelegatingHandler fakeOne, FakeDelegatingHandler fakeTwo) + { + fakeOne.TimeCalled.ShouldBeGreaterThan(fakeTwo.TimeCalled); + } + + private void GivenTheProviderReturns() + { + _provider + .Setup(x => x.Get()) + .Returns(new List>(){ () => new FakeDelegatingHandler()}); + } + + private void GivenTheProviderReturns(List> handlers) + { + _provider + .Setup(x => x.Get()) + .Returns(handlers); + } + + private void WhenIBuild() + { + _httpClient = _builder.Create(_request); + } + + private void ThenTheHttpClientShouldNotBeNull() + { + _httpClient.ShouldNotBeNull(); + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs b/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs index 2d649cad..29271671 100644 --- a/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs +++ b/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs @@ -10,13 +10,15 @@ using System.Net.Http; using System.Text; using TestStack.BDDfy; using Xunit; +using Shouldly; namespace Ocelot.UnitTests.Requester { public class HttpClientHttpRequesterTest { private readonly Mock _cacheHandlers; - private Mock _serviceProvider; + private Mock _house; + private Mock _provider; private Response _response; private readonly HttpClientHttpRequester _httpClientRequester; private Ocelot.Request.Request _request; @@ -25,29 +27,32 @@ namespace Ocelot.UnitTests.Requester public HttpClientHttpRequesterTest() { - _serviceProvider = new Mock(); + _provider = new Mock(); + _provider.Setup(x => x.Get()).Returns(new List>()); + _house = new Mock(); + _house.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse(_provider.Object)); _logger = new Mock(); _loggerFactory = new Mock(); _loggerFactory .Setup(x => x.CreateLogger()) .Returns(_logger.Object); _cacheHandlers = new Mock(); - _httpClientRequester = new HttpClientHttpRequester(_loggerFactory.Object, _cacheHandlers.Object, _serviceProvider.Object); + _httpClientRequester = new HttpClientHttpRequester(_loggerFactory.Object, _cacheHandlers.Object, _house.Object); } [Fact] public void should_call_request_correctly() { - this.Given(x=>x.GivenTheRequestIs(new Ocelot.Request.Request(new HttpRequestMessage() { RequestUri = new Uri("http://www.bbc.co.uk") }, false, new NoQoSProvider(), false, false, false))) + this.Given(x=>x.GivenTheRequestIs(new Ocelot.Request.Request(new HttpRequestMessage() { RequestUri = new Uri("http://www.bbc.co.uk") }, false, new NoQoSProvider(), false, false, "", false))) .When(x=>x.WhenIGetResponse()) .Then(x => x.ThenTheResponseIsCalledCorrectly()) .BDDfy(); } [Fact] - public void should_call_request_UnableToCompleteRequest() + public void should_call_request_unable_to_complete_request() { - this.Given(x => x.GivenTheRequestIs(new Ocelot.Request.Request(new HttpRequestMessage() { RequestUri = new Uri("http://localhost:60080") }, false, new NoQoSProvider(), false, false, false))) + this.Given(x => x.GivenTheRequestIs(new Ocelot.Request.Request(new HttpRequestMessage() { RequestUri = new Uri("http://localhost:60080") }, false, new NoQoSProvider(), false, false, "", false))) .When(x => x.WhenIGetResponse()) .Then(x => x.ThenTheResponseIsCalledError()) .BDDfy(); @@ -65,12 +70,12 @@ namespace Ocelot.UnitTests.Requester private void ThenTheResponseIsCalledCorrectly() { - Assert.True(_response.IsError == false); + _response.IsError.ShouldBeFalse(); } private void ThenTheResponseIsCalledError() { - Assert.True(_response.IsError == true); + _response.IsError.ShouldBeTrue(); } } } diff --git a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs index 8f51cb7d..5658984a 100644 --- a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs @@ -1,4 +1,4 @@ -namespace Ocelot.UnitTests.Requester +namespace Ocelot.UnitTests.Requester { using System.Net.Http; using Microsoft.AspNetCore.Builder; @@ -28,7 +28,7 @@ [Fact] public void should_call_scoped_data_repository_correctly() { - this.Given(x => x.GivenTheRequestIs(new Ocelot.Request.Request(new HttpRequestMessage(),true, new NoQoSProvider(), false, false,false))) + this.Given(x => x.GivenTheRequestIs(new Ocelot.Request.Request(new HttpRequestMessage(),true, new NoQoSProvider(), false, false, "", false))) .And(x => x.GivenTheRequesterReturns(new HttpResponseMessage())) .And(x => x.GivenTheScopedRepoReturns()) .When(x => x.WhenICallTheMiddleware()) diff --git a/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs b/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs index 71929404..b3c91c5b 100644 --- a/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs +++ b/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs @@ -1,10 +1,8 @@ using Moq; using Ocelot.Configuration; using Ocelot.Configuration.Builder; -using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.Requester.QoS; using Ocelot.Responses; -using Ocelot.UnitTests.LoadBalancer; using Shouldly; using TestStack.BDDfy; using Xunit; diff --git a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs index 06eebb4b..9922df6a 100644 --- a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs +++ b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs @@ -121,7 +121,7 @@ namespace Ocelot.UnitTests.Responder // If this test fails then it's because the number of error codes has changed. // You should make the appropriate changes to the test cases here to ensure // they cover all the error codes, and then modify this assertion. - Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(32, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); + Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(33, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); } private void ShouldMapErrorToStatusCode(OcelotErrorCode errorCode, HttpStatusCode expectedHttpStatusCode) diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ConsulServiceDiscoveryProviderTests.cs new file mode 100644 index 00000000..a272d1b4 --- /dev/null +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ConsulServiceDiscoveryProviderTests.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Consul; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Moq; +using Ocelot.Logging; +using Ocelot.ServiceDiscovery; +using Ocelot.Values; +using Xunit; +using TestStack.BDDfy; +using Shouldly; + +namespace Ocelot.UnitTests.ServiceDiscovery +{ + public class ConsulServiceDiscoveryProviderTests : IDisposable + { + private IWebHost _fakeConsulBuilder; + private readonly List _serviceEntries; + private readonly ConsulServiceDiscoveryProvider _provider; + private readonly string _serviceName; + private readonly int _port; + private readonly string _consulHost; + private readonly string _fakeConsulServiceDiscoveryUrl; + private List _services; + private Mock _factory; + private readonly Mock _logger; + + public ConsulServiceDiscoveryProviderTests() + { + _serviceName = "test"; + _port = 8500; + _consulHost = "localhost"; + _fakeConsulServiceDiscoveryUrl = $"http://{_consulHost}:{_port}"; + _serviceEntries = new List(); + + _factory = new Mock(); + _logger = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + + var config = new ConsulRegistryConfiguration(_consulHost, _port, _serviceName); + _provider = new ConsulServiceDiscoveryProvider(config, _factory.Object); + } + + [Fact] + public void should_return_service_from_consul() + { + var serviceEntryOne = new ServiceEntry() + { + Service = new AgentService() + { + Service = _serviceName, + Address = "localhost", + Port = 50881, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + + this.Given(x =>GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) + .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .When(x => WhenIGetTheServices()) + .Then(x => ThenTheCountIs(1)) + .BDDfy(); + } + + [Fact] + public void should_not_return_services_with_invalid_address() + { + var serviceEntryOne = new ServiceEntry() + { + Service = new AgentService() + { + Service = _serviceName, + Address = "http://localhost", + Port = 50881, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + + var serviceEntryTwo = new ServiceEntry() + { + Service = new AgentService() + { + Service = _serviceName, + Address = "http://localhost", + Port = 50888, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + + this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) + .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .When(x => WhenIGetTheServices()) + .Then(x => ThenTheCountIs(0)) + .And(x => ThenTheLoggerHasBeenCalledCorrectlyForInvalidAddress()) + .BDDfy(); + } + + [Fact] + public void should_not_return_services_with_invalid_port() + { + var serviceEntryOne = new ServiceEntry() + { + Service = new AgentService() + { + Service = _serviceName, + Address = "localhost", + Port = -1, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + + var serviceEntryTwo = new ServiceEntry() + { + Service = new AgentService() + { + Service = _serviceName, + Address = "localhost", + Port = 0, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + + this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) + .And(x => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .When(x => WhenIGetTheServices()) + .Then(x => ThenTheCountIs(0)) + .And(x => ThenTheLoggerHasBeenCalledCorrectlyForInvalidPorts()) + .BDDfy(); + } + + private void ThenTheLoggerHasBeenCalledCorrectlyForInvalidAddress() + { + _logger.Verify( + x => x.LogError( + "Unable to use service Address: http://localhost and Port: 50881 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"), + Times.Once); + + _logger.Verify( + x => x.LogError( + "Unable to use service Address: http://localhost and Port: 50888 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"), + Times.Once); + } + + private void ThenTheLoggerHasBeenCalledCorrectlyForInvalidPorts() + { + _logger.Verify( + x => x.LogError( + "Unable to use service Address: localhost and Port: -1 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"), + Times.Once); + + _logger.Verify( + x => x.LogError( + "Unable to use service Address: localhost and Port: 0 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"), + Times.Once); + } + + private void ThenTheCountIs(int count) + { + _services.Count.ShouldBe(count); + } + + private void WhenIGetTheServices() + { + _services = _provider.Get().GetAwaiter().GetResult(); + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach (var serviceEntry in serviceEntries) + { + _serviceEntries.Add(serviceEntry); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) + { + _fakeConsulBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") + { + await context.Response.WriteJsonAsync(_serviceEntries); + } + }); + }) + .Build(); + + _fakeConsulBuilder.Start(); + } + + public void Dispose() + { + _fakeConsulBuilder?.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs index 55ecf502..625ffaa3 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using Moq; using Ocelot.Configuration; using Ocelot.Configuration.Builder; +using Ocelot.Logging; using Ocelot.ServiceDiscovery; using Shouldly; using TestStack.BDDfy; @@ -15,10 +17,12 @@ namespace Ocelot.UnitTests.ServiceDiscovery private IServiceDiscoveryProvider _result; private readonly ServiceDiscoveryProviderFactory _factory; private ReRoute _reRoute; + private Mock _loggerFactory; public ServiceProviderFactoryTests() { - _factory = new ServiceDiscoveryProviderFactory(); + _loggerFactory = new Mock(); + _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object); } [Fact] @@ -104,4 +108,4 @@ namespace Ocelot.UnitTests.ServiceDiscovery _result.ShouldBeOfType(); } } -} \ No newline at end of file +}