diff --git a/README.md b/README.md index 0c1c880c..a1793f62 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,7 @@ A quick list of Ocelot's capabilities for more information see the [documentatio ## How to install -Ocelot is designed to work with ASP.NET core only and is currently -built to netcoreapp2.0 [this](https://docs.microsoft.com/en-us/dotnet/articles/standard/library) documentation may prove helpful when working out if Ocelot would be suitable for you. +Ocelot is designed to work with ASP.NET Core only and it targets `netstandard2.0`. This means it can be used anywhere `.NET Standard 2.0` is supported, including `.NET Core 2.0` and `.NET Framework 4.6.1` and up. [This](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) documentation may prove helpful when working out if Ocelot would be suitable for you. Install Ocelot and it's dependencies using NuGet. diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 4e11e7ed..220ae2bc 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -1,174 +1,193 @@ -Configuration -============ - -An example configuration can be found `here `_. -There are two sections to the configuration. An array of ReRoutes and a GlobalConfiguration. -The ReRoutes are the objects that tell Ocelot how to treat an upstream request. The Global -configuration is a bit hacky and allows overrides of ReRoute specific settings. It's useful -if you don't want to manage lots of ReRoute specific settings. - -.. code-block:: json - - { - "ReRoutes": [], - "GlobalConfiguration": {} - } - -Here is an example ReRoute configuration, You don't need to set all of these things but this is everything that is available at the moment: - -.. code-block:: json - - { - "DownstreamPathTemplate": "/", - "UpstreamPathTemplate": "/", - "UpstreamHttpMethod": [ - "Get" - ], - "AddHeadersToRequest": {}, - "AddClaimsToRequest": {}, - "RouteClaimsRequirement": {}, - "AddQueriesToRequest": {}, - "RequestIdKey": "", - "FileCacheOptions": { - "TtlSeconds": 0, - "Region": "" - }, - "ReRouteIsCaseSensitive": false, - "ServiceName": "", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 51876, - } - ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 0, - "DurationOfBreak": 0, - "TimeoutValue": 0 - }, - "LoadBalancer": "", - "RateLimitOptions": { - "ClientWhitelist": [], - "EnableRateLimiting": false, - "Period": "", - "PeriodTimespan": 0, - "Limit": 0 - }, - "AuthenticationOptions": { - "AuthenticationProviderKey": "", - "AllowedScopes": [] - }, - "HttpHandlerOptions": { - "AllowAutoRedirect": true, - "UseCookieContainer": true, - "UseTracing": true - }, - "UseServiceDiscovery": false, - "DangerousAcceptAnyServerCertificateValidator": false - } - -More information on how to use these options is below.. - -Multiple environments -^^^^^^^^^^^^^^^^^^^^^ - -Like any other asp.net core project Ocelot supports configuration file names such as configuration.dev.json, configuration.test.json etc. In order to implement this add the following -to you - -.. code-block:: csharp - - .ConfigureAppConfiguration((hostingContext, config) => - { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("ocelot.json") - .AddJsonFile($"configuration.{hostingContext.HostingEnvironment.EnvironmentName}.json") - .AddEnvironmentVariables(); - }) - -Ocelot will now use the environment specific configuration and fall back to ocelot.json if there isnt one. - -You also need to set the corresponding environment variable which is ASPNETCORE_ENVIRONMENT. More info on this can be found in the `asp.net core docs `_. - -Merging configuration files -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This feature was requested in `Issue 296 `_ and allows users to have multiple configuration files to make managing large configurations easier. - -Instead of adding the configuration directly e.g. AddJsonFile("ocelot.json") you can call AddOcelot() like below. - -.. code-block:: csharp - - .ConfigureAppConfiguration((hostingContext, config) => - { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddOcelot() - .AddEnvironmentVariables(); - }) - -In this scenario Ocelot will look for any files that match the pattern (?i)ocelot.([a-zA-Z0-9]*).json and then merge these together. If you want to set the GlobalConfiguration property you must have a file called ocelot.global.json. - -The way Ocelot merges the files is basically load them, loop over them, add any ReRoutes, add any AggregateReRoutes and if the file is called ocelot.global.json add the GlobalConfiguration aswell as any ReRoutes or AggregateReRoutes. Ocelot will then save the merged configuration to a file called ocelot.json and this will be used as the source of truth while ocelot is running. - -At the moment there is no validation at this stage it only happens when Ocelot validates the final merged configuration. This is something to be aware of when you are investigating problems. I would advise always checking what is in ocelot.json if you have any problems. - -Store configuration in consul -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If you add the following when you register your services Ocelot will attempt to store and retrieve its configuration in consul KV store. - -.. code-block:: csharp - - services - .AddOcelot() - .AddStoreOcelotConfigurationInConsul(); - -You also need to add the following to your ocelot.json. This is how Ocelot -finds your Consul agent and interacts to load and store the configuration from Consul. - -.. code-block:: json - - "GlobalConfiguration": { - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 9500 - } - } - -I decided to create this feature after working on the raft consensus algorithm and finding out its super hard. Why not take advantage of the fact Consul already gives you this! -I guess it means if you want to use Ocelot to its fullest you take on Consul as a dependency for now. - -This feature has a 3 second ttl cache before making a new request to your local consul agent. - -Follow Redirects / Use CookieContainer -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Use HttpHandlerOptions in ReRoute configuration to set up HttpHandler behavior: - -1. AllowAutoRedirect is a value that indicates whether the request should follow redirection responses. Set it true if the request should automatically -follow redirection responses from the Downstream resource; otherwise false. The default value is false. -2. UseCookieContainer is a value that indicates whether the handler uses the CookieContainer -property to store server cookies and uses these cookies when sending requests. The default value is false. Please note -that if you are using the CookieContainer Ocelot caches the HttpClient for each downstream service. This means that all requests -to that DownstreamService will share the same cookies. `Issue 274 `_ was created because a user -noticed that the cookies were being shared. I tried to think of a nice way to handle this but I think it is impossible. If you don't cache the clients -that means each request gets a new client and therefore a new cookie container. If you clear the cookies from the cached client container you get race conditions due to inflight -requests. This would also mean that subsequent requests dont use the cookies from the previous response! All in all not a great situation. I would avoid setting -UseCookieContainer to true unless you have a really really good reason. Just look at your response headers and forward the cookies back with your next request! - -SSL Errors -^^^^^^^^^^ - -Id you want to ignore SSL warnings / errors set the following in your ReRoute config. - -.. code-block:: json - - "DangerousAcceptAnyServerCertificateValidator": false - -I don't reccomend doing this, I suggest creating your own certificate and then getting it trusted by your local / remote machine if you can. \ No newline at end of file +Configuration +============ + +An example configuration can be found `here `_. +There are two sections to the configuration. An array of ReRoutes and a GlobalConfiguration. +The ReRoutes are the objects that tell Ocelot how to treat an upstream request. The Global +configuration is a bit hacky and allows overrides of ReRoute specific settings. It's useful +if you don't want to manage lots of ReRoute specific settings. + +.. code-block:: json + + { + "ReRoutes": [], + "GlobalConfiguration": {} + } + +Here is an example ReRoute configuration, You don't need to set all of these things but this is everything that is available at the moment: + +.. code-block:: json + + { + "DownstreamPathTemplate": "/", + "UpstreamPathTemplate": "/", + "UpstreamHttpMethod": [ + "Get" + ], + "AddHeadersToRequest": {}, + "AddClaimsToRequest": {}, + "RouteClaimsRequirement": {}, + "AddQueriesToRequest": {}, + "RequestIdKey": "", + "FileCacheOptions": { + "TtlSeconds": 0, + "Region": "" + }, + "ReRouteIsCaseSensitive": false, + "ServiceName": "", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 51876, + } + ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 0, + "DurationOfBreak": 0, + "TimeoutValue": 0 + }, + "LoadBalancer": "", + "RateLimitOptions": { + "ClientWhitelist": [], + "EnableRateLimiting": false, + "Period": "", + "PeriodTimespan": 0, + "Limit": 0 + }, + "AuthenticationOptions": { + "AuthenticationProviderKey": "", + "AllowedScopes": [] + }, + "HttpHandlerOptions": { + "AllowAutoRedirect": true, + "UseCookieContainer": true, + "UseTracing": true + }, + "UseServiceDiscovery": false, + "DangerousAcceptAnyServerCertificateValidator": false + } + +More information on how to use these options is below.. + +Multiple environments +^^^^^^^^^^^^^^^^^^^^^ + +Like any other asp.net core project Ocelot supports configuration file names such as configuration.dev.json, configuration.test.json etc. In order to implement this add the following +to you + +.. code-block:: csharp + + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json") + .AddJsonFile($"configuration.{hostingContext.HostingEnvironment.EnvironmentName}.json") + .AddEnvironmentVariables(); + }) + +Ocelot will now use the environment specific configuration and fall back to ocelot.json if there isnt one. + +You also need to set the corresponding environment variable which is ASPNETCORE_ENVIRONMENT. More info on this can be found in the `asp.net core docs `_. + +Merging configuration files +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This feature was requested in `Issue 296 `_ and allows users to have multiple configuration files to make managing large configurations easier. + +Instead of adding the configuration directly e.g. AddJsonFile("ocelot.json") you can call AddOcelot() like below. + +.. code-block:: csharp + + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddOcelot() + .AddEnvironmentVariables(); + }) + +In this scenario Ocelot will look for any files that match the pattern (?i)ocelot.([a-zA-Z0-9]*).json and then merge these together. If you want to set the GlobalConfiguration property you must have a file called ocelot.global.json. + +The way Ocelot merges the files is basically load them, loop over them, add any ReRoutes, add any AggregateReRoutes and if the file is called ocelot.global.json add the GlobalConfiguration aswell as any ReRoutes or AggregateReRoutes. Ocelot will then save the merged configuration to a file called ocelot.json and this will be used as the source of truth while ocelot is running. + +At the moment there is no validation at this stage it only happens when Ocelot validates the final merged configuration. This is something to be aware of when you are investigating problems. I would advise always checking what is in ocelot.json if you have any problems. + +Store configuration in consul +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you add the following when you register your services Ocelot will attempt to store and retrieve its configuration in consul KV store. + +.. code-block:: csharp + + services + .AddOcelot() + .AddStoreOcelotConfigurationInConsul(); + +You also need to add the following to your ocelot.json. This is how Ocelot +finds your Consul agent and interacts to load and store the configuration from Consul. + +.. code-block:: json + + "GlobalConfiguration": { + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 9500 + } + } + +I decided to create this feature after working on the raft consensus algorithm and finding out its super hard. Why not take advantage of the fact Consul already gives you this! +I guess it means if you want to use Ocelot to its fullest you take on Consul as a dependency for now. + +This feature has a 3 second ttl cache before making a new request to your local consul agent. + +Configuration Key +----------------- + +If you are using Consul for configuration (or other providers in the future) you might want to key your configurations so you can have multiple configurations :) This feature was requested in `issue 346 `_! In order to specify the key you need to set the ConfigurationKey property in the ServiceDiscoveryProvider section of the configuration json file e.g. + +.. code-block:: json + + "GlobalConfiguration": { + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 9500, + "ConfigurationKey": "Oceolot_A" + } + } + +In this example Ocelot will use Oceolot_A as the key for your configuration when looking it up in Consul. + +If you do not set the ConfigurationKey Ocelot will use the string InternalConfiguration as the key. + +Follow Redirects / Use CookieContainer +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use HttpHandlerOptions in ReRoute configuration to set up HttpHandler behavior: + +1. AllowAutoRedirect is a value that indicates whether the request should follow redirection responses. Set it true if the request should automatically +follow redirection responses from the Downstream resource; otherwise false. The default value is false. +2. UseCookieContainer is a value that indicates whether the handler uses the CookieContainer +property to store server cookies and uses these cookies when sending requests. The default value is false. Please note +that if you are using the CookieContainer Ocelot caches the HttpClient for each downstream service. This means that all requests +to that DownstreamService will share the same cookies. `Issue 274 `_ was created because a user +noticed that the cookies were being shared. I tried to think of a nice way to handle this but I think it is impossible. If you don't cache the clients +that means each request gets a new client and therefore a new cookie container. If you clear the cookies from the cached client container you get race conditions due to inflight +requests. This would also mean that subsequent requests dont use the cookies from the previous response! All in all not a great situation. I would avoid setting +UseCookieContainer to true unless you have a really really good reason. Just look at your response headers and forward the cookies back with your next request! + +SSL Errors +^^^^^^^^^^ + +Id you want to ignore SSL warnings / errors set the following in your ReRoute config. + +.. code-block:: json + + "DangerousAcceptAnyServerCertificateValidator": false + +I don't reccomend doing this, I suggest creating your own certificate and then getting it trusted by your local / remote machine if you can. diff --git a/docs/introduction/gettingstarted.rst b/docs/introduction/gettingstarted.rst index 089acc4f..f523ea1b 100644 --- a/docs/introduction/gettingstarted.rst +++ b/docs/introduction/gettingstarted.rst @@ -2,14 +2,14 @@ Getting Started =============== Ocelot is designed to work with .NET Core only and is currently -built to netcoreapp2.0 `this `_ documentation may prove helpful when working out if Ocelot would be suitable for you. +built to netstandard2.0 `this `_ documentation may prove helpful when working out if Ocelot would be suitable for you. .NET Core 2.0 ^^^^^^^^^^^^^ **Install NuGet package** -Install Ocelot and it's dependencies using nuget. You will need to create a netcoreapp2.0 project and bring the package into it. Then follow the Startup below and :doc:`../features/configuration` sections +Install Ocelot and it's dependencies using nuget. You will need to create a netstandard2.0 project and bring the package into it. Then follow the Startup below and :doc:`../features/configuration` sections to get up and running. ``Install-Package Ocelot`` diff --git a/src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs b/src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs index cd8446a5..25df651c 100644 --- a/src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs +++ b/src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs @@ -1,39 +1,46 @@ -namespace Ocelot.Configuration.Builder -{ - public class ServiceProviderConfigurationBuilder - { - private string _serviceDiscoveryProviderHost; - private int _serviceDiscoveryProviderPort; - private string _type; - private string _token; - - public ServiceProviderConfigurationBuilder WithHost(string serviceDiscoveryProviderHost) - { - _serviceDiscoveryProviderHost = serviceDiscoveryProviderHost; - return this; - } - - public ServiceProviderConfigurationBuilder WithPort(int serviceDiscoveryProviderPort) - { - _serviceDiscoveryProviderPort = serviceDiscoveryProviderPort; - return this; - } - - public ServiceProviderConfigurationBuilder WithType(string type) - { - _type = type; - return this; - } - - public ServiceProviderConfigurationBuilder WithToken(string token) - { - _token = token; - return this; - } - - public ServiceProviderConfiguration Build() - { - return new ServiceProviderConfiguration(_type, _serviceDiscoveryProviderHost, _serviceDiscoveryProviderPort, _token); - } - } -} +namespace Ocelot.Configuration.Builder +{ + public class ServiceProviderConfigurationBuilder + { + private string _serviceDiscoveryProviderHost; + private int _serviceDiscoveryProviderPort; + private string _type; + private string _token; + private string _configurationKey; + + public ServiceProviderConfigurationBuilder WithHost(string serviceDiscoveryProviderHost) + { + _serviceDiscoveryProviderHost = serviceDiscoveryProviderHost; + return this; + } + + public ServiceProviderConfigurationBuilder WithPort(int serviceDiscoveryProviderPort) + { + _serviceDiscoveryProviderPort = serviceDiscoveryProviderPort; + return this; + } + + public ServiceProviderConfigurationBuilder WithType(string type) + { + _type = type; + return this; + } + + public ServiceProviderConfigurationBuilder WithToken(string token) + { + _token = token; + return this; + } + + public ServiceProviderConfigurationBuilder WithConfigurationKey(string configurationKey) + { + _configurationKey = configurationKey; + return this; + } + + public ServiceProviderConfiguration Build() + { + return new ServiceProviderConfiguration(_type, _serviceDiscoveryProviderHost, _serviceDiscoveryProviderPort, _token, _configurationKey); + } + } +} diff --git a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs index 1a5f1b6a..53f629af 100644 --- a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs +++ b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Ocelot.Configuration.File; using Ocelot.Infrastructure; +using Ocelot.Infrastructure.Extensions; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Responses; diff --git a/src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs index aa735f1a..7fbb49e4 100644 --- a/src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs @@ -1,21 +1,22 @@ -using Ocelot.Configuration.Builder; -using Ocelot.Configuration.File; - -namespace Ocelot.Configuration.Creator -{ - public class ServiceProviderConfigurationCreator : IServiceProviderConfigurationCreator - { - public ServiceProviderConfiguration Create(FileGlobalConfiguration globalConfiguration) - { - //todo log or return error here dont just default to something that wont work.. - var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0; - - return new ServiceProviderConfigurationBuilder() - .WithHost(globalConfiguration?.ServiceDiscoveryProvider?.Host) - .WithPort(serviceProviderPort) - .WithType(globalConfiguration?.ServiceDiscoveryProvider?.Type) - .WithToken(globalConfiguration?.ServiceDiscoveryProvider?.Token) - .Build(); - } - } -} +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public class ServiceProviderConfigurationCreator : IServiceProviderConfigurationCreator + { + public ServiceProviderConfiguration Create(FileGlobalConfiguration globalConfiguration) + { + //todo log or return error here dont just default to something that wont work.. + var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0; + + return new ServiceProviderConfigurationBuilder() + .WithHost(globalConfiguration?.ServiceDiscoveryProvider?.Host) + .WithPort(serviceProviderPort) + .WithType(globalConfiguration?.ServiceDiscoveryProvider?.Type) + .WithToken(globalConfiguration?.ServiceDiscoveryProvider?.Token) + .WithConfigurationKey(globalConfiguration?.ServiceDiscoveryProvider?.ConfigurationKey) + .Build(); + } + } +} diff --git a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs index 70e37690..873d1b5b 100644 --- a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs +++ b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Text; +using Ocelot.Infrastructure.Extensions; namespace Ocelot.Configuration.File { diff --git a/src/Ocelot/Configuration/File/FileRateLimitRule.cs b/src/Ocelot/Configuration/File/FileRateLimitRule.cs index 5e1616e6..7d1ca1ef 100644 --- a/src/Ocelot/Configuration/File/FileRateLimitRule.cs +++ b/src/Ocelot/Configuration/File/FileRateLimitRule.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Ocelot.Infrastructure.Extensions; namespace Ocelot.Configuration.File { diff --git a/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs b/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs index 9a96a6d3..df6c7e27 100644 --- a/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs +++ b/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs @@ -1,10 +1,11 @@ -namespace Ocelot.Configuration.File -{ - public class FileServiceDiscoveryProvider - { - public string Host {get;set;} - public int Port { get; set; } - public string Type { get; set; } - public string Token { get; set; } - } -} +namespace Ocelot.Configuration.File +{ + public class FileServiceDiscoveryProvider + { + public string Host {get;set;} + public int Port { get; set; } + public string Type { get; set; } + public string Token { get; set; } + public string ConfigurationKey { get; set; } + } +} diff --git a/src/Ocelot/Configuration/FileConfigurationController.cs b/src/Ocelot/Configuration/FileConfigurationController.cs index 707eb61d..1ea7fa7d 100644 --- a/src/Ocelot/Configuration/FileConfigurationController.cs +++ b/src/Ocelot/Configuration/FileConfigurationController.cs @@ -1,69 +1,76 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Ocelot.Configuration.File; -using Ocelot.Configuration.Setter; -using Ocelot.Raft; -using Rafty.Concensus; - -namespace Ocelot.Configuration -{ - using Repository; - - [Authorize] - [Route("configuration")] - public class FileConfigurationController : Controller - { - private readonly IFileConfigurationRepository _repo; - private readonly IFileConfigurationSetter _setter; - private readonly IServiceProvider _provider; - - public FileConfigurationController(IFileConfigurationRepository repo, IFileConfigurationSetter setter, IServiceProvider provider) - { - _repo = repo; - _setter = setter; - _provider = provider; - } - - [HttpGet] - public async Task Get() - { - var response = await _repo.Get(); - - if(response.IsError) - { - return new BadRequestObjectResult(response.Errors); - } - - return new OkObjectResult(response.Data); - } - - [HttpPost] - public async Task Post([FromBody]FileConfiguration fileConfiguration) - { - //todo - this code is a bit shit sort it out.. - var test = _provider.GetService(typeof(INode)); - if (test != null) - { - var node = (INode)test; - var result = node.Accept(new UpdateFileConfiguration(fileConfiguration)); - if (result.GetType() == typeof(Rafty.Concensus.ErrorResponse)) - { - return new BadRequestObjectResult("There was a problem. This error message sucks raise an issue in GitHub."); - } - - return new OkObjectResult(result.Command.Configuration); - } - - var response = await _setter.Set(fileConfiguration); - - if (response.IsError) - { - return new BadRequestObjectResult(response.Errors); - } - - return new OkObjectResult(fileConfiguration); - } - } -} +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Setter; +using Ocelot.Raft; +using Rafty.Concensus; + +namespace Ocelot.Configuration +{ + using Repository; + + [Authorize] + [Route("configuration")] + public class FileConfigurationController : Controller + { + private readonly IFileConfigurationRepository _repo; + private readonly IFileConfigurationSetter _setter; + private readonly IServiceProvider _provider; + + public FileConfigurationController(IFileConfigurationRepository repo, IFileConfigurationSetter setter, IServiceProvider provider) + { + _repo = repo; + _setter = setter; + _provider = provider; + } + + [HttpGet] + public async Task Get() + { + var response = await _repo.Get(); + + if(response.IsError) + { + return new BadRequestObjectResult(response.Errors); + } + + return new OkObjectResult(response.Data); + } + + [HttpPost] + public async Task Post([FromBody]FileConfiguration fileConfiguration) + { + try + { + //todo - this code is a bit shit sort it out.. + var test = _provider.GetService(typeof(INode)); + if (test != null) + { + var node = (INode)test; + var result = await node.Accept(new UpdateFileConfiguration(fileConfiguration)); + if (result.GetType() == typeof(Rafty.Concensus.ErrorResponse)) + { + return new BadRequestObjectResult("There was a problem. This error message sucks raise an issue in GitHub."); + } + + return new OkObjectResult(result.Command.Configuration); + } + + var response = await _setter.Set(fileConfiguration); + + if (response.IsError) + { + return new BadRequestObjectResult(response.Errors); + } + + return new OkObjectResult(fileConfiguration); + } + catch(Exception e) + { + return new BadRequestObjectResult($"{e.Message}:{e.StackTrace}"); + } + } + } +} diff --git a/src/Ocelot/Configuration/Repository/ConsulFileConfigurationPoller.cs b/src/Ocelot/Configuration/Repository/ConsulFileConfigurationPoller.cs index 16108d15..8dabeaf2 100644 --- a/src/Ocelot/Configuration/Repository/ConsulFileConfigurationPoller.cs +++ b/src/Ocelot/Configuration/Repository/ConsulFileConfigurationPoller.cs @@ -40,7 +40,7 @@ namespace Ocelot.Configuration.Repository _polling = true; await Poll(); _polling = false; - }, null, 0, _config.Delay); + }, null, _config.Delay, _config.Delay); } private async Task Poll() diff --git a/src/Ocelot/Configuration/Repository/ConsulFileConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/ConsulFileConfigurationRepository.cs index 165e035b..e1b6c086 100644 --- a/src/Ocelot/Configuration/Repository/ConsulFileConfigurationRepository.cs +++ b/src/Ocelot/Configuration/Repository/ConsulFileConfigurationRepository.cs @@ -1,96 +1,99 @@ -namespace Ocelot.Configuration.Repository -{ - using System; - using System.Text; - using System.Threading.Tasks; - using Consul; - using Newtonsoft.Json; - using Ocelot.Configuration.File; - using Ocelot.Infrastructure.Consul; - using Ocelot.Logging; - using Ocelot.Responses; - using Ocelot.ServiceDiscovery.Configuration; - - public class ConsulFileConfigurationRepository : IFileConfigurationRepository - { - private readonly IConsulClient _consul; - private const string OcelotConfiguration = "InternalConfiguration"; - private readonly Cache.IOcelotCache _cache; - private readonly IOcelotLogger _logger; - - public ConsulFileConfigurationRepository( - Cache.IOcelotCache cache, - IInternalConfigurationRepository repo, - IConsulClientFactory factory, - IOcelotLoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - _cache = cache; - - var internalConfig = repo.Get(); - - var consulHost = "localhost"; - var consulPort = 8500; - string token = null; - - if (!internalConfig.IsError) - { - consulHost = string.IsNullOrEmpty(internalConfig.Data.ServiceProviderConfiguration?.Host) ? consulHost : internalConfig.Data.ServiceProviderConfiguration?.Host; - consulPort = internalConfig.Data.ServiceProviderConfiguration?.Port ?? consulPort; - token = internalConfig.Data.ServiceProviderConfiguration?.Token; - } - - var config = new ConsulRegistryConfiguration(consulHost, consulPort, OcelotConfiguration, token); - - _consul = factory.Get(config); - } - - public async Task> Get() - { - var config = _cache.Get(OcelotConfiguration, OcelotConfiguration); - - if (config != null) - { - return new OkResponse(config); - } - - var queryResult = await _consul.KV.Get(OcelotConfiguration); - - if (queryResult.Response == null) - { - return new OkResponse(null); - } - - var bytes = queryResult.Response.Value; - - var json = Encoding.UTF8.GetString(bytes); - - var consulConfig = JsonConvert.DeserializeObject(json); - - return new OkResponse(consulConfig); - } - - public async Task Set(FileConfiguration ocelotConfiguration) - { - var json = JsonConvert.SerializeObject(ocelotConfiguration, Formatting.Indented); - - var bytes = Encoding.UTF8.GetBytes(json); - - var kvPair = new KVPair(OcelotConfiguration) - { - Value = bytes - }; - - var result = await _consul.KV.Put(kvPair); - - if (result.Response) - { - _cache.AddAndDelete(OcelotConfiguration, ocelotConfiguration, TimeSpan.FromSeconds(3), OcelotConfiguration); - - return new OkResponse(); - } - - return new ErrorResponse(new UnableToSetConfigInConsulError($"Unable to set FileConfiguration in consul, response status code from consul was {result.StatusCode}")); - } - } -} +namespace Ocelot.Configuration.Repository +{ + using System; + using System.Text; + using System.Threading.Tasks; + using Consul; + using Newtonsoft.Json; + using Ocelot.Configuration.File; + using Ocelot.Infrastructure.Consul; + using Ocelot.Logging; + using Ocelot.Responses; + using Ocelot.ServiceDiscovery.Configuration; + + public class ConsulFileConfigurationRepository : IFileConfigurationRepository + { + private readonly IConsulClient _consul; + private readonly string _configurationKey; + private readonly Cache.IOcelotCache _cache; + private readonly IOcelotLogger _logger; + + public ConsulFileConfigurationRepository( + Cache.IOcelotCache cache, + IInternalConfigurationRepository repo, + IConsulClientFactory factory, + IOcelotLoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + _cache = cache; + + var internalConfig = repo.Get(); + + _configurationKey = "InternalConfiguration"; + var consulHost = "localhost"; + var consulPort = 8500; + string token = null; + + if (!internalConfig.IsError) + { + consulHost = string.IsNullOrEmpty(internalConfig.Data.ServiceProviderConfiguration?.Host) ? consulHost : internalConfig.Data.ServiceProviderConfiguration?.Host; + consulPort = internalConfig.Data.ServiceProviderConfiguration?.Port ?? consulPort; + token = internalConfig.Data.ServiceProviderConfiguration?.Token; + _configurationKey = !string.IsNullOrEmpty(internalConfig.Data.ServiceProviderConfiguration?.ConfigurationKey) ? + internalConfig.Data.ServiceProviderConfiguration?.ConfigurationKey : _configurationKey; + } + + var config = new ConsulRegistryConfiguration(consulHost, consulPort, _configurationKey, token); + + _consul = factory.Get(config); + } + + public async Task> Get() + { + var config = _cache.Get(_configurationKey, _configurationKey); + + if (config != null) + { + return new OkResponse(config); + } + + var queryResult = await _consul.KV.Get(_configurationKey); + + if (queryResult.Response == null) + { + return new OkResponse(null); + } + + var bytes = queryResult.Response.Value; + + var json = Encoding.UTF8.GetString(bytes); + + var consulConfig = JsonConvert.DeserializeObject(json); + + return new OkResponse(consulConfig); + } + + public async Task Set(FileConfiguration ocelotConfiguration) + { + var json = JsonConvert.SerializeObject(ocelotConfiguration, Formatting.Indented); + + var bytes = Encoding.UTF8.GetBytes(json); + + var kvPair = new KVPair(_configurationKey) + { + Value = bytes + }; + + var result = await _consul.KV.Put(kvPair); + + if (result.Response) + { + _cache.AddAndDelete(_configurationKey, ocelotConfiguration, TimeSpan.FromSeconds(3), _configurationKey); + + return new OkResponse(); + } + + return new ErrorResponse(new UnableToSetConfigInConsulError($"Unable to set FileConfiguration in consul, response status code from consul was {result.StatusCode}")); + } + } +} diff --git a/src/Ocelot/Configuration/ServiceProviderConfiguration.cs b/src/Ocelot/Configuration/ServiceProviderConfiguration.cs index 129d24db..c0bb30fb 100644 --- a/src/Ocelot/Configuration/ServiceProviderConfiguration.cs +++ b/src/Ocelot/Configuration/ServiceProviderConfiguration.cs @@ -2,8 +2,9 @@ { public class ServiceProviderConfiguration { - public ServiceProviderConfiguration(string type, string host, int port, string token) + public ServiceProviderConfiguration(string type, string host, int port, string token, string configurationKey) { + ConfigurationKey = configurationKey; Host = host; Port = port; Token = token; @@ -14,5 +15,6 @@ public int Port { get; } public string Type { get; } public string Token { get; } + public string ConfigurationKey { get; } } } diff --git a/src/Ocelot/Headers/AddHeadersToResponse.cs b/src/Ocelot/Headers/AddHeadersToResponse.cs index 6cdf9a22..7c440d6a 100644 --- a/src/Ocelot/Headers/AddHeadersToResponse.cs +++ b/src/Ocelot/Headers/AddHeadersToResponse.cs @@ -3,6 +3,7 @@ namespace Ocelot.Headers using System.Collections.Generic; using Ocelot.Configuration.Creator; using Ocelot.Infrastructure; + using Ocelot.Infrastructure.Extensions; using Ocelot.Logging; using Ocelot.Middleware; diff --git a/src/Ocelot/Infrastructure/DelayedMessage.cs b/src/Ocelot/Infrastructure/DelayedMessage.cs new file mode 100644 index 00000000..f7b5d4f8 --- /dev/null +++ b/src/Ocelot/Infrastructure/DelayedMessage.cs @@ -0,0 +1,15 @@ +namespace Ocelot.Infrastructure +{ + internal class DelayedMessage + { + public DelayedMessage(T message, int delay) + { + Delay = delay; + Message = message; + } + + public T Message { get; set; } + + public int Delay { get; set; } + } +} diff --git a/src/Ocelot/Infrastructure/Extensions/NetCoreSupportExtensions.cs b/src/Ocelot/Infrastructure/Extensions/NetCoreSupportExtensions.cs new file mode 100644 index 00000000..3dee5caf --- /dev/null +++ b/src/Ocelot/Infrastructure/Extensions/NetCoreSupportExtensions.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Ocelot.Infrastructure.Extensions +{ + /// + /// Trivial implementations of methods present in .NET Core 2 but not supported on .NET Standard 2.0. + /// + internal static class NetCoreSupportExtensions + { + internal static void AppendJoin(this StringBuilder builder, char separator, IEnumerable values) + { + builder.Append(string.Join(separator.ToString(), values)); + } + + internal static string[] Split(this string input, string separator, StringSplitOptions options = StringSplitOptions.None) + { + return input.Split(new[] { separator }, options); + } + + internal static bool StartsWith(this string input, char value) + { + return input.StartsWith(value.ToString()); + } + + internal static bool EndsWith(this string input, char value) + { + return input.EndsWith(value.ToString()); + } + } +} diff --git a/src/Ocelot/Infrastructure/IBus.cs b/src/Ocelot/Infrastructure/IBus.cs new file mode 100644 index 00000000..0fc227dd --- /dev/null +++ b/src/Ocelot/Infrastructure/IBus.cs @@ -0,0 +1,10 @@ +using System; + +namespace Ocelot.Infrastructure +{ + public interface IBus + { + void Subscribe(Action action); + void Publish(T message, int delay); + } +} diff --git a/src/Ocelot/Infrastructure/InMemoryBus.cs b/src/Ocelot/Infrastructure/InMemoryBus.cs new file mode 100644 index 00000000..c4dcc7a2 --- /dev/null +++ b/src/Ocelot/Infrastructure/InMemoryBus.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Ocelot.Infrastructure +{ + public class InMemoryBus : IBus + { + private readonly BlockingCollection> _queue; + private readonly List> _subscriptions; + private Thread _processing; + + public InMemoryBus() + { + _queue = new BlockingCollection>(); + _subscriptions = new List>(); + _processing = new Thread(async () => await Process()); + _processing.Start(); + } + + public void Subscribe(Action action) + { + _subscriptions.Add(action); + } + + public void Publish(T message, int delay) + { + var delayed = new DelayedMessage(message, delay); + _queue.Add(delayed); + } + + private async Task Process() + { + foreach(var delayedMessage in _queue.GetConsumingEnumerable()) + { + await Task.Delay(delayedMessage.Delay); + + foreach (var subscription in _subscriptions) + { + subscription(delayedMessage.Message); + } + } + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessions.cs b/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessions.cs index 8721939a..357ea84e 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessions.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessions.cs @@ -3,61 +3,64 @@ namespace Ocelot.LoadBalancer.LoadBalancers using System; using System.Collections.Concurrent; using System.Collections.Generic; - using System.Linq; using System.Threading; using System.Threading.Tasks; + using Ocelot.Infrastructure; using Ocelot.Middleware; using Responses; using Values; - public class CookieStickySessions : ILoadBalancer, IDisposable + public class CookieStickySessions : ILoadBalancer { - private readonly int _expiryInMs; + private readonly int _keyExpiryInMs; private readonly string _key; private readonly ILoadBalancer _loadBalancer; private readonly ConcurrentDictionary _stored; - private readonly Timer _timer; - private bool _expiring; + private readonly IBus _bus; + private readonly object _lock = new object(); - public CookieStickySessions(ILoadBalancer loadBalancer, string key, int expiryInMs) + public CookieStickySessions(ILoadBalancer loadBalancer, string key, int keyExpiryInMs, IBus bus) { + _bus = bus; _key = key; - _expiryInMs = expiryInMs; + _keyExpiryInMs = keyExpiryInMs; _loadBalancer = loadBalancer; _stored = new ConcurrentDictionary(); - _timer = new Timer(x => + _bus.Subscribe(ss => { - if (_expiring) + //todo - get test coverage for this. + if (_stored.TryGetValue(ss.Key, out var stickySession)) { - return; + lock (_lock) + { + if (stickySession.Expiry < DateTime.UtcNow) + { + _stored.TryRemove(stickySession.Key, out _); + _loadBalancer.Release(stickySession.HostAndPort); + } + } } - - _expiring = true; - - Expire(); - - _expiring = false; - }, null, 0, 50); - } - - public void Dispose() - { - _timer?.Dispose(); + }); } public async Task> Lease(DownstreamContext context) { - var value = context.HttpContext.Request.Cookies[_key]; + var key = context.HttpContext.Request.Cookies[_key]; - if (!string.IsNullOrEmpty(value) && _stored.ContainsKey(value)) + lock (_lock) { - var cached = _stored[value]; + if (!string.IsNullOrEmpty(key) && _stored.ContainsKey(key)) + { + var cached = _stored[key]; - var updated = new StickySession(cached.HostAndPort, DateTime.UtcNow.AddMilliseconds(_expiryInMs)); + var updated = new StickySession(cached.HostAndPort, DateTime.UtcNow.AddMilliseconds(_keyExpiryInMs), key); - _stored[value] = updated; + _stored[key] = updated; - return new OkResponse(updated.HostAndPort); + _bus.Publish(updated, _keyExpiryInMs); + + return new OkResponse(updated.HostAndPort); + } } var next = await _loadBalancer.Lease(context); @@ -67,9 +70,14 @@ namespace Ocelot.LoadBalancer.LoadBalancers return new ErrorResponse(next.Errors); } - if (!string.IsNullOrEmpty(value) && !_stored.ContainsKey(value)) + lock (_lock) { - _stored[value] = new StickySession(next.Data, DateTime.UtcNow.AddMilliseconds(_expiryInMs)); + if (!string.IsNullOrEmpty(key) && !_stored.ContainsKey(key)) + { + var ss = new StickySession(next.Data, DateTime.UtcNow.AddMilliseconds(_keyExpiryInMs), key); + _stored[key] = ss; + _bus.Publish(ss, _keyExpiryInMs); + } } return new OkResponse(next.Data); @@ -78,16 +86,5 @@ namespace Ocelot.LoadBalancer.LoadBalancers public void Release(ServiceHostAndPort hostAndPort) { } - - private void Expire() - { - var expired = _stored.Where(x => x.Value.Expiry < DateTime.UtcNow); - - foreach (var expire in expired) - { - _stored.Remove(expire.Key, out _); - _loadBalancer.Release(expire.Value.HostAndPort); - } - } } } diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs index 1f0ecc72..58f6ff4e 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Ocelot.Configuration; +using Ocelot.Infrastructure; using Ocelot.ServiceDiscovery; namespace Ocelot.LoadBalancer.LoadBalancers @@ -25,7 +26,8 @@ namespace Ocelot.LoadBalancer.LoadBalancers return new LeastConnection(async () => await serviceProvider.Get(), reRoute.ServiceName); case nameof(CookieStickySessions): var loadBalancer = new RoundRobin(async () => await serviceProvider.Get()); - return new CookieStickySessions(loadBalancer, reRoute.LoadBalancerOptions.Key, reRoute.LoadBalancerOptions.ExpiryInMs); + var bus = new InMemoryBus(); + return new CookieStickySessions(loadBalancer, reRoute.LoadBalancerOptions.Key, reRoute.LoadBalancerOptions.ExpiryInMs, bus); default: return new NoLoadBalancer(await serviceProvider.Get()); } diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/StickySession.cs b/src/Ocelot/LoadBalancer/LoadBalancers/StickySession.cs index 152bdf4b..bce476c0 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/StickySession.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/StickySession.cs @@ -5,14 +5,17 @@ namespace Ocelot.LoadBalancer.LoadBalancers { public class StickySession { - public StickySession(ServiceHostAndPort hostAndPort, DateTime expiry) + public StickySession(ServiceHostAndPort hostAndPort, DateTime expiry, string key) { HostAndPort = hostAndPort; Expiry = expiry; + Key = key; } public ServiceHostAndPort HostAndPort { get; } public DateTime Expiry { get; } + + public string Key {get;} } } diff --git a/src/Ocelot/Ocelot.csproj b/src/Ocelot/Ocelot.csproj index 833887a2..5bd94b0c 100644 --- a/src/Ocelot/Ocelot.csproj +++ b/src/Ocelot/Ocelot.csproj @@ -1,6 +1,6 @@ - netcoreapp2.0 + netstandard2.0 2.0.0 2.0.0 true @@ -25,18 +25,26 @@ True - + + + NU1701 + - - - - - - - - - - + + + + + + + + + + NU1701 + + + + + all @@ -45,9 +53,9 @@ - + - - + + diff --git a/src/Ocelot/Raft/FileFsm.cs b/src/Ocelot/Raft/FileFsm.cs index 13c41c9d..afa47d26 100644 --- a/src/Ocelot/Raft/FileFsm.cs +++ b/src/Ocelot/Raft/FileFsm.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading.Tasks; using Newtonsoft.Json; using Rafty.FiniteStateMachine; using Rafty.Infrastructure; @@ -17,7 +18,7 @@ namespace Ocelot.Raft _id = nodeId.Id.Replace("/","").Replace(":",""); } - public void Handle(LogEntry log) + public Task Handle(LogEntry log) { try { @@ -28,6 +29,8 @@ namespace Ocelot.Raft { Console.WriteLine(exception); } + + return Task.CompletedTask; } } } diff --git a/src/Ocelot/Raft/HttpPeer.cs b/src/Ocelot/Raft/HttpPeer.cs index fcec77ea..93d81cd4 100644 --- a/src/Ocelot/Raft/HttpPeer.cs +++ b/src/Ocelot/Raft/HttpPeer.cs @@ -1,128 +1,127 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using Newtonsoft.Json; -using Ocelot.Configuration; -using Ocelot.Middleware; -using Rafty.Concensus; -using Rafty.FiniteStateMachine; - -namespace Ocelot.Raft -{ - [ExcludeFromCoverage] - public class HttpPeer : IPeer - { - private readonly string _hostAndPort; - private readonly HttpClient _httpClient; - private readonly JsonSerializerSettings _jsonSerializerSettings; - private readonly string _baseSchemeUrlAndPort; - private BearerToken _token; - private readonly IInternalConfiguration _config; - private readonly IIdentityServerConfiguration _identityServerConfiguration; - - public HttpPeer(string hostAndPort, HttpClient httpClient, IBaseUrlFinder finder, IInternalConfiguration config, IIdentityServerConfiguration identityServerConfiguration) - { - _identityServerConfiguration = identityServerConfiguration; - _config = config; - Id = hostAndPort; - _hostAndPort = hostAndPort; - _httpClient = httpClient; - _jsonSerializerSettings = new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }; - _baseSchemeUrlAndPort = finder.Find(); - } - - public string Id {get; private set;} - - public RequestVoteResponse Request(RequestVote requestVote) - { - if(_token == null) - { - SetToken(); - } - - var json = JsonConvert.SerializeObject(requestVote, _jsonSerializerSettings); - var content = new StringContent(json); - content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); - var response = _httpClient.PostAsync($"{_hostAndPort}/administration/raft/requestvote", content).GetAwaiter().GetResult(); - if(response.IsSuccessStatusCode) - { - return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(), _jsonSerializerSettings); - } - else - { - return new RequestVoteResponse(false, requestVote.Term); - } - } - - public AppendEntriesResponse Request(AppendEntries appendEntries) - { - try - { - if(_token == null) - { - SetToken(); - } - - var json = JsonConvert.SerializeObject(appendEntries, _jsonSerializerSettings); - var content = new StringContent(json); - content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); - var response = _httpClient.PostAsync($"{_hostAndPort}/administration/raft/appendEntries", content).GetAwaiter().GetResult(); - if(response.IsSuccessStatusCode) - { - return JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(),_jsonSerializerSettings); - } - else - { - return new AppendEntriesResponse(appendEntries.Term, false); - } - } - catch(Exception ex) - { - Console.WriteLine(ex); - return new AppendEntriesResponse(appendEntries.Term, false); - } - } - - public Response Request(T command) - where T : ICommand - { - if(_token == null) - { - SetToken(); - } - - var json = JsonConvert.SerializeObject(command, _jsonSerializerSettings); - var content = new StringContent(json); - content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); - var response = _httpClient.PostAsync($"{_hostAndPort}/administration/raft/command", content).GetAwaiter().GetResult(); - if(response.IsSuccessStatusCode) - { - return JsonConvert.DeserializeObject>(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(), _jsonSerializerSettings); - } - else - { - return new ErrorResponse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult(), command); - } - } - - private void SetToken() - { - var tokenUrl = $"{_baseSchemeUrlAndPort}{_config.AdministrationPath}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", _identityServerConfiguration.ApiName), - new KeyValuePair("client_secret", _identityServerConfiguration.ApiSecret), - new KeyValuePair("scope", _identityServerConfiguration.ApiName), - new KeyValuePair("grant_type", "client_credentials") - }; - var content = new FormUrlEncodedContent(formData); - var response = _httpClient.PostAsync(tokenUrl, content).GetAwaiter().GetResult(); - var responseContent = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(_token.TokenType, _token.AccessToken); - } - } -} +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Ocelot.Configuration; +using Ocelot.Middleware; +using Rafty.Concensus; +using Rafty.FiniteStateMachine; + +namespace Ocelot.Raft +{ + [ExcludeFromCoverage] + public class HttpPeer : IPeer + { + private readonly string _hostAndPort; + private readonly HttpClient _httpClient; + private readonly JsonSerializerSettings _jsonSerializerSettings; + private readonly string _baseSchemeUrlAndPort; + private BearerToken _token; + private readonly IInternalConfiguration _config; + private readonly IIdentityServerConfiguration _identityServerConfiguration; + + public HttpPeer(string hostAndPort, HttpClient httpClient, IBaseUrlFinder finder, IInternalConfiguration config, IIdentityServerConfiguration identityServerConfiguration) + { + _identityServerConfiguration = identityServerConfiguration; + _config = config; + Id = hostAndPort; + _hostAndPort = hostAndPort; + _httpClient = httpClient; + _jsonSerializerSettings = new JsonSerializerSettings() { + TypeNameHandling = TypeNameHandling.All + }; + _baseSchemeUrlAndPort = finder.Find(); + } + + public string Id { get; } + + public async Task Request(RequestVote requestVote) + { + if(_token == null) + { + await SetToken(); + } + + var json = JsonConvert.SerializeObject(requestVote, _jsonSerializerSettings); + var content = new StringContent(json); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + var response = await _httpClient.PostAsync($"{_hostAndPort}/administration/raft/requestvote", content); + if(response.IsSuccessStatusCode) + { + return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(), _jsonSerializerSettings); + } + + return new RequestVoteResponse(false, requestVote.Term); + } + + public async Task Request(AppendEntries appendEntries) + { + try + { + if(_token == null) + { + await SetToken(); + } + + var json = JsonConvert.SerializeObject(appendEntries, _jsonSerializerSettings); + var content = new StringContent(json); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + var response = await _httpClient.PostAsync($"{_hostAndPort}/administration/raft/appendEntries", content); + if(response.IsSuccessStatusCode) + { + return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(), _jsonSerializerSettings); + } + + return new AppendEntriesResponse(appendEntries.Term, false); + } + catch(Exception ex) + { + Console.WriteLine(ex); + return new AppendEntriesResponse(appendEntries.Term, false); + } + } + + public async Task> Request(T command) + where T : ICommand + { + Console.WriteLine("SENDING REQUEST...."); + if(_token == null) + { + await SetToken(); + } + + var json = JsonConvert.SerializeObject(command, _jsonSerializerSettings); + var content = new StringContent(json); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + var response = await _httpClient.PostAsync($"{_hostAndPort}/administration/raft/command", content); + if(response.IsSuccessStatusCode) + { + Console.WriteLine("REQUEST OK...."); + var okResponse = JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync(), _jsonSerializerSettings); + return new OkResponse((T)okResponse.Command); + } + + Console.WriteLine("REQUEST NOT OK...."); + return new ErrorResponse(await response.Content.ReadAsStringAsync(), command); + } + + private async Task SetToken() + { + var tokenUrl = $"{_baseSchemeUrlAndPort}{_config.AdministrationPath}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", _identityServerConfiguration.ApiName), + new KeyValuePair("client_secret", _identityServerConfiguration.ApiSecret), + new KeyValuePair("scope", _identityServerConfiguration.ApiName), + new KeyValuePair("grant_type", "client_credentials") + }; + var content = new FormUrlEncodedContent(formData); + var response = await _httpClient.PostAsync(tokenUrl, content); + var responseContent = await response.Content.ReadAsStringAsync(); + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(_token.TokenType, _token.AccessToken); + } + } +} diff --git a/src/Ocelot/Raft/OcelotFiniteStateMachine.cs b/src/Ocelot/Raft/OcelotFiniteStateMachine.cs index feecd7d7..b3d7720f 100644 --- a/src/Ocelot/Raft/OcelotFiniteStateMachine.cs +++ b/src/Ocelot/Raft/OcelotFiniteStateMachine.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using Ocelot.Configuration.Setter; using Rafty.FiniteStateMachine; using Rafty.Log; @@ -14,12 +15,12 @@ namespace Ocelot.Raft _setter = setter; } - public void Handle(LogEntry log) + public async Task Handle(LogEntry log) { //todo - handle an error //hack it to just cast as at the moment we know this is the only command :P var hack = (UpdateFileConfiguration)log.CommandData; - _setter.Set(hack.Configuration).GetAwaiter().GetResult(); + await _setter.Set(hack.Configuration); } } } diff --git a/src/Ocelot/Raft/RaftController.cs b/src/Ocelot/Raft/RaftController.cs index c1222e4d..0b8d4989 100644 --- a/src/Ocelot/Raft/RaftController.cs +++ b/src/Ocelot/Raft/RaftController.cs @@ -20,9 +20,9 @@ namespace Ocelot.Raft public class RaftController : Controller { private readonly INode _node; - private IOcelotLogger _logger; - private string _baseSchemeUrlAndPort; - private JsonSerializerSettings _jsonSerialiserSettings; + private readonly IOcelotLogger _logger; + private readonly string _baseSchemeUrlAndPort; + private readonly JsonSerializerSettings _jsonSerialiserSettings; public RaftController(INode node, IOcelotLoggerFactory loggerFactory, IBaseUrlFinder finder) { @@ -45,7 +45,7 @@ namespace Ocelot.Raft _logger.LogDebug($"{_baseSchemeUrlAndPort}/appendentries called, my state is {_node.State.GetType().FullName}"); - var appendEntriesResponse = _node.Handle(appendEntries); + var appendEntriesResponse = await _node.Handle(appendEntries); return new OkObjectResult(appendEntriesResponse); } @@ -62,7 +62,7 @@ namespace Ocelot.Raft _logger.LogDebug($"{_baseSchemeUrlAndPort}/requestvote called, my state is {_node.State.GetType().FullName}"); - var requestVoteResponse = _node.Handle(requestVote); + var requestVoteResponse = await _node.Handle(requestVote); return new OkObjectResult(requestVoteResponse); } @@ -81,7 +81,7 @@ namespace Ocelot.Raft _logger.LogDebug($"{_baseSchemeUrlAndPort}/command called, my state is {_node.State.GetType().FullName}"); - var commandResponse = _node.Accept(command); + var commandResponse = await _node.Accept(command); json = JsonConvert.SerializeObject(commandResponse, _jsonSerialiserSettings); @@ -91,7 +91,7 @@ namespace Ocelot.Raft catch(Exception e) { _logger.LogError($"THERE WAS A PROBLEM ON NODE {_node.State.CurrentState.Id}", e); - throw e; + throw; } } } diff --git a/src/Ocelot/Raft/SqlLiteLog.cs b/src/Ocelot/Raft/SqlLiteLog.cs index aff04bf5..f0db2047 100644 --- a/src/Ocelot/Raft/SqlLiteLog.cs +++ b/src/Ocelot/Raft/SqlLiteLog.cs @@ -5,9 +5,11 @@ using Newtonsoft.Json; using System; using Rafty.Infrastructure; using System.Collections.Generic; +using System.Threading.Tasks; namespace Ocelot.Raft -{ +{ + //todo - use async await [ExcludeFromCoverage] public class SqlLiteLog : ILog { @@ -40,89 +42,80 @@ namespace Ocelot.Raft } } - public int LastLogIndex + public Task LastLogIndex() { - get + lock(_lock) { - lock(_lock) + var result = 1; + using(var connection = new SqliteConnection($"Data Source={_path};")) { - var result = 1; - using(var connection = new SqliteConnection($"Data Source={_path};")) + connection.Open(); + var sql = @"select id from logs order by id desc limit 1"; + using(var command = new SqliteCommand(sql, connection)) { - connection.Open(); - var sql = @"select id from logs order by id desc limit 1"; - using(var command = new SqliteCommand(sql, connection)) + var index = Convert.ToInt32(command.ExecuteScalar()); + if(index > result) { - var index = Convert.ToInt32(command.ExecuteScalar()); - if(index > result) - { - result = index; - } + result = index; } } - - return result; } + + return Task.FromResult(result); } } - public long LastLogTerm + public Task LastLogTerm () { - get + lock(_lock) { - lock(_lock) + long result = 0; + using(var connection = new SqliteConnection($"Data Source={_path};")) { - long result = 0; - using(var connection = new SqliteConnection($"Data Source={_path};")) + connection.Open(); + var sql = @"select data from logs order by id desc limit 1"; + using(var command = new SqliteCommand(sql, connection)) { - connection.Open(); - var sql = @"select data from logs order by id desc limit 1"; - using(var command = new SqliteCommand(sql, connection)) + var data = Convert.ToString(command.ExecuteScalar()); + var jsonSerializerSettings = new JsonSerializerSettings() { + TypeNameHandling = TypeNameHandling.All + }; + var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); + if(log != null && log.Term > result) { - var data = Convert.ToString(command.ExecuteScalar()); - var jsonSerializerSettings = new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }; - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - if(log != null && log.Term > result) - { - result = log.Term; - } + result = log.Term; } } - - return result; } + + return Task.FromResult(result); } } - public int Count + public Task Count () { - get + lock(_lock) { - lock(_lock) + var result = 0; + using(var connection = new SqliteConnection($"Data Source={_path};")) { - var result = 0; - using(var connection = new SqliteConnection($"Data Source={_path};")) + connection.Open(); + var sql = @"select count(id) from logs"; + using(var command = new SqliteCommand(sql, connection)) { - connection.Open(); - var sql = @"select count(id) from logs"; - using(var command = new SqliteCommand(sql, connection)) + var index = Convert.ToInt32(command.ExecuteScalar()); + if(index > result) { - var index = Convert.ToInt32(command.ExecuteScalar()); - if(index > result) - { - result = index; - } + result = index; } } - - return result; } + + return Task.FromResult(result); } } - public int Apply(LogEntry log) + public Task Apply(LogEntry log) { lock(_lock) { @@ -145,13 +138,13 @@ namespace Ocelot.Raft using(var command = new SqliteCommand(sql, connection)) { var result = command.ExecuteScalar(); - return Convert.ToInt32(result); + return Task.FromResult(Convert.ToInt32(result)); } } } } - public void DeleteConflictsFromThisLog(int index, LogEntry logEntry) + public Task DeleteConflictsFromThisLog(int index, LogEntry logEntry) { lock(_lock) { @@ -174,15 +167,17 @@ namespace Ocelot.Raft var deleteSql = $"delete from logs where id >= {index};"; using(var deleteCommand = new SqliteCommand(deleteSql, connection)) { - var result = deleteCommand.ExecuteNonQuery(); + var result = deleteCommand.ExecuteNonQuery(); } } } } } + + return Task.CompletedTask; } - public LogEntry Get(int index) + public Task Get(int index) { lock(_lock) { @@ -199,13 +194,13 @@ namespace Ocelot.Raft TypeNameHandling = TypeNameHandling.All }; var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - return log; + return Task.FromResult(log); } } } } - public System.Collections.Generic.List<(int index, LogEntry logEntry)> GetFrom(int index) + public Task> GetFrom(int index) { lock(_lock) { @@ -235,11 +230,11 @@ namespace Ocelot.Raft } } - return logsToReturn; + return Task.FromResult(logsToReturn); } } - public long GetTermAtIndex(int index) + public Task GetTermAtIndex(int index) { lock(_lock) { @@ -264,11 +259,11 @@ namespace Ocelot.Raft } } - return result; + return Task.FromResult(result); } } - public void Remove(int indexOfCommand) + public Task Remove(int indexOfCommand) { lock(_lock) { @@ -284,6 +279,8 @@ namespace Ocelot.Raft } } } + + return Task.CompletedTask; } } } diff --git a/src/Ocelot/Requester/HttpClientBuilder.cs b/src/Ocelot/Requester/HttpClientBuilder.cs index 6d3ffcb0..18160e02 100644 --- a/src/Ocelot/Requester/HttpClientBuilder.cs +++ b/src/Ocelot/Requester/HttpClientBuilder.cs @@ -52,7 +52,7 @@ namespace Ocelot.Requester if(context.DownstreamReRoute.DangerousAcceptAnyServerCertificateValidator) { - httpclientHandler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + httpclientHandler.ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) => true; _logger .LogWarning($"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamReRoute, UpstreamPathTemplate: {context.DownstreamReRoute.UpstreamPathTemplate}, DownstreamPathTemplate: {context.DownstreamReRoute.DownstreamPathTemplate}"); diff --git a/test/Ocelot.AcceptanceTests/AuthenticationTests.cs b/test/Ocelot.AcceptanceTests/AuthenticationTests.cs index bc76ccff..770deaa1 100644 --- a/test/Ocelot.AcceptanceTests/AuthenticationTests.cs +++ b/test/Ocelot.AcceptanceTests/AuthenticationTests.cs @@ -316,8 +316,6 @@ namespace Ocelot.AcceptanceTests { new Scope("api2"), new Scope("api2.readOnly"), - new Scope("openid"), - new Scope("offline_access") }, ApiSecrets = new List() { diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index 9edbfd74..fe4877ec 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -34,26 +34,27 @@ - - + + all - - - - - - - - + + + + + + + + - + + diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 2742aa60..5041088e 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -1,865 +1,866 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; -using CacheManager.Core; -using IdentityServer4.AccessTokenValidation; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Ocelot.Configuration.File; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; -using Shouldly; -using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; -using Ocelot.AcceptanceTests.Caching; -using System.IO.Compression; -using System.Text; -using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests; -using Ocelot.Requester; -using Ocelot.Middleware.Multiplexer; - -namespace Ocelot.AcceptanceTests -{ - using Microsoft.Net.Http.Headers; - using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue; - - public class Steps : IDisposable - { - private TestServer _ocelotServer; - private HttpClient _ocelotClient; - private HttpResponseMessage _response; - private HttpContent _postContent; - private BearerToken _token; - public HttpClient OcelotClient => _ocelotClient; - public string RequestIdKey = "OcRequestId"; - private readonly Random _random; - private IWebHostBuilder _webHostBuilder; - private WebHostBuilder _ocelotBuilder; - private IWebHost _ocelotHost; - - public Steps() - { - _random = new Random(); - } - - public async Task StartFakeOcelotWithWebSockets() - { - _ocelotBuilder = new WebHostBuilder(); - _ocelotBuilder.ConfigureServices(s => - { - s.AddSingleton(_ocelotBuilder); - s.AddOcelot(); - }); - _ocelotBuilder.UseKestrel() - .UseUrls("http://localhost:5000") - .UseContentRoot(Directory.GetCurrentDirectory()) - .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("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureLogging((hostingContext, logging) => - { - logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); - logging.AddConsole(); - }) - .Configure(app => - { - app.UseWebSockets(); - app.UseOcelot().Wait(); - }) - .UseIISIntegration(); - _ocelotHost = _ocelotBuilder.Build(); - await _ocelotHost.StartAsync(); - } - - public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) - { - var configurationPath = TestConfiguration.ConfigurationPath; - - var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - } - - public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration, string configurationPath) - { - var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - } - - /// - /// 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 GivenOcelotIsRunning() - { - _webHostBuilder = new WebHostBuilder(); - - _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("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - internal void GivenOcelotIsRunningUsingButterfly(string butterflyUrl) - { - _webHostBuilder = new WebHostBuilder(); - - _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("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddOpenTracing(option => - { - //this is the url that the butterfly collector server is running on... - option.CollectorUrl = butterflyUrl; - option.Service = "Ocelot"; - }); - }) - .Configure(app => - { - app.Use(async (context, next) => - { - await next.Invoke(); - }); - app.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - public void GivenOcelotIsRunningWithMiddleareBeforePipeline(Func callback) - { - _webHostBuilder = new WebHostBuilder(); - - _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("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - }) - .Configure(app => - { - app.UseMiddleware(callback); - app.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - public void GivenOcelotIsRunningWithSpecficHandlersRegisteredInDi() - where TOne : DelegatingHandler - where TWo : DelegatingHandler - { - _webHostBuilder = new WebHostBuilder(); - - _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("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddOcelot() - .AddSingletonDelegatingHandler() - .AddSingletonDelegatingHandler(); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - public void GivenOcelotIsRunningWithSpecficAggregatorsRegisteredInDi() - where TAggregator : class, IDefinedAggregator - where TDepedency : class - { - _webHostBuilder = new WebHostBuilder(); - - _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("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddSingleton(); - s.AddOcelot() - .AddSingletonDefinedAggregator(); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi() - where TOne : DelegatingHandler - where TWo : DelegatingHandler - { - _webHostBuilder = new WebHostBuilder(); - - _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("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddOcelot() - .AddSingletonDelegatingHandler(true) - .AddSingletonDelegatingHandler(true); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi(FakeDependency dependency) - where TOne : DelegatingHandler - { - _webHostBuilder = new WebHostBuilder(); - - _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("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddSingleton(dependency); - s.AddOcelot() - .AddSingletonDelegatingHandler(true); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - internal void GivenIAddCookieToMyRequest(string cookie) - { +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using CacheManager.Core; +using IdentityServer4.AccessTokenValidation; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; +using Shouldly; +using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; +using Ocelot.AcceptanceTests.Caching; +using System.IO.Compression; +using System.Text; +using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests; +using Ocelot.Requester; +using Ocelot.Middleware.Multiplexer; + +namespace Ocelot.AcceptanceTests +{ + using Microsoft.Net.Http.Headers; + using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue; + + public class Steps : IDisposable + { + private TestServer _ocelotServer; + private HttpClient _ocelotClient; + private HttpResponseMessage _response; + private HttpContent _postContent; + private BearerToken _token; + public HttpClient OcelotClient => _ocelotClient; + public string RequestIdKey = "OcRequestId"; + private readonly Random _random; + private IWebHostBuilder _webHostBuilder; + private WebHostBuilder _ocelotBuilder; + private IWebHost _ocelotHost; + + public Steps() + { + _random = new Random(); + } + + public async Task StartFakeOcelotWithWebSockets() + { + _ocelotBuilder = new WebHostBuilder(); + _ocelotBuilder.ConfigureServices(s => + { + s.AddSingleton(_ocelotBuilder); + s.AddOcelot(); + }); + _ocelotBuilder.UseKestrel() + .UseUrls("http://localhost:5000") + .UseContentRoot(Directory.GetCurrentDirectory()) + .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("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + logging.AddConsole(); + }) + .Configure(app => + { + app.UseWebSockets(); + app.UseOcelot().Wait(); + }) + .UseIISIntegration(); + _ocelotHost = _ocelotBuilder.Build(); + await _ocelotHost.StartAsync(); + } + + public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = TestConfiguration.ConfigurationPath; + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + } + + public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration, string configurationPath) + { + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + } + + /// + /// 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 GivenOcelotIsRunning() + { + _webHostBuilder = new WebHostBuilder(); + + _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("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot(); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + internal void GivenOcelotIsRunningUsingButterfly(string butterflyUrl) + { + _webHostBuilder = new WebHostBuilder(); + + _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("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot() + .AddOpenTracing(option => + { + //this is the url that the butterfly collector server is running on... + option.CollectorUrl = butterflyUrl; + option.Service = "Ocelot"; + }); + }) + .Configure(app => + { + app.Use(async (context, next) => + { + await next.Invoke(); + }); + app.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + public void GivenOcelotIsRunningWithMiddleareBeforePipeline(Func callback) + { + _webHostBuilder = new WebHostBuilder(); + + _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("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot(); + }) + .Configure(app => + { + app.UseMiddleware(callback); + app.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + public void GivenOcelotIsRunningWithSpecficHandlersRegisteredInDi() + where TOne : DelegatingHandler + where TWo : DelegatingHandler + { + _webHostBuilder = new WebHostBuilder(); + + _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("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddOcelot() + .AddSingletonDelegatingHandler() + .AddSingletonDelegatingHandler(); + }) + .Configure(a => + { + a.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + public void GivenOcelotIsRunningWithSpecficAggregatorsRegisteredInDi() + where TAggregator : class, IDefinedAggregator + where TDepedency : class + { + _webHostBuilder = new WebHostBuilder(); + + _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("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddSingleton(); + s.AddOcelot() + .AddSingletonDefinedAggregator(); + }) + .Configure(a => + { + a.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi() + where TOne : DelegatingHandler + where TWo : DelegatingHandler + { + _webHostBuilder = new WebHostBuilder(); + + _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("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddOcelot() + .AddSingletonDelegatingHandler(true) + .AddSingletonDelegatingHandler(true); + }) + .Configure(a => + { + a.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi(FakeDependency dependency) + where TOne : DelegatingHandler + { + _webHostBuilder = new WebHostBuilder(); + + _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("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddSingleton(dependency); + s.AddOcelot() + .AddSingletonDelegatingHandler(true); + }) + .Configure(a => + { + a.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + internal void GivenIAddCookieToMyRequest(string cookie) + { _ocelotClient.DefaultRequestHeaders.Add("Set-Cookie", cookie); - } - - /// - /// 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 GivenOcelotIsRunning(Action options, string authenticationProviderKey) - { - _webHostBuilder = new WebHostBuilder(); - - _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("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - s.AddAuthentication() - .AddIdentityServerAuthentication(authenticationProviderKey, options); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - public void ThenTheResponseHeaderIs(string key, string value) - { - var header = _response.Headers.GetValues(key); - header.First().ShouldBe(value); - } - - public void ThenTheTraceHeaderIsSet(string key) - { - var header = _response.Headers.GetValues(key); - header.First().ShouldNotBeNullOrEmpty(); - } - - public void GivenOcelotIsRunningUsingJsonSerializedCache() - { - _webHostBuilder = new WebHostBuilder(); - - _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("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddCacheManager((x) => - { - x.WithMicrosoftLogging(log => - { - log.AddConsole(LogLevel.Debug); - }) - .WithJsonSerializer() - .WithHandle(typeof(InMemoryJsonHandle<>)); - }); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - public void GivenOcelotIsRunningUsingConsulToStoreConfig() - { - _webHostBuilder = new WebHostBuilder(); - - _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("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot().AddStoreOcelotConfigurationInConsul(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - public void GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache() - { - _webHostBuilder = new WebHostBuilder(); - - _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("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddCacheManager((x) => - { - x.WithMicrosoftLogging(log => - { - log.AddConsole(LogLevel.Debug); - }) - .WithJsonSerializer() - .WithHandle(typeof(InMemoryJsonHandle<>)); - }) - .AddStoreOcelotConfigurationInConsul(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - internal void ThenTheResponseShouldBe(FileConfiguration expecteds) - { - var response = JsonConvert.DeserializeObject(_response.Content.ReadAsStringAsync().Result); - - response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey); - response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host); - response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port); - - for (var i = 0; i < response.ReRoutes.Count; i++) - { - for (var j = 0; j < response.ReRoutes[i].DownstreamHostAndPorts.Count; j++) - { - var result = response.ReRoutes[i].DownstreamHostAndPorts[j]; - var expected = expecteds.ReRoutes[i].DownstreamHostAndPorts[j]; - result.Host.ShouldBe(expected.Host); - result.Port.ShouldBe(expected.Port); - } - - response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].DownstreamPathTemplate); - response.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.ReRoutes[i].DownstreamScheme); - response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].UpstreamPathTemplate); - response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expecteds.ReRoutes[i].UpstreamHttpMethod); - } - } - - /// - /// 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 GivenOcelotIsRunning(OcelotPipelineConfiguration ocelotPipelineConfig) - { - var builder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile("ocelot.json") - .AddEnvironmentVariables(); - - var configuration = builder.Build(); - _webHostBuilder = new WebHostBuilder(); - _webHostBuilder.ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - }); - - _ocelotServer = new TestServer(_webHostBuilder - .UseConfiguration(configuration) - .ConfigureServices(s => - { - Action settings = (x) => - { - x.WithMicrosoftLogging(log => - { - log.AddConsole(LogLevel.Debug); - }) - .WithDictionaryHandle(); - }; - - s.AddOcelot(configuration); - }) - .ConfigureLogging(l => - { - l.AddConsole(); - l.AddDebug(); - }) - .Configure(a => - { - a.UseOcelot(ocelotPipelineConfig).Wait(); - })); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - public void GivenIHaveAddedATokenToMyRequest() - { - _ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - } - - public void GivenIHaveAToken(string url) - { - var tokenUrl = $"{url}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", "client"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "api"), - new KeyValuePair("username", "test"), - new KeyValuePair("password", "test"), - new KeyValuePair("grant_type", "password") - }; - var content = new FormUrlEncodedContent(formData); - - using (var httpClient = new HttpClient()) - { - var response = httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } - } - - public void GivenIHaveATokenForApiReadOnlyScope(string url) - { - var tokenUrl = $"{url}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", "client"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "api.readOnly"), - new KeyValuePair("username", "test"), - new KeyValuePair("password", "test"), - new KeyValuePair("grant_type", "password") - }; - var content = new FormUrlEncodedContent(formData); - - using (var httpClient = new HttpClient()) - { - var response = httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } - } - - public void GivenIHaveATokenForApi2(string url) - { - var tokenUrl = $"{url}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", "client"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "api2"), - new KeyValuePair("username", "test"), - new KeyValuePair("password", "test"), - new KeyValuePair("grant_type", "password") - }; - var content = new FormUrlEncodedContent(formData); - - using (var httpClient = new HttpClient()) - { - var response = httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } - } - - public void GivenIHaveAnOcelotToken(string adminPath) - { - var tokenUrl = $"{adminPath}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", "admin"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "admin"), - new KeyValuePair("username", "admin"), - new KeyValuePair("password", "admin"), - new KeyValuePair("grant_type", "password") - }; - var content = new FormUrlEncodedContent(formData); - - var response = _ocelotClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } - - public void VerifyIdentiryServerStarted(string url) - { - using (var httpClient = new HttpClient()) - { - var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result; - response.EnsureSuccessStatusCode(); - } - } - - public void WhenIGetUrlOnTheApiGateway(string url) - { - _response = _ocelotClient.GetAsync(url).Result; - } - - public void WhenIGetUrlOnTheApiGateway(string url, string cookie, string value) - { - var request = _ocelotServer.CreateRequest(url); - request.And(x => { x.Headers.Add("Cookie", new CookieHeaderValue(cookie, value).ToString()); }); - var response = request.GetAsync().Result; - _response = response; - } - - public void GivenIAddAHeader(string key, string value) - { - _ocelotClient.DefaultRequestHeaders.Add(key, value); - } - - public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) - { - var tasks = new Task[times]; - - for (int i = 0; i < times; i++) - { - var urlCopy = url; - tasks[i] = GetForServiceDiscoveryTest(urlCopy); - Thread.Sleep(_random.Next(40, 60)); - } - - Task.WaitAll(tasks); - } - - public async Task WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times, string cookie, string value) - { - var tasks = new Task[times]; - - for (int i = 0; i < times; i++) - { - var urlCopy = url; - tasks[i] = GetForServiceDiscoveryTest(urlCopy, cookie, value); - Thread.Sleep(_random.Next(40, 60)); - } - - Task.WaitAll(tasks); - } - - private async Task GetForServiceDiscoveryTest(string url, string cookie, string value) - { - var request = _ocelotServer.CreateRequest(url); - request.And(x => { x.Headers.Add("Cookie", new CookieHeaderValue(cookie, value).ToString()); }); - var response = await request.GetAsync(); - var content = await response.Content.ReadAsStringAsync(); - int count = int.Parse(content); - count.ShouldBeGreaterThan(0); - } - - private async Task GetForServiceDiscoveryTest(string url) - { - var response = await _ocelotClient.GetAsync(url); - var content = await response.Content.ReadAsStringAsync(); - int count = int.Parse(content); - count.ShouldBeGreaterThan(0); - } - - public void WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(string url, int times) - { - for (int i = 0; i < times; i++) - { - var clientId = "ocelotclient1"; - var request = new HttpRequestMessage(new HttpMethod("GET"), url); - request.Headers.Add("ClientId", clientId); - _response = _ocelotClient.SendAsync(request).Result; - } - } - - public void WhenIGetUrlOnTheApiGateway(string url, string requestId) - { - _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId); - - _response = _ocelotClient.GetAsync(url).Result; - } - - public void WhenIPostUrlOnTheApiGateway(string url) - { - _response = _ocelotClient.PostAsync(url, _postContent).Result; - } - - public void GivenThePostHasContent(string postcontent) - { - _postContent = new StringContent(postcontent); - } - - public void GivenThePostHasGzipContent(object input) - { - var json = JsonConvert.SerializeObject(input); - var jsonBytes = Encoding.UTF8.GetBytes(json); - var ms = new MemoryStream(); - using (var gzip = new GZipStream(ms, CompressionMode.Compress, true)) - { - gzip.Write(jsonBytes, 0, jsonBytes.Length); - } - - ms.Position = 0; - var content = new StreamContent(ms); - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - content.Headers.ContentEncoding.Add("gzip"); - _postContent = content; - } - - public void ThenTheResponseBodyShouldBe(string expectedBody) - { - _response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody); - } - - public void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) - { - _response.StatusCode.ShouldBe(expectedHttpStatusCode); - } - - public void ThenTheStatusCodeShouldBe(int expectedHttpStatusCode) - { - var responseStatusCode = (int)_response.StatusCode; - responseStatusCode.ShouldBe(expectedHttpStatusCode); - } - - public void Dispose() - { - _ocelotClient?.Dispose(); - _ocelotServer?.Dispose(); - _ocelotHost?.Dispose(); - } - - public void ThenTheRequestIdIsReturned() - { - _response.Headers.GetValues(RequestIdKey).First().ShouldNotBeNullOrEmpty(); - } - - public void ThenTheRequestIdIsReturned(string expected) - { - _response.Headers.GetValues(RequestIdKey).First().ShouldBe(expected); - } - - public void ThenTheContentLengthIs(int expected) - { - _response.Content.Headers.ContentLength.ShouldBe(expected); - } - - public void WhenIMakeLotsOfDifferentRequestsToTheApiGateway() - { - int numberOfRequests = 100; - var aggregateUrl = "/"; - var aggregateExpected = "{\"Laura\":{Hello from Laura},\"Tom\":{Hello from Tom}}"; - var tomUrl = "/tom"; - var tomExpected = "{Hello from Tom}"; - var lauraUrl = "/laura"; - var lauraExpected = "{Hello from Laura}"; - var random = new Random(); - - var aggregateTasks = new Task[numberOfRequests]; - - for (int i = 0; i < numberOfRequests; i++) - { - aggregateTasks[i] = Fire(aggregateUrl, aggregateExpected, random); - } - - var tomTasks = new Task[numberOfRequests]; - - for (int i = 0; i < numberOfRequests; i++) - { - tomTasks[i] = Fire(tomUrl, tomExpected, random); - } - - var lauraTasks = new Task[numberOfRequests]; - - for (int i = 0; i < numberOfRequests; i++) - { - lauraTasks[i] = Fire(lauraUrl, lauraExpected, random); - } - - Task.WaitAll(lauraTasks); - Task.WaitAll(tomTasks); - Task.WaitAll(aggregateTasks); - } - - private async Task Fire(string url, string expectedBody, Random random) - { - var request = new HttpRequestMessage(new HttpMethod("GET"), url); - await Task.Delay(random.Next(0, 2)); - var response = await _ocelotClient.SendAsync(request); - var content = await response.Content.ReadAsStringAsync(); - content.ShouldBe(expectedBody); - } - } -} + } + + /// + /// 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 GivenOcelotIsRunning(Action options, string authenticationProviderKey) + { + _webHostBuilder = new WebHostBuilder(); + + _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("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot(); + s.AddAuthentication() + .AddIdentityServerAuthentication(authenticationProviderKey, options); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + public void ThenTheResponseHeaderIs(string key, string value) + { + var header = _response.Headers.GetValues(key); + header.First().ShouldBe(value); + } + + public void ThenTheTraceHeaderIsSet(string key) + { + var header = _response.Headers.GetValues(key); + header.First().ShouldNotBeNullOrEmpty(); + } + + public void GivenOcelotIsRunningUsingJsonSerializedCache() + { + _webHostBuilder = new WebHostBuilder(); + + _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("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot() + .AddCacheManager((x) => + { + x.WithMicrosoftLogging(log => + { + log.AddConsole(LogLevel.Debug); + }) + .WithJsonSerializer() + .WithHandle(typeof(InMemoryJsonHandle<>)); + }); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + public void GivenOcelotIsRunningUsingConsulToStoreConfig() + { + _webHostBuilder = new WebHostBuilder(); + + _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("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot().AddStoreOcelotConfigurationInConsul(); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + public void GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache() + { + _webHostBuilder = new WebHostBuilder(); + + _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("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot() + .AddCacheManager((x) => + { + x.WithMicrosoftLogging(log => + { + log.AddConsole(LogLevel.Debug); + }) + .WithJsonSerializer() + .WithHandle(typeof(InMemoryJsonHandle<>)); + }) + .AddStoreOcelotConfigurationInConsul(); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + internal void ThenTheResponseShouldBe(FileConfiguration expecteds) + { + var response = JsonConvert.DeserializeObject(_response.Content.ReadAsStringAsync().Result); + + response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey); + response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host); + response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port); + + for (var i = 0; i < response.ReRoutes.Count; i++) + { + for (var j = 0; j < response.ReRoutes[i].DownstreamHostAndPorts.Count; j++) + { + var result = response.ReRoutes[i].DownstreamHostAndPorts[j]; + var expected = expecteds.ReRoutes[i].DownstreamHostAndPorts[j]; + result.Host.ShouldBe(expected.Host); + result.Port.ShouldBe(expected.Port); + } + + response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].DownstreamPathTemplate); + response.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.ReRoutes[i].DownstreamScheme); + response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].UpstreamPathTemplate); + response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expecteds.ReRoutes[i].UpstreamHttpMethod); + } + } + + /// + /// 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 GivenOcelotIsRunning(OcelotPipelineConfiguration ocelotPipelineConfig) + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile("ocelot.json") + .AddEnvironmentVariables(); + + var configuration = builder.Build(); + _webHostBuilder = new WebHostBuilder(); + _webHostBuilder.ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + }); + + _ocelotServer = new TestServer(_webHostBuilder + .UseConfiguration(configuration) + .ConfigureServices(s => + { + Action settings = (x) => + { + x.WithMicrosoftLogging(log => + { + log.AddConsole(LogLevel.Debug); + }) + .WithDictionaryHandle(); + }; + + s.AddOcelot(configuration); + }) + .ConfigureLogging(l => + { + l.AddConsole(); + l.AddDebug(); + }) + .Configure(a => + { + a.UseOcelot(ocelotPipelineConfig).Wait(); + })); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + public void GivenIHaveAddedATokenToMyRequest() + { + _ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + } + + public void GivenIHaveAToken(string url) + { + var tokenUrl = $"{url}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", "client"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "api"), + new KeyValuePair("username", "test"), + new KeyValuePair("password", "test"), + new KeyValuePair("grant_type", "password") + }; + var content = new FormUrlEncodedContent(formData); + + using (var httpClient = new HttpClient()) + { + var response = httpClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + } + } + + public void GivenIHaveATokenForApiReadOnlyScope(string url) + { + var tokenUrl = $"{url}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", "client"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "api.readOnly"), + new KeyValuePair("username", "test"), + new KeyValuePair("password", "test"), + new KeyValuePair("grant_type", "password") + }; + var content = new FormUrlEncodedContent(formData); + + using (var httpClient = new HttpClient()) + { + var response = httpClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + } + } + + public void GivenIHaveATokenForApi2(string url) + { + var tokenUrl = $"{url}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", "client"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "api2"), + new KeyValuePair("username", "test"), + new KeyValuePair("password", "test"), + new KeyValuePair("grant_type", "password") + }; + var content = new FormUrlEncodedContent(formData); + + using (var httpClient = new HttpClient()) + { + var response = httpClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + } + } + + public void GivenIHaveAnOcelotToken(string adminPath) + { + var tokenUrl = $"{adminPath}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", "admin"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "admin"), + new KeyValuePair("username", "admin"), + new KeyValuePair("password", "admin"), + new KeyValuePair("grant_type", "password") + }; + var content = new FormUrlEncodedContent(formData); + + var response = _ocelotClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + } + + public void VerifyIdentiryServerStarted(string url) + { + using (var httpClient = new HttpClient()) + { + var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").GetAwaiter().GetResult(); + var content = response.Content.ReadAsStringAsync().GetAwaiter(); + response.EnsureSuccessStatusCode(); + } + } + + public void WhenIGetUrlOnTheApiGateway(string url) + { + _response = _ocelotClient.GetAsync(url).Result; + } + + public void WhenIGetUrlOnTheApiGateway(string url, string cookie, string value) + { + var request = _ocelotServer.CreateRequest(url); + request.And(x => { x.Headers.Add("Cookie", new CookieHeaderValue(cookie, value).ToString()); }); + var response = request.GetAsync().Result; + _response = response; + } + + public void GivenIAddAHeader(string key, string value) + { + _ocelotClient.DefaultRequestHeaders.Add(key, value); + } + + public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) + { + var tasks = new Task[times]; + + for (int i = 0; i < times; i++) + { + var urlCopy = url; + tasks[i] = GetForServiceDiscoveryTest(urlCopy); + Thread.Sleep(_random.Next(40, 60)); + } + + Task.WaitAll(tasks); + } + + public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times, string cookie, string value) + { + var tasks = new Task[times]; + + for (int i = 0; i < times; i++) + { + var urlCopy = url; + tasks[i] = GetForServiceDiscoveryTest(urlCopy, cookie, value); + Thread.Sleep(_random.Next(40, 60)); + } + + Task.WaitAll(tasks); + } + + private async Task GetForServiceDiscoveryTest(string url, string cookie, string value) + { + var request = _ocelotServer.CreateRequest(url); + request.And(x => { x.Headers.Add("Cookie", new CookieHeaderValue(cookie, value).ToString()); }); + var response = await request.GetAsync(); + var content = await response.Content.ReadAsStringAsync(); + int count = int.Parse(content); + count.ShouldBeGreaterThan(0); + } + + private async Task GetForServiceDiscoveryTest(string url) + { + var response = await _ocelotClient.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + int count = int.Parse(content); + count.ShouldBeGreaterThan(0); + } + + public void WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(string url, int times) + { + for (int i = 0; i < times; i++) + { + var clientId = "ocelotclient1"; + var request = new HttpRequestMessage(new HttpMethod("GET"), url); + request.Headers.Add("ClientId", clientId); + _response = _ocelotClient.SendAsync(request).Result; + } + } + + public void WhenIGetUrlOnTheApiGateway(string url, string requestId) + { + _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId); + + _response = _ocelotClient.GetAsync(url).Result; + } + + public void WhenIPostUrlOnTheApiGateway(string url) + { + _response = _ocelotClient.PostAsync(url, _postContent).Result; + } + + public void GivenThePostHasContent(string postcontent) + { + _postContent = new StringContent(postcontent); + } + + public void GivenThePostHasGzipContent(object input) + { + var json = JsonConvert.SerializeObject(input); + var jsonBytes = Encoding.UTF8.GetBytes(json); + var ms = new MemoryStream(); + using (var gzip = new GZipStream(ms, CompressionMode.Compress, true)) + { + gzip.Write(jsonBytes, 0, jsonBytes.Length); + } + + ms.Position = 0; + var content = new StreamContent(ms); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + content.Headers.ContentEncoding.Add("gzip"); + _postContent = content; + } + + public void ThenTheResponseBodyShouldBe(string expectedBody) + { + _response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody); + } + + public void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) + { + _response.StatusCode.ShouldBe(expectedHttpStatusCode); + } + + public void ThenTheStatusCodeShouldBe(int expectedHttpStatusCode) + { + var responseStatusCode = (int)_response.StatusCode; + responseStatusCode.ShouldBe(expectedHttpStatusCode); + } + + public void Dispose() + { + _ocelotClient?.Dispose(); + _ocelotServer?.Dispose(); + _ocelotHost?.Dispose(); + } + + public void ThenTheRequestIdIsReturned() + { + _response.Headers.GetValues(RequestIdKey).First().ShouldNotBeNullOrEmpty(); + } + + public void ThenTheRequestIdIsReturned(string expected) + { + _response.Headers.GetValues(RequestIdKey).First().ShouldBe(expected); + } + + public void ThenTheContentLengthIs(int expected) + { + _response.Content.Headers.ContentLength.ShouldBe(expected); + } + + public void WhenIMakeLotsOfDifferentRequestsToTheApiGateway() + { + int numberOfRequests = 100; + var aggregateUrl = "/"; + var aggregateExpected = "{\"Laura\":{Hello from Laura},\"Tom\":{Hello from Tom}}"; + var tomUrl = "/tom"; + var tomExpected = "{Hello from Tom}"; + var lauraUrl = "/laura"; + var lauraExpected = "{Hello from Laura}"; + var random = new Random(); + + var aggregateTasks = new Task[numberOfRequests]; + + for (int i = 0; i < numberOfRequests; i++) + { + aggregateTasks[i] = Fire(aggregateUrl, aggregateExpected, random); + } + + var tomTasks = new Task[numberOfRequests]; + + for (int i = 0; i < numberOfRequests; i++) + { + tomTasks[i] = Fire(tomUrl, tomExpected, random); + } + + var lauraTasks = new Task[numberOfRequests]; + + for (int i = 0; i < numberOfRequests; i++) + { + lauraTasks[i] = Fire(lauraUrl, lauraExpected, random); + } + + Task.WaitAll(lauraTasks); + Task.WaitAll(tomTasks); + Task.WaitAll(aggregateTasks); + } + + private async Task Fire(string url, string expectedBody, Random random) + { + var request = new HttpRequestMessage(new HttpMethod("GET"), url); + await Task.Delay(random.Next(0, 2)); + var response = await _ocelotClient.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + content.ShouldBe(expectedBody); + } + } +} diff --git a/test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs b/test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs index 55e7a435..053ac7ea 100644 --- a/test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs +++ b/test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs @@ -76,16 +76,16 @@ namespace Ocelot.Benchmarks response.EnsureSuccessStatusCode(); } - // * Summary* - // BenchmarkDotNet = v0.10.13, OS = macOS 10.12.6 (16G1212) [Darwin 16.7.0] - // Intel Core i5-4278U CPU 2.60GHz(Haswell), 1 CPU, 4 logical cores and 2 physical cores - //.NET Core SDK = 2.1.4 +/* * Summary* + BenchmarkDotNet = v0.10.13, OS = macOS 10.12.6 (16G1212) [Darwin 16.7.0] + Intel Core i5-4278U CPU 2.60GHz(Haswell), 1 CPU, 4 logical cores and 2 physical cores + .NET Core SDK = 2.1.4 - // [Host] : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT - // DefaultJob : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT - // Method | Mean | Error | StdDev | StdErr | Min | Q1 | Median | Q3 | Max | Op/s | Scaled | Gen 0 | Gen 1 | Allocated | - // --------- |---------:|----------:|----------:|----------:|---------:|---------:|---------:|---------:|---------:|------:|-------:|--------:|-------:|----------:| - // Baseline | 2.102 ms | 0.0292 ms | 0.0273 ms | 0.0070 ms | 2.063 ms | 2.080 ms | 2.093 ms | 2.122 ms | 2.152 ms | 475.8 | 1.00 | 31.2500 | 3.9063 | 1.63 KB | + [Host] : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT + DefaultJob : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT + Method | Mean | Error | StdDev | StdErr | Min | Q1 | Median | Q3 | Max | Op/s | Scaled | Gen 0 | Gen 1 | Allocated | + --------- |---------:|----------:|----------:|----------:|---------:|---------:|---------:|---------:|---------:|------:|-------:|--------:|-------:|----------:| + Baseline | 2.102 ms | 0.0292 ms | 0.0273 ms | 0.0070 ms | 2.063 ms | 2.080 ms | 2.093 ms | 2.122 ms | 2.152 ms | 475.8 | 1.00 | 31.2500 | 3.9063 | 1.63 KB |*/ private void GivenOcelotIsRunning(string url) { diff --git a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj index 7761dab1..e97450d3 100644 --- a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj +++ b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj @@ -19,7 +19,7 @@ - + all diff --git a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj index 97c8782f..5ec67320 100644 --- a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj +++ b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj @@ -25,26 +25,26 @@ - + all - - - - - - - + + + + + + + - + - + \ No newline at end of file diff --git a/test/Ocelot.IntegrationTests/RaftTests.cs b/test/Ocelot.IntegrationTests/RaftTests.cs index baad5091..96cd6687 100644 --- a/test/Ocelot.IntegrationTests/RaftTests.cs +++ b/test/Ocelot.IntegrationTests/RaftTests.cs @@ -1,393 +1,450 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using Ocelot.Configuration.File; -using Ocelot.Raft; -using Rafty.Concensus; -using Rafty.Infrastructure; -using Shouldly; -using Xunit; -using static Rafty.Infrastructure.Wait; -using Microsoft.Data.Sqlite; -using Microsoft.Extensions.Configuration; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; - -namespace Ocelot.IntegrationTests -{ - public class RaftTests : IDisposable - { - private readonly List _builders; - private readonly List _webHostBuilders; - private readonly List _threads; - private FilePeers _peers; - private readonly HttpClient _httpClient; - private readonly HttpClient _httpClientForAssertions; - private BearerToken _token; - private HttpResponseMessage _response; - private static readonly object _lock = new object(); - - public RaftTests() - { - _httpClientForAssertions = new HttpClient(); - _httpClient = new HttpClient(); - var ocelotBaseUrl = "http://localhost:5000"; - _httpClient.BaseAddress = new Uri(ocelotBaseUrl); - _webHostBuilders = new List(); - _builders = new List(); - _threads = new List(); - } - - public void Dispose() - { - foreach (var builder in _builders) - { - builder?.Dispose(); - } - - foreach (var peer in _peers.Peers) - { - File.Delete(peer.HostAndPort.Replace("/","").Replace(":","")); - File.Delete($"{peer.HostAndPort.Replace("/","").Replace(":","")}.db"); - } - } - - [Fact(Skip = "This tests is flakey at the moment so ignoring will be fixed long term see https://github.com/TomPallister/Ocelot/issues/245")] - public void should_persist_command_to_five_servers() - { - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - } - }; - - var updatedConfiguration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "127.0.0.1", - Port = 80, - } - }, - DownstreamScheme = "http", - DownstreamPathTemplate = "/geoffrey", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/" - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "123.123.123", - Port = 443, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/blooper/{productId}", - UpstreamHttpMethod = new List { "post" }, - UpstreamPathTemplate = "/test" - } - } - }; - - var command = new UpdateFileConfiguration(updatedConfiguration); - GivenThereIsAConfiguration(configuration); - GivenFiveServersAreRunning(); - GivenALeaderIsElected(); - GivenIHaveAnOcelotToken("/administration"); - WhenISendACommandIntoTheCluster(command); - ThenTheCommandIsReplicatedToAllStateMachines(command); - } - - [Fact(Skip = "This tests is flakey at the moment so ignoring will be fixed long term see https://github.com/TomPallister/Ocelot/issues/245")] - public void should_persist_command_to_five_servers_when_using_administration_api() - { - var configuration = new FileConfiguration - { - }; - - var updatedConfiguration = new FileConfiguration - { - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "127.0.0.1", - Port = 80, - } - }, - DownstreamScheme = "http", - DownstreamPathTemplate = "/geoffrey", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/" - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "123.123.123", - Port = 443, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/blooper/{productId}", - UpstreamHttpMethod = new List { "post" }, - UpstreamPathTemplate = "/test" - } - } - }; - - var command = new UpdateFileConfiguration(updatedConfiguration); - GivenThereIsAConfiguration(configuration); - GivenFiveServersAreRunning(); - GivenALeaderIsElected(); - GivenIHaveAnOcelotToken("/administration"); - GivenIHaveAddedATokenToMyRequest(); - WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration); - ThenTheCommandIsReplicatedToAllStateMachines(command); - } - - private void WhenISendACommandIntoTheCluster(UpdateFileConfiguration command) - { - var p = _peers.Peers.First(); - var json = JsonConvert.SerializeObject(command,new JsonSerializerSettings() { - TypeNameHandling = TypeNameHandling.All - }); - var httpContent = new StringContent(json); - httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); - using(var httpClient = new HttpClient()) - { - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - var response = httpClient.PostAsync($"{p.HostAndPort}/administration/raft/command", httpContent).GetAwaiter().GetResult(); - response.EnsureSuccessStatusCode(); - var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - var result = JsonConvert.DeserializeObject>(content); - result.Command.Configuration.ReRoutes.Count.ShouldBe(2); - } - - //dirty sleep to make sure command replicated... - var stopwatch = Stopwatch.StartNew(); - while(stopwatch.ElapsedMilliseconds < 10000) - { - } - } - - private void ThenTheCommandIsReplicatedToAllStateMachines(UpdateFileConfiguration expecteds) - { - //dirty sleep to give a chance to replicate... - var stopwatch = Stopwatch.StartNew(); - while(stopwatch.ElapsedMilliseconds < 2000) - { - } - - bool CommandCalledOnAllStateMachines() - { - try - { - var passed = 0; - foreach (var peer in _peers.Peers) - { - var path = $"{peer.HostAndPort.Replace("/","").Replace(":","")}.db"; - using(var connection = new SqliteConnection($"Data Source={path};")) - { - connection.Open(); - var sql = @"select count(id) from logs"; - using(var command = new SqliteCommand(sql, connection)) - { - var index = Convert.ToInt32(command.ExecuteScalar()); - index.ShouldBe(1); - } - } - - _httpClientForAssertions.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - var result = _httpClientForAssertions.GetAsync($"{peer.HostAndPort}/administration/configuration").Result; - var json = result.Content.ReadAsStringAsync().Result; - var response = JsonConvert.DeserializeObject(json, new JsonSerializerSettings{TypeNameHandling = TypeNameHandling.All}); - response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.Configuration.GlobalConfiguration.RequestIdKey); - response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Host); - response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Port); - - for (var i = 0; i < response.ReRoutes.Count; i++) - { - for (var j = 0; j < response.ReRoutes[i].DownstreamHostAndPorts.Count; j++) - { - var res = response.ReRoutes[i].DownstreamHostAndPorts[j]; - var expected = expecteds.Configuration.ReRoutes[i].DownstreamHostAndPorts[j]; - res.Host.ShouldBe(expected.Host); - res.Port.ShouldBe(expected.Port); - } - - response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.Configuration.ReRoutes[i].DownstreamPathTemplate); - response.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.Configuration.ReRoutes[i].DownstreamScheme); - response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expecteds.Configuration.ReRoutes[i].UpstreamPathTemplate); - response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expecteds.Configuration.ReRoutes[i].UpstreamHttpMethod); - } - - passed++; - } - - return passed == 5; - } - catch(Exception e) - { - Console.WriteLine(e); - return false; - } - } - - var commandOnAllStateMachines = WaitFor(20000).Until(() => CommandCalledOnAllStateMachines()); - commandOnAllStateMachines.ShouldBeTrue(); - } - - private void WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration) - { - var json = JsonConvert.SerializeObject(updatedConfiguration); - var content = new StringContent(json); - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - _response = _httpClient.PostAsync(url, content).Result; - } - - private void GivenIHaveAddedATokenToMyRequest() - { - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - } - - private void GivenIHaveAnOcelotToken(string adminPath) - { - var tokenUrl = $"{adminPath}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", "admin"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "admin"), - new KeyValuePair("grant_type", "client_credentials") - }; - var content = new FormUrlEncodedContent(formData); - - var response = _httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - var configPath = $"{adminPath}/.well-known/openid-configuration"; - response = _httpClient.GetAsync(configPath).Result; - response.EnsureSuccessStatusCode(); - } - - private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) - { - var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; - - var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - - var text = File.ReadAllText(configurationPath); - - configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - - text = File.ReadAllText(configurationPath); - } - - private void GivenAServerIsRunning(string url) - { - lock(_lock) - { - IWebHostBuilder webHostBuilder = new WebHostBuilder(); - webHostBuilder.UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .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("ocelot.json"); - config.AddJsonFile("peers.json", optional: true, reloadOnChange: true); - #pragma warning disable CS0618 - config.AddOcelotBaseUrl(url); - #pragma warning restore CS0618 - config.AddEnvironmentVariables(); - }) - .ConfigureServices(x => - { - x.AddSingleton(new NodeId(url)); - x - .AddOcelot() - .AddAdministration("/administration", "secret") - .AddRafty(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - var builder = webHostBuilder.Build(); - builder.Start(); - - _webHostBuilders.Add(webHostBuilder); - _builders.Add(builder); - } - } - - private void GivenFiveServersAreRunning() - { - var bytes = File.ReadAllText("peers.json"); - _peers = JsonConvert.DeserializeObject(bytes); - - foreach (var peer in _peers.Peers) - { - var thread = new Thread(() => GivenAServerIsRunning(peer.HostAndPort)); - thread.Start(); - _threads.Add(thread); - } - } - - private void GivenALeaderIsElected() - { - //dirty sleep to make sure we have a leader - var stopwatch = Stopwatch.StartNew(); - while(stopwatch.ElapsedMilliseconds < 20000) - { - } - } - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.Raft; +using Rafty.Concensus; +using Rafty.Infrastructure; +using Shouldly; +using Xunit; +using static Rafty.Infrastructure.Wait; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Configuration; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; + +namespace Ocelot.IntegrationTests +{ + using Xunit.Abstractions; + + public class RaftTests : IDisposable + { + private readonly List _builders; + private readonly List _webHostBuilders; + private readonly List _threads; + private FilePeers _peers; + private readonly HttpClient _httpClient; + private readonly HttpClient _httpClientForAssertions; + private BearerToken _token; + private HttpResponseMessage _response; + private static readonly object _lock = new object(); + private ITestOutputHelper _output; + + public RaftTests(ITestOutputHelper output) + { + _output = output; + _httpClientForAssertions = new HttpClient(); + _httpClient = new HttpClient(); + var ocelotBaseUrl = "http://localhost:5000"; + _httpClient.BaseAddress = new Uri(ocelotBaseUrl); + _webHostBuilders = new List(); + _builders = new List(); + _threads = new List(); + } + + [Fact(Skip = "still broken waiting for work in rafty")] + public void should_persist_command_to_five_servers() + { + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + } + }; + + var updatedConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "127.0.0.1", + Port = 80, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/geoffrey", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "123.123.123", + Port = 443, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/blooper/{productId}", + UpstreamHttpMethod = new List { "post" }, + UpstreamPathTemplate = "/test" + } + } + }; + + var command = new UpdateFileConfiguration(updatedConfiguration); + GivenThereIsAConfiguration(configuration); + GivenFiveServersAreRunning(); + GivenIHaveAnOcelotToken("/administration"); + WhenISendACommandIntoTheCluster(command); + Thread.Sleep(5000); + ThenTheCommandIsReplicatedToAllStateMachines(command); + } + + [Fact(Skip = "still broken waiting for work in rafty")] + public void should_persist_command_to_five_servers_when_using_administration_api() + { + var configuration = new FileConfiguration + { + }; + + var updatedConfiguration = new FileConfiguration + { + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "127.0.0.1", + Port = 80, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/geoffrey", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "123.123.123", + Port = 443, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/blooper/{productId}", + UpstreamHttpMethod = new List { "post" }, + UpstreamPathTemplate = "/test" + } + } + }; + + var command = new UpdateFileConfiguration(updatedConfiguration); + GivenThereIsAConfiguration(configuration); + GivenFiveServersAreRunning(); + GivenIHaveAnOcelotToken("/administration"); + GivenIHaveAddedATokenToMyRequest(); + WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration); + ThenTheCommandIsReplicatedToAllStateMachines(command); + } + + private void WhenISendACommandIntoTheCluster(UpdateFileConfiguration command) + { + bool SendCommand() + { + try + { + var p = _peers.Peers.First(); + var json = JsonConvert.SerializeObject(command, new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.All + }); + var httpContent = new StringContent(json); + httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); + using (var httpClient = new HttpClient()) + { + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + var response = httpClient.PostAsync($"{p.HostAndPort}/administration/raft/command", httpContent).GetAwaiter().GetResult(); + response.EnsureSuccessStatusCode(); + var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + var errorResult = JsonConvert.DeserializeObject>(content); + + if (!string.IsNullOrEmpty(errorResult.Error)) + { + return false; + } + + var okResult = JsonConvert.DeserializeObject>(content); + + if (okResult.Command.Configuration.ReRoutes.Count == 2) + { + return true; + } + } + + return false; + } + catch (Exception e) + { + Console.WriteLine(e); + return false; + } + } + + var commandSent = WaitFor(20000).Until(() => SendCommand()); + commandSent.ShouldBeTrue(); + } + + private void ThenTheCommandIsReplicatedToAllStateMachines(UpdateFileConfiguration expecteds) + { + bool CommandCalledOnAllStateMachines() + { + try + { + var passed = 0; + foreach (var peer in _peers.Peers) + { + var path = $"{peer.HostAndPort.Replace("/","").Replace(":","")}.db"; + using(var connection = new SqliteConnection($"Data Source={path};")) + { + connection.Open(); + var sql = @"select count(id) from logs"; + using(var command = new SqliteCommand(sql, connection)) + { + var index = Convert.ToInt32(command.ExecuteScalar()); + index.ShouldBe(1); + } + } + + _httpClientForAssertions.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + var result = _httpClientForAssertions.GetAsync($"{peer.HostAndPort}/administration/configuration").Result; + var json = result.Content.ReadAsStringAsync().Result; + var response = JsonConvert.DeserializeObject(json, new JsonSerializerSettings{TypeNameHandling = TypeNameHandling.All}); + response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.Configuration.GlobalConfiguration.RequestIdKey); + response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Host); + response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Port); + + for (var i = 0; i < response.ReRoutes.Count; i++) + { + for (var j = 0; j < response.ReRoutes[i].DownstreamHostAndPorts.Count; j++) + { + var res = response.ReRoutes[i].DownstreamHostAndPorts[j]; + var expected = expecteds.Configuration.ReRoutes[i].DownstreamHostAndPorts[j]; + res.Host.ShouldBe(expected.Host); + res.Port.ShouldBe(expected.Port); + } + + response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.Configuration.ReRoutes[i].DownstreamPathTemplate); + response.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.Configuration.ReRoutes[i].DownstreamScheme); + response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expecteds.Configuration.ReRoutes[i].UpstreamPathTemplate); + response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expecteds.Configuration.ReRoutes[i].UpstreamHttpMethod); + } + + passed++; + } + + return passed == 5; + } + catch(Exception e) + { + //_output.WriteLine($"{e.Message}, {e.StackTrace}"); + Console.WriteLine(e); + return false; + } + } + + var commandOnAllStateMachines = WaitFor(20000).Until(() => CommandCalledOnAllStateMachines()); + commandOnAllStateMachines.ShouldBeTrue(); + } + + private void WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration) + { + bool SendCommand() + { + var json = JsonConvert.SerializeObject(updatedConfiguration); + var content = new StringContent(json); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + _response = _httpClient.PostAsync(url, content).Result; + var responseContent = _response.Content.ReadAsStringAsync().Result; + + if(responseContent == "There was a problem. This error message sucks raise an issue in GitHub.") + { + return false; + } + + if(string.IsNullOrEmpty(responseContent)) + { + return false; + } + + return _response.IsSuccessStatusCode; + } + + var commandSent = WaitFor(20000).Until(() => SendCommand()); + commandSent.ShouldBeTrue(); + } + + private void GivenIHaveAddedATokenToMyRequest() + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + } + + private void GivenIHaveAnOcelotToken(string adminPath) + { + bool AddToken() + { + try + { + var tokenUrl = $"{adminPath}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", "admin"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "admin"), + new KeyValuePair("grant_type", "client_credentials") + }; + var content = new FormUrlEncodedContent(formData); + + var response = _httpClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + if(!response.IsSuccessStatusCode) + { + return false; + } + + _token = JsonConvert.DeserializeObject(responseContent); + var configPath = $"{adminPath}/.well-known/openid-configuration"; + response = _httpClient.GetAsync(configPath).Result; + return response.IsSuccessStatusCode; + } + catch(Exception) + { + return false; + } + } + + var addToken = WaitFor(20000).Until(() => AddToken()); + addToken.ShouldBeTrue(); + } + + private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + var text = File.ReadAllText(configurationPath); + + configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + text = File.ReadAllText(configurationPath); + } + + private void GivenAServerIsRunning(string url) + { + lock(_lock) + { + IWebHostBuilder webHostBuilder = new WebHostBuilder(); + webHostBuilder.UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .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("ocelot.json"); + config.AddJsonFile("peers.json", optional: true, reloadOnChange: true); + #pragma warning disable CS0618 + config.AddOcelotBaseUrl(url); + #pragma warning restore CS0618 + config.AddEnvironmentVariables(); + }) + .ConfigureServices(x => + { + x.AddSingleton(new NodeId(url)); + x + .AddOcelot() + .AddAdministration("/administration", "secret") + .AddRafty(); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + + var builder = webHostBuilder.Build(); + builder.Start(); + + _webHostBuilders.Add(webHostBuilder); + _builders.Add(builder); + } + } + + private void GivenFiveServersAreRunning() + { + var bytes = File.ReadAllText("peers.json"); + _peers = JsonConvert.DeserializeObject(bytes); + + foreach (var peer in _peers.Peers) + { + File.Delete(peer.HostAndPort.Replace("/", "").Replace(":", "")); + File.Delete($"{peer.HostAndPort.Replace("/", "").Replace(":", "")}.db"); + var thread = new Thread(() => GivenAServerIsRunning(peer.HostAndPort)); + thread.Start(); + _threads.Add(thread); + } + } + + public void Dispose() + { + foreach (var builder in _builders) + { + builder?.Dispose(); + } + + foreach (var peer in _peers.Peers) + { + try + { + File.Delete(peer.HostAndPort.Replace("/", "").Replace(":", "")); + File.Delete($"{peer.HostAndPort.Replace("/", "").Replace(":", "")}.db"); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + } + } +} diff --git a/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj b/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj index a43e6b23..b7c3ac18 100644 --- a/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj +++ b/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj @@ -24,16 +24,16 @@ - - - - - - - - + + + + + + + + - + all diff --git a/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs b/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs index 7393b631..17fc604d 100644 --- a/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs @@ -1,158 +1,174 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using Moq; -using Ocelot.Configuration.File; -using Ocelot.Configuration.Repository; -using Ocelot.Configuration.Setter; -using Ocelot.Logging; -using Ocelot.Responses; -using Ocelot.UnitTests.Responder; -using TestStack.BDDfy; -using Xunit; -using Shouldly; -using static Ocelot.Infrastructure.Wait; - -namespace Ocelot.UnitTests.Configuration -{ - public class ConsulFileConfigurationPollerTests : IDisposable - { - private ConsulFileConfigurationPoller _poller; - private Mock _factory; - private Mock _repo; - private Mock _setter; - private FileConfiguration _fileConfig; - private Mock _config; - - public ConsulFileConfigurationPollerTests() - { - var logger = new Mock(); - _factory = new Mock(); - _factory.Setup(x => x.CreateLogger()).Returns(logger.Object); - _repo = new Mock(); - _setter = new Mock(); - _fileConfig = new FileConfiguration(); - _config = new Mock(); - _repo.Setup(x => x.Get()).ReturnsAsync(new OkResponse(_fileConfig)); - _config.Setup(x => x.Delay).Returns(100); - _poller = new ConsulFileConfigurationPoller(_factory.Object, _repo.Object, _setter.Object, _config.Object); - } - - public void Dispose() - { - _poller.Dispose(); - } - - [Fact] - public void should_start() - { - this.Given(x => ThenTheSetterIsCalled(_fileConfig, 1)) - .BDDfy(); - } - - [Fact] - public void should_call_setter_when_gets_new_config() - { - var newConfig = new FileConfiguration { - ReRoutes = new List - { - new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "test" - } - }, - } - } - }; - - this.Given(x => WhenTheConfigIsChangedInConsul(newConfig, 0)) - .Then(x => ThenTheSetterIsCalled(newConfig, 1)) - .BDDfy(); - } - - [Fact] - public void should_not_poll_if_already_polling() - { - var newConfig = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "test" - } - }, - } - } - }; - - this.Given(x => WhenTheConfigIsChangedInConsul(newConfig, 10)) - .Then(x => ThenTheSetterIsCalled(newConfig, 1)) - .BDDfy(); - } - - [Fact] - public void should_do_nothing_if_call_to_consul_fails() - { - var newConfig = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "test" - } - }, - } - } - }; - - this.Given(x => WhenConsulErrors()) - .Then(x => ThenTheSetterIsCalled(newConfig, 0)) - .BDDfy(); - } - - private void WhenConsulErrors() - { - _repo - .Setup(x => x.Get()) - .ReturnsAsync(new ErrorResponse(new AnyError())); - } - - private void WhenTheConfigIsChangedInConsul(FileConfiguration newConfig, int delay) - { - _repo - .Setup(x => x.Get()) - .Callback(() => Thread.Sleep(delay)) - .ReturnsAsync(new OkResponse(newConfig)); - } - - private void ThenTheSetterIsCalled(FileConfiguration fileConfig, int times) - { - var result = WaitFor(2000).Until(() => { - try - { - _setter.Verify(x => x.Set(fileConfig), Times.Exactly(times)); - return true; - } - catch(Exception) - { - return false; - } - }); - result.ShouldBeTrue(); - } - } -} +using System; +using System.Collections.Generic; +using System.Threading; +using Moq; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Repository; +using Ocelot.Configuration.Setter; +using Ocelot.Logging; +using Ocelot.Responses; +using Ocelot.UnitTests.Responder; +using TestStack.BDDfy; +using Xunit; +using Shouldly; +using static Ocelot.Infrastructure.Wait; + +namespace Ocelot.UnitTests.Configuration +{ + public class ConsulFileConfigurationPollerTests : IDisposable + { + private readonly ConsulFileConfigurationPoller _poller; + private Mock _factory; + private readonly Mock _repo; + private readonly Mock _setter; + private readonly FileConfiguration _fileConfig; + private Mock _config; + + public ConsulFileConfigurationPollerTests() + { + var logger = new Mock(); + _factory = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(logger.Object); + _repo = new Mock(); + _setter = new Mock(); + _fileConfig = new FileConfiguration(); + _config = new Mock(); + _repo.Setup(x => x.Get()).ReturnsAsync(new OkResponse(_fileConfig)); + _config.Setup(x => x.Delay).Returns(100); + _poller = new ConsulFileConfigurationPoller(_factory.Object, _repo.Object, _setter.Object, _config.Object); + } + + public void Dispose() + { + _poller.Dispose(); + } + + [Fact] + public void should_start() + { + this.Given(x => ThenTheSetterIsCalled(_fileConfig, 1)) + .BDDfy(); + } + + [Fact] + public void should_call_setter_when_gets_new_config() + { + var newConfig = new FileConfiguration { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "test" + } + }, + } + } + }; + + this.Given(x => WhenTheConfigIsChangedInConsul(newConfig, 0)) + .Then(x => ThenTheSetterIsCalledAtLeast(newConfig, 1)) + .BDDfy(); + } + + [Fact] + public void should_not_poll_if_already_polling() + { + var newConfig = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "test" + } + }, + } + } + }; + + this.Given(x => WhenTheConfigIsChangedInConsul(newConfig, 10)) + .Then(x => ThenTheSetterIsCalled(newConfig, 1)) + .BDDfy(); + } + + [Fact] + public void should_do_nothing_if_call_to_consul_fails() + { + var newConfig = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "test" + } + }, + } + } + }; + + this.Given(x => WhenConsulErrors()) + .Then(x => ThenTheSetterIsCalled(newConfig, 0)) + .BDDfy(); + } + + private void WhenConsulErrors() + { + _repo + .Setup(x => x.Get()) + .ReturnsAsync(new ErrorResponse(new AnyError())); + } + + private void WhenTheConfigIsChangedInConsul(FileConfiguration newConfig, int delay) + { + _repo + .Setup(x => x.Get()) + .Callback(() => Thread.Sleep(delay)) + .ReturnsAsync(new OkResponse(newConfig)); + } + + private void ThenTheSetterIsCalled(FileConfiguration fileConfig, int times) + { + var result = WaitFor(2000).Until(() => { + try + { + _setter.Verify(x => x.Set(fileConfig), Times.Exactly(times)); + return true; + } + catch(Exception) + { + return false; + } + }); + result.ShouldBeTrue(); + } + + private void ThenTheSetterIsCalledAtLeast(FileConfiguration fileConfig, int times) + { + var result = WaitFor(2000).Until(() => { + try + { + _setter.Verify(x => x.Set(fileConfig), Times.AtLeast(times)); + return true; + } + catch(Exception) + { + return false; + } + }); + result.ShouldBeTrue(); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationRepositoryTests.cs index 784b38d4..6642bb8d 100644 --- a/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationRepositoryTests.cs @@ -1,219 +1,260 @@ -namespace Ocelot.UnitTests.Configuration -{ - using Xunit; - using TestStack.BDDfy; - using Shouldly; - using Ocelot.Configuration.Repository; - using Moq; - using Ocelot.Infrastructure.Consul; - using Ocelot.Logging; - using Ocelot.Configuration.File; - using Ocelot.Cache; - using System; - using System.Collections.Generic; - using Ocelot.Responses; - using System.Threading.Tasks; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.ServiceDiscovery.Configuration; - using Consul; - using Newtonsoft.Json; - using System.Text; - using System.Threading; - using System.Linq; - - public class ConsulFileConfigurationRepositoryTests - { - private ConsulFileConfigurationRepository _repo; - private Mock> _cache; - private Mock _internalRepo; - private Mock _factory; - private Mock _loggerFactory; - private Mock _client; - private Mock _kvEndpoint; - private FileConfiguration _fileConfiguration; - private Response _setResult; - private Response _getResult; - - public ConsulFileConfigurationRepositoryTests() - { - _cache = new Mock>(); - _internalRepo = new Mock(); - _loggerFactory = new Mock(); - - _factory = new Mock(); - _client = new Mock(); - _kvEndpoint = new Mock(); - - _client - .Setup(x => x.KV) - .Returns(_kvEndpoint.Object); - _factory - .Setup(x => x.Get(It.IsAny())) - .Returns(_client.Object); - - _internalRepo - .Setup(x => x.Get()) - .Returns(new OkResponse(new InternalConfiguration(new List(), "", new ServiceProviderConfigurationBuilder().Build(), ""))); - - _repo = new ConsulFileConfigurationRepository(_cache.Object, _internalRepo.Object, _factory.Object, _loggerFactory.Object); - } - - [Fact] - public void should_set_config() - { - var config = FakeFileConfiguration(); - - this.Given(_ => GivenIHaveAConfiguration(config)) - .And(_ => GivenWritingToConsulSucceeds()) - .When(_ => WhenISetTheConfiguration()) - .Then(_ => ThenTheConfigurationIsStoredAs(config)) - .BDDfy(); - } - - [Fact] - public void should_get_config() - { - var config = FakeFileConfiguration(); - - this.Given(_ => GivenIHaveAConfiguration(config)) - .And(_ => GivenFetchFromConsulSucceeds()) - .When(_ => WhenIGetTheConfiguration()) - .Then(_ => ThenTheConfigurationIs(config)) - .BDDfy(); - } - - [Fact] - public void should_get_null_config() - { - this.Given(_ => GivenFetchFromConsulReturnsNull()) - .When(_ => WhenIGetTheConfiguration()) - .Then(_ => ThenTheConfigurationIsNull()) - .BDDfy(); - } - - [Fact] - public void should_get_config_from_cache() - { - var config = FakeFileConfiguration(); - - this.Given(_ => GivenIHaveAConfiguration(config)) - .And(_ => GivenFetchFromCacheSucceeds()) - .When(_ => WhenIGetTheConfiguration()) - .Then(_ => ThenTheConfigurationIs(config)) - .BDDfy(); - } - - private void ThenTheConfigurationIsNull() - { - _getResult.Data.ShouldBeNull(); - } - - private void ThenTheConfigurationIs(FileConfiguration config) - { - var expected = JsonConvert.SerializeObject(config, Formatting.Indented); - var result = JsonConvert.SerializeObject(_getResult.Data, Formatting.Indented); - result.ShouldBe(expected); - } - - private async Task WhenIGetTheConfiguration() - { - _getResult = await _repo.Get(); - } - - private void GivenWritingToConsulSucceeds() - { - var response = new WriteResult(); - response.Response = true; - - _kvEndpoint - .Setup(x => x.Put(It.IsAny(), It.IsAny())).ReturnsAsync(response); - } - - private void GivenFetchFromCacheSucceeds() - { - _cache.Setup(x => x.Get(It.IsAny(), It.IsAny())).Returns(_fileConfiguration); - } - - private void GivenFetchFromConsulReturnsNull() - { - QueryResult result = new QueryResult(); - - _kvEndpoint - .Setup(x => x.Get(It.IsAny(), It.IsAny())) - .ReturnsAsync(result); - } - - private void GivenFetchFromConsulSucceeds() - { - var json = JsonConvert.SerializeObject(_fileConfiguration, Formatting.Indented); - - var bytes = Encoding.UTF8.GetBytes(json); - - var kvp = new KVPair("OcelotConfiguration"); - kvp.Value = bytes; - - var query = new QueryResult(); - query.Response = kvp; - - _kvEndpoint - .Setup(x => x.Get(It.IsAny(), It.IsAny())) - .ReturnsAsync(query); - } - - private void ThenTheConfigurationIsStoredAs(FileConfiguration config) - { - var json = JsonConvert.SerializeObject(config, Formatting.Indented); - - var bytes = Encoding.UTF8.GetBytes(json); - - _kvEndpoint - .Verify(x => x.Put(It.Is(k => k.Value.SequenceEqual(bytes)), It.IsAny()), Times.Once); - } - - private async Task WhenISetTheConfiguration() - { - _setResult = await _repo.Set(_fileConfiguration); - } - - private void GivenIHaveAConfiguration(FileConfiguration config) - { - _fileConfiguration = config; - } - - private FileConfiguration FakeFileConfiguration() - { - var reRoutes = new List - { - new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "123.12.12.12", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/asdfs/test/{test}" - } - }; - - var globalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Port = 198, - Host = "blah" - } - }; - - return new FileConfiguration - { - GlobalConfiguration = globalConfiguration, - ReRoutes = reRoutes - }; - } - } -} +namespace Ocelot.UnitTests.Configuration +{ + using Xunit; + using TestStack.BDDfy; + using Shouldly; + using Ocelot.Configuration.Repository; + using Moq; + using Ocelot.Infrastructure.Consul; + using Ocelot.Logging; + using Ocelot.Configuration.File; + using Ocelot.Cache; + using System; + using System.Collections.Generic; + using Ocelot.Responses; + using System.Threading.Tasks; + using Ocelot.Configuration; + using Ocelot.Configuration.Builder; + using Ocelot.ServiceDiscovery.Configuration; + using Consul; + using Newtonsoft.Json; + using System.Text; + using System.Threading; + using System.Linq; + + public class ConsulFileConfigurationRepositoryTests + { + private ConsulFileConfigurationRepository _repo; + private Mock> _cache; + private Mock _internalRepo; + private Mock _factory; + private Mock _loggerFactory; + private Mock _client; + private Mock _kvEndpoint; + private FileConfiguration _fileConfiguration; + private Response _setResult; + private Response _getResult; + + public ConsulFileConfigurationRepositoryTests() + { + _cache = new Mock>(); + _internalRepo = new Mock(); + _loggerFactory = new Mock(); + + _factory = new Mock(); + _client = new Mock(); + _kvEndpoint = new Mock(); + + _client + .Setup(x => x.KV) + .Returns(_kvEndpoint.Object); + + _factory + .Setup(x => x.Get(It.IsAny())) + .Returns(_client.Object); + + _internalRepo + .Setup(x => x.Get()) + .Returns(new OkResponse(new InternalConfiguration(new List(), "", new ServiceProviderConfigurationBuilder().Build(), ""))); + + _repo = new ConsulFileConfigurationRepository(_cache.Object, _internalRepo.Object, _factory.Object, _loggerFactory.Object); + } + + [Fact] + public void should_set_config() + { + var config = FakeFileConfiguration(); + + this.Given(_ => GivenIHaveAConfiguration(config)) + .And(_ => GivenWritingToConsulSucceeds()) + .When(_ => WhenISetTheConfiguration()) + .Then(_ => ThenTheConfigurationIsStoredAs(config)) + .BDDfy(); + } + + [Fact] + public void should_get_config() + { + var config = FakeFileConfiguration(); + + this.Given(_ => GivenIHaveAConfiguration(config)) + .And(_ => GivenFetchFromConsulSucceeds()) + .When(_ => WhenIGetTheConfiguration()) + .Then(_ => ThenTheConfigurationIs(config)) + .BDDfy(); + } + + [Fact] + public void should_get_null_config() + { + this.Given(_ => GivenFetchFromConsulReturnsNull()) + .When(_ => WhenIGetTheConfiguration()) + .Then(_ => ThenTheConfigurationIsNull()) + .BDDfy(); + } + + [Fact] + public void should_get_config_from_cache() + { + var config = FakeFileConfiguration(); + + this.Given(_ => GivenIHaveAConfiguration(config)) + .And(_ => GivenFetchFromCacheSucceeds()) + .When(_ => WhenIGetTheConfiguration()) + .Then(_ => ThenTheConfigurationIs(config)) + .BDDfy(); + } + + [Fact] + public void should_set_config_key() + { + var config = FakeFileConfiguration(); + + this.Given(_ => GivenIHaveAConfiguration(config)) + .And(_ => GivenTheConfigKeyComesFromFileConfig("Tom")) + .And(_ => GivenFetchFromConsulSucceeds()) + .When(_ => WhenIGetTheConfiguration()) + .And(_ => ThenTheConfigKeyIs("Tom")) + .BDDfy(); + } + + [Fact] + public void should_set_default_config_key() + { + var config = FakeFileConfiguration(); + + this.Given(_ => GivenIHaveAConfiguration(config)) + .And(_ => GivenFetchFromConsulSucceeds()) + .When(_ => WhenIGetTheConfiguration()) + .And(_ => ThenTheConfigKeyIs("InternalConfiguration")) + .BDDfy(); + } + + private void ThenTheConfigKeyIs(string expected) + { + _kvEndpoint + .Verify(x => x.Get(expected, It.IsAny()), Times.Once); + } + + private void GivenTheConfigKeyComesFromFileConfig(string key) + { + _internalRepo + .Setup(x => x.Get()) + .Returns(new OkResponse(new InternalConfiguration(new List(), "", new ServiceProviderConfigurationBuilder().WithConfigurationKey(key).Build(), ""))); + + _repo = new ConsulFileConfigurationRepository(_cache.Object, _internalRepo.Object, _factory.Object, _loggerFactory.Object); + } + + private void ThenTheConfigurationIsNull() + { + _getResult.Data.ShouldBeNull(); + } + + private void ThenTheConfigurationIs(FileConfiguration config) + { + var expected = JsonConvert.SerializeObject(config, Formatting.Indented); + var result = JsonConvert.SerializeObject(_getResult.Data, Formatting.Indented); + result.ShouldBe(expected); + } + + private async Task WhenIGetTheConfiguration() + { + _getResult = await _repo.Get(); + } + + private void GivenWritingToConsulSucceeds() + { + var response = new WriteResult(); + response.Response = true; + + _kvEndpoint + .Setup(x => x.Put(It.IsAny(), It.IsAny())).ReturnsAsync(response); + } + + private void GivenFetchFromCacheSucceeds() + { + _cache.Setup(x => x.Get(It.IsAny(), It.IsAny())).Returns(_fileConfiguration); + } + + private void GivenFetchFromConsulReturnsNull() + { + QueryResult result = new QueryResult(); + + _kvEndpoint + .Setup(x => x.Get(It.IsAny(), It.IsAny())) + .ReturnsAsync(result); + } + + private void GivenFetchFromConsulSucceeds() + { + var json = JsonConvert.SerializeObject(_fileConfiguration, Formatting.Indented); + + var bytes = Encoding.UTF8.GetBytes(json); + + var kvp = new KVPair("OcelotConfiguration"); + kvp.Value = bytes; + + var query = new QueryResult(); + query.Response = kvp; + + _kvEndpoint + .Setup(x => x.Get(It.IsAny(), It.IsAny())) + .ReturnsAsync(query); + } + + private void ThenTheConfigurationIsStoredAs(FileConfiguration config) + { + var json = JsonConvert.SerializeObject(config, Formatting.Indented); + + var bytes = Encoding.UTF8.GetBytes(json); + + _kvEndpoint + .Verify(x => x.Put(It.Is(k => k.Value.SequenceEqual(bytes)), It.IsAny()), Times.Once); + } + + private async Task WhenISetTheConfiguration() + { + _setResult = await _repo.Set(_fileConfiguration); + } + + private void GivenIHaveAConfiguration(FileConfiguration config) + { + _fileConfiguration = config; + } + + private FileConfiguration FakeFileConfiguration() + { + var reRoutes = new List + { + new FileReRoute + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "123.12.12.12", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/asdfs/test/{test}" + } + }; + + var globalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Port = 198, + Host = "blah" + } + }; + + return new FileConfiguration + { + GlobalConfiguration = globalConfiguration, + ReRoutes = reRoutes + }; + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs index 214d6d54..f1ec3a07 100644 --- a/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs @@ -1,67 +1,70 @@ -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Configuration -{ - public class ServiceProviderCreatorTests - { - private readonly ServiceProviderConfigurationCreator _creator; - private FileGlobalConfiguration _globalConfig; - private ServiceProviderConfiguration _result; - - public ServiceProviderCreatorTests() - { - _creator = new ServiceProviderConfigurationCreator(); - } - - [Fact] - public void should_create_service_provider_config() - { - var globalConfig = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "127.0.0.1", - Port = 1234, - Type = "ServiceFabric", - Token = "testtoken" - } - }; - - var expected = new ServiceProviderConfigurationBuilder() - .WithHost("127.0.0.1") - .WithPort(1234) - .WithType("ServiceFabric") - .WithToken("testtoken") - .Build(); - - this.Given(x => x.GivenTheFollowingGlobalConfig(globalConfig)) - .When(x => x.WhenICreate()) - .Then(x => x.ThenTheConfigIs(expected)) - .BDDfy(); - } - - private void GivenTheFollowingGlobalConfig(FileGlobalConfiguration fileGlobalConfig) - { - _globalConfig = fileGlobalConfig; - } - - private void WhenICreate() - { - _result = _creator.Create(_globalConfig); - } - - private void ThenTheConfigIs(ServiceProviderConfiguration expected) - { - _result.Host.ShouldBe(expected.Host); - _result.Port.ShouldBe(expected.Port); - _result.Token.ShouldBe(expected.Token); - _result.Type.ShouldBe(expected.Type); - } - } -} +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class ServiceProviderCreatorTests + { + private readonly ServiceProviderConfigurationCreator _creator; + private FileGlobalConfiguration _globalConfig; + private ServiceProviderConfiguration _result; + + public ServiceProviderCreatorTests() + { + _creator = new ServiceProviderConfigurationCreator(); + } + + [Fact] + public void should_create_service_provider_config() + { + var globalConfig = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "127.0.0.1", + Port = 1234, + Type = "ServiceFabric", + Token = "testtoken", + ConfigurationKey = "woo" + } + }; + + var expected = new ServiceProviderConfigurationBuilder() + .WithHost("127.0.0.1") + .WithPort(1234) + .WithType("ServiceFabric") + .WithToken("testtoken") + .WithConfigurationKey("woo") + .Build(); + + this.Given(x => x.GivenTheFollowingGlobalConfig(globalConfig)) + .When(x => x.WhenICreate()) + .Then(x => x.ThenTheConfigIs(expected)) + .BDDfy(); + } + + private void GivenTheFollowingGlobalConfig(FileGlobalConfiguration fileGlobalConfig) + { + _globalConfig = fileGlobalConfig; + } + + private void WhenICreate() + { + _result = _creator.Create(_globalConfig); + } + + private void ThenTheConfigIs(ServiceProviderConfiguration expected) + { + _result.Host.ShouldBe(expected.Host); + _result.Port.ShouldBe(expected.Port); + _result.Token.ShouldBe(expected.Token); + _result.Type.ShouldBe(expected.Type); + _result.ConfigurationKey.ShouldBe(expected.ConfigurationKey); + } + } +} diff --git a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs index 520d8274..41cc3573 100644 --- a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs +++ b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs @@ -126,14 +126,14 @@ namespace Ocelot.UnitTests.Controllers { _node .Setup(x => x.Accept(It.IsAny())) - .Returns(new Rafty.Concensus.OkResponse(new UpdateFileConfiguration(new FileConfiguration()))); + .ReturnsAsync(new Rafty.Concensus.OkResponse(new UpdateFileConfiguration(new FileConfiguration()))); } private void GivenTheNodeReturnsError() { _node .Setup(x => x.Accept(It.IsAny())) - .Returns(new Rafty.Concensus.ErrorResponse("error", new UpdateFileConfiguration(new FileConfiguration()))); + .ReturnsAsync(new Rafty.Concensus.ErrorResponse("error", new UpdateFileConfiguration(new FileConfiguration()))); } private void GivenTheConfigSetterReturns(Response response) diff --git a/test/Ocelot.UnitTests/Infrastructure/InMemoryBusTests.cs b/test/Ocelot.UnitTests/Infrastructure/InMemoryBusTests.cs new file mode 100644 index 00000000..35849dbf --- /dev/null +++ b/test/Ocelot.UnitTests/Infrastructure/InMemoryBusTests.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; +using Ocelot.Infrastructure; +using Shouldly; +using Xunit; + +namespace Ocelot.UnitTests.Infrastructure +{ + public class InMemoryBusTests + { + private readonly InMemoryBus _bus; + + public InMemoryBusTests() + { + _bus = new InMemoryBus(); + } + + [Fact] + public async Task should_publish_with_delay() + { + var called = false; + _bus.Subscribe(x => { + called = true; + }); + _bus.Publish(new object(), 1); + await Task.Delay(10); + called.ShouldBeTrue(); + } + + [Fact] + public void should_not_be_publish_yet_as_no_delay_in_caller() + { + var called = false; + _bus.Subscribe(x => { + called = true; + }); + _bus.Publish(new object(), 1); + called.ShouldBeFalse(); + } + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs index b81ca32e..08dfb124 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs @@ -1,5 +1,6 @@ namespace Ocelot.UnitTests.LoadBalancer { + using System; using System.Threading.Tasks; using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.Responses; @@ -10,28 +11,43 @@ namespace Ocelot.UnitTests.LoadBalancer using Microsoft.AspNetCore.Http; using System.Collections.Generic; using System.Collections; - using System.Threading; using Ocelot.Middleware; using Ocelot.UnitTests.Responder; using TestStack.BDDfy; + using Ocelot.Infrastructure; public class CookieStickySessionsTests { private readonly CookieStickySessions _stickySessions; private readonly Mock _loadBalancer; + private readonly int _defaultExpiryInMs; private DownstreamContext _downstreamContext; private Response _result; private Response _firstHostAndPort; private Response _secondHostAndPort; + private readonly FakeBus _bus; public CookieStickySessionsTests() { + _bus = new FakeBus(); _loadBalancer = new Mock(); - const int defaultExpiryInMs = 100; - _stickySessions = new CookieStickySessions(_loadBalancer.Object, "sessionid", defaultExpiryInMs); + _defaultExpiryInMs = 0; + _stickySessions = new CookieStickySessions(_loadBalancer.Object, "sessionid", _defaultExpiryInMs, _bus); _downstreamContext = new DownstreamContext(new DefaultHttpContext()); } + [Fact] + public void should_expire_sticky_session() + { + this.Given(_ => GivenTheLoadBalancerReturns()) + .And(_ => GivenTheDownstreamRequestHasSessionId("321")) + .And(_ => GivenIHackAMessageInWithAPastExpiry()) + .And(_ => WhenILease()) + .When(_ => WhenTheMessagesAreProcessed()) + .Then(_ => ThenTheLoadBalancerIsCalled()) + .BDDfy(); + } + [Fact] public void should_return_host_and_port() { @@ -48,6 +64,7 @@ namespace Ocelot.UnitTests.LoadBalancer .And(_ => GivenTheDownstreamRequestHasSessionId("321")) .When(_ => WhenILeaseTwiceInARow()) .Then(_ => ThenTheFirstAndSecondResponseAreTheSame()) + .And(_ => ThenTheStickySessionWillTimeout()) .BDDfy(); } @@ -69,92 +86,26 @@ namespace Ocelot.UnitTests.LoadBalancer .BDDfy(); } - [Fact] - public void should_expire_sticky_session() - { - this.Given(_ => GivenTheLoadBalancerReturnsSequence()) - .When(_ => WhenTheStickySessionExpires()) - .Then(_ => ThenANewHostAndPortIsReturned()) - .BDDfy(); - } - - [Fact] - public void should_refresh_sticky_session() - { - this.Given(_ => GivenTheLoadBalancerReturnsSequence()) - .When(_ => WhenIMakeRequestsToKeepRefreshingTheSession()) - .Then(_ => ThenTheSessionIsRefreshed()) - .BDDfy(); - } - - [Fact] - public void should_dispose() - { - _stickySessions.Dispose(); - } - [Fact] public void should_release() { _stickySessions.Release(new ServiceHostAndPort("", 0)); } - private async Task ThenTheSessionIsRefreshed() + private void ThenTheLoadBalancerIsCalled() { - var postExpireHostAndPort = await _stickySessions.Lease(_downstreamContext); - postExpireHostAndPort.Data.DownstreamHost.ShouldBe("one"); - postExpireHostAndPort.Data.DownstreamPort.ShouldBe(80); - - _loadBalancer - .Verify(x => x.Lease(It.IsAny()), Times.Once); + _loadBalancer.Verify(x => x.Release(It.IsAny()), Times.Once); } - private async Task WhenIMakeRequestsToKeepRefreshingTheSession() + private void WhenTheMessagesAreProcessed() { - var context = new DefaultHttpContext(); - var cookies = new FakeCookies(); - cookies.AddCookie("sessionid", "321"); - context.Request.Cookies = cookies; - _downstreamContext = new DownstreamContext(context); - - var firstHostAndPort = await _stickySessions.Lease(_downstreamContext); - firstHostAndPort.Data.DownstreamHost.ShouldBe("one"); - firstHostAndPort.Data.DownstreamPort.ShouldBe(80); - - Thread.Sleep(80); - - var secondHostAndPort = await _stickySessions.Lease(_downstreamContext); - secondHostAndPort.Data.DownstreamHost.ShouldBe("one"); - secondHostAndPort.Data.DownstreamPort.ShouldBe(80); - - Thread.Sleep(80); + _bus.Process(); } - private async Task ThenANewHostAndPortIsReturned() + private void GivenIHackAMessageInWithAPastExpiry() { - var postExpireHostAndPort = await _stickySessions.Lease(_downstreamContext); - postExpireHostAndPort.Data.DownstreamHost.ShouldBe("two"); - postExpireHostAndPort.Data.DownstreamPort.ShouldBe(80); - } - - private async Task WhenTheStickySessionExpires() - { - var context = new DefaultHttpContext(); - var cookies = new FakeCookies(); - cookies.AddCookie("sessionid", "321"); - context.Request.Cookies = cookies; - _downstreamContext = new DownstreamContext(context); - - var firstHostAndPort = await _stickySessions.Lease(_downstreamContext); - var secondHostAndPort = await _stickySessions.Lease(_downstreamContext); - - firstHostAndPort.Data.DownstreamHost.ShouldBe("one"); - firstHostAndPort.Data.DownstreamPort.ShouldBe(80); - - secondHostAndPort.Data.DownstreamHost.ShouldBe("one"); - secondHostAndPort.Data.DownstreamPort.ShouldBe(80); - - Thread.Sleep(150); + var hostAndPort = new ServiceHostAndPort("999", 999); + _bus.Publish(new StickySession(hostAndPort, DateTime.UtcNow.AddDays(-1), "321"), 0); } private void ThenAnErrorIsReturned() @@ -236,9 +187,14 @@ namespace Ocelot.UnitTests.LoadBalancer { _result.Data.ShouldNotBeNull(); } + + private void ThenTheStickySessionWillTimeout() + { + _bus.Messages.Count.ShouldBe(2); + } } - - class FakeCookies : IRequestCookieCollection + + internal class FakeCookies : IRequestCookieCollection { private readonly Dictionary _cookies = new Dictionary(); @@ -273,4 +229,37 @@ namespace Ocelot.UnitTests.LoadBalancer return _cookies.GetEnumerator(); } } + + internal class FakeBus : IBus + { + public FakeBus() + { + Messages = new List(); + Subscriptions = new List>(); + } + + public List Messages { get; } + public List> Subscriptions { get; } + + public void Subscribe(Action action) + { + Subscriptions.Add(action); + } + + public void Publish(T message, int delay) + { + Messages.Add(message); + } + + public void Process() + { + foreach (var message in Messages) + { + foreach (var subscription in Subscriptions) + { + subscription(message); + } + } + } + } } diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs index ffb84518..1ea409df 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs @@ -26,7 +26,7 @@ namespace Ocelot.UnitTests.LoadBalancer { _factory = new Mock(); _loadBalancerHouse = new LoadBalancerHouse(_factory.Object); - _serviceProviderConfig = new ServiceProviderConfiguration("myType","myHost",123, string.Empty); + _serviceProviderConfig = new ServiceProviderConfiguration("myType","myHost",123, string.Empty, "configKey"); } [Fact] diff --git a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj index 395dde67..9aa7db12 100644 --- a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +++ b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj @@ -29,30 +29,30 @@ - - + + all - - - - - - - + + + + + + + + -