diff --git a/Ocelot.sln b/Ocelot.sln index 1215130de..e40f83cfb 100644 --- a/Ocelot.sln +++ b/Ocelot.sln @@ -93,6 +93,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDisco EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.DownstreamService", "samples\OcelotServiceDiscovery\DownstreamService\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "{E2AC741A-4120-4D59-B5E4-16382ED45E8D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Testing", "test\Ocelot.Testing\Ocelot.Testing.csproj", "{AE6BCCBD-0687-4C58-B30F-4ABBC6422087}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -203,6 +205,10 @@ Global {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Debug|Any CPU.Build.0 = Debug|Any CPU {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Release|Any CPU.ActiveCfg = Release|Any CPU {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Release|Any CPU.Build.0 = Release|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -242,6 +248,7 @@ Global {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} {D37209EA-C13E-42AE-B851-A8604F1FCD0E} = {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} {E2AC741A-4120-4D59-B5E4-16382ED45E8D} = {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087} = {5B401523-36DA-4491-B73A-7590A26E420B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48} diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 1193d5db1..e3df12c17 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,17 +1,33 @@ -## Upgrade to .NET 8 (version {0}) aka [.NET 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) release -> Read article: [Announcing .NET 8](https://devblogs.microsoft.com/dotnet/announcing-dotnet-8/) by Gaurav Seth, on November 14th, 2023 +## October 2023 (version {0}) aka [Swiss Locomotive](https://en.wikipedia.org/wiki/SBB-CFF-FFS_Ae_6/6) release +> Codenamed as **[Swiss Locomotive](https://www.google.com/search?q=swiss+locomotive)** -### About -We are pleased to announce to you that we can now offer the support of [.NET 8](https://dotnet.microsoft.com/en-us/download). -But that is not all, in this release, we are adopting support of several versions of the .NET framework through [multitargeting](https://learn.microsoft.com/en-us/dotnet/standard/frameworks). -The Ocelot distribution is now compatible with .NET **6**, **7** and **8**. :tada: +### Focused On +
+ Logging feature. Performance review, redesign and improvements with new best practices to log -In the future, we will try to ensure the support of the [.NET SDKs](https://dotnet.microsoft.com/en-us/download/dotnet) that are still actively maintained by the .NET team and community. -Current .NET versions in support are the following: [6, 7, 8](https://dotnet.microsoft.com/en-us/download/dotnet). + - Proposing a centralized `WriteLog` method for the `OcelotLogger` + - Factory methods for computed strings such as `string.Format` or interpolated strings + - Using `ILogger.IsEnabled` before calling the native `WriteLog` implementation and invoking string factory method +
+
+ Quality of Service feature. Redesign and stabilization, and it produces less log records now. + + - Fixing issue with [Polly](https://www.thepollyproject.org/) Circuit Breaker not opening after max number of retries reached + - Removing useless log calls that could have an impact on performance + - Polly [lib](https://www.nuget.org/packages/Polly#versions-body-tab) reference updating to latest `8.2.0` with some code improvements +
+
+ Documentation for Logging, Request ID, Routing and Websockets + + - [Logging](https://ocelot.readthedocs.io/en/latest/features/logging.html) + - [Request ID](https://ocelot.readthedocs.io/en/latest/features/requestid.html) + - [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html) + - [Websockets](https://ocelot.readthedocs.io/en/latest/features/websockets.html) +
+
+ Testing improvements and stabilization aka bug fixing -### Technical info -As an ASP.NET Core app, now Ocelot targets `net6.0`, `net7.0` and `net8.0` frameworks. - -Starting with **v{0}**, the solution's code base supports [Multitargeting](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-multitargeting-overview) as SDK-style projects. -It should be easier for teams to move between (migrate to) .NET 6, 7 and 8 frameworks. Also, new features will be available for all .NET SDKs which we support via multitargeting. -Find out more here: [Target frameworks in SDK-style projects](https://learn.microsoft.com/en-us/dotnet/standard/frameworks) + - [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html) bug fixing: query string placeholders including **CatchAll** one aka `{{everything}}` and query string duplicates removal + - [QoS](https://ocelot.readthedocs.io/en/latest/features/qualityofservice.html) bug fixing: Polly circuit breaker exceptions + - Testing bug fixing: rare failed builds because of unstable Polly tests. Acceptance common logic for ports +
diff --git a/build.cake b/build.cake index 75f30a755..1e8f4a032 100644 --- a/build.cake +++ b/build.cake @@ -161,7 +161,8 @@ Task("CreateReleaseNotes") var releaseHeader = string.Format(System.IO.File.ReadAllText("./ReleaseNotes.md"), releaseVersion, lastRelease); releaseNotes = new List { releaseHeader }; - var shortlogSummary = GitHelper($"shortlog --no-merges --numbered --summary {lastRelease}..HEAD"); + var shortlogSummary = GitHelper($"shortlog --no-merges --numbered --summary {lastRelease}..HEAD") + .ToList(); var re = new Regex(@"^[\s\t]*(?'commits'\d+)[\s\t]+(?'author'.*)$"); var summary = shortlogSummary .Where(x => re.IsMatch(x)) @@ -207,7 +208,6 @@ Task("CreateReleaseNotes") static string HonorForDeletions(string place, string author, int commits, int files, int insertions, int deletions) => HonorForInsertions(place, author, commits, files, insertions, $"and **{deletions}** deletion{Plural(deletions)}"); - var statistics = new List<(string Contributor, int Files, int Insertions, int Deletions)>(); foreach (var group in commitsGrouping) { if (topContributors.Count >= top3) break; @@ -220,6 +220,7 @@ Task("CreateReleaseNotes") } else // multiple candidates with the same number of commits, so, group by files changed { + var statistics = new List<(string Contributor, int Files, int Insertions, int Deletions)>(); var shortstatRegex = new Regex(@"^\s*(?'files'\d+)\s+files?\s+changed(?'ins',\s+(?'insertions'\d+)\s+insertions?\(\+\))?(?'del',\s+(?'deletions'\d+)\s+deletions?\(\-\))?\s*$"); // Collect statistics from git log & shortlog foreach (var author in group.authors) @@ -315,15 +316,15 @@ private void WriteReleaseNotes() Information($"RUN {nameof(WriteReleaseNotes)} ..."); EnsureDirectoryExists(packagesDir); - System.IO.File.WriteAllLines(releaseNotesFile, releaseNotes); + System.IO.File.WriteAllLines(releaseNotesFile, releaseNotes, Encoding.UTF8); - var content = System.IO.File.ReadAllText(releaseNotesFile); + var content = System.IO.File.ReadAllText(releaseNotesFile, Encoding.UTF8); if (string.IsNullOrEmpty(content)) { System.IO.File.WriteAllText(releaseNotesFile, "No commits since last release"); } - Information($"Release notes are >>>\n{content}<<<"); + Information("Release notes are >>>\n{0}<<<", content); Information($"EXITED {nameof(WriteReleaseNotes)}"); } @@ -510,10 +511,11 @@ Task("PublishToNuget") .IsDependentOn("DownloadGitHubReleaseArtifacts") .Does(() => { - if (IsRunningOnCircleCI()) - { - PublishPackages(packagesDir, artifactsFile, nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl); - } + Information("Skipping of publishing to NuGet..."); + // if (IsRunningOnCircleCI()) + // { + // PublishPackages(packagesDir, artifactsFile, nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl); + // } }); RunTarget(target); diff --git a/docs/conf.py b/docs/conf.py index 8bd28ecbf..dca77bb40 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ project = 'Ocelot' copyright = ' 2023 ThreeMammals Ocelot team' author = 'Tom Pallister, Ocelot Core team at ThreeMammals' -release = '21.0' +release = '22.0' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/features/caching.rst b/docs/features/caching.rst index f557d1817..a7cd4096f 100644 --- a/docs/features/caching.rst +++ b/docs/features/caching.rst @@ -30,7 +30,7 @@ Finally, in order to use caching on a route in your Route configuration add this .. code-block:: json - "FileCacheOptions": { "TtlSeconds": 15, "Region": "somename" } + "FileCacheOptions": { "TtlSeconds": 15, "Region": "europe-central" } In this example **TtlSeconds** is set to 15 which means the cache will expire after 15 seconds. The **Region** represents a region of caching. diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 589c4a0b0..936a78a9d 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -241,6 +241,8 @@ Use ``HttpHandlerOptions`` in a Route configuration to set up ``HttpHandler`` be * **MaxConnectionsPerServer** This controls how many connections the internal ``HttpClient`` will open. This can be set at Route or global level. +.. _ssl-errors: + SSL Errors ---------- diff --git a/docs/features/logging.rst b/docs/features/logging.rst index 979b16145..037502db3 100644 --- a/docs/features/logging.rst +++ b/docs/features/logging.rst @@ -2,23 +2,158 @@ Logging ======= Ocelot uses the standard logging interfaces ``ILoggerFactory`` and ``ILogger`` at the moment. -This is encapsulated in ``IOcelotLogger`` and ``IOcelotLoggerFactory`` with an implementation for the standard `ASP.NET Core logging `_ stuff at the moment. -This is because Ocelot adds some extra info to the logs such as **request ID** if it is configured. +This is encapsulated in ``IOcelotLogger`` and ``IOcelotLoggerFactory`` with the implementation for the standard `ASP.NET Core logging `_ stuff at the moment. +This is because Ocelot adds some extra info to the logs such as **RequestId** if it is configured. There is a global `error handler middleware `_ that should catch any exceptions thrown and log them as errors. -Finally, if logging is set to **Trace** level, Ocelot will log starting, finishing and any middlewares that throw an exception which can be quite useful. +Finally, if logging is set to ``Trace`` level, Ocelot will log starting, finishing and any middlewares that throw an exception which can be quite useful. + +Request ID +---------- The reason for not just using `bog standard `_ framework logging is that -we could not work out how to override the request id that get's logged when setting **IncludeScopes** to ``true`` for logging settings. +we could not work out how to override the **RequestId** that get's logged when setting **IncludeScopes** to ``true`` for logging settings. Nicely onto the next feature. +Every log record has these 2 properties: + +* **RequestId** represents ID of the current request as plain string, for example ``0HMVD33IIJRFR:00000001`` +* **PreviousRequestId** represents ID of the previous request + +As an ``IOcelotLogger`` interface object being injected to constructors of service classes, current default Ocelot logger (``OcelotLogger`` class) reads these 2 properties from the ``IRequestScopedDataRepository`` interface object. +Find out more about these properties and other details on the *Request ID* logging feature in the :doc:`../features/requestid` chapter. + +.. _logging-warning: + Warning ------- -If you are logging to `Console `_, you will get terrible performance. -The team has had so many issues about performance issues with Ocelot and it is always logging level **Debug**, logging to `Console `_. +If you are logging to MS `Console `_, you will get terrible performance. +The team has had so many issues about performance issues with Ocelot and it is always logging level ``Debug``, logging to `Console `_. * **Warning!** Make sure you are logging to something proper in production environment! -* Use **Error** and **Critical** levels in production environment! -* Use **Warning** level in testing environment! +* Use ``Error`` and ``Critical`` levels in production environment! +* Use ``Warning`` level in testing & staging environments! + +These and other recommendations are below in the :ref:`logging-best-practices` section. + +.. _logging-best-practices: + +Best Practices +-------------- + + | Microsoft Learn сomplete reference: `Logging in .NET Core and ASP.NET Core `_ + +Our recommendations to gain Ocelot best logging are the following. + +First +^^^^^ + +Ensure minimum level while `Configure logging `_. +The minimum log level is set in the application's ``appsettings.json`` file. This level is defined in the **Logging** section, for example: + +.. code-block:: json + + { + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } + } + +Whether using `Serilog `_ or the standard Microsoft providers, the logging configuration will be retrieved from this section. + +.. code-block:: csharp + + .ConfigureAppConfiguration((_, config) => + { + config.AddJsonFile($"appsettings.{env.EnvironmentName}.json", false, false); + // ... + }) + +However, there is one thing to be aware of. It is possible to use the ``SetMinimumLevel()`` method to define the minimum logging level. +Be careful and make sure you set the log level in one place only, like: + +.. code-block:: csharp + + ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.SetMinimumLevel(minLogLevel); + logging.AddConsole(); // MS Console for Development and/or Testing environments only + }) + +Please also use the ``ClearProviders()`` method, so that only the providers you wish to use are taken into account, as in the example above, the console. + +Second +^^^^^^ + +Ensure proper usage of minimum logging level for each environment: development, testing, production, etc. +So, once again, read important notes of the :ref:`logging-warning` section! + +Third +^^^^^ + +Ocelot's logging has been improved in `22.0 `_ version: +it is now possible to use a factory method for message strings that will only be executed if the minimum log level allows it. + +For example, let's take a message containing information about several variables that should only be generated if the minimum log level is ``Debug``. +If the minimum log level is ``Warning`` then the string is never generated. + +Therefore, when the string contains dynamic information aka ``string.Format``, or string value is generated by `string interpolation `_ expression, +it is recommended to call the log method using anonymous delegate via an ``=>`` expression function: + +.. code-block:: csharp + + Logger.LogDebug(() => $"downstream templates are {string.Join(", ", response.Data.Route.DownstreamRoute.Select(r => r.DownstreamPathTemplate.Value))}"); + +otherwise a constant string is sufficient + +.. code-block:: csharp + + Logger.LogDebug("My const string"); + +Performance Review +------------------ + +Ocelot's logging performance has been improved in version `22.0 `__ (see PR `1745 `_). +These changes were requested as part of issue `1744 `_ after team's `discussion `_. + +Top Logging Performance? +^^^^^^^^^^^^^^^^^^^^^^^^ + +Here is a quick recipe for your Production environment! +You need to ensure the minimal level is ``Critical`` or ``None``. Nothing more! +For sure, having top logging performance means having less log records written by logging provider. So, logs should be pretty empty. + +Anyway, during the first time after a version release to production, we recommend to watch the system and current version app behavior by specifying ``Error`` minimum level. +If release engineer will ensure stability of the version in production then minimum level can be increased to ``Critical`` or ``None`` to gain top performance. +Technically this will switch off the logging feature at all. + +Run Benchmarks +^^^^^^^^^^^^^^ + +We have 2 types of benchmarks currently + +* ``SerilogBenchmarks`` with Serilog logging to a file. See ``ConfigureLogging`` method with ``logging.AddSerilog(_logger);`` +* ``MsLoggerBenchmarks`` with MS default logging to MS Console. See ``ConfigureLogging`` method with ``logging.AddConsole();`` + +Benchmark results largely depend on the environment and hardware on which they run. +We are pleased to invite you to run Logging benchmarks on your machine by the following instructions below. + +1. Open PowerShell or Command Prompt console +2. Build Ocelot solution in Release mode: ``dotnet build --configuration Release`` +3. Go to ``test\Ocelot.Benchmarks\bin\Release\`` folder. +4. Choose .NET version changing the folder, for example to ``net8.0`` +5. Run **Ocelot.Benchmarks.exe**: ``.\Ocelot.Benchmarks.exe`` +6. Run ``SerilogBenchmarks`` or ``MsLoggerBenchmarks`` by pressing appropriate number of a benchmark: ``5`` or ``6``, + Enter +7. Wait for 3+ minutes to complete benchmark, and get final results. +8. Read and analize your benchmark session results. + +Indicators +^^^^^^^^^^ + +``To be developed...`` diff --git a/docs/features/requestid.rst b/docs/features/requestid.rst index 5fc46b936..014aaf3c5 100644 --- a/docs/features/requestid.rst +++ b/docs/features/requestid.rst @@ -1,13 +1,13 @@ Request ID ========== - aka **Correlation ID** + aka **Correlation ID** or `HttpContext.TraceIdentifier `_ Ocelot supports a client sending a *request ID* in the form of a header. -If set, Ocelot will use the **requestId** for logging as soon as it becomes available in the middleware pipeline. -Ocelot will also forward the *request ID* with the specified header to the downstream service. +If set, Ocelot will use the **RequestId** for logging as soon as it becomes available in the middleware pipeline. +Ocelot will also forward the *RequestId* with the specified header to the downstream service. -You can still get the ASP.NET Core *request ID* in the logs if you set **IncludeScopes** ``true`` in your logging config. +You can still get the ASP.NET Core *Request ID* in the logs if you set **IncludeScopes** ``true`` in your logging config. In order to use the *Request ID* feature you have two options. @@ -67,3 +67,24 @@ Below is an example of the logging when set at ``Debug`` level for a normal requ requestId: 1234, previousRequestId: asdf, message: no pipeline errors, setting and returning completed response, dbug: Ocelot.Errors.Middleware.ExceptionHandlerMiddleware[0] requestId: 1234, previousRequestId: asdf, message: ocelot pipeline finished, + +And more practical example from secret production environment in Switzerland: + +.. code-block:: text + + warn: Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware[0] + requestId: 0HMVD33IIJRFR:00000001, previousRequestId: no previous request id, message: DownstreamRouteFinderMiddleware setting pipeline errors. IDownstreamRouteFinder returned Error Code: UnableToFindDownstreamRouteError Message: Failed to match Route configuration for upstream path: /, verb: GET. + warn: Ocelot.Responder.Middleware.ResponderMiddleware[0] + requestId: 0HMVD33IIJRFR:00000001, previousRequestId: no previous request id, message: Error Code: UnableToFindDownstreamRouteError Message: Failed to match Route configuration for upstream path: /, verb: GET. errors found in ResponderMiddleware. Setting error response for request path:/, request method: GET + +Curious? +-------- + +*Request ID* is a part of big :doc:`../features/logging` feature. + +Every log record has these 2 properties: + +* **RequestId** represents ID of the current request as plain string, for example ``0HMVD33IIJRFR:00000001`` +* **PreviousRequestId** represents ID of the previous request + +As an ``IOcelotLogger`` interface object being injected to constructors of service classes, current default Ocelot logger (the ``OcelotLogger`` class) reads these 2 properties from the ``IRequestScopedDataRepository`` interface object. diff --git a/docs/features/routing.rst b/docs/features/routing.rst index 10c4ba1e3..e2ca941bf 100644 --- a/docs/features/routing.rst +++ b/docs/features/routing.rst @@ -169,42 +169,104 @@ This feature was requested in `issue 340 `_ Ocelot is able to forward query string parameters with their processing in the form of ``{something}``. +Also, the query parameter placeholder needs to be present in both the **DownstreamPathTemplate** and **UpstreamPathTemplate** properties. +Placeholder replacement works bi-directionally between path and query strings, with some `restrictions <#restrictions-on-use>`_ on usage. + +Path to Query String direction +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ocelot allows you to specify a query string as part of the **DownstreamPathTemplate** like the example below: .. code-block:: json { - "UpstreamHttpMethod": [ "Get" ], - "UpstreamPathTemplate": "/api/units/{subscriptionId}/{unitId}/updates", - "DownstreamPathTemplate": "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { "Host": "localhost", "Port": 50110 } - ] + "UpstreamPathTemplate": "/api/units/{subscription}/{unit}/updates", + "DownstreamPathTemplate": "/api/subscriptions/{subscription}/updates?unitId={unit}", } -In this example Ocelot will use the value from the ``{unitId}`` placeholder in the upstream path template and add it to the downstream request as a query string parameter called ``unitId``! +In this example Ocelot will use the value from the ``{unit}`` placeholder in the upstream path template and add it to the downstream request as a query string parameter called ``unitId``! Make sure you name the placeholder differently due to `restrictions <#restrictions-on-use>`_ on usage. + + +Query String to Path direction +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ocelot will also allow you to put query string parameters in the **UpstreamPathTemplate** so you can match certain queries to certain services: .. code-block:: json { - "UpstreamHttpMethod": [ "Get" ], - "UpstreamPathTemplate": "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - "DownstreamPathTemplate": "/api/units/{subscriptionId}/{unitId}/updates", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { "Host": "localhost", "Port": 50110 } - ] + "UpstreamPathTemplate": "/api/subscriptions/{subscriptionId}/updates?unitId={uid}", + "DownstreamPathTemplate": "/api/units/{subscriptionId}/{uid}/updates", } In this example Ocelot will only match requests that have a matching URL path and the query string starts with ``unitId=something``. You can have other queries after this but you must start with the matching parameter. -Also Ocelot will swap the ``{unitId}`` parameter from the query string and use it in the downstream request path. +Also Ocelot will swap the ``{uid}`` parameter from the query string and use it in the downstream request path. +Note, the best practice is giving different placeholder name than the name of query parameter due to `restrictions <#restrictions-on-use>`_ on usage. + +Catch All Query String +^^^^^^^^^^^^^^^^^^^^^^ + +Ocelot's routing also supports a *Catch All* style routing to forward all query string parameters. +The placeholder ``{everything}`` name does not matter, any name will work. + +.. code-block:: json + + { + "UpstreamPathTemplate": "/contracts?{everything}", + "DownstreamPathTemplate": "/apipath/contracts?{everything}", + } + +This entire query string routing feature is very useful in cases where the query string should not be transformed but rather routed without any changes, +such as OData filters and etc (see issue `1174 `_). + +Restrictions on use +^^^^^^^^^^^^^^^^^^^ + +The query string parameters are ordered and merged to produce the final downstream URL. +This is necessary because the ``DownstreamUrlCreatorMiddleware`` needs to have some control when replacing placeholders and merging duplicate parameters. +So, even if your parameter is presented as the first parameter in the upstream, then in the final downstream URL the said query parameter will have a different position. +But this doesn't seem to break anything in the downstream API. + +Because of parameters merging, special ASP.NET API `model binding `_ +for arrays is not supported if you use array items representation like ``selectedCourses=1050&selectedCourses=2000``. +This query string will be merged as ``selectedCourses=1050`` in downstream URL. So, array data will be lost! +Make sure upstream clients generate correct query string for array models like ``selectedCourses[0]=1050&selectedCourses[1]=2000``. +To understand array model bidings, see `Bind arrays and string values from headers and query strings `_ docs. + +**Warning!** Query string placeholders have naming restrictions due to ``DownstreamUrlCreatorMiddleware`` implementations. +On the other hand, it gives you the flexibility to control whether the parameter is present in the final downstream URL. +Here are two user scenarios. + +* User wants to save the parameter after replacing the placeholder (see issue `473 `_). + To do this you need to use the following template definition: + + .. code-block:: json + + { + "UpstreamPathTemplate": "/path/{serverId}/{action}", + "DownstreamPathTemplate": "/path2/{action}?server={serverId}" + } + + So, ``{serverId}`` placeholder and ``server`` parameter **names are different**! + Finally, the ``server`` parameter is kept. + +* User wants to remove old parameter after replacing placeholder (see issue `952 `_). + To do this you need to use the same names: + + .. code-block:: json + + { + "UpstreamPathTemplate": "/users?userId={userId}", + "DownstreamPathTemplate": "/persons?personId={userId}" + } + + So, both ``{userId}`` placeholder and ``userId`` parameter **names are the same**! + Finally, the ``userId`` parameter is removed. Security Options ---------------- diff --git a/docs/features/websockets.rst b/docs/features/websockets.rst index 5e1e4fc54..1501915d5 100644 --- a/docs/features/websockets.rst +++ b/docs/features/websockets.rst @@ -1,7 +1,8 @@ Websockets ========== - `WebSockets Standard `_ by WHATWG organization + * `WebSockets Standard `_ by WHATWG organization + * `The WebSocket Protocol `_ by Internet Engineering Task Force (IETF) organization Ocelot supports proxying `WebSockets `_ with some extra bits. This functionality was requested in `issue 212 `_. @@ -90,12 +91,56 @@ Note normal Ocelot routing rules apply the main thing is the scheme which is set "UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE", "OPTIONS" ], "UpstreamPathTemplate": "/gateway/{catchAll}", "DownstreamPathTemplate": "/{catchAll}", - "DownstreamScheme": "ws", + "DownstreamScheme": "ws", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5001 } ] } +WebSocket Secure +---------------- + +If you define a route with Secured WebSocket protocol, use the ``wss`` scheme: + +.. code-block:: json + + { + "DownstreamScheme": "wss", + // ... + } + +Keep in mind: you can use WebSocket SSL for both `SignalR <#signalr>`_ and `WebSockets <#websockets>`__. + +To understand ``wss`` scheme, browse to this: + +* Microsoft Learn: `Secure your connection with TLS/SSL `_ +* IETF | The WebSocket Protocol: `WebSocket URIs `_ + +If you have questions, it may be helpful to search for documentation on MS Learn: + +* `Search for "secure websocket" `_ + +SSL Errors +^^^^^^^^^^ + +If you want to ignore SSL warnings (errors), set the following in your Route config: + +.. code-block:: json + + { + "DownstreamScheme": "wss", + "DangerousAcceptAnyServerCertificateValidator": true, + // ... + } + +**But we don't recommend doing this!** Read the official notes regarding :ref:`ssl-errors` in the :doc:`../features/configuration` doc, +where you will also find best practices for your environments. + +**Note**, the ``wss`` scheme fake validator was added by `PR 1377 `_, +as a part of issues `1375 `_, `1237 `_ and etc. +This life hacking feature for self-signed SSL certificates is available in version `20.0 `_. +It will be removed and/or reworked in future releases. See the :ref:`ssl-errors` section for details. + Supported --------- diff --git a/docs/index.rst b/docs/index.rst index 3ec283703..87d0afa5f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,4 @@ -Welcome to Ocelot 21.0 +Welcome to Ocelot 22.0 ====================== Thanks for taking a look at the Ocelot documentation! Please use the left hand navigation to get around. diff --git a/src/Ocelot.Provider.Consul/Consul.cs b/src/Ocelot.Provider.Consul/Consul.cs index 84bc7ceee..273fca2ab 100644 --- a/src/Ocelot.Provider.Consul/Consul.cs +++ b/src/Ocelot.Provider.Consul/Consul.cs @@ -44,7 +44,7 @@ public async Task> GetAsync() else { _logger.LogWarning( - $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."); + () => $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."); } } diff --git a/src/Ocelot.Provider.Consul/PollConsul.cs b/src/Ocelot.Provider.Consul/PollConsul.cs index 5806e60f1..45fd10b19 100644 --- a/src/Ocelot.Provider.Consul/PollConsul.cs +++ b/src/Ocelot.Provider.Consul/PollConsul.cs @@ -50,7 +50,7 @@ public Task> GetAsync() try { - _logger.LogInformation($"Retrieving new client information for service: {ServiceName}..."); + _logger.LogInformation(() => $"Retrieving new client information for service: {ServiceName}..."); _services = _consulServiceDiscoveryProvider.GetAsync().Result; return Task.FromResult(_services); } diff --git a/src/Ocelot.Provider.Kubernetes/KubernetesServiceDiscoveryProvider.cs b/src/Ocelot.Provider.Kubernetes/KubernetesServiceDiscoveryProvider.cs index ce3ca316f..92c7b99cb 100644 --- a/src/Ocelot.Provider.Kubernetes/KubernetesServiceDiscoveryProvider.cs +++ b/src/Ocelot.Provider.Kubernetes/KubernetesServiceDiscoveryProvider.cs @@ -30,7 +30,7 @@ public async Task> GetAsync() } else { - _logger.LogWarning($"namespace:{_kubeRegistryConfiguration.KubeNamespace}service:{_kubeRegistryConfiguration.KeyOfServiceInK8s} Unable to use ,it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"); + _logger.LogWarning(() => $"namespace:{_kubeRegistryConfiguration.KubeNamespace}service:{_kubeRegistryConfiguration.KeyOfServiceInK8s} Unable to use ,it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"); } return services; diff --git a/src/Ocelot.Provider.Polly/CircuitBreaker.cs b/src/Ocelot.Provider.Polly/CircuitBreaker.cs deleted file mode 100644 index ce2a89bf2..000000000 --- a/src/Ocelot.Provider.Polly/CircuitBreaker.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Polly; - -namespace Ocelot.Provider.Polly -{ - public class CircuitBreaker - { - private readonly List _policies = new(); - - public CircuitBreaker(params IAsyncPolicy[] policies) - { - foreach (var policy in policies.Where(p => p != null)) - { - _policies.Add(policy); - } - } - - public IAsyncPolicy[] Policies => _policies.ToArray(); - } -} diff --git a/src/Ocelot.Provider.Polly/Interfaces/IPollyQoSProvider.cs b/src/Ocelot.Provider.Polly/Interfaces/IPollyQoSProvider.cs index 4b693ede0..214597531 100644 --- a/src/Ocelot.Provider.Polly/Interfaces/IPollyQoSProvider.cs +++ b/src/Ocelot.Provider.Polly/Interfaces/IPollyQoSProvider.cs @@ -1,6 +1,9 @@ -namespace Ocelot.Provider.Polly.Interfaces; +using Ocelot.Configuration; -public interface IPollyQoSProvider +namespace Ocelot.Provider.Polly.Interfaces; + +public interface IPollyQoSProvider + where TResult : class { - CircuitBreaker CircuitBreaker { get; } + PollyPolicyWrapper GetPollyPolicyWrapper(DownstreamRoute route); } diff --git a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs index 0d167f571..c37c690d8 100644 --- a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs @@ -1,34 +1,43 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.DependencyInjection; using Ocelot.Errors; using Ocelot.Logging; +using Ocelot.Provider.Polly.Interfaces; using Ocelot.Requester; using Polly.CircuitBreaker; using Polly.Timeout; - -namespace Ocelot.Provider.Polly -{ - public static class OcelotBuilderExtensions - { - public static IOcelotBuilder AddPolly(this IOcelotBuilder builder) - { - var errorMapping = new Dictionary> - { - {typeof(TaskCanceledException), e => new RequestTimedOutError(e)}, - {typeof(TimeoutRejectedException), e => new RequestTimedOutError(e)}, - {typeof(BrokenCircuitException), e => new RequestTimedOutError(e)}, - }; - - builder.Services.AddSingleton(errorMapping); - - static DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute route, IOcelotLoggerFactory logger) - { - return new PollyCircuitBreakingDelegatingHandler(new PollyQoSProvider(route, logger), logger); - } - - builder.Services.AddSingleton((QosDelegatingHandlerDelegate)QosDelegatingHandlerDelegate); - return builder; - } - } + +namespace Ocelot.Provider.Polly; + +public static class OcelotBuilderExtensions +{ + public static IOcelotBuilder AddPolly(this IOcelotBuilder builder, + QosDelegatingHandlerDelegate delegatingHandler, + Dictionary> errorMapping) + where T : class, IPollyQoSProvider + { + builder.Services + .AddSingleton(errorMapping) + .AddSingleton, T>() + .AddSingleton(delegatingHandler); + + return builder; + } + + public static IOcelotBuilder AddPolly(this IOcelotBuilder builder) + { + var errorMapping = new Dictionary> + { + { typeof(TaskCanceledException), e => new RequestTimedOutError(e) }, + { typeof(TimeoutRejectedException), e => new RequestTimedOutError(e) }, + { typeof(BrokenCircuitException), e => new RequestTimedOutError(e) }, + { typeof(BrokenCircuitException), e => new RequestTimedOutError(e) }, + }; + return AddPolly(builder, GetDelegatingHandler, errorMapping); + } + + private static DelegatingHandler GetDelegatingHandler(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) + => new PollyPoliciesDelegatingHandler(route, contextAccessor, loggerFactory); } diff --git a/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs b/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs deleted file mode 100644 index 9153b70ce..000000000 --- a/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Ocelot.Logging; -using Ocelot.Provider.Polly.Interfaces; -using Polly; -using Polly.CircuitBreaker; - -namespace Ocelot.Provider.Polly -{ - public class PollyCircuitBreakingDelegatingHandler : DelegatingHandler - { - private readonly IPollyQoSProvider _qoSProvider; - private readonly IOcelotLogger _logger; - - public PollyCircuitBreakingDelegatingHandler( - IPollyQoSProvider qoSProvider, - IOcelotLoggerFactory loggerFactory) - { - _qoSProvider = qoSProvider; - _logger = loggerFactory.CreateLogger(); - } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - try - { - var policies = _qoSProvider.CircuitBreaker.Policies; - if (!policies.Any()) - { - return await base.SendAsync(request, cancellationToken); - } - - IAsyncPolicy policy = policies.Length > 1 - ? Policy.WrapAsync(policies) - : policies[0]; - - return await policy.ExecuteAsync(() => base.SendAsync(request, cancellationToken)); - } - catch (BrokenCircuitException ex) - { - _logger.LogError("Reached to allowed number of exceptions. Circuit is open", ex); - throw; - } - catch (HttpRequestException ex) - { - _logger.LogError($"Error in {nameof(PollyCircuitBreakingDelegatingHandler)}.{nameof(SendAsync)}", ex); - throw; - } - } - } -} diff --git a/src/Ocelot.Provider.Polly/PollyPoliciesDelegatingHandler.cs b/src/Ocelot.Provider.Polly/PollyPoliciesDelegatingHandler.cs new file mode 100644 index 000000000..03be86495 --- /dev/null +++ b/src/Ocelot.Provider.Polly/PollyPoliciesDelegatingHandler.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration; +using Ocelot.Logging; +using Ocelot.Provider.Polly.Interfaces; +using Polly.CircuitBreaker; +using System.Diagnostics; + +namespace Ocelot.Provider.Polly; + +public class PollyPoliciesDelegatingHandler : DelegatingHandler +{ + private readonly DownstreamRoute _route; + private readonly IHttpContextAccessor _contextAccessor; + private readonly IOcelotLogger _logger; + + public PollyPoliciesDelegatingHandler( + DownstreamRoute route, + IHttpContextAccessor contextAccessor, + IOcelotLoggerFactory loggerFactory) + { + _route = route; + _contextAccessor = contextAccessor; + _logger = loggerFactory.CreateLogger(); + } + + private IPollyQoSProvider GetQoSProvider() + { + Debug.Assert(_contextAccessor.HttpContext != null, "_contextAccessor.HttpContext != null"); + return _contextAccessor.HttpContext.RequestServices.GetService>(); + } + + /// + /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation. + /// + /// Downstream request. + /// Token to cancel the task. + /// A object of a result. + /// Exception thrown when a circuit is broken. + /// Exception thrown by and classes. + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var qoSProvider = GetQoSProvider(); + + // At least one policy (timeout) will be returned + // AsyncPollyPolicy can't be null + // AsyncPollyPolicy constructor will throw if no policy is provided + var policy = qoSProvider.GetPollyPolicyWrapper(_route).AsyncPollyPolicy; + + return await policy.ExecuteAsync(async () => await base.SendAsync(request, cancellationToken)); + } +} diff --git a/src/Ocelot.Provider.Polly/PollyPolicyWrapper.cs b/src/Ocelot.Provider.Polly/PollyPolicyWrapper.cs new file mode 100644 index 000000000..0b3058cbb --- /dev/null +++ b/src/Ocelot.Provider.Polly/PollyPolicyWrapper.cs @@ -0,0 +1,21 @@ +namespace Ocelot.Provider.Polly; + +public class PollyPolicyWrapper + where TResult : class +{ + /// + /// Initializes a new instance of the class. + /// We expect at least one policy to be passed in, default can't be null. + /// + /// The policies with at least a policy. + public PollyPolicyWrapper(params IAsyncPolicy[] policies) + { + var allPolicies = policies.Where(p => p != null).ToArray(); + + AsyncPollyPolicy = allPolicies.Length > 1 ? + Policy.WrapAsync(allPolicies) : + allPolicies[0]; + } + + public IAsyncPolicy AsyncPollyPolicy { get; } +} diff --git a/src/Ocelot.Provider.Polly/PollyQoSProvider.cs b/src/Ocelot.Provider.Polly/PollyQoSProvider.cs index 420939de4..edd460e77 100644 --- a/src/Ocelot.Provider.Polly/PollyQoSProvider.cs +++ b/src/Ocelot.Provider.Polly/PollyQoSProvider.cs @@ -1,65 +1,78 @@ using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Provider.Polly.Interfaces; -using Polly; using Polly.CircuitBreaker; using Polly.Timeout; - -namespace Ocelot.Provider.Polly -{ - public class PollyQoSProvider : IPollyQoSProvider - { - private readonly AsyncCircuitBreakerPolicy _circuitBreakerPolicy; - private readonly AsyncTimeoutPolicy _timeoutPolicy; - private readonly IOcelotLogger _logger; - - public PollyQoSProvider(AsyncCircuitBreakerPolicy circuitBreakerPolicy, AsyncTimeoutPolicy timeoutPolicy, IOcelotLogger logger) +using System.Net; + +namespace Ocelot.Provider.Polly; + +public class PollyQoSProvider : IPollyQoSProvider +{ + private readonly Dictionary> _policyWrappers = new(); + private readonly object _lockObject = new(); + private readonly IOcelotLogger _logger; + + private readonly HashSet _serverErrorCodes = new() + { + HttpStatusCode.InternalServerError, + HttpStatusCode.NotImplemented, + HttpStatusCode.BadGateway, + HttpStatusCode.ServiceUnavailable, + HttpStatusCode.GatewayTimeout, + HttpStatusCode.HttpVersionNotSupported, + HttpStatusCode.VariantAlsoNegotiates, + HttpStatusCode.InsufficientStorage, + HttpStatusCode.LoopDetected, + }; + + public PollyQoSProvider(IOcelotLoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + private static string GetRouteName(DownstreamRoute route) + => string.IsNullOrWhiteSpace(route.ServiceName) + ? route.UpstreamPathTemplate?.Template ?? route.DownstreamPathTemplate?.Value ?? string.Empty + : route.ServiceName; + + public PollyPolicyWrapper GetPollyPolicyWrapper(DownstreamRoute route) + { + lock (_lockObject) { - _circuitBreakerPolicy = circuitBreakerPolicy; - _timeoutPolicy = timeoutPolicy; - _logger = logger; - } - - public PollyQoSProvider(DownstreamRoute route, IOcelotLoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - - _ = Enum.TryParse(route.QosOptions.TimeoutStrategy, out TimeoutStrategy strategy); - - _timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromMilliseconds(route.QosOptions.TimeoutValue), strategy); - - if (route.QosOptions.ExceptionsAllowedBeforeBreaking > 0) - { - _circuitBreakerPolicy = Policy - .Handle() - .Or() - .Or() - .CircuitBreakerAsync( - exceptionsAllowedBeforeBreaking: route.QosOptions.ExceptionsAllowedBeforeBreaking, - durationOfBreak: TimeSpan.FromMilliseconds(route.QosOptions.DurationOfBreak), - onBreak: (ex, breakDelay) => - { - _logger.LogError( - ".Breaker logging: Breaking the circuit for " + breakDelay.TotalMilliseconds + "ms!", ex); - }, - onReset: () => - { - _logger.LogDebug(".Breaker logging: Call ok! Closed the circuit again."); - }, - onHalfOpen: () => - { - _logger.LogDebug(".Breaker logging: Half-open; next call is a trial."); - } - ); - } - else - { - _circuitBreakerPolicy = null; - } - - CircuitBreaker = new CircuitBreaker(_circuitBreakerPolicy, _timeoutPolicy); - } - - public CircuitBreaker CircuitBreaker { get; } - } -} + var currentRouteName = GetRouteName(route); + if (!_policyWrappers.ContainsKey(currentRouteName)) + { + _policyWrappers.Add(currentRouteName, PollyPolicyWrapperFactory(route)); + } + + return _policyWrappers[currentRouteName]; + } + } + + private PollyPolicyWrapper PollyPolicyWrapperFactory(DownstreamRoute route) + { + AsyncCircuitBreakerPolicy exceptionsAllowedBeforeBreakingPolicy = null; + if (route.QosOptions.ExceptionsAllowedBeforeBreaking > 0) + { + var info = $"Route: {GetRouteName(route)}; Breaker logging in {nameof(PollyQoSProvider)}: "; + + exceptionsAllowedBeforeBreakingPolicy = Policy + .HandleResult(r => _serverErrorCodes.Contains(r.StatusCode)) + .Or() + .Or() + .CircuitBreakerAsync(route.QosOptions.ExceptionsAllowedBeforeBreaking, + durationOfBreak: TimeSpan.FromMilliseconds(route.QosOptions.DurationOfBreak), + onBreak: (ex, breakDelay) => _logger.LogError(info + $"Breaking the circuit for {breakDelay.TotalMilliseconds} ms!", ex.Exception), + onReset: () => _logger.LogDebug(info + "Call OK! Closed the circuit again."), + onHalfOpen: () => _logger.LogDebug(info + "Half-open; Next call is a trial.")); + } + + var timeoutPolicy = Policy + .TimeoutAsync( + TimeSpan.FromMilliseconds(route.QosOptions.TimeoutValue), + TimeoutStrategy.Pessimistic); + + return new PollyPolicyWrapper(exceptionsAllowedBeforeBreakingPolicy, timeoutPolicy); + } +} diff --git a/src/Ocelot.Provider.Polly/Usings.cs b/src/Ocelot.Provider.Polly/Usings.cs index 0cc4c6d4f..fb5c12dc6 100644 --- a/src/Ocelot.Provider.Polly/Usings.cs +++ b/src/Ocelot.Provider.Polly/Usings.cs @@ -1,12 +1,10 @@ // Default Microsoft.NET.Sdk namespaces global using System; global using System.Collections.Generic; -global using System.IO; global using System.Linq; global using System.Net.Http; global using System.Threading; global using System.Threading.Tasks; // Project extra global namespaces -global using Ocelot; global using Polly; diff --git a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs index 7e94118d2..843c3dac5 100644 --- a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs +++ b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs @@ -23,7 +23,7 @@ public async Task Invoke(HttpContext httpContext) if (httpContext.Request.Method.ToUpper() != "OPTIONS" && IsAuthenticatedRoute(downstreamRoute)) { - Logger.LogInformation($"{httpContext.Request.Path} is an authenticated route. {MiddlewareName} checking if client is authenticated"); + Logger.LogInformation(() => $"{httpContext.Request.Path} is an authenticated route. {MiddlewareName} checking if client is authenticated"); var result = await httpContext.AuthenticateAsync(downstreamRoute.AuthenticationOptions.AuthenticationProviderKey); @@ -31,7 +31,7 @@ public async Task Invoke(HttpContext httpContext) if (httpContext.User.Identity.IsAuthenticated) { - Logger.LogInformation($"Client has been authenticated for {httpContext.Request.Path}"); + Logger.LogInformation(() => $"Client has been authenticated for {httpContext.Request.Path}"); await _next.Invoke(httpContext); } else @@ -39,14 +39,14 @@ public async Task Invoke(HttpContext httpContext) var error = new UnauthenticatedError( $"Request for authenticated route {httpContext.Request.Path} by {httpContext.User.Identity.Name} was unauthenticated"); - Logger.LogWarning($"Client has NOT been authenticated for {httpContext.Request.Path} and pipeline error set. {error}"); + Logger.LogWarning(() =>$"Client has NOT been authenticated for {httpContext.Request.Path} and pipeline error set. {error}"); httpContext.Items.SetError(error); } } else { - Logger.LogInformation($"No authentication needed for {httpContext.Request.Path}"); + Logger.LogInformation(() => $"No authentication needed for {httpContext.Request.Path}"); await _next.Invoke(httpContext); } diff --git a/src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs b/src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs index 45c142631..84ccf526d 100644 --- a/src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs +++ b/src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs @@ -62,7 +62,7 @@ public async Task Invoke(HttpContext httpContext) if (authorized.IsError) { - Logger.LogWarning($"Error whilst authorizing {httpContext.User.Identity.Name}. Setting pipeline error"); + Logger.LogWarning(() => $"Error whilst authorizing {httpContext.User.Identity.Name}. Setting pipeline error"); httpContext.Items.UpsertErrors(authorized.Errors); return; @@ -70,19 +70,19 @@ public async Task Invoke(HttpContext httpContext) if (IsAuthorized(authorized)) { - Logger.LogInformation($"{httpContext.User.Identity.Name} has succesfully been authorized for {downstreamRoute.UpstreamPathTemplate.OriginalValue}."); + Logger.LogInformation(() => $"{httpContext.User.Identity.Name} has succesfully been authorized for {downstreamRoute.UpstreamPathTemplate.OriginalValue}."); await _next.Invoke(httpContext); } else { - Logger.LogWarning($"{httpContext.User.Identity.Name} is not authorized to access {downstreamRoute.UpstreamPathTemplate.OriginalValue}. Setting pipeline error"); + Logger.LogWarning(() => $"{httpContext.User.Identity.Name} is not authorized to access {downstreamRoute.UpstreamPathTemplate.OriginalValue}. Setting pipeline error"); httpContext.Items.SetError(new UnauthorizedError($"{httpContext.User.Identity.Name} is not authorized to access {downstreamRoute.UpstreamPathTemplate.OriginalValue}")); } } else { - Logger.LogInformation($"{downstreamRoute.DownstreamPathTemplate.Value} route does not require user to be authorized"); + Logger.LogInformation(() => $"{downstreamRoute.DownstreamPathTemplate.Value} route does not require user to be authorized"); await _next.Invoke(httpContext); } } diff --git a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs index 0117c4536..5e3158b48 100644 --- a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs +++ b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs @@ -35,29 +35,29 @@ public async Task Invoke(HttpContext httpContext) var downstreamUrlKey = $"{downstreamRequest.Method}-{downstreamRequest.OriginalString}"; var downStreamRequestCacheKey = _cacheGenerator.GenerateRequestCacheKey(downstreamRequest); - Logger.LogDebug($"Started checking cache for the '{downstreamUrlKey}' key."); + Logger.LogDebug(() => $"Started checking cache for the '{downstreamUrlKey}' key."); var cached = _outputCache.Get(downStreamRequestCacheKey, downstreamRoute.CacheOptions.Region); if (cached != null) { - Logger.LogDebug($"Cache entry exists for the '{downstreamUrlKey}' key."); + Logger.LogDebug(() => $"Cache entry exists for the '{downstreamUrlKey}' key."); var response = CreateHttpResponseMessage(cached); SetHttpResponseMessageThisRequest(httpContext, response); - Logger.LogDebug($"Finished returning of cached response for the '{downstreamUrlKey}' key."); + Logger.LogDebug(() => $"Finished returning of cached response for the '{downstreamUrlKey}' key."); return; } - Logger.LogDebug($"No response cached for the '{downstreamUrlKey}' key."); + Logger.LogDebug(() => $"No response cached for the '{downstreamUrlKey}' key."); await _next.Invoke(httpContext); if (httpContext.Items.Errors().Count > 0) { - Logger.LogDebug($"There was a pipeline error for the '{downstreamUrlKey}' key."); + Logger.LogDebug(() => $"There was a pipeline error for the '{downstreamUrlKey}' key."); return; } @@ -68,7 +68,7 @@ public async Task Invoke(HttpContext httpContext) _outputCache.Add(downStreamRequestCacheKey, cached, TimeSpan.FromSeconds(downstreamRoute.CacheOptions.TtlSeconds), downstreamRoute.CacheOptions.Region); - Logger.LogDebug($"Finished response added to cache for the '{downstreamUrlKey}' key."); + Logger.LogDebug(() => $"Finished response added to cache for the '{downstreamUrlKey}' key."); } private static void SetHttpResponseMessageThisRequest(HttpContext context, diff --git a/src/Ocelot/Claims/Middleware/ClaimsToClaimsMiddleware.cs b/src/Ocelot/Claims/Middleware/ClaimsToClaimsMiddleware.cs index 2b4db0309..8e82d5606 100644 --- a/src/Ocelot/Claims/Middleware/ClaimsToClaimsMiddleware.cs +++ b/src/Ocelot/Claims/Middleware/ClaimsToClaimsMiddleware.cs @@ -1,6 +1,6 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Logging; -using Ocelot.Middleware; +using Microsoft.AspNetCore.Http; +using Ocelot.Logging; +using Ocelot.Middleware; namespace Ocelot.Claims.Middleware { diff --git a/src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs b/src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs index e9ff42f8a..c6c1da38e 100644 --- a/src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs +++ b/src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs @@ -25,7 +25,7 @@ public List Create(Dictionary inputToBeParsed) if (claimToThing.IsError) { - _logger.LogDebug($"Unable to extract configuration for key: {input.Key} and value: {input.Value} your configuration file is incorrect"); + _logger.LogDebug(() => $"Unable to extract configuration for key: {input.Key} and value: {input.Value} your configuration file is incorrect"); } else { diff --git a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs index 3c53b543e..07c558fb0 100644 --- a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs +++ b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs @@ -32,7 +32,7 @@ public HeaderTransformations Create(FileRoute fileRoute) } else { - _logger.LogWarning($"Unable to add UpstreamHeaderTransform {input.Key}: {input.Value}"); + _logger.LogWarning(() => $"Unable to add UpstreamHeaderTransform {input.Key}: {input.Value}"); } } else @@ -55,7 +55,7 @@ public HeaderTransformations Create(FileRoute fileRoute) } else { - _logger.LogWarning($"Unable to add DownstreamHeaderTransform {input.Key}: {input.Value}"); + _logger.LogWarning(() => $"Unable to add DownstreamHeaderTransform {input.Key}: {input.Value}"); } } else diff --git a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs index afac5d922..eebce214c 100644 --- a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs +++ b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs @@ -6,6 +6,12 @@ public FileAuthenticationOptions() { AllowedScopes = new List(); } + + public FileAuthenticationOptions(FileAuthenticationOptions from) + { + AllowedScopes = new(from.AllowedScopes); + AuthenticationProviderKey = from.AuthenticationProviderKey; + } public string AuthenticationProviderKey { get; set; } public List AllowedScopes { get; set; } diff --git a/src/Ocelot/Configuration/File/FileCacheOptions.cs b/src/Ocelot/Configuration/File/FileCacheOptions.cs index b5168b3c0..65c481344 100644 --- a/src/Ocelot/Configuration/File/FileCacheOptions.cs +++ b/src/Ocelot/Configuration/File/FileCacheOptions.cs @@ -2,7 +2,19 @@ { public class FileCacheOptions { - public int TtlSeconds { get; set; } + public FileCacheOptions() + { + Region = string.Empty; + TtlSeconds = 0; + } + + public FileCacheOptions(FileCacheOptions from) + { + Region = from.Region; + TtlSeconds = from.TtlSeconds; + } + public string Region { get; set; } + public int TtlSeconds { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileHostAndPort.cs b/src/Ocelot/Configuration/File/FileHostAndPort.cs index 23c745e09..a3eeaae83 100644 --- a/src/Ocelot/Configuration/File/FileHostAndPort.cs +++ b/src/Ocelot/Configuration/File/FileHostAndPort.cs @@ -1,7 +1,21 @@ namespace Ocelot.Configuration.File { public class FileHostAndPort - { + { + public FileHostAndPort() { } + + public FileHostAndPort(FileHostAndPort from) + { + Host = from.Host; + Port = from.Port; + } + + public FileHostAndPort(string host, int port) + { + Host = host; + Port = port; + } + public string Host { get; set; } public int Port { get; set; } } diff --git a/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs b/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs index b83be428b..33e1a15cb 100644 --- a/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs +++ b/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs @@ -5,19 +5,23 @@ public class FileHttpHandlerOptions public FileHttpHandlerOptions() { AllowAutoRedirect = false; + MaxConnectionsPerServer = int.MaxValue; UseCookieContainer = false; UseProxy = true; - MaxConnectionsPerServer = int.MaxValue; + } + + public FileHttpHandlerOptions(FileHttpHandlerOptions from) + { + AllowAutoRedirect = from.AllowAutoRedirect; + MaxConnectionsPerServer = from.MaxConnectionsPerServer; + UseCookieContainer = from.UseCookieContainer; + UseProxy = from.UseProxy; } public bool AllowAutoRedirect { get; set; } - + public int MaxConnectionsPerServer { get; set; } public bool UseCookieContainer { get; set; } - - public bool UseTracing { get; set; } - public bool UseProxy { get; set; } - - public int MaxConnectionsPerServer { get; set; } + public bool UseTracing { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs b/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs index 105ccc7a5..45ea8a0a5 100644 --- a/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs +++ b/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs @@ -2,8 +2,22 @@ namespace Ocelot.Configuration.File { public class FileLoadBalancerOptions { - public string Type { get; set; } - public string Key { get; set; } + public FileLoadBalancerOptions() + { + Expiry = int.MaxValue; + Key = string.Empty; + Type = string.Empty; + } + + public FileLoadBalancerOptions(FileLoadBalancerOptions from) + { + Expiry = from.Expiry; + Key = from.Key; + Type = from.Type; + } + public int Expiry { get; set; } + public string Key { get; set; } + public string Type { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileQoSOptions.cs b/src/Ocelot/Configuration/File/FileQoSOptions.cs index 4e12b4a3a..85b51fa43 100644 --- a/src/Ocelot/Configuration/File/FileQoSOptions.cs +++ b/src/Ocelot/Configuration/File/FileQoSOptions.cs @@ -1,11 +1,30 @@ namespace Ocelot.Configuration.File { public class FileQoSOptions - { - public int ExceptionsAllowedBeforeBreaking { get; set; } + { + public FileQoSOptions() + { + DurationOfBreak = 1; + ExceptionsAllowedBeforeBreaking = 0; + TimeoutValue = int.MaxValue; + } + + public FileQoSOptions(FileQoSOptions from) + { + DurationOfBreak = from.DurationOfBreak; + ExceptionsAllowedBeforeBreaking = from.ExceptionsAllowedBeforeBreaking; + TimeoutValue = from.TimeoutValue; + } + + public FileQoSOptions(QoSOptions from) + { + DurationOfBreak = from.DurationOfBreak; + ExceptionsAllowedBeforeBreaking = from.ExceptionsAllowedBeforeBreaking; + TimeoutValue = from.TimeoutValue; + } public int DurationOfBreak { get; set; } - + public int ExceptionsAllowedBeforeBreaking { get; set; } public int TimeoutValue { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileRateLimitRule.cs b/src/Ocelot/Configuration/File/FileRateLimitRule.cs index 907e5341c..ffbc0c994 100644 --- a/src/Ocelot/Configuration/File/FileRateLimitRule.cs +++ b/src/Ocelot/Configuration/File/FileRateLimitRule.cs @@ -7,6 +7,15 @@ public FileRateLimitRule() ClientWhitelist = new List(); } + public FileRateLimitRule(FileRateLimitRule from) + { + ClientWhitelist = new(from.ClientWhitelist); + EnableRateLimiting = from.EnableRateLimiting; + Limit = from.Limit; + Period = from.Period; + PeriodTimespan = from.PeriodTimespan; + } + /// /// The list of allowed clients. /// diff --git a/src/Ocelot/Configuration/File/FileRoute.cs b/src/Ocelot/Configuration/File/FileRoute.cs index e99ed2bc1..5823113ad 100644 --- a/src/Ocelot/Configuration/File/FileRoute.cs +++ b/src/Ocelot/Configuration/File/FileRoute.cs @@ -1,59 +1,110 @@ namespace Ocelot.Configuration.File { - public class FileRoute : IRoute + public class FileRoute : IRoute, ICloneable { public FileRoute() { - UpstreamHttpMethod = new List(); - AddHeadersToRequest = new Dictionary(); AddClaimsToRequest = new Dictionary(); - RouteClaimsRequirement = new Dictionary(); + AddHeadersToRequest = new Dictionary(); AddQueriesToRequest = new Dictionary(); + AuthenticationOptions = new FileAuthenticationOptions(); ChangeDownstreamPathTemplate = new Dictionary(); + DelegatingHandlers = new List(); DownstreamHeaderTransform = new Dictionary(); + DownstreamHostAndPorts = new List(); FileCacheOptions = new FileCacheOptions(); - QoSOptions = new FileQoSOptions(); - RateLimitOptions = new FileRateLimitRule(); - AuthenticationOptions = new FileAuthenticationOptions(); HttpHandlerOptions = new FileHttpHandlerOptions(); - UpstreamHeaderTransform = new Dictionary(); - DownstreamHostAndPorts = new List(); - DelegatingHandlers = new List(); LoadBalancerOptions = new FileLoadBalancerOptions(); - SecurityOptions = new FileSecurityOptions(); Priority = 1; - } - - public string DownstreamPathTemplate { get; set; } - public string UpstreamPathTemplate { get; set; } - public List UpstreamHttpMethod { get; set; } - public string DownstreamHttpMethod { get; set; } - public Dictionary AddHeadersToRequest { get; set; } - public Dictionary UpstreamHeaderTransform { get; set; } - public Dictionary DownstreamHeaderTransform { get; set; } - public Dictionary AddClaimsToRequest { get; set; } - public Dictionary RouteClaimsRequirement { get; set; } - public Dictionary AddQueriesToRequest { get; set; } - public Dictionary ChangeDownstreamPathTemplate { get; set; } - public string RequestIdKey { get; set; } - public FileCacheOptions FileCacheOptions { get; set; } - public bool RouteIsCaseSensitive { get; set; } - public string ServiceName { get; set; } - public string ServiceNamespace { get; set; } - public string DownstreamScheme { get; set; } - public FileQoSOptions QoSOptions { get; set; } - public FileLoadBalancerOptions LoadBalancerOptions { get; set; } - public FileRateLimitRule RateLimitOptions { get; set; } - public FileAuthenticationOptions AuthenticationOptions { get; set; } - public FileHttpHandlerOptions HttpHandlerOptions { get; set; } - public List DownstreamHostAndPorts { get; set; } - public string UpstreamHost { get; set; } - public string Key { get; set; } - public List DelegatingHandlers { get; set; } - public int Priority { get; set; } - public int Timeout { get; set; } - public bool DangerousAcceptAnyServerCertificateValidator { get; set; } - public FileSecurityOptions SecurityOptions { get; set; } - public string DownstreamHttpVersion { get; set; } + QoSOptions = new FileQoSOptions(); + RateLimitOptions = new FileRateLimitRule(); + RouteClaimsRequirement = new Dictionary(); + SecurityOptions = new FileSecurityOptions(); + UpstreamHeaderTransform = new Dictionary(); + UpstreamHttpMethod = new List(); + } + + public FileRoute(FileRoute from) + { + DeepCopy(from, this); + } + + public Dictionary AddClaimsToRequest { get; set; } + public Dictionary AddHeadersToRequest { get; set; } + public Dictionary AddQueriesToRequest { get; set; } + public FileAuthenticationOptions AuthenticationOptions { get; set; } + public Dictionary ChangeDownstreamPathTemplate { get; set; } + public bool DangerousAcceptAnyServerCertificateValidator { get; set; } + public List DelegatingHandlers { get; set; } + public Dictionary DownstreamHeaderTransform { get; set; } + public List DownstreamHostAndPorts { get; set; } + public string DownstreamHttpMethod { get; set; } + public string DownstreamHttpVersion { get; set; } + public string DownstreamPathTemplate { get; set; } + public string DownstreamScheme { get; set; } + public FileCacheOptions FileCacheOptions { get; set; } + public FileHttpHandlerOptions HttpHandlerOptions { get; set; } + public string Key { get; set; } + public FileLoadBalancerOptions LoadBalancerOptions { get; set; } + public int Priority { get; set; } + public FileQoSOptions QoSOptions { get; set; } + public FileRateLimitRule RateLimitOptions { get; set; } + public string RequestIdKey { get; set; } + public Dictionary RouteClaimsRequirement { get; set; } + public bool RouteIsCaseSensitive { get; set; } + public FileSecurityOptions SecurityOptions { get; set; } + public string ServiceName { get; set; } + public string ServiceNamespace { get; set; } + public int Timeout { get; set; } + public Dictionary UpstreamHeaderTransform { get; set; } + public string UpstreamHost { get; set; } + public List UpstreamHttpMethod { get; set; } + public string UpstreamPathTemplate { get; set; } + + /// + /// Clones this object by making a deep copy. + /// + /// A deeply copied object. + public object Clone() + { + var other = (FileRoute)MemberwiseClone(); + DeepCopy(this, other); + return other; + } + + public static void DeepCopy(FileRoute from, FileRoute to) + { + to.AddClaimsToRequest = new(from.AddClaimsToRequest); + to.AddHeadersToRequest = new(from.AddHeadersToRequest); + to.AddQueriesToRequest = new(from.AddQueriesToRequest); + to.AuthenticationOptions = new(from.AuthenticationOptions); + to.ChangeDownstreamPathTemplate = new(from.ChangeDownstreamPathTemplate); + to.DangerousAcceptAnyServerCertificateValidator = from.DangerousAcceptAnyServerCertificateValidator; + to.DelegatingHandlers = new(from.DelegatingHandlers); + to.DownstreamHeaderTransform = new(from.DownstreamHeaderTransform); + to.DownstreamHostAndPorts = from.DownstreamHostAndPorts.Select(x => new FileHostAndPort(x)).ToList(); + to.DownstreamHttpMethod = from.DownstreamHttpMethod; + to.DownstreamHttpVersion = from.DownstreamHttpVersion; + to.DownstreamPathTemplate = from.DownstreamPathTemplate; + to.DownstreamScheme = from.DownstreamScheme; + to.FileCacheOptions = new(from.FileCacheOptions); + to.HttpHandlerOptions = new(from.HttpHandlerOptions); + to.Key = from.Key; + to.LoadBalancerOptions = new(from.LoadBalancerOptions); + to.Priority = from.Priority; + to.QoSOptions = new(from.QoSOptions); + to.RateLimitOptions = new(from.RateLimitOptions); + to.RequestIdKey = from.RequestIdKey; + to.RouteClaimsRequirement = new(from.RouteClaimsRequirement); + to.RouteIsCaseSensitive = from.RouteIsCaseSensitive; + to.SecurityOptions = new(from.SecurityOptions); + to.ServiceName = from.ServiceName; + to.ServiceNamespace = from.ServiceNamespace; + to.Timeout = from.Timeout; + to.UpstreamHeaderTransform = new(from.UpstreamHeaderTransform); + to.UpstreamHost = from.UpstreamHost; + to.UpstreamHttpMethod = new(from.UpstreamHttpMethod); + to.UpstreamPathTemplate = from.UpstreamPathTemplate; + } } } diff --git a/src/Ocelot/Configuration/File/FileSecurityOptions.cs b/src/Ocelot/Configuration/File/FileSecurityOptions.cs index 77d762b11..ffd595cb4 100644 --- a/src/Ocelot/Configuration/File/FileSecurityOptions.cs +++ b/src/Ocelot/Configuration/File/FileSecurityOptions.cs @@ -9,6 +9,13 @@ public FileSecurityOptions() ExcludeAllowedFromBlocked = false; } + public FileSecurityOptions(FileSecurityOptions from) + { + IPAllowedList = new(from.IPAllowedList); + IPBlockedList = new(from.IPBlockedList); + ExcludeAllowedFromBlocked = from.ExcludeAllowedFromBlocked; + } + public FileSecurityOptions(string allowedIPs = null, string blockedIPs = null, bool? excludeAllowedFromBlocked = null) : this() { diff --git a/src/Ocelot/Configuration/QoSOptions.cs b/src/Ocelot/Configuration/QoSOptions.cs index 461ff292a..915f07e2a 100644 --- a/src/Ocelot/Configuration/QoSOptions.cs +++ b/src/Ocelot/Configuration/QoSOptions.cs @@ -1,31 +1,41 @@ -namespace Ocelot.Configuration +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration { public class QoSOptions { + public QoSOptions(QoSOptions from) + { + DurationOfBreak = from.DurationOfBreak; + ExceptionsAllowedBeforeBreaking = from.ExceptionsAllowedBeforeBreaking; + Key = from.Key; + TimeoutValue = from.TimeoutValue; + } + + public QoSOptions(FileQoSOptions from) + { + DurationOfBreak = from.DurationOfBreak; + ExceptionsAllowedBeforeBreaking = from.ExceptionsAllowedBeforeBreaking; + Key = string.Empty; + TimeoutValue = from.TimeoutValue; + } + public QoSOptions( int exceptionsAllowedBeforeBreaking, - int durationofBreak, - int timeoutValue, - string key, - string timeoutStrategy = "Pessimistic") + int durationOfBreak, + int timeoutValue, + string key) { + DurationOfBreak = durationOfBreak; ExceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking; - DurationOfBreak = durationofBreak; - TimeoutValue = timeoutValue; - TimeoutStrategy = timeoutStrategy; Key = key; + TimeoutValue = timeoutValue; } - public int ExceptionsAllowedBeforeBreaking { get; } - public int DurationOfBreak { get; } - + public int ExceptionsAllowedBeforeBreaking { get; } + public string Key { get; } public int TimeoutValue { get; } - - public string TimeoutStrategy { get; } - public bool UseQos => ExceptionsAllowedBeforeBreaking > 0 || TimeoutValue > 0; - - public string Key { get; } } } diff --git a/src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs b/src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs index 61538a5d7..969a34161 100644 --- a/src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs +++ b/src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs @@ -68,7 +68,7 @@ private async Task Poll() if (fileConfig.IsError) { - _logger.LogWarning($"error geting file config, errors are {string.Join(',', fileConfig.Errors.Select(x => x.Message))}"); + _logger.LogWarning(() =>$"error geting file config, errors are {string.Join(',', fileConfig.Errors.Select(x => x.Message))}"); return; } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 159d336ca..4d198711e 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -16,7 +16,7 @@ using Ocelot.Configuration.Validator; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; +using Ocelot.DownstreamUrlCreator; using Ocelot.Headers; using Ocelot.Infrastructure; using Ocelot.Infrastructure.Claims.Parser; @@ -91,7 +91,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.AddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); @@ -103,7 +103,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.AddSingleton(); Services.AddSingleton(); Services.TryAddSingleton(); diff --git a/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs b/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs index 149a9ea09..e9c54c9dd 100644 --- a/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs +++ b/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs @@ -25,7 +25,7 @@ public async Task Invoke(HttpContext httpContext) if (downstreamRoute.ClaimsToPath.Any()) { - Logger.LogInformation($"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to path"); + Logger.LogInformation(() => $"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to path"); var templatePlaceholderNameAndValues = httpContext.Items.TemplatePlaceholderNameAndValues(); diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs index b10ced14f..236941e4b 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs @@ -1,6 +1,6 @@ -using Microsoft.Extensions.DependencyInjection; -using Ocelot.Configuration; -using Ocelot.Logging; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration; +using Ocelot.Logging; namespace Ocelot.DownstreamRouteFinder.Finder { @@ -22,6 +22,7 @@ public IDownstreamRouteProvider Get(IInternalConfiguration config) if ((!config.Routes.Any() || config.Routes.All(x => string.IsNullOrEmpty(x.UpstreamTemplatePattern?.OriginalValue))) && IsServiceDiscovery(config.ServiceProviderConfiguration)) { _logger.LogInformation($"Selected {nameof(DownstreamRouteCreator)} as DownstreamRouteProvider for this request"); + return _providers[nameof(DownstreamRouteCreator)]; } diff --git a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs index 678ed7ec2..63c21b76c 100644 --- a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs +++ b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs @@ -32,7 +32,7 @@ public async Task Invoke(HttpContext httpContext) ? hostHeader.Split(':')[0] : hostHeader; - Logger.LogDebug($"Upstream url path is {upstreamUrlPath}"); + Logger.LogDebug(() => $"Upstream url path is {upstreamUrlPath}"); var internalConfiguration = httpContext.Items.IInternalConfiguration(); @@ -42,14 +42,13 @@ public async Task Invoke(HttpContext httpContext) if (response.IsError) { - Logger.LogWarning($"{MiddlewareName} setting pipeline errors. IDownstreamRouteFinder returned {response.Errors.ToErrorString()}"); + Logger.LogWarning(() => $"{MiddlewareName} setting pipeline errors. IDownstreamRouteFinder returned {response.Errors.ToErrorString()}"); httpContext.Items.UpsertErrors(response.Errors); return; } - var downstreamPathTemplates = string.Join(", ", response.Data.Route.DownstreamRoute.Select(r => r.DownstreamPathTemplate.Value)); - Logger.LogDebug($"downstream templates are {downstreamPathTemplates}"); + Logger.LogDebug(() => $"downstream templates are {string.Join(", ", response.Data.Route.DownstreamRoute.Select(r => r.DownstreamPathTemplate.Value))}"); // why set both of these on HttpContext httpContext.Items.UpsertTemplatePlaceholderNameAndValues(response.Data.TemplatePlaceholderNameAndValues); diff --git a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamTemplatePathPlaceholderReplacer.cs b/src/Ocelot/DownstreamUrlCreator/DownstreamPathPlaceholderReplacer.cs similarity index 82% rename from src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamTemplatePathPlaceholderReplacer.cs rename to src/Ocelot/DownstreamUrlCreator/DownstreamPathPlaceholderReplacer.cs index cebbfda76..11bbae38b 100644 --- a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamTemplatePathPlaceholderReplacer.cs +++ b/src/Ocelot/DownstreamUrlCreator/DownstreamPathPlaceholderReplacer.cs @@ -2,9 +2,9 @@ using Ocelot.Responses; using Ocelot.Values; -namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer +namespace Ocelot.DownstreamUrlCreator { - public class DownstreamTemplatePathPlaceholderReplacer : IDownstreamPathPlaceholderReplacer + public class DownstreamPathPlaceholderReplacer : IDownstreamPathPlaceholderReplacer { public Response Replace(string downstreamPathTemplate, List urlPathPlaceholderNameAndValues) diff --git a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamPathPlaceholderReplacer.cs b/src/Ocelot/DownstreamUrlCreator/IDownstreamPathPlaceholderReplacer.cs similarity index 83% rename from src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamPathPlaceholderReplacer.cs rename to src/Ocelot/DownstreamUrlCreator/IDownstreamPathPlaceholderReplacer.cs index 6cbb83b02..edc657e72 100644 --- a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamPathPlaceholderReplacer.cs +++ b/src/Ocelot/DownstreamUrlCreator/IDownstreamPathPlaceholderReplacer.cs @@ -2,7 +2,7 @@ using Ocelot.Responses; using Ocelot.Values; -namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer +namespace Ocelot.DownstreamUrlCreator { public interface IDownstreamPathPlaceholderReplacer { diff --git a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs index a9513c4af..f8ae6ed26 100644 --- a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs +++ b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs @@ -1,12 +1,12 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.Responses; using Ocelot.Values; +using System.Web; namespace Ocelot.DownstreamUrlCreator.Middleware { @@ -15,10 +15,15 @@ public class DownstreamUrlCreatorMiddleware : OcelotMiddleware private readonly RequestDelegate _next; private readonly IDownstreamPathPlaceholderReplacer _replacer; - public DownstreamUrlCreatorMiddleware(RequestDelegate next, + private const char Ampersand = '&'; + private const char QuestionMark = '?'; + private const char OpeningBrace = '{'; + private const char ClosingBrace = '}'; + + public DownstreamUrlCreatorMiddleware( + RequestDelegate next, IOcelotLoggerFactory loggerFactory, - IDownstreamPathPlaceholderReplacer replacer - ) + IDownstreamPathPlaceholderReplacer replacer) : base(loggerFactory.CreateLogger()) { _next = next; @@ -28,17 +33,13 @@ IDownstreamPathPlaceholderReplacer replacer public async Task Invoke(HttpContext httpContext) { var downstreamRoute = httpContext.Items.DownstreamRoute(); - - var templatePlaceholderNameAndValues = httpContext.Items.TemplatePlaceholderNameAndValues(); - - var response = _replacer - .Replace(downstreamRoute.DownstreamPathTemplate.Value, templatePlaceholderNameAndValues); - + var placeholders = httpContext.Items.TemplatePlaceholderNameAndValues(); + var response = _replacer.Replace(downstreamRoute.DownstreamPathTemplate.Value, placeholders); var downstreamRequest = httpContext.Items.DownstreamRequest(); if (response.IsError) { - Logger.LogDebug("IDownstreamPathPlaceholderReplacer returned an error, setting pipeline error"); + Logger.LogDebug($"{nameof(IDownstreamPathPlaceholderReplacer)} returned an error, setting pipeline error"); httpContext.Items.UpsertErrors(response.Errors); return; @@ -54,7 +55,7 @@ public async Task Invoke(HttpContext httpContext) if (ServiceFabricRequest(internalConfiguration, downstreamRoute)) { - var (path, query) = CreateServiceFabricUri(downstreamRequest, downstreamRoute, templatePlaceholderNameAndValues, response); + var (path, query) = CreateServiceFabricUri(downstreamRequest, downstreamRoute, placeholders, response); //todo check this works again hope there is a test.. downstreamRequest.AbsolutePath = path; @@ -63,38 +64,56 @@ public async Task Invoke(HttpContext httpContext) else { var dsPath = response.Data; - - if (ContainsQueryString(dsPath)) + if (dsPath.Value.Contains(QuestionMark)) { downstreamRequest.AbsolutePath = GetPath(dsPath); - - if (string.IsNullOrEmpty(downstreamRequest.Query)) - { - downstreamRequest.Query = GetQueryString(dsPath); - } - else - { - downstreamRequest.Query += GetQueryString(dsPath).Replace('?', '&'); - } + var newQuery = GetQueryString(dsPath); + downstreamRequest.Query = string.IsNullOrEmpty(downstreamRequest.Query) + ? newQuery + : MergeQueryStringsWithoutDuplicateValues(downstreamRequest.Query, newQuery, placeholders); } else { - RemoveQueryStringParametersThatHaveBeenUsedInTemplate(downstreamRequest, templatePlaceholderNameAndValues); + RemoveQueryStringParametersThatHaveBeenUsedInTemplate(downstreamRequest, placeholders); downstreamRequest.AbsolutePath = dsPath.Value; } } - Logger.LogDebug($"Downstream url is {downstreamRequest}"); + Logger.LogDebug(() => $"Downstream url is {downstreamRequest}"); await _next.Invoke(httpContext); } + private static string MergeQueryStringsWithoutDuplicateValues(string queryString, string newQueryString, List placeholders) + { + newQueryString = newQueryString.Replace(QuestionMark, Ampersand); + var queries = HttpUtility.ParseQueryString(queryString); + var newQueries = HttpUtility.ParseQueryString(newQueryString); + + var parameters = newQueries.AllKeys + .Where(key => !string.IsNullOrEmpty(key)) + .ToDictionary(key => key, key => newQueries[key]); + + _ = queries.AllKeys + .Where(key => !string.IsNullOrEmpty(key) && !parameters.ContainsKey(key)) + .All(key => parameters.TryAdd(key, queries[key])); + + // Remove old replaced query parameters + foreach (var placeholder in placeholders) + { + parameters.Remove(placeholder.Name.Trim(OpeningBrace, ClosingBrace)); + } + + var orderedParams = parameters.OrderBy(x => x.Key).Select(x => $"{x.Key}={x.Value}"); + return QuestionMark + string.Join(Ampersand, orderedParams); + } + private static void RemoveQueryStringParametersThatHaveBeenUsedInTemplate(DownstreamRequest downstreamRequest, List templatePlaceholderNameAndValues) { foreach (var nAndV in templatePlaceholderNameAndValues) { - var name = nAndV.Name.Replace("{", string.Empty).Replace("}", string.Empty); + var name = nAndV.Name.Trim(OpeningBrace, ClosingBrace); var rgx = new Regex($@"\b{name}={nAndV.Value}\b"); @@ -106,29 +125,24 @@ private static void RemoveQueryStringParametersThatHaveBeenUsedInTemplate(Downst if (!string.IsNullOrEmpty(downstreamRequest.Query)) { - downstreamRequest.Query = string.Concat("?", downstreamRequest.Query.AsSpan(1)); + downstreamRequest.Query = QuestionMark + downstreamRequest.Query[1..]; } } - } + } } private static string GetPath(DownstreamPath dsPath) { - int length = dsPath.Value.IndexOf('?', StringComparison.Ordinal); + int length = dsPath.Value.IndexOf(QuestionMark, StringComparison.Ordinal); return dsPath.Value[..length]; } private static string GetQueryString(DownstreamPath dsPath) { - int startIndex = dsPath.Value.IndexOf('?', StringComparison.Ordinal); + int startIndex = dsPath.Value.IndexOf(QuestionMark, StringComparison.Ordinal); return dsPath.Value[startIndex..]; } - private static bool ContainsQueryString(DownstreamPath dsPath) - { - return dsPath.Value.Contains('?'); - } - private (string Path, string Query) CreateServiceFabricUri(DownstreamRequest downstreamRequest, DownstreamRoute downstreamRoute, List templatePlaceholderNameAndValues, Response dsPath) { var query = downstreamRequest.Query; diff --git a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs index 0d2a961fb..cea6fd2d0 100644 --- a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs @@ -48,10 +48,7 @@ public async Task Invoke(HttpContext httpContext) catch (Exception e) { Logger.LogDebug("error calling middleware"); - - var message = CreateMessage(httpContext, e); - - Logger.LogError(message, e); + Logger.LogError(() => CreateMessage(httpContext, e), e); SetInternalServerErrorOnResponse(httpContext); } diff --git a/src/Ocelot/Headers/AddHeadersToRequest.cs b/src/Ocelot/Headers/AddHeadersToRequest.cs index d5a8c0629..d4a7bffab 100644 --- a/src/Ocelot/Headers/AddHeadersToRequest.cs +++ b/src/Ocelot/Headers/AddHeadersToRequest.cs @@ -64,7 +64,7 @@ public void SetHeadersOnDownstreamRequest(IEnumerable headers, HttpCo if (value.IsError) { - _logger.LogWarning($"Unable to add header to response {header.Key}: {header.Value}"); + _logger.LogWarning(() => $"Unable to add header to response {header.Key}: {header.Value}"); continue; } diff --git a/src/Ocelot/Headers/AddHeadersToResponse.cs b/src/Ocelot/Headers/AddHeadersToResponse.cs index 5ab5c175c..8260d1051 100644 --- a/src/Ocelot/Headers/AddHeadersToResponse.cs +++ b/src/Ocelot/Headers/AddHeadersToResponse.cs @@ -26,7 +26,7 @@ public void Add(List addHeaders, DownstreamResponse response) if (value.IsError) { - _logger.LogWarning($"Unable to add header to response {add.Key}: {add.Value}"); + _logger.LogWarning(() => $"Unable to add header to response {add.Key}: {add.Value}"); continue; } diff --git a/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddleware.cs b/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddleware.cs index 2feb14e0f..df8836409 100644 --- a/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddleware.cs +++ b/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddleware.cs @@ -24,7 +24,7 @@ public async Task Invoke(HttpContext httpContext) if (downstreamRoute.ClaimsToHeaders.Any()) { - Logger.LogInformation($"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to headers"); + Logger.LogInformation(() => $"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to headers"); var downstreamRequest = httpContext.Items.DownstreamRequest(); diff --git a/src/Ocelot/Logging/AspDotNetLogger.cs b/src/Ocelot/Logging/AspDotNetLogger.cs deleted file mode 100644 index c3aaf228e..000000000 --- a/src/Ocelot/Logging/AspDotNetLogger.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Microsoft.Extensions.Logging; -using Ocelot.Infrastructure.RequestData; - -namespace Ocelot.Logging -{ - public class AspDotNetLogger : IOcelotLogger - { - private readonly ILogger _logger; - private readonly IRequestScopedDataRepository _scopedDataRepository; - private readonly Func _func; - - public AspDotNetLogger(ILogger logger, IRequestScopedDataRepository scopedDataRepository) - { - _logger = logger; - _scopedDataRepository = scopedDataRepository; - _func = (state, exception) => exception == null ? state : $"{state}, exception: {exception}"; - } - - public void LogTrace(string message) - { - var requestId = GetOcelotRequestId(); - var previousRequestId = GetOcelotPreviousRequestId(); - - var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - - _logger.Log(LogLevel.Trace, default, state, null, _func); - } - - public void LogDebug(string message) - { - var requestId = GetOcelotRequestId(); - var previousRequestId = GetOcelotPreviousRequestId(); - - var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - - _logger.Log(LogLevel.Debug, default, state, null, _func); - } - - public void LogInformation(string message) - { - var requestId = GetOcelotRequestId(); - var previousRequestId = GetOcelotPreviousRequestId(); - - var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - - _logger.Log(LogLevel.Information, default, state, null, _func); - } - - public void LogWarning(string message) - { - var requestId = GetOcelotRequestId(); - var previousRequestId = GetOcelotPreviousRequestId(); - - var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - - _logger.Log(LogLevel.Warning, default, state, null, _func); - } - - public void LogError(string message, Exception exception) - { - var requestId = GetOcelotRequestId(); - var previousRequestId = GetOcelotPreviousRequestId(); - - var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - - _logger.Log(LogLevel.Error, default, state, exception, _func); - } - - public void LogCritical(string message, Exception exception) - { - var requestId = GetOcelotRequestId(); - var previousRequestId = GetOcelotPreviousRequestId(); - - var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - - _logger.Log(LogLevel.Critical, default, state, exception, _func); - } - - private string GetOcelotRequestId() - { - var requestId = _scopedDataRepository.Get("RequestId"); - - return requestId == null || requestId.IsError ? "no request id" : requestId.Data; - } - - private string GetOcelotPreviousRequestId() - { - var requestId = _scopedDataRepository.Get("PreviousRequestId"); - - return requestId == null || requestId.IsError ? "no previous request id" : requestId.Data; - } - } -} diff --git a/src/Ocelot/Logging/IOcelotLogger.cs b/src/Ocelot/Logging/IOcelotLogger.cs index 4be17052e..5b8a68fd4 100644 --- a/src/Ocelot/Logging/IOcelotLogger.cs +++ b/src/Ocelot/Logging/IOcelotLogger.cs @@ -1,20 +1,28 @@ -namespace Ocelot.Logging +using Ocelot.Configuration; +using Ocelot.Infrastructure.RequestData; + +namespace Ocelot.Logging; + +/// +/// Thin wrapper around the .NET Core logging framework, used to allow the object to be injected giving access to the Ocelot . +/// +public interface IOcelotLogger { - /// - /// Thin wrapper around the DotNet core logging framework, used to allow the scopedDataRepository to be injected giving access to the Ocelot RequestId. - /// - public interface IOcelotLogger - { - void LogTrace(string message); + void LogTrace(string message); + void LogTrace(Func messageFactory); - void LogDebug(string message); + void LogDebug(string message); + void LogDebug(Func messageFactory); - void LogInformation(string message); + void LogInformation(string message); + void LogInformation(Func messageFactory); - void LogWarning(string message); + void LogWarning(string message); + void LogWarning(Func messageFactory); - void LogError(string message, Exception exception); + void LogError(string message, Exception exception); + void LogError(Func messageFactory, Exception exception); - void LogCritical(string message, Exception exception); - } + void LogCritical(string message, Exception exception); + void LogCritical(Func messageFactory, Exception exception); } diff --git a/src/Ocelot/Logging/OcelotDiagnosticListener.cs b/src/Ocelot/Logging/OcelotDiagnosticListener.cs index 156ee1cc9..3cdb64aad 100644 --- a/src/Ocelot/Logging/OcelotDiagnosticListener.cs +++ b/src/Ocelot/Logging/OcelotDiagnosticListener.cs @@ -18,20 +18,20 @@ public OcelotDiagnosticListener(IOcelotLoggerFactory factory, IServiceProvider s [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareStarting")] public virtual void OnMiddlewareStarting(HttpContext httpContext, string name) { - _logger.LogTrace($"MiddlewareStarting: {name}; {httpContext.Request.Path}"); + _logger.LogTrace(() => $"MiddlewareStarting: {name}; {httpContext.Request.Path}"); Event(httpContext, $"MiddlewareStarting: {name}; {httpContext.Request.Path}"); } [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareException")] public virtual void OnMiddlewareException(Exception exception, string name) { - _logger.LogTrace($"MiddlewareException: {name}; {exception.Message};"); + _logger.LogTrace(() => $"MiddlewareException: {name}; {exception.Message};"); } [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareFinished")] public virtual void OnMiddlewareFinished(HttpContext httpContext, string name) { - _logger.LogTrace($"MiddlewareFinished: {name}; {httpContext.Response.StatusCode}"); + _logger.LogTrace(() => $"MiddlewareFinished: {name}; {httpContext.Response.StatusCode}"); Event(httpContext, $"MiddlewareFinished: {name}; {httpContext.Response.StatusCode}"); } diff --git a/src/Ocelot/Logging/OcelotLogger.cs b/src/Ocelot/Logging/OcelotLogger.cs new file mode 100644 index 000000000..1528f3957 --- /dev/null +++ b/src/Ocelot/Logging/OcelotLogger.cs @@ -0,0 +1,95 @@ +using Microsoft.Extensions.Logging; +using Ocelot.Infrastructure.RequestData; +using Ocelot.RequestId.Middleware; + +namespace Ocelot.Logging; + +/// +/// Default implementation of the interface. +/// +public class OcelotLogger : IOcelotLogger +{ + private readonly ILogger _logger; + private readonly IRequestScopedDataRepository _scopedDataRepository; + private readonly Func _func; + + /// + /// Initializes a new instance of the class. + /// + /// Please note: + /// the log event message is designed to use placeholders ({RequestId}, {PreviousRequestId}, and {Message}). + /// If you're using a logger like Serilog, it will automatically capture these as structured data properties, making it easier to query and analyze the logs later. + /// + /// + /// The main logger type, per default the Microsoft implementation. + /// Repository, saving and getting data to/from HttpContext.Items. + /// The ILogger object is injected in OcelotLoggerFactory, it can't be verified before. + public OcelotLogger(ILogger logger, IRequestScopedDataRepository scopedDataRepository) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _scopedDataRepository = scopedDataRepository; + _func = (state, exception) => exception == null ? state : $"{state}, {nameof(exception)}: {exception}"; + } + + public void LogTrace(string message) => WriteLog(LogLevel.Trace, message); + public void LogTrace(Func messageFactory) => WriteLog(LogLevel.Trace, messageFactory); + + public void LogDebug(string message) => WriteLog(LogLevel.Debug, message); + public void LogDebug(Func messageFactory) => WriteLog(LogLevel.Debug, messageFactory); + + public void LogInformation(string message) => WriteLog(LogLevel.Information, message); + public void LogInformation(Func messageFactory) => WriteLog(LogLevel.Information, messageFactory); + + public void LogWarning(string message) => WriteLog(LogLevel.Warning, message); + public void LogWarning(Func messageFactory) => WriteLog(LogLevel.Warning, messageFactory); + + public void LogError(string message, Exception exception) => WriteLog(LogLevel.Error, message, exception); + public void LogError(Func messageFactory, Exception exception) => WriteLog(LogLevel.Error, messageFactory, exception); + + public void LogCritical(string message, Exception exception) => WriteLog(LogLevel.Critical, message, exception); + public void LogCritical(Func messageFactory, Exception exception) => WriteLog(LogLevel.Critical, messageFactory, exception); + + private string GetOcelotRequestId() + { + var requestId = _scopedDataRepository.Get(RequestIdMiddleware.RequestIdName); + return requestId?.IsError ?? true ? $"No {RequestIdMiddleware.RequestIdName}" : requestId.Data; + } + + private string GetOcelotPreviousRequestId() + { + var requestId = _scopedDataRepository.Get(RequestIdMiddleware.PreviousRequestIdName); + return requestId?.IsError ?? true ? $"No {RequestIdMiddleware.PreviousRequestIdName}" : requestId.Data; + } + + private void WriteLog(LogLevel logLevel, string message, Exception exception = null) + { + WriteLog(logLevel, null, message, exception); + } + + private void WriteLog(LogLevel logLevel, Func messageFactory, Exception exception = null) + { + WriteLog(logLevel, messageFactory, null, exception); + } + + private void WriteLog(LogLevel logLevel, Func messageFactory, string message, Exception exception = null) + { + if (!_logger.IsEnabled(logLevel)) + { + return; + } + + var requestId = GetOcelotRequestId(); + var previousRequestId = GetOcelotPreviousRequestId(); + + if (messageFactory != null) + { + message = messageFactory.Invoke() ?? string.Empty; + } + + _logger.Log(logLevel, + default, + $"{nameof(requestId)}: {requestId}, {nameof(previousRequestId)}: {previousRequestId}, {nameof(message)}: '{message}'", + exception, + _func); + } +} diff --git a/src/Ocelot/Logging/AspDotNetLoggerFactory.cs b/src/Ocelot/Logging/OcelotLoggerFactory.cs similarity index 66% rename from src/Ocelot/Logging/AspDotNetLoggerFactory.cs rename to src/Ocelot/Logging/OcelotLoggerFactory.cs index 8c53f6846..123caf2d2 100644 --- a/src/Ocelot/Logging/AspDotNetLoggerFactory.cs +++ b/src/Ocelot/Logging/OcelotLoggerFactory.cs @@ -3,12 +3,12 @@ namespace Ocelot.Logging { - public class AspDotNetLoggerFactory : IOcelotLoggerFactory + public class OcelotLoggerFactory : IOcelotLoggerFactory { private readonly ILoggerFactory _loggerFactory; private readonly IRequestScopedDataRepository _scopedDataRepository; - public AspDotNetLoggerFactory(ILoggerFactory loggerFactory, IRequestScopedDataRepository scopedDataRepository) + public OcelotLoggerFactory(ILoggerFactory loggerFactory, IRequestScopedDataRepository scopedDataRepository) { _loggerFactory = loggerFactory; _scopedDataRepository = scopedDataRepository; @@ -17,7 +17,7 @@ public AspDotNetLoggerFactory(ILoggerFactory loggerFactory, IRequestScopedDataRe public IOcelotLogger CreateLogger() { var logger = _loggerFactory.CreateLogger(); - return new AspDotNetLogger(logger, _scopedDataRepository); + return new OcelotLogger(logger, _scopedDataRepository); } } } diff --git a/src/Ocelot/QueryStrings/Middleware/ClaimsToQueryStringMiddleware.cs b/src/Ocelot/QueryStrings/Middleware/ClaimsToQueryStringMiddleware.cs index eb5c30fc8..0083fa453 100644 --- a/src/Ocelot/QueryStrings/Middleware/ClaimsToQueryStringMiddleware.cs +++ b/src/Ocelot/QueryStrings/Middleware/ClaimsToQueryStringMiddleware.cs @@ -24,7 +24,7 @@ public async Task Invoke(HttpContext httpContext) if (downstreamRoute.ClaimsToQueries.Any()) { - Logger.LogInformation($"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to queries"); + Logger.LogInformation(() => $"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to queries"); var downstreamRequest = httpContext.Items.DownstreamRequest(); diff --git a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs b/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs index 6b62a14d2..47571046f 100644 --- a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs +++ b/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs @@ -28,7 +28,7 @@ public async Task Invoke(HttpContext httpContext) // check if rate limiting is enabled if (!downstreamRoute.EnableEndpointEndpointRateLimiting) { - Logger.LogInformation($"EndpointRateLimiting is not enabled for {downstreamRoute.DownstreamPathTemplate.Value}"); + Logger.LogInformation(() => $"EndpointRateLimiting is not enabled for {downstreamRoute.DownstreamPathTemplate.Value}"); await _next.Invoke(httpContext); return; } @@ -39,7 +39,7 @@ public async Task Invoke(HttpContext httpContext) // check white list if (IsWhitelisted(identity, options)) { - Logger.LogInformation($"{downstreamRoute.DownstreamPathTemplate.Value} is white listed from rate limiting"); + Logger.LogInformation(() => $"{downstreamRoute.DownstreamPathTemplate.Value} is white listed from rate limiting"); await _next.Invoke(httpContext); return; } @@ -110,7 +110,7 @@ public bool IsWhitelisted(ClientRequestIdentity requestIdentity, RateLimitOption public virtual void LogBlockedRequest(HttpContext httpContext, ClientRequestIdentity identity, RateLimitCounter counter, RateLimitRule rule, DownstreamRoute downstreamRoute) { Logger.LogInformation( - $"Request {identity.HttpVerb}:{identity.Path} from ClientId {identity.ClientId} has been blocked, quota {rule.Limit}/{rule.Period} exceeded by {counter.TotalRequests}. Blocked by rule {downstreamRoute.UpstreamPathTemplate.OriginalValue}, TraceIdentifier {httpContext.TraceIdentifier}."); + () => $"Request {identity.HttpVerb}:{identity.Path} from ClientId {identity.ClientId} has been blocked, quota {rule.Limit}/{rule.Period} exceeded by {counter.TotalRequests}. Blocked by rule {downstreamRoute.UpstreamPathTemplate.OriginalValue}, TraceIdentifier {httpContext.TraceIdentifier}."); } public virtual DownstreamResponse ReturnQuotaExceededResponse(HttpContext httpContext, RateLimitOptions option, string retryAfter) diff --git a/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs b/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs index f2fc77ca1..5fef5a918 100644 --- a/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs +++ b/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; @@ -9,8 +10,12 @@ namespace Ocelot.RequestId.Middleware { public class RequestIdMiddleware : OcelotMiddleware { + public const string RequestIdName = nameof(IInternalConfiguration.RequestId); + public const string PreviousRequestIdName = "Previous" + nameof(IInternalConfiguration.RequestId); + private readonly RequestDelegate _next; private readonly IRequestScopedDataRepository _requestScopedDataRepository; + public RequestIdMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IRequestScopedDataRepository requestScopedDataRepository) @@ -36,15 +41,15 @@ private void SetOcelotRequestId(HttpContext httpContext) { httpContext.TraceIdentifier = upstreamRequestIds.First(); - var previousRequestId = _requestScopedDataRepository.Get("RequestId"); + var previousRequestId = _requestScopedDataRepository.Get(RequestIdName); if (!previousRequestId.IsError && !string.IsNullOrEmpty(previousRequestId.Data) && previousRequestId.Data != httpContext.TraceIdentifier) { - _requestScopedDataRepository.Add("PreviousRequestId", previousRequestId.Data); - _requestScopedDataRepository.Update("RequestId", httpContext.TraceIdentifier); + _requestScopedDataRepository.Add(PreviousRequestIdName, previousRequestId.Data); + _requestScopedDataRepository.Update(RequestIdName, httpContext.TraceIdentifier); } else { - _requestScopedDataRepository.Add("RequestId", httpContext.TraceIdentifier); + _requestScopedDataRepository.Add(RequestIdName, httpContext.TraceIdentifier); } } diff --git a/src/Ocelot/Requester/DelegatingHandlerHandlerFactory.cs b/src/Ocelot/Requester/DelegatingHandlerHandlerFactory.cs index 02b691c4c..8b7cc7955 100644 --- a/src/Ocelot/Requester/DelegatingHandlerHandlerFactory.cs +++ b/src/Ocelot/Requester/DelegatingHandlerHandlerFactory.cs @@ -71,7 +71,7 @@ public Response>> Get(DownstreamRoute downstreamRou } else { - _logger.LogWarning($"Route {downstreamRoute.UpstreamPathTemplate} specifies use QoS but no QosHandler found in DI container. Will use not use a QosHandler, please check your setup!"); + _logger.LogWarning(() => $"Route {downstreamRoute.UpstreamPathTemplate} specifies use QoS but no QosHandler found in DI container. Will use not use a QosHandler, please check your setup!"); handlers.Add(() => new NoQosDelegatingHandler()); } } diff --git a/src/Ocelot/Requester/HttpClientBuilder.cs b/src/Ocelot/Requester/HttpClientBuilder.cs index 0fd818e66..99b2bec3e 100644 --- a/src/Ocelot/Requester/HttpClientBuilder.cs +++ b/src/Ocelot/Requester/HttpClientBuilder.cs @@ -47,7 +47,7 @@ public IHttpClient Create(DownstreamRoute downstreamRoute) HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; _logger - .LogWarning($"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamRoute, UpstreamPathTemplate: {downstreamRoute.UpstreamPathTemplate}, DownstreamPathTemplate: {downstreamRoute.DownstreamPathTemplate}"); + .LogWarning(() => $"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamRoute, UpstreamPathTemplate: {downstreamRoute.UpstreamPathTemplate}, DownstreamPathTemplate: {downstreamRoute.DownstreamPathTemplate}"); } var timeout = downstreamRoute.QosOptions.TimeoutValue == 0 diff --git a/src/Ocelot/Requester/HttpExceptionToErrorMapper.cs b/src/Ocelot/Requester/HttpExceptionToErrorMapper.cs index 80b7b15e8..62b03cfa9 100644 --- a/src/Ocelot/Requester/HttpExceptionToErrorMapper.cs +++ b/src/Ocelot/Requester/HttpExceptionToErrorMapper.cs @@ -16,9 +16,9 @@ public Error Map(Exception exception) { var type = exception.GetType(); - if (_mappers != null && _mappers.ContainsKey(type)) + if (_mappers != null && _mappers.TryGetValue(type, out var mapper)) { - return _mappers[type](exception); + return mapper(exception); } if (type == typeof(OperationCanceledException) || type.IsSubclassOf(typeof(OperationCanceledException))) diff --git a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs index 5a56ab7a4..d996d2983 100644 --- a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs +++ b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs @@ -46,13 +46,13 @@ private void CreateLogBasedOnResponse(Response response) { if (response.Data?.StatusCode <= HttpStatusCode.BadRequest) { - Logger.LogInformation( + Logger.LogInformation(() => $"{(int)response.Data.StatusCode} ({response.Data.ReasonPhrase}) status code, request uri: {response.Data.RequestMessage?.RequestUri}"); } else if (response.Data?.StatusCode >= HttpStatusCode.BadRequest) { Logger.LogWarning( - $"{(int)response.Data.StatusCode} ({response.Data.ReasonPhrase}) status code, request uri: {response.Data.RequestMessage?.RequestUri}"); + () => $"{(int)response.Data.StatusCode} ({response.Data.ReasonPhrase}) status code, request uri: {response.Data.RequestMessage?.RequestUri}"); } } } diff --git a/src/Ocelot/Requester/QoS/QosFactory.cs b/src/Ocelot/Requester/QoS/QosFactory.cs index 480116a74..be8569938 100644 --- a/src/Ocelot/Requester/QoS/QosFactory.cs +++ b/src/Ocelot/Requester/QoS/QosFactory.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Logging; @@ -8,12 +9,14 @@ namespace Ocelot.Requester.QoS public class QoSFactory : IQoSFactory { private readonly IServiceProvider _serviceProvider; - private readonly IOcelotLoggerFactory _ocelotLoggerFactory; + private readonly IOcelotLoggerFactory _ocelotLoggerFactory; + private readonly IHttpContextAccessor _contextAccessor; - public QoSFactory(IServiceProvider serviceProvider, IOcelotLoggerFactory ocelotLoggerFactory) + public QoSFactory(IServiceProvider serviceProvider, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory ocelotLoggerFactory) { _serviceProvider = serviceProvider; - _ocelotLoggerFactory = ocelotLoggerFactory; + _ocelotLoggerFactory = ocelotLoggerFactory; + _contextAccessor = contextAccessor; } public Response Get(DownstreamRoute request) @@ -22,7 +25,7 @@ public Response Get(DownstreamRoute request) if (handler != null) { - return new OkResponse(handler(request, _ocelotLoggerFactory)); + return new OkResponse(handler(request, _contextAccessor, _ocelotLoggerFactory)); } return new ErrorResponse(new UnableToFindQoSProviderError($"could not find qosProvider for {request.DownstreamScheme}{request.DownstreamAddresses}{request.DownstreamPathTemplate}")); diff --git a/src/Ocelot/Requester/QosDelegatingHandlerDelegate.cs b/src/Ocelot/Requester/QosDelegatingHandlerDelegate.cs index 992601cb0..5af77f60b 100644 --- a/src/Ocelot/Requester/QosDelegatingHandlerDelegate.cs +++ b/src/Ocelot/Requester/QosDelegatingHandlerDelegate.cs @@ -1,7 +1,8 @@ -using Ocelot.Configuration; -using Ocelot.Logging; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Logging; namespace Ocelot.Requester { - public delegate DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute route, IOcelotLoggerFactory logger); + public delegate DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory); } diff --git a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs index 925663f71..76db720a1 100644 --- a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs +++ b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs @@ -37,13 +37,13 @@ public async Task Invoke(HttpContext httpContext) // todo check errors is ok if (errors.Count > 0) { - Logger.LogWarning($"{errors.ToErrorString()} errors found in {MiddlewareName}. Setting error response for request path:{httpContext.Request.Path}, request method: {httpContext.Request.Method}"); + Logger.LogWarning(() => $"{errors.ToErrorString()} errors found in {MiddlewareName}. Setting error response for request path:{httpContext.Request.Path}, request method: {httpContext.Request.Method}"); SetErrorResponse(httpContext, errors); } else if (downstreamResponse == null) { - Logger.LogDebug($"Pipeline was terminated early in {MiddlewareName}"); + Logger.LogDebug(() => $"Pipeline was terminated early in {MiddlewareName}"); } else { diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs index 174c973ab..b47e4d921 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs @@ -1,51 +1,51 @@ -using Microsoft.Extensions.DependencyInjection; -using Ocelot.Configuration; -using Ocelot.Logging; -using Ocelot.Responses; -using Ocelot.ServiceDiscovery.Configuration; -using Ocelot.ServiceDiscovery.Providers; -using Ocelot.Values; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration; +using Ocelot.Logging; +using Ocelot.Responses; +using Ocelot.ServiceDiscovery.Configuration; +using Ocelot.ServiceDiscovery.Providers; +using Ocelot.Values; namespace Ocelot.ServiceDiscovery { public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory { - private readonly IServiceProvider _provider; - private readonly ServiceDiscoveryFinderDelegate _delegates; + private readonly IServiceProvider _provider; + private readonly ServiceDiscoveryFinderDelegate _delegates; private readonly IOcelotLogger _logger; public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IServiceProvider provider) { _provider = provider; - _delegates = provider.GetService(); + _delegates = provider.GetService(); _logger = factory.CreateLogger(); } public Response Get(ServiceProviderConfiguration serviceConfig, DownstreamRoute route) { if (route.UseServiceDiscovery) - { - var routeName = route.UpstreamPathTemplate?.Template ?? route.ServiceName ?? string.Empty; - _logger.LogInformation($"The {nameof(DownstreamRoute.UseServiceDiscovery)} mode of the route '{routeName}' is enabled."); + { + var routeName = route.UpstreamPathTemplate?.Template ?? route.ServiceName ?? string.Empty; + _logger.LogInformation(() => $"The {nameof(DownstreamRoute.UseServiceDiscovery)} mode of the route '{routeName}' is enabled."); return GetServiceDiscoveryProvider(serviceConfig, route); } - var services = route.DownstreamAddresses - .Select(address => new Service( - route.ServiceName, - new ServiceHostAndPort(address.Host, address.Port, route.DownstreamScheme), - string.Empty, - string.Empty, - Enumerable.Empty())) - .ToList(); + var services = route.DownstreamAddresses + .Select(address => new Service( + route.ServiceName, + new ServiceHostAndPort(address.Host, address.Port, route.DownstreamScheme), + string.Empty, + string.Empty, + Enumerable.Empty())) + .ToList(); return new OkResponse(new ConfigurationServiceProvider(services)); } private Response GetServiceDiscoveryProvider(ServiceProviderConfiguration config, DownstreamRoute route) - { - _logger.LogInformation($"Getting service discovery provider of {nameof(config.Type)} '{config.Type}'..."); - + { + _logger.LogInformation(() => $"Getting service discovery provider of {nameof(config.Type)} '{config.Type}'..."); + if (config.Type?.ToLower() == "servicefabric") { var sfConfig = new ServiceFabricConfiguration(config.Host, config.Port, route.ServiceName); @@ -61,10 +61,10 @@ private Response GetServiceDiscoveryProvider(ServiceP return new OkResponse(provider); } } - - var message = $"Unable to find service discovery provider for {nameof(config.Type)}: '{config.Type}'!"; - _logger.LogWarning(message); - + + var message = $"Unable to find service discovery provider for {nameof(config.Type)}: '{config.Type}'!"; + _logger.LogWarning(() => $"Unable to find service discovery provider for {nameof(config.Type)}: '{config.Type}'!"); + return new ErrorResponse(new UnableToFindServiceDiscoveryProviderError(message)); } } diff --git a/src/Ocelot/WebSockets/WebSocketsProxyMiddleware.cs b/src/Ocelot/WebSockets/WebSocketsProxyMiddleware.cs index 86b7d963f..385efc831 100644 --- a/src/Ocelot/WebSockets/WebSocketsProxyMiddleware.cs +++ b/src/Ocelot/WebSockets/WebSocketsProxyMiddleware.cs @@ -110,7 +110,7 @@ private async Task Proxy(HttpContext context, DownstreamRequest request, Downstr if (route.DangerousAcceptAnyServerCertificateValidator) { client.Options.RemoteCertificateValidationCallback = (request, certificate, chain, errors) => true; - Logger.LogWarning(string.Format(IgnoredSslWarningFormat, route.UpstreamPathTemplate, route.DownstreamPathTemplate)); + Logger.LogWarning(() => string.Format(IgnoredSslWarningFormat, route.UpstreamPathTemplate, route.DownstreamPathTemplate)); } foreach (var protocol in context.WebSockets.WebSocketRequestedProtocols) @@ -139,7 +139,7 @@ private async Task Proxy(HttpContext context, DownstreamRequest request, Downstr var scheme = request.Scheme; if (!scheme.StartsWith(Uri.UriSchemeWs)) { - Logger.LogWarning(string.Format(InvalidSchemeWarningFormat, scheme, request.ToUri())); + Logger.LogWarning(() => string.Format(InvalidSchemeWarningFormat, scheme, request.ToUri())); request.Scheme = scheme == Uri.UriSchemeHttp ? Uri.UriSchemeWs : scheme == Uri.UriSchemeHttps ? Uri.UriSchemeWss : scheme; } diff --git a/test/Ocelot.AcceptanceTests/AggregateTests.cs b/test/Ocelot.AcceptanceTests/AggregateTests.cs index 5810bf457..7cb4c03e3 100644 --- a/test/Ocelot.AcceptanceTests/AggregateTests.cs +++ b/test/Ocelot.AcceptanceTests/AggregateTests.cs @@ -21,7 +21,7 @@ public AggregateTests() [Fact] public void should_fix_issue_597() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -132,9 +132,9 @@ public void should_fix_issue_597() [Fact] public void should_return_response_200_with_advanced_aggregate_configs() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); - var port3 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var port3 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -229,8 +229,8 @@ public void should_return_response_200_with_advanced_aggregate_configs() [Fact] public void should_return_response_200_with_simple_url_user_defined_aggregate() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -289,7 +289,7 @@ public void should_return_response_200_with_simple_url_user_defined_aggregate() this.Given(x => x.GivenServiceOneIsRunning($"http://localhost:{port1}", "/", 200, "{Hello from Laura}")) .Given(x => x.GivenServiceTwoIsRunning($"http://localhost:{port2}", "/", 200, "{Hello from Tom}")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithSpecficAggregatorsRegisteredInDi()) + .And(x => _steps.GivenOcelotIsRunningWithSpecificAggregatorsRegisteredInDi()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) @@ -300,8 +300,8 @@ public void should_return_response_200_with_simple_url_user_defined_aggregate() [Fact] public void should_return_response_200_with_simple_url() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -370,8 +370,8 @@ public void should_return_response_200_with_simple_url() [Fact] public void should_return_response_200_with_simple_url_one_service_404() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -440,8 +440,8 @@ public void should_return_response_200_with_simple_url_one_service_404() [Fact] public void should_return_response_200_with_simple_url_both_service_404() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -510,8 +510,8 @@ public void should_return_response_200_with_simple_url_both_service_404() [Fact] public void should_be_thread_safe() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List diff --git a/test/Ocelot.AcceptanceTests/AuthenticationTests.cs b/test/Ocelot.AcceptanceTests/AuthenticationTests.cs index 8316d926c..35329847f 100644 --- a/test/Ocelot.AcceptanceTests/AuthenticationTests.cs +++ b/test/Ocelot.AcceptanceTests/AuthenticationTests.cs @@ -26,7 +26,7 @@ public AuthenticationTests() { _serviceHandler = new ServiceHandler(); _steps = new Steps(); - var identityServerPort = RandomPortFinder.GetRandomPort(); + var identityServerPort = PortFinder.GetRandomPort(); _identityServerRootUrl = $"http://localhost:{identityServerPort}"; _options = o => { @@ -41,7 +41,7 @@ public AuthenticationTests() [Fact] public void should_return_401_using_identity_server_access_token() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -82,7 +82,7 @@ public void should_return_401_using_identity_server_access_token() [Fact] public void should_return_response_200_using_identity_server() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -125,7 +125,7 @@ public void should_return_response_200_using_identity_server() [Fact] public void should_return_response_401_using_identity_server_with_token_requested_for_other_api() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -167,7 +167,7 @@ public void should_return_response_401_using_identity_server_with_token_requeste [Fact] public void should_return_201_using_identity_server_access_token() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -210,7 +210,7 @@ public void should_return_201_using_identity_server_access_token() [Fact] public void should_return_201_using_identity_server_reference_token() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -364,7 +364,7 @@ private void GivenThereIsAnIdentityServerOn(string url, string apiName, string a _identityServerBuilder.Start(); - _steps.VerifyIdentiryServerStarted(url); + Steps.VerifyIdentityServerStarted(url); } public void Dispose() diff --git a/test/Ocelot.AcceptanceTests/AuthorizationTests.cs b/test/Ocelot.AcceptanceTests/AuthorizationTests.cs index f44add4ae..26dcfb1a4 100644 --- a/test/Ocelot.AcceptanceTests/AuthorizationTests.cs +++ b/test/Ocelot.AcceptanceTests/AuthorizationTests.cs @@ -22,7 +22,7 @@ public AuthorizationTests() { _serviceHandler = new ServiceHandler(); _steps = new Steps(); - var identityServerPort = RandomPortFinder.GetRandomPort(); + var identityServerPort = PortFinder.GetRandomPort(); _identityServerRootUrl = $"http://localhost:{identityServerPort}"; _options = o => { @@ -37,7 +37,7 @@ public AuthorizationTests() [Fact] public void should_return_response_200_authorizing_route() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -97,7 +97,7 @@ public void should_return_response_200_authorizing_route() [Fact] public void should_return_response_403_authorizing_route() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -155,7 +155,7 @@ public void should_return_response_403_authorizing_route() [Fact] public void should_return_response_200_using_identity_server_with_allowed_scope() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -198,7 +198,7 @@ public void should_return_response_200_using_identity_server_with_allowed_scope( [Fact] public void should_return_response_403_using_identity_server_with_scope_not_allowed() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -241,7 +241,7 @@ public void should_return_response_403_using_identity_server_with_scope_not_allo [Fact] public void should_fix_issue_240() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -393,7 +393,7 @@ private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTo _identityServerBuilder.Start(); - _steps.VerifyIdentiryServerStarted(url); + Steps.VerifyIdentityServerStarted(url); } private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTokenType tokenType, List users) @@ -464,7 +464,7 @@ private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTo _identityServerBuilder.Start(); - _steps.VerifyIdentiryServerStarted(url); + Steps.VerifyIdentityServerStarted(url); } public void Dispose() diff --git a/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs b/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs index c3a4f19e9..a90560f02 100644 --- a/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs +++ b/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs @@ -27,8 +27,8 @@ public ButterflyTracingTests(ITestOutputHelper output) [Fact] public void should_forward_tracing_information_from_ocelot_and_downstream_services() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -74,7 +74,7 @@ public void should_forward_tracing_information_from_ocelot_and_downstream_servic }, }; - var butterflyPort = RandomPortFinder.GetRandomPort(); + var butterflyPort = PortFinder.GetRandomPort(); var butterflyUrl = $"http://localhost:{butterflyPort}"; this.Given(x => GivenFakeButterfly(butterflyUrl)) @@ -100,7 +100,7 @@ public void should_forward_tracing_information_from_ocelot_and_downstream_servic [Fact] public void should_return_tracing_header() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -132,7 +132,7 @@ public void should_return_tracing_header() }, }; - var butterflyPort = RandomPortFinder.GetRandomPort(); + var butterflyPort = PortFinder.GetRandomPort(); var butterflyUrl = $"http://localhost:{butterflyPort}"; this.Given(x => GivenFakeButterfly(butterflyUrl)) diff --git a/test/Ocelot.AcceptanceTests/CachingTests.cs b/test/Ocelot.AcceptanceTests/CachingTests.cs index b2ef34d6c..fc1f91000 100644 --- a/test/Ocelot.AcceptanceTests/CachingTests.cs +++ b/test/Ocelot.AcceptanceTests/CachingTests.cs @@ -17,7 +17,7 @@ public CachingTests() [Fact] public void should_return_cached_response() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -62,7 +62,7 @@ public void should_return_cached_response() [Fact] public void should_return_cached_response_with_expires_header() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -108,7 +108,7 @@ public void should_return_cached_response_with_expires_header() [Fact] public void should_return_cached_response_when_using_jsonserialized_cache() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -152,7 +152,7 @@ public void should_return_cached_response_when_using_jsonserialized_cache() [Fact] public void should_not_return_cached_response_as_ttl_expires() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/CancelRequestTests.cs b/test/Ocelot.AcceptanceTests/CancelRequestTests.cs index 983d98fc0..984160591 100644 --- a/test/Ocelot.AcceptanceTests/CancelRequestTests.cs +++ b/test/Ocelot.AcceptanceTests/CancelRequestTests.cs @@ -33,7 +33,7 @@ public CancelRequestTests() [Fact] public void Should_abort_service_work_when_cancelling_the_request() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs b/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs index 64759aed2..202fe2d27 100644 --- a/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs @@ -17,7 +17,7 @@ public CaseSensitiveRoutingTests() [Fact] public void should_return_response_200_when_global_ignore_case_sensitivity_set() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -52,7 +52,7 @@ public void should_return_response_200_when_global_ignore_case_sensitivity_set() [Fact] public void should_return_response_200_when_route_ignore_case_sensitivity_set() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -88,7 +88,7 @@ public void should_return_response_200_when_route_ignore_case_sensitivity_set() [Fact] public void should_return_response_404_when_route_respect_case_sensitivity_set() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -124,7 +124,7 @@ public void should_return_response_404_when_route_respect_case_sensitivity_set() [Fact] public void should_return_response_200_when_route_respect_case_sensitivity_set() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -160,7 +160,7 @@ public void should_return_response_200_when_route_respect_case_sensitivity_set() [Fact] public void should_return_response_404_when_global_respect_case_sensitivity_set() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -196,7 +196,7 @@ public void should_return_response_404_when_global_respect_case_sensitivity_set( [Fact] public void should_return_response_200_when_global_respect_case_sensitivity_set() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs index 445239c4b..b3697f0d6 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs @@ -20,7 +20,7 @@ public class ClaimsToDownstreamPathTests : IDisposable public ClaimsToDownstreamPathTests() { - var identityServerPort = RandomPortFinder.GetRandomPort(); + var identityServerPort = PortFinder.GetRandomPort(); _identityServerRootUrl = $"http://localhost:{identityServerPort}"; _steps = new Steps(); _options = o => @@ -43,7 +43,7 @@ public void should_return_200_and_change_downstream_path() SubjectId = "registered|1231231", }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -196,7 +196,7 @@ private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTo _identityServerBuilder.Start(); - _steps.VerifyIdentiryServerStarted(url); + Steps.VerifyIdentityServerStarted(url); } public void Dispose() diff --git a/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs index b8f2ece35..18cf8cdd0 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs @@ -24,7 +24,7 @@ public ClaimsToHeadersForwardingTests() { _serviceHandler = new ServiceHandler(); _steps = new Steps(); - var identityServerPort = RandomPortFinder.GetRandomPort(); + var identityServerPort = PortFinder.GetRandomPort(); _identityServerRootUrl = $"http://localhost:{identityServerPort}"; _options = o => { @@ -51,7 +51,7 @@ public void should_return_response_200_and_foward_claim_as_header() }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -190,7 +190,7 @@ private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTo _identityServerBuilder.Start(); - _steps.VerifyIdentiryServerStarted(url); + Steps.VerifyIdentityServerStarted(url); } public void Dispose() diff --git a/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs index 969faebb0..9a89fef62 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs @@ -22,7 +22,7 @@ public class ClaimsToQueryStringForwardingTests : IDisposable public ClaimsToQueryStringForwardingTests() { _steps = new Steps(); - var identityServerPort = RandomPortFinder.GetRandomPort(); + var identityServerPort = PortFinder.GetRandomPort(); _identityServerRootUrl = $"http://localhost:{identityServerPort}"; _options = o => { @@ -49,7 +49,7 @@ public void should_return_response_200_and_foward_claim_as_query_string() }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -115,7 +115,7 @@ public void should_return_response_200_and_foward_claim_as_query_string_and_pres }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -277,7 +277,7 @@ private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTo _identityServerBuilder.Start(); - _steps.VerifyIdentiryServerStarted(url); + Steps.VerifyIdentityServerStarted(url); } public void Dispose() diff --git a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs index 3b04ca849..dad9af3dc 100644 --- a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs +++ b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs @@ -18,7 +18,7 @@ public ClientRateLimitTests() [Fact] public void should_call_withratelimiting() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -78,7 +78,7 @@ public void should_call_withratelimiting() [Fact] public void should_wait_for_period_timespan_to_elapse_before_making_next_request() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -145,7 +145,7 @@ public void should_wait_for_period_timespan_to_elapse_before_making_next_request [Fact] public void should_call_middleware_withWhitelistClient() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs b/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs index ef35a8efc..c889f353e 100644 --- a/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs +++ b/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs @@ -27,8 +27,8 @@ public ConfigurationInConsulTests() [Fact] public void should_return_response_200_with_simple_url_when_using_jsonserialized_cache() { - var consulPort = RandomPortFinder.GetRandomPort(); - var servicePort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs b/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs index 6d0c31a3a..978626c14 100644 --- a/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs +++ b/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs @@ -26,8 +26,8 @@ public ConsulConfigurationInConsulTests() [Fact] public void should_return_response_200_with_simple_url() { - var consulPort = RandomPortFinder.GetRandomPort(); - var servicePort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -75,8 +75,8 @@ public void should_return_response_200_with_simple_url() [Fact] public void should_load_configuration_out_of_consul() { - var consulPort = RandomPortFinder.GetRandomPort(); - var servicePort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -138,8 +138,8 @@ public void should_load_configuration_out_of_consul() [Fact] public void should_load_configuration_out_of_consul_if_it_is_changed() { - var consulPort = RandomPortFinder.GetRandomPort(); - var servicePort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -234,9 +234,9 @@ public void should_load_configuration_out_of_consul_if_it_is_changed() [Fact] public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes_and_rate_limit() { - var consulPort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); const string serviceName = "web"; - var downstreamServicePort = RandomPortFinder.GetRandomPort(); + var downstreamServicePort = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; var serviceEntryOne = new ServiceEntry diff --git a/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs b/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs index 38e4e18c8..dceafc0d4 100644 --- a/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs +++ b/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs @@ -28,14 +28,14 @@ public ConsulWebSocketTests() [Fact] public void ShouldProxyWebsocketInputToDownstreamServiceAndUseServiceDiscoveryAndLoadBalancer() { - var downstreamPort = RandomPortFinder.GetRandomPort(); + var downstreamPort = PortFinder.GetRandomPort(); var downstreamHost = "localhost"; - var secondDownstreamPort = RandomPortFinder.GetRandomPort(); + var secondDownstreamPort = PortFinder.GetRandomPort(); var secondDownstreamHost = "localhost"; var serviceName = "websockets"; - var consulPort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; var serviceEntryOne = new ServiceEntry { diff --git a/test/Ocelot.AcceptanceTests/ContentTests.cs b/test/Ocelot.AcceptanceTests/ContentTests.cs index a976ddb70..ba95dfd22 100644 --- a/test/Ocelot.AcceptanceTests/ContentTests.cs +++ b/test/Ocelot.AcceptanceTests/ContentTests.cs @@ -20,7 +20,7 @@ public ContentTests() [Fact] public void should_not_add_content_type_or_content_length_headers() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -58,7 +58,7 @@ public void should_not_add_content_type_or_content_length_headers() [Fact] public void should_add_content_type_and_content_length_headers() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -99,7 +99,7 @@ public void should_add_content_type_and_content_length_headers() [Fact] public void should_add_default_content_type_header() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs b/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs index 5f9bc2926..5fc91dd8d 100644 --- a/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs +++ b/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs @@ -30,7 +30,7 @@ public void should_call_pre_query_string_builder_middleware() }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var fileConfiguration = new FileConfiguration { @@ -75,7 +75,7 @@ public void should_call_authorization_middleware() }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var fileConfiguration = new FileConfiguration { @@ -120,7 +120,7 @@ public void should_call_authentication_middleware() }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var fileConfiguration = new FileConfiguration { @@ -165,7 +165,7 @@ public void should_call_pre_error_middleware() }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var fileConfiguration = new FileConfiguration { @@ -210,7 +210,7 @@ public void should_call_pre_authorization_middleware() }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var fileConfiguration = new FileConfiguration { @@ -255,7 +255,7 @@ public void should_call_pre_http_authentication_middleware() }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var fileConfiguration = new FileConfiguration { @@ -301,7 +301,7 @@ public void should_not_throw_when_pipeline_terminates_early() }), }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var fileConfiguration = new FileConfiguration { @@ -350,7 +350,7 @@ public void should_fix_issue_237() return Task.CompletedTask; }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var fileConfiguration = new FileConfiguration { @@ -376,7 +376,7 @@ public void should_fix_issue_237() this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "/test")) .And(x => _steps.GivenThereIsAConfiguration(fileConfiguration)) - .And(x => _steps.GivenOcelotIsRunningWithMiddleareBeforePipeline(callback)) + .And(x => _steps.GivenOcelotIsRunningWithMiddlewareBeforePipeline(callback)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); diff --git a/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs index 1be8c7798..1b7c1e99f 100644 --- a/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs @@ -26,7 +26,7 @@ public void should_use_eureka_service_discovery_and_make_request(bool dotnetRunn Environment.SetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER", dotnetRunningInContainer.ToString()); var eurekaPort = 8761; var serviceName = "product"; - var downstreamServicePort = RandomPortFinder.GetRandomPort(); + var downstreamServicePort = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; var fakeEurekaServiceDiscoveryUrl = $"http://localhost:{eurekaPort}"; diff --git a/test/Ocelot.AcceptanceTests/GzipTests.cs b/test/Ocelot.AcceptanceTests/GzipTests.cs index ca10deedc..753ce70a9 100644 --- a/test/Ocelot.AcceptanceTests/GzipTests.cs +++ b/test/Ocelot.AcceptanceTests/GzipTests.cs @@ -18,7 +18,7 @@ public GzipTests() [Fact] public void should_return_response_200_with_simple_url() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/HeaderTests.cs b/test/Ocelot.AcceptanceTests/HeaderTests.cs index 1a7979a1d..6b1060383 100644 --- a/test/Ocelot.AcceptanceTests/HeaderTests.cs +++ b/test/Ocelot.AcceptanceTests/HeaderTests.cs @@ -18,7 +18,7 @@ public HeaderTests() [Fact] public void should_transform_upstream_header() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -59,7 +59,7 @@ public void should_transform_upstream_header() [Fact] public void should_transform_downstream_header() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -99,7 +99,7 @@ public void should_transform_downstream_header() [Fact] public void should_fix_issue_190() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -143,7 +143,7 @@ public void should_fix_issue_190() [Fact] public void should_fix_issue_205() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -187,7 +187,7 @@ public void should_fix_issue_205() [Fact] public void should_fix_issue_417() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -235,7 +235,7 @@ public void should_fix_issue_417() [Fact] public void request_should_reuse_cookies_with_cookie_container() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -278,7 +278,7 @@ public void request_should_reuse_cookies_with_cookie_container() [Fact] public void request_should_have_own_cookies_no_cookie_container() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -321,7 +321,7 @@ public void request_should_have_own_cookies_no_cookie_container() [Fact] public void issue_474_should_not_put_spaces_in_header() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -358,7 +358,7 @@ public void issue_474_should_not_put_spaces_in_header() [Fact] public void issue_474_should_put_spaces_in_header() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/HttpClientCachingTests.cs b/test/Ocelot.AcceptanceTests/HttpClientCachingTests.cs index 39a42ad71..eca147ac8 100644 --- a/test/Ocelot.AcceptanceTests/HttpClientCachingTests.cs +++ b/test/Ocelot.AcceptanceTests/HttpClientCachingTests.cs @@ -20,7 +20,7 @@ public HttpClientCachingTests() [Fact] public void should_cache_one_http_client_same_re_route() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -62,7 +62,7 @@ public void should_cache_one_http_client_same_re_route() [Fact] public void should_cache_two_http_client_different_re_route() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs index 51ef4051c..83f79e720 100644 --- a/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs +++ b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs @@ -18,7 +18,7 @@ public HttpDelegatingHandlersTests() [Fact] public void should_call_re_route_ordered_specific_handlers() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -49,7 +49,7 @@ public void should_call_re_route_ordered_specific_handlers() this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithSpecficHandlersRegisteredInDi()) + .And(x => _steps.GivenOcelotIsRunningWithSpecificHandlersRegisteredInDi()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) @@ -60,7 +60,7 @@ public void should_call_re_route_ordered_specific_handlers() [Fact] public void should_call_global_di_handlers() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -97,7 +97,7 @@ public void should_call_global_di_handlers() [Fact] public void should_call_global_di_handlers_multiple_times() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -150,7 +150,7 @@ public void should_call_global_di_handlers_multiple_times() [Fact] public void should_call_global_di_handlers_with_dependency() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/HttpTests.cs b/test/Ocelot.AcceptanceTests/HttpTests.cs index 9a80a4519..532bd6276 100644 --- a/test/Ocelot.AcceptanceTests/HttpTests.cs +++ b/test/Ocelot.AcceptanceTests/HttpTests.cs @@ -18,7 +18,7 @@ public HttpTests() [Fact] public void should_return_response_200_when_using_http_one() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -55,7 +55,7 @@ public void should_return_response_200_when_using_http_one() [Fact] public void should_return_response_200_when_using_http_one_point_one() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -92,7 +92,7 @@ public void should_return_response_200_when_using_http_one_point_one() [Fact] public void should_return_response_200_when_using_http_two_point_zero() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -134,7 +134,7 @@ public void should_return_response_200_when_using_http_two_point_zero() [Fact] public void should_return_response_502_when_using_http_one_to_talk_to_server_running_http_two() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -176,7 +176,7 @@ public void should_return_response_502_when_using_http_one_to_talk_to_server_run [Fact] public void should_return_response_200_when_using_http_two_to_talk_to_server_running_http_one_point_one() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs b/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs index dfc4c3486..f882868d6 100644 --- a/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs +++ b/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs @@ -25,8 +25,8 @@ public LoadBalancerTests() [Fact] public void should_load_balance_request_with_least_connection() { - var portOne = RandomPortFinder.GetRandomPort(); - var portTwo = RandomPortFinder.GetRandomPort(); + var portOne = PortFinder.GetRandomPort(); + var portTwo = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{portOne}"; var downstreamServiceTwoUrl = $"http://localhost:{portTwo}"; @@ -73,8 +73,8 @@ public void should_load_balance_request_with_least_connection() [Fact] public void should_load_balance_request_with_round_robin() { - var downstreamPortOne = RandomPortFinder.GetRandomPort(); - var downstreamPortTwo = RandomPortFinder.GetRandomPort(); + var downstreamPortOne = PortFinder.GetRandomPort(); + var downstreamPortTwo = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; @@ -120,8 +120,8 @@ public void should_load_balance_request_with_round_robin() [Fact] public void should_load_balance_request_with_custom_load_balancer() { - var downstreamPortOne = RandomPortFinder.GetRandomPort(); - var downstreamPortTwo = RandomPortFinder.GetRandomPort(); + var downstreamPortOne = PortFinder.GetRandomPort(); + var downstreamPortTwo = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; diff --git a/test/Ocelot.AcceptanceTests/LogLevelTests.cs b/test/Ocelot.AcceptanceTests/LogLevelTests.cs new file mode 100644 index 000000000..f4fb67bdc --- /dev/null +++ b/test/Ocelot.AcceptanceTests/LogLevelTests.cs @@ -0,0 +1,175 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Ocelot.Configuration.File; +using Serilog; +using Serilog.Core; + +namespace Ocelot.AcceptanceTests; + +public class LogLevelTests : IDisposable +{ + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + private readonly string _logFileName; + private readonly string _appSettingsFileName; + + private const string AppSettingsFormat = + "{{\"Logging\":{{\"LogLevel\":{{\"Default\":\"{0}\",\"System\":\"{0}\",\"Microsoft\":\"{0}\"}}}}}}"; + + public LogLevelTests() + { + _steps = new Steps(); + _serviceHandler = new ServiceHandler(); + _logFileName = $"ocelot_logs_{Guid.NewGuid()}.log"; + _appSettingsFileName = $"appsettings_{Guid.NewGuid()}.json"; + } + + private void ThenMessagesAreLogged(string[] notAllowedMessageTypes, string[] allowedMessageTypes) + { + var logFilePath = GetLogFilePath(); + var logFileContent = File.ReadAllText(logFilePath); + var logFileLines = logFileContent.Split(Environment.NewLine); + + var logFileLinesWithLogLevel = logFileLines.Where(x => notAllowedMessageTypes.Any(x.Contains)).ToList(); + logFileLinesWithLogLevel.Count.ShouldBe(0); + + var logFileLinesWithAllowedLogLevel = logFileLines.Where(x => allowedMessageTypes.Any(x.Contains)).ToList(); + logFileLinesWithAllowedLogLevel.Count.ShouldBe(2 * allowedMessageTypes.Length); + } + + private void TestFactory(string[] notAllowedMessageTypes, string[] allowedMessageTypes, LogLevel level) + { + var port = PortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + RequestIdKey = _steps.RequestIdKey, + }, + }, + }; + + var logger = GetLogger(level); + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithMinimumLogLevel(logger, _appSettingsFileName)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .Then(x => _steps.Dispose()) + .Then(x => logger.Dispose()) + .Then(x => ThenMessagesAreLogged(notAllowedMessageTypes, allowedMessageTypes)) + .BDDfy(); + } + + [Fact] + public void if_minimum_log_level_is_critical_then_only_critical_messages_are_logged() => TestFactory(new[] { "TRACE", "INFORMATION", "WARNING", "ERROR" }, new[] { "CRITICAL" }, LogLevel.Critical); + + [Fact] + public void if_minimum_log_level_is_error_then_critical_and_error_are_logged() => TestFactory(new[] { "TRACE", "INFORMATION", "WARNING", "DEBUG" }, new[] { "CRITICAL", "ERROR" }, LogLevel.Error); + + [Fact] + public void if_minimum_log_level_is_warning_then_critical_error_and_warning_are_logged() => TestFactory(new[] { "TRACE", "INFORMATION", "DEBUG" }, new[] { "CRITICAL", "ERROR", "WARNING" }, LogLevel.Warning); + + [Fact] + public void if_minimum_log_level_is_information_then_critical_error_warning_and_information_are_logged() => TestFactory(new[] { "TRACE", "DEBUG" }, new[] { "CRITICAL", "ERROR", "WARNING", "INFORMATION" }, LogLevel.Information); + + [Fact] + public void if_minimum_log_level_is_debug_then_critical_error_warning_information_and_debug_are_logged() => TestFactory(new[] { "TRACE" }, new[] { "DEBUG", "CRITICAL", "ERROR", "WARNING", "INFORMATION" }, LogLevel.Debug); + + [Fact] + public void if_minimum_log_level_is_trace_then_critical_error_warning_information_debug_and_trace_are_logged() => TestFactory(Array.Empty(), new[] { "TRACE", "DEBUG", "CRITICAL", "ERROR", "WARNING", "INFORMATION" }, LogLevel.Trace); + + private Logger GetLogger(LogLevel logLevel) + { + var logFilePath = ResetLogFile(); + UpdateAppSettings(logLevel); + var logger = logLevel switch + { + LogLevel.Information => new LoggerConfiguration().MinimumLevel.Information() + .WriteTo.File(logFilePath) + .CreateLogger(), + LogLevel.Warning => new LoggerConfiguration().MinimumLevel.Warning() + .WriteTo.File(logFilePath) + .CreateLogger(), + LogLevel.Error => new LoggerConfiguration().MinimumLevel.Error() + .WriteTo.File(logFilePath) + .CreateLogger(), + LogLevel.Critical => new LoggerConfiguration().MinimumLevel.Fatal() + .WriteTo.File(logFilePath) + .CreateLogger(), + LogLevel.Debug => new LoggerConfiguration().MinimumLevel.Debug() + .WriteTo.File(logFilePath) + .CreateLogger(), + LogLevel.Trace => new LoggerConfiguration().MinimumLevel.Verbose() + .WriteTo.File(logFilePath) + .CreateLogger(), + LogLevel.None => new LoggerConfiguration() + .WriteTo.File(logFilePath) + .CreateLogger(), + _ => throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null), + }; + return logger; + } + + private void UpdateAppSettings(LogLevel logLevel) + { + var appSettingsFilePath = Path.Combine(AppContext.BaseDirectory, _appSettingsFileName); + if (File.Exists(appSettingsFilePath)) + { + File.Delete(appSettingsFilePath); + } + + var appSettings = string.Format(AppSettingsFormat, Enum.GetName(typeof(LogLevel), logLevel)); + File.WriteAllText(appSettingsFilePath, appSettings); + } + + private string ResetLogFile() + { + var logFilePath = GetLogFilePath(); + if (File.Exists(logFilePath)) + { + File.Delete(logFilePath); + } + + return logFilePath; + } + + private string GetLogFilePath() + { + var logFilePath = Path.Combine(AppContext.BaseDirectory, _logFileName); + return logFilePath; + } + + private void GivenThereIsAServiceRunningOn(string baseUrl) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, async context => + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync(string.Empty); + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + ResetLogFile(); + GC.SuppressFinalize(this); + } +} diff --git a/test/Ocelot.AcceptanceTests/MethodTests.cs b/test/Ocelot.AcceptanceTests/MethodTests.cs index 6ea2977e4..c71fdd473 100644 --- a/test/Ocelot.AcceptanceTests/MethodTests.cs +++ b/test/Ocelot.AcceptanceTests/MethodTests.cs @@ -17,7 +17,7 @@ public MethodTests() [Fact] public void should_return_response_200_when_get_converted_to_post() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -53,7 +53,7 @@ public void should_return_response_200_when_get_converted_to_post() [Fact] public void should_return_response_200_when_get_converted_to_post_with_content() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -93,7 +93,7 @@ public void should_return_response_200_when_get_converted_to_post_with_content() [Fact] public void should_return_response_200_when_get_converted_to_get_with_content() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index 760b03bae..390879fc4 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -38,12 +38,14 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -79,6 +81,7 @@ + @@ -91,6 +94,7 @@ + @@ -103,6 +107,7 @@ + diff --git a/test/Ocelot.AcceptanceTests/OpenTracingTests.cs b/test/Ocelot.AcceptanceTests/OpenTracingTests.cs index 74468ae21..1f467afba 100644 --- a/test/Ocelot.AcceptanceTests/OpenTracingTests.cs +++ b/test/Ocelot.AcceptanceTests/OpenTracingTests.cs @@ -30,8 +30,8 @@ public OpenTracingTests(ITestOutputHelper output) [Fact] public void should_forward_tracing_information_from_ocelot_and_downstream_services() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -77,7 +77,7 @@ public void should_forward_tracing_information_from_ocelot_and_downstream_servic }, }; - var tracingPort = RandomPortFinder.GetRandomPort(); + var tracingPort = PortFinder.GetRandomPort(); var tracingUrl = $"http://localhost:{tracingPort}"; var fakeTracer = new FakeTracer(); @@ -100,7 +100,7 @@ public void should_forward_tracing_information_from_ocelot_and_downstream_servic [Fact] public void should_return_tracing_header() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -132,7 +132,7 @@ public void should_return_tracing_header() }, }; - var butterflyPort = RandomPortFinder.GetRandomPort(); + var butterflyPort = PortFinder.GetRandomPort(); var butterflyUrl = $"http://localhost:{butterflyPort}"; diff --git a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs index 9d8f3ec39..f15838169 100644 --- a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs +++ b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; using Ocelot.Configuration.File; namespace Ocelot.AcceptanceTests @@ -8,221 +9,150 @@ public class PollyQoSTests : IDisposable private readonly Steps _steps; private readonly ServiceHandler _serviceHandler; - public PollyQoSTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - - [Fact] - public void Should_not_timeout() - { - var port = RandomPortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Post" }, - QoSOptions = new FileQoSOptions - { - TimeoutValue = 1000, - ExceptionsAllowedBeforeBreaking = 10, - }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, string.Empty, 10)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void Should_timeout() - { - var port = RandomPortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Post" }, - QoSOptions = new FileQoSOptions - { - TimeoutValue = 10, - }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 201, string.Empty, 1000)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .BDDfy(); - } - - [Fact] - public void Should_open_circuit_breaker_then_close() - { - var port = RandomPortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - QoSOptions = new FileQoSOptions - { - ExceptionsAllowedBeforeBreaking = 1, - TimeoutValue = 500, - DurationOfBreak = 1000, - }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port}", "Hello from Laura")) - .Given(x => _steps.GivenThereIsAConfiguration(configuration)) - .Given(x => _steps.GivenOcelotIsRunningWithPolly()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => GivenIWaitMilliseconds(3000)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void Open_circuit_should_not_effect_different_route() - { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port1, - }, - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - QoSOptions = new FileQoSOptions - { - ExceptionsAllowedBeforeBreaking = 1, - TimeoutValue = 500, - DurationOfBreak = 1000, - }, - }, - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port2, - }, - }, - UpstreamPathTemplate = "/working", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port1}", "Hello from Laura")) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port2}/", 200, "Hello from Tom", 0)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/working")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => GivenIWaitMilliseconds(3000)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - private static void GivenIWaitMilliseconds(int ms) - { - Thread.Sleep(ms); - } + public PollyQoSTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + private static FileConfiguration FileConfigurationFactory(int port, QoSOptions options, + string httpMethod = nameof(HttpMethods.Get)) => new() + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = Uri.UriSchemeHttp, + DownstreamHostAndPorts = new() + { + new("localhost", port), + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() {httpMethod}, + QoSOptions = new FileQoSOptions(options), + }, + }, + }; + + [Fact] + public void Should_not_timeout() + { + var port = PortFinder.GetRandomPort(); + var configuration = FileConfigurationFactory(port, new QoSOptions(10, 0, 1000, null), HttpMethods.Post); + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, string.Empty, 10)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithPolly()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void Should_timeout() + { + var port = PortFinder.GetRandomPort(); + var configuration = FileConfigurationFactory(port, new QoSOptions(0, 0, 10, null), HttpMethods.Post); + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 201, string.Empty, 1000)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithPolly()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .BDDfy(); + } + + [Fact] + public void Should_open_circuit_breaker_after_two_exceptions() + { + var port = PortFinder.GetRandomPort(); + var configuration = FileConfigurationFactory(port, new QoSOptions(2, 5000, 100000, null)); + + this.Given(x => x.GivenThereIsABrokenServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithPolly()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .BDDfy(); + } + + [Fact] + public void Should_open_circuit_breaker_then_close() + { + var port = PortFinder.GetRandomPort(); + var configuration = FileConfigurationFactory(port, new QoSOptions(1, 500, 1000, null)); + + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port}", "Hello from Laura")) + .Given(x => _steps.GivenThereIsAConfiguration(configuration)) + .Given(x => _steps.GivenOcelotIsRunningWithPolly()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => GivenIWaitMilliseconds(3000)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void Open_circuit_should_not_effect_different_route() + { + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var qos1 = new QoSOptions(1, 1000, 500, null); + + var configuration = FileConfigurationFactory(port1, qos1); + var route2 = configuration.Routes[0].Clone() as FileRoute; + route2.DownstreamHostAndPorts[0].Port = port2; + route2.UpstreamPathTemplate = "/working"; + route2.QoSOptions = new(); + configuration.Routes.Add(route2); + + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port1}", "Hello from Laura")) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port2}/", 200, "Hello from Tom", 0)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithPolly()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/working")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => GivenIWaitMilliseconds(3000)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + private static void GivenIWaitMilliseconds(int ms) => Thread.Sleep(ms); + + private void GivenThereIsABrokenServiceRunningOn(string url) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + context.Response.StatusCode = 500; + await context.Response.WriteAsync("this is an exception"); + }); + } private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string responseBody) { @@ -240,21 +170,21 @@ private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string resp }); } - private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, int timeout) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - Thread.Sleep(timeout); - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, int timeout) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + Thread.Sleep(timeout); + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); _steps.Dispose(); - GC.SuppressFinalize(this); - } - } + GC.SuppressFinalize(this); + } + } } diff --git a/test/Ocelot.AcceptanceTests/RandomPortFinder.cs b/test/Ocelot.AcceptanceTests/RandomPortFinder.cs deleted file mode 100644 index 24fdce68c..000000000 --- a/test/Ocelot.AcceptanceTests/RandomPortFinder.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Concurrent; -using System.Net.Sockets; - -namespace Ocelot.AcceptanceTests -{ - public static class RandomPortFinder - { - private const int EndPortRange = 45000; - private static int _currentPort = 20000; - private static readonly object LockObj = new(); - private static readonly ConcurrentBag UsedPorts = new(); - - public static int GetRandomPort() - { - lock (LockObj) - { - if (_currentPort > EndPortRange) - { - throw new Exception("Cannot find available port to bind to."); - } - - var port = UsePort(_currentPort); - _currentPort += 1; - return port; - } - } - - private static int UsePort(int randomPort) - { - UsedPorts.Add(randomPort); - - var ipe = new IPEndPoint(IPAddress.Loopback, randomPort); - - using var socket = new Socket(ipe.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - socket.Bind(ipe); - socket.Close(); - return randomPort; - } - } -} diff --git a/test/Ocelot.AcceptanceTests/ReasonPhraseTests.cs b/test/Ocelot.AcceptanceTests/ReasonPhraseTests.cs index 20f537777..7bbae6c3e 100644 --- a/test/Ocelot.AcceptanceTests/ReasonPhraseTests.cs +++ b/test/Ocelot.AcceptanceTests/ReasonPhraseTests.cs @@ -18,7 +18,7 @@ public ReasonPhraseTests() [Fact] public void should_return_reason_phrase() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/RequestIdTests.cs b/test/Ocelot.AcceptanceTests/RequestIdTests.cs index e1de4f313..d5e291807 100644 --- a/test/Ocelot.AcceptanceTests/RequestIdTests.cs +++ b/test/Ocelot.AcceptanceTests/RequestIdTests.cs @@ -16,7 +16,7 @@ public RequestIdTests() [Fact] public void should_use_default_request_id_and_forward() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -52,7 +52,7 @@ public void should_use_default_request_id_and_forward() [Fact] public void should_use_request_id_and_forward() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -89,7 +89,7 @@ public void should_use_request_id_and_forward() [Fact] public void should_use_global_request_id_and_forward() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -130,7 +130,7 @@ public void should_use_global_request_id_and_forward() [Fact] public void should_use_global_request_id_create_and_forward() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs b/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs index 58964fe0f..c72c1fb62 100644 --- a/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs +++ b/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs @@ -16,7 +16,7 @@ public ResponseCodeTests() [Fact] public void ShouldReturnResponse304WhenServiceReturns304() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs index 926a0e36f..454b41455 100644 --- a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs +++ b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs @@ -14,7 +14,7 @@ public ReturnsErrorTests() } [Fact] - public void should_return_bad_gateway_error_if_downstream_service_doesnt_respond() + public void Should_return_bad_gateway_error_if_downstream_service_doesnt_respond() { var configuration = new FileConfiguration { @@ -46,9 +46,9 @@ public void should_return_bad_gateway_error_if_downstream_service_doesnt_respond } [Fact] - public void should_return_internal_server_error_if_downstream_service_returns_internal_server_error() + public void Should_return_internal_server_error_if_downstream_service_returns_internal_server_error() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -81,9 +81,9 @@ public void should_return_internal_server_error_if_downstream_service_returns_in } [Fact] - public void should_log_warning_if_downstream_service_returns_internal_server_error() + public void Should_log_warning_if_downstream_service_returns_internal_server_error() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -111,7 +111,7 @@ public void should_log_warning_if_downstream_service_returns_internal_server_err .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningWithLogger()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenWarningShouldBeLogged()) + .Then(x => _steps.ThenWarningShouldBeLogged(2)) .BDDfy(); } @@ -123,7 +123,8 @@ private void GivenThereIsAServiceRunningOn(string url) public void Dispose() { _serviceHandler?.Dispose(); - _steps.Dispose(); + _steps.Dispose(); + GC.SuppressFinalize(this); } } } diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/RoutingTests.cs index 7b808ad0a..243d973d8 100644 --- a/test/Ocelot.AcceptanceTests/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingTests.cs @@ -18,7 +18,7 @@ public RoutingTests() [Fact] public void should_not_match_forward_slash_in_pattern_before_next_forward_slash() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -63,7 +63,7 @@ public void should_return_response_404_when_no_configuration_at_all() [Fact] public void should_return_response_200_with_forward_slash_and_placeholder_only() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -99,7 +99,7 @@ public void should_return_response_200_with_forward_slash_and_placeholder_only() [Fact] public void should_return_response_200_favouring_forward_slash_with_path_route() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -150,7 +150,7 @@ public void should_return_response_200_favouring_forward_slash_with_path_route() [Fact] public void should_return_response_200_favouring_forward_slash() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -200,7 +200,7 @@ public void should_return_response_200_favouring_forward_slash() [Fact] public void should_return_response_200_favouring_forward_slash_route_because_it_is_first() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -251,7 +251,7 @@ public void should_return_response_200_favouring_forward_slash_route_because_it_ [Fact] public void should_return_response_200_with_nothing_and_placeholder_only() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -287,7 +287,7 @@ public void should_return_response_200_with_nothing_and_placeholder_only() [Fact] public void should_return_response_200_with_simple_url() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -323,7 +323,7 @@ public void should_return_response_200_with_simple_url() [Fact] public void Bug() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -376,7 +376,7 @@ public void Bug() [Fact] public void should_return_response_200_when_path_missing_forward_slash_as_first_char() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -412,7 +412,7 @@ public void should_return_response_200_when_path_missing_forward_slash_as_first_ [Fact] public void should_return_response_200_when_host_has_trailing_slash() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -448,7 +448,7 @@ public void should_return_response_200_when_host_has_trailing_slash() [Fact] public void should_return_ok_when_upstream_url_ends_with_forward_slash_but_template_does_not() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -484,7 +484,7 @@ public void should_return_ok_when_upstream_url_ends_with_forward_slash_but_templ [Fact] public void should_return_not_found_when_upstream_url_ends_with_forward_slash_but_template_does_not() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -519,7 +519,7 @@ public void should_return_not_found_when_upstream_url_ends_with_forward_slash_bu [Fact] public void should_return_not_found() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -554,7 +554,7 @@ public void should_return_not_found() [Fact] public void should_return_response_200_with_complex_url() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -590,7 +590,7 @@ public void should_return_response_200_with_complex_url() [Fact] public void should_return_response_200_with_complex_url_that_starts_with_placeholder() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -626,7 +626,7 @@ public void should_return_response_200_with_complex_url_that_starts_with_placeho [Fact] public void should_not_add_trailing_slash_to_downstream_url() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -661,7 +661,7 @@ public void should_not_add_trailing_slash_to_downstream_url() [Fact] public void should_return_response_201_with_simple_url() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -697,7 +697,7 @@ public void should_return_response_201_with_simple_url() [Fact] public void should_return_response_201_with_complex_query_string() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -733,7 +733,7 @@ public void should_return_response_201_with_complex_query_string() [Fact] public void should_return_response_200_with_placeholder_for_final_url_path() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -769,7 +769,7 @@ public void should_return_response_200_with_placeholder_for_final_url_path() [Fact] public void should_return_response_201_with_simple_url_and_multiple_upstream_http_method() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -805,7 +805,7 @@ public void should_return_response_201_with_simple_url_and_multiple_upstream_htt [Fact] public void should_return_response_200_with_simple_url_and_any_upstream_http_method() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -841,7 +841,7 @@ public void should_return_response_200_with_simple_url_and_any_upstream_http_met [Fact] public void should_return_404_when_calling_upstream_route_with_no_matching_downstream_re_route_github_issue_134() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -893,7 +893,7 @@ public void should_return_404_when_calling_upstream_route_with_no_matching_downs [Fact] public void should_not_set_trailing_slash_on_url_template() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -930,7 +930,7 @@ public void should_not_set_trailing_slash_on_url_template() [Fact] public void should_use_priority() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -982,7 +982,7 @@ public void should_use_priority() [Fact] public void should_match_multiple_paths_with_catch_all() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -1018,7 +1018,7 @@ public void should_match_multiple_paths_with_catch_all() [Fact] public void should_fix_issue_271() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs b/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs index 9fdff5378..5c34167ac 100644 --- a/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; - + namespace Ocelot.AcceptanceTests { public class RoutingWithQueryStringTests : IDisposable @@ -15,11 +15,11 @@ public RoutingWithQueryStringTests() } [Fact] - public void should_return_response_200_with_query_string_template() + public void Should_return_response_200_with_query_string_template() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -43,7 +43,7 @@ public void should_return_response_200_with_query_string_template() }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}", 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}", "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/{unitId}/updates")) @@ -51,13 +51,86 @@ public void should_return_response_200_with_query_string_template() .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } + + [Theory(DisplayName = "1182: " + nameof(Should_return_200_with_query_string_template_different_keys))] + [InlineData("")] + [InlineData("&x=xxx")] + public void Should_return_200_with_query_string_template_different_keys(string additionalParams) + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unit}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/api/units/{subscriptionId}/updates?unit={unit}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}{additionalParams}", "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/updates?unit={unitId}{additionalParams}")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Theory(DisplayName = "1174: " + nameof(Should_return_200_and_forward_query_parameters_without_duplicates))] + [InlineData("projectNumber=45&startDate=2019-12-12&endDate=2019-12-12", "endDate=2019-12-12&projectNumber=45&startDate=2019-12-12")] + [InlineData("$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z", "$filter=ProjectNumber%20eq%2045%20and%20DateOfSale%20ge%202020-03-01T00:00:00z%20and%20DateOfSale%20le%202020-03-15T00:00:00z")] + public void Should_return_200_and_forward_query_parameters_without_duplicates(string everythingelse, string expectedOrdered) + { + var port = PortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/contracts?{everythingelse}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() { Host = "localhost", Port = port }, + }, + UpstreamPathTemplate = "/contracts?{everythingelse}", + UpstreamHttpMethod = new() { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/contracts", $"?{expectedOrdered}", "Hello from @sunilk3")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/contracts?{everythingelse}")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from @sunilk3")) + .BDDfy(); + } + [Fact] - public void should_return_response_200_with_odata_query_string() + public void Should_return_response_200_with_odata_query_string() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -81,7 +154,7 @@ public void should_return_response_200_with_odata_query_string() }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/odata/customers", "?$filter=Name%20eq%20'Sam'", 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/odata/customers", "?$filter=Name%20eq%20'Sam'", "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/odata/customers?$filter=Name eq 'Sam' ")) @@ -91,11 +164,11 @@ public void should_return_response_200_with_odata_query_string() } [Fact] - public void should_return_response_200_with_query_string_upstream_template() + public void Should_return_response_200_with_query_string_upstream_template() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -119,7 +192,7 @@ public void should_return_response_200_with_query_string_upstream_template() }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}")) @@ -129,11 +202,11 @@ public void should_return_response_200_with_query_string_upstream_template() } [Fact] - public void should_return_response_404_with_query_string_upstream_template_no_query_string() + public void Should_return_response_404_with_query_string_upstream_template_no_query_string() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -157,7 +230,7 @@ public void should_return_response_404_with_query_string_upstream_template_no_qu }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates")) @@ -166,11 +239,11 @@ public void should_return_response_404_with_query_string_upstream_template_no_qu } [Fact] - public void should_return_response_404_with_query_string_upstream_template_different_query_string() + public void Should_return_response_404_with_query_string_upstream_template_different_query_string() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -194,7 +267,7 @@ public void should_return_response_404_with_query_string_upstream_template_diffe }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?test=1")) @@ -203,11 +276,11 @@ public void should_return_response_404_with_query_string_upstream_template_diffe } [Fact] - public void should_return_response_200_with_query_string_upstream_template_multiple_params() + public void Should_return_response_200_with_query_string_upstream_template_multiple_params() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -231,24 +304,26 @@ public void should_return_response_200_with_query_string_upstream_template_multi }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", "?productId=1", 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", "?productId=1", "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}&productId=1")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); - } - - // to reproduce 1288: query string should contain the placeholder name and value - [Fact] - public void should_copy_query_string_to_downstream_path_issue_1288() + } + + /// + /// To reproduce 1288: query string should contain the placeholder name and value. + /// + [Fact(DisplayName = "1288: " + nameof(Should_copy_query_string_to_downstream_path))] + public void Should_copy_query_string_to_downstream_path() { var idName = "id"; var idValue = "3"; var queryName = idName + "1"; var queryValue = "2" + idValue + "12"; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -260,11 +335,7 @@ public void should_copy_query_string_to_downstream_path_issue_1288() DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort - { - Host = "localhost", - Port = port, - }, + new() { Host = "localhost", Port = port }, }, UpstreamPathTemplate = $"/safe/{{{idName}}}", UpstreamHttpMethod = new List { "Get" }, @@ -272,7 +343,7 @@ public void should_copy_query_string_to_downstream_path_issue_1288() }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/cpx/t1/{idValue}", $"?{queryName}={queryValue}", 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/cpx/t1/{idValue}", $"?{queryName}={queryValue}", "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/safe/{idValue}?{queryName}={queryValue}")) @@ -281,18 +352,18 @@ public void should_copy_query_string_to_downstream_path_issue_1288() .BDDfy(); } - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string queryString, int statusCode, string responseBody) + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string queryString, string responseBody) { _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => { if ((context.Request.PathBase.Value != basePath) || context.Request.QueryString.Value != queryString) { - context.Response.StatusCode = 500; + context.Response.StatusCode = StatusCodes.Status500InternalServerError; await context.Response.WriteAsync("downstream path didnt match base path"); } else { - context.Response.StatusCode = statusCode; + context.Response.StatusCode = StatusCodes.Status200OK; await context.Response.WriteAsync(responseBody); } }); @@ -301,7 +372,8 @@ private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, stri public void Dispose() { _serviceHandler?.Dispose(); - _steps.Dispose(); + _steps.Dispose(); + GC.SuppressFinalize(this); } } } diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs index c9f093bf0..79e0dd505 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs @@ -26,9 +26,9 @@ public ServiceDiscoveryTests() [Fact] public void should_use_consul_service_discovery_and_load_balance_request() { - var consulPort = RandomPortFinder.GetRandomPort(); - var servicePort1 = RandomPortFinder.GetRandomPort(); - var servicePort2 = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); + var servicePort1 = PortFinder.GetRandomPort(); + var servicePort2 = PortFinder.GetRandomPort(); var serviceName = "product"; var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; @@ -96,8 +96,8 @@ public void should_use_consul_service_discovery_and_load_balance_request() [Fact] public void should_handle_request_to_consul_for_downstream_service_and_make_request() { - var consulPort = RandomPortFinder.GetRandomPort(); - var servicePort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); const string serviceName = "web"; var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; @@ -152,9 +152,9 @@ public void should_handle_request_to_consul_for_downstream_service_and_make_requ [Fact] public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes() { - var consulPort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); const string serviceName = "web"; - var downstreamServicePort = RandomPortFinder.GetRandomPort(); + var downstreamServicePort = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; var serviceEntryOne = new ServiceEntry @@ -203,10 +203,10 @@ public void should_handle_request_to_consul_for_downstream_service_and_make_requ [Fact] public void should_use_consul_service_discovery_and_load_balance_request_no_re_routes() { - var consulPort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); var serviceName = "product"; - var serviceOnePort = RandomPortFinder.GetRandomPort(); - var serviceTwoPort = RandomPortFinder.GetRandomPort(); + var serviceOnePort = PortFinder.GetRandomPort(); + var serviceTwoPort = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{serviceOnePort}"; var downstreamServiceTwoUrl = $"http://localhost:{serviceTwoPort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; @@ -264,9 +264,9 @@ public void should_use_consul_service_discovery_and_load_balance_request_no_re_r public void should_use_token_to_make_request_to_consul() { var token = "abctoken"; - var consulPort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); var serviceName = "web"; - var servicePort = RandomPortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; var serviceEntryOne = new ServiceEntry @@ -322,10 +322,10 @@ public void should_use_token_to_make_request_to_consul() [Fact] public void should_send_request_to_service_after_it_becomes_available_in_consul() { - var consulPort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); var serviceName = "product"; - var servicePort1 = RandomPortFinder.GetRandomPort(); - var servicePort2 = RandomPortFinder.GetRandomPort(); + var servicePort1 = PortFinder.GetRandomPort(); + var servicePort2 = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; @@ -401,9 +401,9 @@ public void should_send_request_to_service_after_it_becomes_available_in_consul( [Fact] public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request() { - var consulPort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); const string serviceName = "web"; - var downstreamServicePort = RandomPortFinder.GetRandomPort(); + var downstreamServicePort = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; var serviceEntryOne = new ServiceEntry diff --git a/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs b/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs index 69361d8d7..87d511a33 100644 --- a/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs @@ -18,7 +18,7 @@ public ServiceFabricTests() [Fact] public void should_fix_issue_555() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -56,7 +56,7 @@ public void should_fix_issue_555() [Fact] public void should_support_service_fabric_naming_and_dns_service_stateless_and_guest() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -94,7 +94,7 @@ public void should_support_service_fabric_naming_and_dns_service_stateless_and_g [Fact] public void should_support_service_fabric_naming_and_dns_service_statefull_and_actors() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -132,7 +132,7 @@ public void should_support_service_fabric_naming_and_dns_service_statefull_and_a [Fact] public void should_support_placeholder_in_service_fabric_service_name() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/SslTests.cs b/test/Ocelot.AcceptanceTests/SslTests.cs index f37f1b430..649285f12 100644 --- a/test/Ocelot.AcceptanceTests/SslTests.cs +++ b/test/Ocelot.AcceptanceTests/SslTests.cs @@ -18,7 +18,7 @@ public SslTests() [Fact] public void should_dangerous_accept_any_server_certificate_validator() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -55,7 +55,7 @@ public void should_dangerous_accept_any_server_certificate_validator() [Fact] public void should_not_dangerous_accept_any_server_certificate_validator() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/StartupTests.cs b/test/Ocelot.AcceptanceTests/StartupTests.cs index afd10e3cc..04672354e 100644 --- a/test/Ocelot.AcceptanceTests/StartupTests.cs +++ b/test/Ocelot.AcceptanceTests/StartupTests.cs @@ -20,7 +20,7 @@ public StartupTests() [Fact] public void should_not_try_and_write_to_disk_on_startup_when_not_using_admin_api() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 0686dd036..d87391b32 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -26,6 +26,8 @@ using Ocelot.ServiceDiscovery.Providers; using Ocelot.Tracing.Butterfly; using Ocelot.Tracing.OpenTracing; +using Serilog; +using Serilog.Core; using System.IO.Compression; using System.Net.Http.Headers; using System.Text; @@ -34,30 +36,43 @@ using CookieHeaderValue = Microsoft.Net.Http.Headers.CookieHeaderValue; using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue; -namespace Ocelot.AcceptanceTests +namespace Ocelot.AcceptanceTests; + +public class Steps : IDisposable { - public class Steps : IDisposable - { - private TestServer _ocelotServer; - private HttpClient _ocelotClient; - private HttpResponseMessage _response; - private HttpContent _postContent; - private BearerToken _token; - public string RequestIdKey = "OcRequestId"; - private readonly Random _random; - private readonly string _ocelotConfigFileName; - private IWebHostBuilder _webHostBuilder; - private WebHostBuilder _ocelotBuilder; - private IWebHost _ocelotHost; - private IOcelotConfigurationChangeTokenSource _changeToken; - - public Steps() - { - _random = new(); - _ocelotConfigFileName = $"{Guid.NewGuid():N}-ocelot.json"; - } + private TestServer _ocelotServer; + private HttpClient _ocelotClient; + private HttpResponseMessage _response; + private HttpContent _postContent; + private BearerToken _token; + public string RequestIdKey = "OcRequestId"; + private readonly Random _random; + private readonly string _ocelotConfigFileName; + private IWebHostBuilder _webHostBuilder; + private WebHostBuilder _ocelotBuilder; + private IWebHost _ocelotHost; + private IOcelotConfigurationChangeTokenSource _changeToken; + + public Steps() + { + _random = new Random(); + _ocelotConfigFileName = $"{Guid.NewGuid():N}-ocelot.json"; + } + + public async Task ThenConfigShouldBe(FileConfiguration fileConfig) + { + var internalConfigCreator = _ocelotServer.Host.Services.GetService(); + var internalConfigRepo = _ocelotServer.Host.Services.GetService(); + + var internalConfig = internalConfigRepo.Get(); + var config = await internalConfigCreator.Create(fileConfig); - public async Task ThenConfigShouldBe(FileConfiguration fileConfig) + internalConfig.Data.RequestId.ShouldBe(config.Data.RequestId); + } + + public async Task ThenConfigShouldBeWithTimeout(FileConfiguration fileConfig, int timeoutMs) + { + var result = await Wait.WaitFor(timeoutMs).Until(async () => { var internalConfigCreator = _ocelotServer.Host.Services.GetService(); var internalConfigRepo = _ocelotServer.Host.Services.GetService(); @@ -65,1242 +80,1184 @@ public async Task ThenConfigShouldBe(FileConfiguration fileConfig) var internalConfig = internalConfigRepo.Get(); var config = await internalConfigCreator.Create(fileConfig); - internalConfig.Data.RequestId.ShouldBe(config.Data.RequestId); - } - - public async Task ThenConfigShouldBeWithTimeout(FileConfiguration fileConfig, int timeoutMs) - { - var result = await Wait.WaitFor(timeoutMs).Until(async () => - { - var internalConfigCreator = _ocelotServer.Host.Services.GetService(); - var internalConfigRepo = _ocelotServer.Host.Services.GetService(); - - var internalConfig = internalConfigRepo.Get(); - var config = await internalConfigCreator.Create(fileConfig); + return internalConfig.Data.RequestId == config.Data.RequestId; + }); - return internalConfig.Data.RequestId == config.Data.RequestId; - }); - - result.ShouldBe(true); - } + result.ShouldBe(true); + } - public async Task StartFakeOcelotWithWebSockets() - { - _ocelotBuilder = new WebHostBuilder(); - _ocelotBuilder.ConfigureServices(s => + 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) => { - 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: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - 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 async Task StartFakeOcelotWithWebSocketsWithConsul() - { - _ocelotBuilder = new WebHostBuilder(); - _ocelotBuilder.ConfigureServices(s => + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, logging) => { - s.AddSingleton(_ocelotBuilder); - s.AddOcelot().AddConsul(); - }); - _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: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - 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 jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); - File.WriteAllText(_ocelotConfigFileName, jsonConfiguration); - } - - private void DeleteOcelotConfig() - { - if (!File.Exists(_ocelotConfigFileName)) + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + logging.AddConsole(); + }) + .Configure(app => { - return; - } + app.UseWebSockets(); + app.UseOcelot().Wait(); + }) + .UseIISIntegration(); + _ocelotHost = _ocelotBuilder.Build(); + await _ocelotHost.StartAsync(); + } - try + public async Task StartFakeOcelotWithWebSocketsWithConsul() + { + _ocelotBuilder = new WebHostBuilder(); + _ocelotBuilder.ConfigureServices(s => + { + s.AddSingleton(_ocelotBuilder); + s.AddOcelot().AddConsul(); + }); + _ocelotBuilder.UseKestrel() + .UseUrls("http://localhost:5000") + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => { - File.Delete(_ocelotConfigFileName); - } - catch (Exception e) + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, logging) => { - Console.WriteLine(e); - } - } + 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 jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); + File.WriteAllText(_ocelotConfigFileName, jsonConfiguration); + } - public void ThenTheResponseBodyHeaderIs(string key, string value) + private void DeleteOcelotConfig() + { + if (!File.Exists(_ocelotConfigFileName)) { - var header = _response.Content.Headers.GetValues(key); - header.First().ShouldBe(value); + return; } - public void GivenOcelotIsRunningReloadingConfig(bool shouldReload) + try { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: false, reloadOnChange: shouldReload); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); + File.Delete(_ocelotConfigFileName); } - - public void GivenIHaveAChangeToken() + catch (Exception e) { - _changeToken = _ocelotServer.Host.Services.GetRequiredService(); + Console.WriteLine(e); } + } - /// - /// 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(); + public void ThenTheResponseBodyHeaderIs(string key, string value) + { + var header = _response.Content.Headers.GetValues(key); + header.First().ShouldBe(value); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + public void GivenOcelotIsRunningReloadingConfig(bool shouldReload) + { + _webHostBuilder = new WebHostBuilder(); - _ocelotServer = new TestServer(_webHostBuilder); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, shouldReload); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => { s.AddOcelot(); }) + .Configure(app => { app.UseOcelot().Wait(); }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } - _ocelotClient = _ocelotServer.CreateClient(); - } + public void GivenIHaveAChangeToken() + { + _changeToken = _ocelotServer.Host.Services.GetRequiredService(); + } - /// - /// 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. - /// - /// The type. - /// The delegate object to load balancer factory. - public void GivenOcelotIsRunningWithCustomLoadBalancer(Func loadBalancerFactoryFunc) - where T : ILoadBalancer - { - _webHostBuilder = new WebHostBuilder(); + /// + /// 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: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddCustomLoadBalancer(loadBalancerFactoryFunc); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => { s.AddOcelot(); }) + .Configure(app => { app.UseOcelot().Wait(); }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } - _ocelotServer = new TestServer(_webHostBuilder); + /// + /// 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. + /// + /// The type. + /// The delegate object to load balancer factory. + public void GivenOcelotIsRunningWithCustomLoadBalancer( + Func loadBalancerFactoryFunc) + where T : ILoadBalancer + { + _webHostBuilder = new WebHostBuilder(); - _ocelotClient = _ocelotServer.CreateClient(); - } + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot() + .AddCustomLoadBalancer(loadBalancerFactoryFunc); + }) + .Configure(app => { app.UseOcelot().Wait(); }); - public void GivenOcelotIsRunningWithConsul() - { - _webHostBuilder = new WebHostBuilder(); + _ocelotServer = new TestServer(_webHostBuilder); - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot().AddConsul(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + _ocelotClient = _ocelotServer.CreateClient(); + } - _ocelotServer = new TestServer(_webHostBuilder); + public void GivenOcelotIsRunningWithConsul() + { + _webHostBuilder = new WebHostBuilder(); - _ocelotClient = _ocelotServer.CreateClient(); - } + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => { s.AddOcelot().AddConsul(); }) + .Configure(app => { app.UseOcelot().Wait(); }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } - public void ThenTheTraceHeaderIsSet(string key) - { - var header = _response.Headers.GetValues(key); - header.First().ShouldNotBeNullOrEmpty(); - } + public void ThenTheTraceHeaderIsSet(string key) + { + var header = _response.Headers.GetValues(key); + header.First().ShouldNotBeNullOrEmpty(); + } - internal void GivenOcelotIsRunningUsingButterfly(string butterflyUrl) - { - _webHostBuilder = new WebHostBuilder(); + 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: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot() .AddButterfly(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(); - }); + }) + .Configure(app => + { + app.Use(async (_, next) => { await next.Invoke(); }); + app.UseOcelot().Wait(); + }); - _ocelotServer = new TestServer(_webHostBuilder); + _ocelotServer = new TestServer(_webHostBuilder); - _ocelotClient = _ocelotServer.CreateClient(); - } + _ocelotClient = _ocelotServer.CreateClient(); + } - public void GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache() - { - _webHostBuilder = new WebHostBuilder(); + 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: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddCacheManager((x) => - { - x.WithMicrosoftLogging(log => - { - //log.AddConsole(LogLevel.Debug); - }) - .WithJsonSerializer() - .WithHandle(typeof(InMemoryJsonHandle<>)); - }) - .AddConsul() - .AddConfigStoredInConsul(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot() + .AddCacheManager((x) => + { + x.WithMicrosoftLogging(_ => + { + //log.AddConsole(LogLevel.Debug); + }) + .WithJsonSerializer() + .WithHandle(typeof(InMemoryJsonHandle<>)); + }) + .AddConsul() + .AddConfigStoredInConsul(); + }) + .Configure(app => { app.UseOcelot().Wait(); }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } - _ocelotServer = new TestServer(_webHostBuilder); + public void GivenOcelotIsRunningUsingConsulToStoreConfig() + { + _webHostBuilder = new WebHostBuilder(); - _ocelotClient = _ocelotServer.CreateClient(); - } + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => { s.AddOcelot().AddConsul().AddConfigStoredInConsul(); }) + .Configure(app => { app.UseOcelot().Wait(); }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + Thread.Sleep(1000); + } - public void GivenOcelotIsRunningUsingConsulToStoreConfig() + public void WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk(string url) + { + var result = Wait.WaitFor(2000).Until(() => { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot().AddConsul().AddConfigStoredInConsul(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + try + { + _response = _ocelotClient.GetAsync(url).Result; + _response.EnsureSuccessStatusCode(); + return true; + } + catch (Exception) + { + return false; + } + }); - _ocelotServer = new TestServer(_webHostBuilder); + result.ShouldBeTrue(); + } - _ocelotClient = _ocelotServer.CreateClient(); - Thread.Sleep(1000); - } + public void GivenOcelotIsRunningUsingJsonSerializedCache() + { + _webHostBuilder = new WebHostBuilder(); - public void WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk(string url) - { - var result = Wait.WaitFor(2000).Until(() => + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => { - try - { - _response = _ocelotClient.GetAsync(url).Result; - _response.EnsureSuccessStatusCode(); - return true; - } - catch (Exception) - { - return false; - } - }); + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot() + .AddCacheManager((x) => + { + x.WithMicrosoftLogging(_ => + { + //log.AddConsole(LogLevel.Debug); + }) + .WithJsonSerializer() + .WithHandle(typeof(InMemoryJsonHandle<>)); + }); + }) + .Configure(app => { app.UseOcelot().Wait(); }); - result.ShouldBeTrue(); - } + _ocelotServer = new TestServer(_webHostBuilder); - public void GivenOcelotIsRunningUsingJsonSerializedCache() - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddCacheManager((x) => - { - x.WithMicrosoftLogging(log => - { - //log.AddConsole(LogLevel.Debug); - }) - .WithJsonSerializer() - .WithHandle(typeof(InMemoryJsonHandle<>)); - }); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + public void GivenOcelotIsRunningWithFakeHttpClientCache(IHttpClientCache cache) + { + _webHostBuilder = new WebHostBuilder(); - _ocelotServer = new TestServer(_webHostBuilder); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(cache); + s.AddOcelot(); + }) + .Configure(app => { app.UseOcelot().Wait(); }); - _ocelotClient = _ocelotServer.CreateClient(); - } + _ocelotServer = new TestServer(_webHostBuilder); - public void GivenOcelotIsRunningWithFakeHttpClientCache(IHttpClientCache cache) - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(cache); - s.AddOcelot(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + internal void GivenIWait(int wait) + { + Thread.Sleep(wait); + } - _ocelotServer = new TestServer(_webHostBuilder); + public void GivenOcelotIsRunningWithMiddlewareBeforePipeline(Func callback) + { + _webHostBuilder = new WebHostBuilder(); - _ocelotClient = _ocelotServer.CreateClient(); - } + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => { s.AddOcelot(); }) + .Configure(app => + { + app.UseMiddleware(callback); + app.UseOcelot().Wait(); + }); - internal void GivenIWait(int wait) - { - Thread.Sleep(wait); - } + _ocelotServer = new TestServer(_webHostBuilder); - public void GivenOcelotIsRunningWithMiddleareBeforePipeline(Func callback) - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - }) - .Configure(app => - { - app.UseMiddleware(callback); - app.UseOcelot().Wait(); - }); + public void GivenOcelotIsRunningWithSpecificHandlersRegisteredInDi() + where TOne : DelegatingHandler + where TWo : DelegatingHandler + { + _webHostBuilder = new WebHostBuilder(); - _ocelotServer = new TestServer(_webHostBuilder); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddOcelot() + .AddDelegatingHandler() + .AddDelegatingHandler(); + }) + .Configure(a => { a.UseOcelot().Wait(); }); - _ocelotClient = _ocelotServer.CreateClient(); - } + _ocelotServer = new TestServer(_webHostBuilder); - public void GivenOcelotIsRunningWithSpecficHandlersRegisteredInDi() - where TOne : DelegatingHandler - where TWo : DelegatingHandler - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddOcelot() - .AddDelegatingHandler() - .AddDelegatingHandler(); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - }); + public void GivenOcelotIsRunningWithSpecificAggregatorsRegisteredInDi() + where TAggregator : class, IDefinedAggregator + where TDependency : class + { + _webHostBuilder = new WebHostBuilder(); - _ocelotServer = new TestServer(_webHostBuilder); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddSingleton(); + s.AddOcelot() + .AddSingletonDefinedAggregator(); + }) + .Configure(a => { a.UseOcelot().Wait(); }); - _ocelotClient = _ocelotServer.CreateClient(); - } + _ocelotServer = new TestServer(_webHostBuilder); - public void GivenOcelotIsRunningWithSpecficAggregatorsRegisteredInDi() - where TAggregator : class, IDefinedAggregator - where TDepedency : class - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddSingleton(); - s.AddOcelot() - .AddSingletonDefinedAggregator(); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - }); + public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi() + where TOne : DelegatingHandler + where TWo : DelegatingHandler + { + _webHostBuilder = new WebHostBuilder(); - _ocelotServer = new TestServer(_webHostBuilder); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddOcelot() + .AddDelegatingHandler(true) + .AddDelegatingHandler(true); + }) + .Configure(a => { a.UseOcelot().Wait(); }); - _ocelotClient = _ocelotServer.CreateClient(); - } + _ocelotServer = new TestServer(_webHostBuilder); - public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi() - where TOne : DelegatingHandler - where TWo : DelegatingHandler - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddOcelot() - .AddDelegatingHandler(true) - .AddDelegatingHandler(true); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - }); + public void GivenOcelotIsRunningWithGlobalHandlerRegisteredInDi() + where TOne : DelegatingHandler + { + _webHostBuilder = new WebHostBuilder(); - _ocelotServer = new TestServer(_webHostBuilder); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddOcelot() + .AddDelegatingHandler(true); + }) + .Configure(a => { a.UseOcelot().Wait(); }); - _ocelotClient = _ocelotServer.CreateClient(); - } + _ocelotServer = new TestServer(_webHostBuilder); - public void GivenOcelotIsRunningWithGlobalHandlerRegisteredInDi() - where TOne : DelegatingHandler - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddOcelot() - .AddDelegatingHandler(true); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - }); + public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi(FakeDependency dependency) + where TOne : DelegatingHandler + { + _webHostBuilder = new WebHostBuilder(); - _ocelotServer = new TestServer(_webHostBuilder); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddSingleton(dependency); + s.AddOcelot() + .AddDelegatingHandler(true); + }) + .Configure(a => { a.UseOcelot().Wait(); }); - _ocelotClient = _ocelotServer.CreateClient(); - } + _ocelotServer = new TestServer(_webHostBuilder); - public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi(FakeDependency dependency) - where TOne : DelegatingHandler - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddSingleton(dependency); - s.AddOcelot() - .AddDelegatingHandler(true); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - }); + internal void GivenIAddCookieToMyRequest(string cookie) + { + _ocelotClient.DefaultRequestHeaders.Add("Set-Cookie", cookie); + } - _ocelotServer = new TestServer(_webHostBuilder); + /// + /// 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(); - _ocelotClient = _ocelotServer.CreateClient(); - } + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot(); + s.AddAuthentication() + .AddIdentityServerAuthentication(authenticationProviderKey, options); + }) + .Configure(app => { app.UseOcelot().Wait(); }); - internal void GivenIAddCookieToMyRequest(string cookie) - { - _ocelotClient.DefaultRequestHeaders.Add("Set-Cookie", cookie); - } + _ocelotServer = new TestServer(_webHostBuilder); - /// - /// 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(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - s.AddAuthentication() - .AddIdentityServerAuthentication(authenticationProviderKey, options); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + public void ThenTheResponseHeaderIs(string key, string value) + { + var header = _response.Headers.GetValues(key); + header.First().ShouldBe(value); + } - _ocelotServer = new TestServer(_webHostBuilder); + public void ThenTheReasonPhraseIs(string expected) + { + _response.ReasonPhrase.ShouldBe(expected); + } - _ocelotClient = _ocelotServer.CreateClient(); - } + /// + /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. + /// + public void GivenOcelotIsRunning(OcelotPipelineConfiguration ocelotPipelineConfig) + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", true, false) + .AddJsonFile(_ocelotConfigFileName, false, false) + .AddEnvironmentVariables(); + + var configuration = builder.Build(); + _webHostBuilder = new WebHostBuilder(); + _webHostBuilder.ConfigureServices(s => { s.AddSingleton(_webHostBuilder); }); + + _ocelotServer = new TestServer(_webHostBuilder + .UseConfiguration(configuration) + .ConfigureServices(s => { s.AddOcelot(configuration); }) + .ConfigureLogging(l => + { + l.AddConsole(); + l.AddDebug(); + }) + .Configure(a => { a.UseOcelot(ocelotPipelineConfig).Wait(); })); - public void ThenTheResponseHeaderIs(string key, string value) - { - var header = _response.Headers.GetValues(key); - header.First().ShouldBe(value); - } + _ocelotClient = _ocelotServer.CreateClient(); + } - public void ThenTheReasonPhraseIs(string expected) - { - _response.ReasonPhrase.ShouldBe(expected); - } + public void GivenIHaveAddedATokenToMyRequest() + { + _ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + } - /// - /// 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: false) - .AddJsonFile(_ocelotConfigFileName, false, false) - .AddEnvironmentVariables(); - - var configuration = builder.Build(); - _webHostBuilder = new WebHostBuilder(); - _webHostBuilder.ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - }); + public void GivenIHaveAToken(string url) + { + var tokenUrl = $"{url}/connect/token"; + var formData = new List> + { + new("client_id", "client"), + new("client_secret", "secret"), + new("scope", "api"), + new("username", "test"), + new("password", "test"), + new("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); + } - _ocelotServer = new TestServer(_webHostBuilder - .UseConfiguration(configuration) - .ConfigureServices(s => - { - s.AddOcelot(configuration); - }) - .ConfigureLogging(l => - { - l.AddConsole(); - l.AddDebug(); - }) - .Configure(a => - { - a.UseOcelot(ocelotPipelineConfig).Wait(); - })); + public void GivenIHaveATokenForApiReadOnlyScope(string url) + { + var tokenUrl = $"{url}/connect/token"; + var formData = new List> + { + new("client_id", "client"), + new("client_secret", "secret"), + new("scope", "api.readOnly"), + new("username", "test"), + new("password", "test"), + new("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); + } - _ocelotClient = _ocelotServer.CreateClient(); - } + public void GivenIHaveATokenForApi2(string url) + { + var tokenUrl = $"{url}/connect/token"; + var formData = new List> + { + new("client_id", "client"), + new("client_secret", "secret"), + new("scope", "api2"), + new("username", "test"), + new("password", "test"), + new("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 GivenIHaveAddedATokenToMyRequest() - { - _ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - } + public static void VerifyIdentityServerStarted(string url) + { + using var httpClient = new HttpClient(); + var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").GetAwaiter().GetResult(); + response.Content.ReadAsStringAsync().GetAwaiter(); + response.EnsureSuccessStatusCode(); + } - public void GivenIHaveAToken(string url) - { - var tokenUrl = $"{url}/connect/token"; - var formData = new List> + public void GivenOcelotIsRunningWithMinimumLogLevel(Logger logger, string appsettingsFileName) + { + _webHostBuilder = new WebHostBuilder() + .UseKestrel() + .ConfigureAppConfiguration((_, config) => { - new("client_id", "client"), - new("client_secret", "secret"), - new("scope", "api"), - new("username", "test"), - new("password", "test"), - new("grant_type", "password"), - }; - var content = new FormUrlEncodedContent(formData); - - using (var httpClient = new HttpClient()) + config.AddJsonFile(appsettingsFileName, false, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => { s.AddOcelot(); }) + .ConfigureLogging(logging => { - var response = httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } - } + logging.ClearProviders(); + logging.AddSerilog(logger); + }) + .Configure(app => + { + app.Use(async (context, next) => + { + var loggerFactory = context.RequestServices.GetService(); + var ocelotLogger = loggerFactory.CreateLogger(); + ocelotLogger.LogDebug(() => $"DEBUG: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogTrace(() => $"TRACE: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogInformation(() => + $"INFORMATION: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogWarning(() => $"WARNING: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogError(() => $"ERROR: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", + new Exception("test")); + ocelotLogger.LogCritical(() => $"CRITICAL: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", + new Exception("test")); + + await next.Invoke(); + }); + app.UseOcelot().Wait(); + }); - public void GivenIHaveATokenForApiReadOnlyScope(string url) - { - var tokenUrl = $"{url}/connect/token"; - var formData = new List> + _ocelotServer = new TestServer(_webHostBuilder); + _ocelotClient = _ocelotServer.CreateClient(); + } + + public void GivenOcelotIsRunningWithEureka() + { + _webHostBuilder = new WebHostBuilder(); + + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => { - new("client_id", "client"), - new("client_secret", "secret"), - new("scope", "api.readOnly"), - new("username", "test"), - new("password", "test"), - new("grant_type", "password"), - }; - var content = new FormUrlEncodedContent(formData); - - using (var httpClient = new HttpClient()) + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => { - var response = httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } - } + s.AddOcelot() + .AddEureka(); + }) + .Configure(app => { app.UseOcelot().Wait(); }); - public void GivenIHaveATokenForApi2(string url) - { - var tokenUrl = $"{url}/connect/token"; - var formData = new List> + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + public void GivenOcelotIsRunningWithPolly() + { + _webHostBuilder = new WebHostBuilder(); + + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => { - new("client_id", "client"), - new("client_secret", "secret"), - new("scope", "api2"), - new("username", "test"), - new("password", "test"), - new("grant_type", "password"), - }; - var content = new FormUrlEncodedContent(formData); - - using (var httpClient = new HttpClient()) + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => { - var response = httpClient.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()) + s.AddOcelot() + .AddPolly(); + }) + .Configure(app => { - var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").GetAwaiter().GetResult(); - var content = response.Content.ReadAsStringAsync().GetAwaiter(); - response.EnsureSuccessStatusCode(); - } - } + app.UseOcelot() + .Wait(); + }); - public void GivenOcelotIsRunningWithEureka() - { - _webHostBuilder = new WebHostBuilder(); + _ocelotServer = new TestServer(_webHostBuilder); - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddEureka(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + _ocelotClient = _ocelotServer.CreateClient(); + } - _ocelotServer = new TestServer(_webHostBuilder); + public void WhenIGetUrlOnTheApiGateway(string url) + { + _response = _ocelotClient.GetAsync(url).Result; + } - _ocelotClient = _ocelotServer.CreateClient(); - } + public void WhenIGetUrlOnTheApiGatewayAndDontWait(string url) + { + _ocelotClient.GetAsync(url); + } - public void GivenOcelotIsRunningWithPolly() - { - _webHostBuilder = new WebHostBuilder(); + public void WhenICancelTheRequest() + { + _ocelotClient.CancelPendingRequests(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddPolly(); - }) - .Configure(app => - { - app.UseOcelot() - .Wait(); - }); + public void WhenIGetUrlOnTheApiGateway(string url, HttpContent content) + { + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url) { Content = content }; + _response = _ocelotClient.SendAsync(httpRequestMessage).Result; + } - _ocelotServer = new TestServer(_webHostBuilder); + public void WhenIPostUrlOnTheApiGateway(string url, HttpContent content) + { + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, url) { Content = content }; + _response = _ocelotClient.SendAsync(httpRequestMessage).Result; + } - _ocelotClient = _ocelotServer.CreateClient(); - } + 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 WhenIGetUrlOnTheApiGateway(string url) - { - _response = _ocelotClient.GetAsync(url).Result; - } + public void GivenIAddAHeader(string key, string value) + { + _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(key, value); + } - public void WhenIGetUrlOnTheApiGatewayAndDontWait(string url) - { - _ocelotClient.GetAsync(url); - } + public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) + { + var tasks = new Task[times]; - public void WhenICancelTheRequest() + for (var i = 0; i < times; i++) { - _ocelotClient.CancelPendingRequests(); + var urlCopy = url; + tasks[i] = GetForServiceDiscoveryTest(urlCopy); + Thread.Sleep(_random.Next(40, 60)); } - public void WhenIGetUrlOnTheApiGateway(string url, HttpContent content) - { - var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url) { Content = content }; - _response = _ocelotClient.SendAsync(httpRequestMessage).Result; - } + Task.WaitAll(tasks); + } - public void WhenIPostUrlOnTheApiGateway(string url, HttpContent content) - { - var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, url) { Content = content }; - _response = _ocelotClient.SendAsync(httpRequestMessage).Result; - } + public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times, string cookie, string value) + { + var tasks = new Task[times]; - public void WhenIGetUrlOnTheApiGateway(string url, string cookie, string value) + for (var i = 0; i < times; i++) { - var request = _ocelotServer.CreateRequest(url); - request.And(x => { x.Headers.Add("Cookie", new CookieHeaderValue(cookie, value).ToString()); }); - var response = request.GetAsync().Result; - _response = response; + tasks[i] = GetForServiceDiscoveryTest(url, cookie, value); + Thread.Sleep(_random.Next(40, 60)); } - public void GivenIAddAHeader(string key, string value) - { - _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(key, value); - } + Task.WaitAll(tasks); + } - public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) - { - var tasks = new Task[times]; + 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(); + var count = int.Parse(content); + count.ShouldBeGreaterThan(0); + } - for (var i = 0; i < times; i++) - { - var urlCopy = url; - tasks[i] = GetForServiceDiscoveryTest(urlCopy); - Thread.Sleep(_random.Next(40, 60)); - } + private async Task GetForServiceDiscoveryTest(string url) + { + var response = await _ocelotClient.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + var count = int.Parse(content); + count.ShouldBeGreaterThan(0); + } - Task.WaitAll(tasks); + public void WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(string url, int times) + { + for (var i = 0; i < times; i++) + { + const string clientId = "ocelotclient1"; + var request = new HttpRequestMessage(new HttpMethod("GET"), url); + request.Headers.Add("ClientId", clientId); + _response = _ocelotClient.SendAsync(request).Result; } + } - public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times, string cookie, string value) - { - var tasks = new Task[times]; + public void WhenIGetUrlOnTheApiGateway(string url, string requestId) + { + _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId); - for (var i = 0; i < times; i++) - { - var urlCopy = url; - tasks[i] = GetForServiceDiscoveryTest(urlCopy, cookie, value); - Thread.Sleep(_random.Next(40, 60)); - } + _response = _ocelotClient.GetAsync(url).Result; + } - Task.WaitAll(tasks); - } + public void WhenIPostUrlOnTheApiGateway(string url) + { + _response = _ocelotClient.PostAsync(url, _postContent).Result; + } - 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(); - var count = int.Parse(content); - count.ShouldBeGreaterThan(0); - } + public void GivenThePostHasContent(string postContent) + { + _postContent = new StringContent(postContent); + } - private async Task GetForServiceDiscoveryTest(string url) - { - var response = await _ocelotClient.GetAsync(url); - var content = await response.Content.ReadAsStringAsync(); - var count = int.Parse(content); - count.ShouldBeGreaterThan(0); - } + public void GivenThePostHasContentType(string postContent) + { + _postContent.Headers.ContentType = new MediaTypeHeaderValue(postContent); + } - public void WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(string url, int times) + 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)) { - for (var 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; - } + gzip.Write(jsonBytes, 0, jsonBytes.Length); } - public void WhenIGetUrlOnTheApiGateway(string url, string requestId) - { - _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId); + ms.Position = 0; + var content = new StreamContent(ms); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + content.Headers.ContentEncoding.Add("gzip"); + _postContent = content; + } - _response = _ocelotClient.GetAsync(url).Result; - } + public void ThenTheResponseBodyShouldBe(string expectedBody) + { + _response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody); + } - public void WhenIPostUrlOnTheApiGateway(string url) - { - _response = _ocelotClient.PostAsync(url, _postContent).Result; - } + public void ThenTheContentLengthIs(int expected) + { + _response.Content.Headers.ContentLength.ShouldBe(expected); + } - public void GivenThePostHasContent(string postcontent) - { - _postContent = new StringContent(postcontent); - } + public void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) + { + _response.StatusCode.ShouldBe(expectedHttpStatusCode); + } - public void GivenThePostHasContentType(string postcontent) - { - _postContent.Headers.ContentType = new MediaTypeHeaderValue(postcontent); - } + public void ThenTheStatusCodeShouldBe(int expectedHttpStatusCode) + { + var responseStatusCode = (int)_response.StatusCode; + responseStatusCode.ShouldBe(expectedHttpStatusCode); + } - 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); - } + public void ThenTheRequestIdIsReturned() + { + _response.Headers.GetValues(RequestIdKey).First().ShouldNotBeNullOrEmpty(); + } - ms.Position = 0; - var content = new StreamContent(ms); - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - content.Headers.ContentEncoding.Add("gzip"); - _postContent = content; - } + public void ThenTheRequestIdIsReturned(string expected) + { + _response.Headers.GetValues(RequestIdKey).First().ShouldBe(expected); + } - public void ThenTheResponseBodyShouldBe(string expectedBody) - { - _response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody); - } + public void WhenIMakeLotsOfDifferentRequestsToTheApiGateway() + { + var 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(); - public void ThenTheContentLengthIs(int expected) - { - _response.Content.Headers.ContentLength.ShouldBe(expected); - } + var aggregateTasks = new Task[numberOfRequests]; - public void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) + for (var i = 0; i < numberOfRequests; i++) { - _response.StatusCode.ShouldBe(expectedHttpStatusCode); + aggregateTasks[i] = Fire(aggregateUrl, aggregateExpected, random); } - public void ThenTheStatusCodeShouldBe(int expectedHttpStatusCode) - { - var responseStatusCode = (int)_response.StatusCode; - responseStatusCode.ShouldBe(expectedHttpStatusCode); - } + var tomTasks = new Task[numberOfRequests]; - public void ThenTheRequestIdIsReturned() + for (var i = 0; i < numberOfRequests; i++) { - _response.Headers.GetValues(RequestIdKey).First().ShouldNotBeNullOrEmpty(); + tomTasks[i] = Fire(tomUrl, tomExpected, random); } - public void ThenTheRequestIdIsReturned(string expected) - { - _response.Headers.GetValues(RequestIdKey).First().ShouldBe(expected); - } + var lauraTasks = new Task[numberOfRequests]; - public void WhenIMakeLotsOfDifferentRequestsToTheApiGateway() + for (var i = 0; i < numberOfRequests; i++) { - var 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 (var i = 0; i < numberOfRequests; i++) - { - aggregateTasks[i] = Fire(aggregateUrl, aggregateExpected, random); - } + lauraTasks[i] = Fire(lauraUrl, lauraExpected, random); + } - var tomTasks = new Task[numberOfRequests]; + Task.WaitAll(lauraTasks); + Task.WaitAll(tomTasks); + Task.WaitAll(aggregateTasks); + } - for (var i = 0; i < numberOfRequests; i++) - { - tomTasks[i] = Fire(tomUrl, tomExpected, random); - } + 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); + } - var lauraTasks = new Task[numberOfRequests]; + public void GivenOcelotIsRunningWithBlowingUpDiskRepo(IFileConfigurationRepository fake) + { + _webHostBuilder = new WebHostBuilder(); - for (var i = 0; i < numberOfRequests; i++) + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => { - 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); - } - - public void GivenOcelotIsRunningWithBlowingUpDiskRepo(IFileConfigurationRepository fake) - { - _webHostBuilder = new WebHostBuilder(); + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(fake); + s.AddOcelot(); + }) + .Configure(app => { app.UseOcelot().Wait(); }); - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(fake); - s.AddOcelot(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + _ocelotServer = new TestServer(_webHostBuilder); - _ocelotServer = new TestServer(_webHostBuilder); + _ocelotClient = _ocelotServer.CreateClient(); + } - _ocelotClient = _ocelotServer.CreateClient(); - } + public void TheChangeTokenShouldBeActive(bool itShouldBeActive) + { + _changeToken.ChangeToken.HasChanged.ShouldBe(itShouldBeActive); + } - public void TheChangeTokenShouldBeActive(bool itShouldBeActive) - { - _changeToken.ChangeToken.HasChanged.ShouldBe(itShouldBeActive); - } + public void GivenOcelotIsRunningWithLogger() + { + _webHostBuilder = new WebHostBuilder(); - public void GivenOcelotIsRunningWithLogger() - { - _webHostBuilder = new WebHostBuilder(); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot(); + s.AddSingleton(); + }) + .Configure(app => { app.UseOcelot().Wait(); }); - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - s.AddSingleton(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + _ocelotServer = new TestServer(_webHostBuilder); - _ocelotServer = new TestServer(_webHostBuilder); + _ocelotClient = _ocelotServer.CreateClient(); + } - _ocelotClient = _ocelotServer.CreateClient(); - } + internal void GivenOcelotIsRunningUsingOpenTracing(OpenTracing.ITracer fakeTracer) + { + _webHostBuilder = new WebHostBuilder(); - internal void GivenOcelotIsRunningUsingOpenTracing(OpenTracing.ITracer fakeTracer) - { - _webHostBuilder = new WebHostBuilder(); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot() + .AddOpenTracing(); - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddOpenTracing(); + s.AddSingleton(fakeTracer); + }) + .Configure(app => + { + app.Use(async (_, next) => { await next.Invoke(); }); + app.UseOcelot().Wait(); + }); - s.AddSingleton(fakeTracer); - }) - .Configure(app => - { - app.Use(async (_, next) => - { - await next.Invoke(); - }); - app.UseOcelot().Wait(); - }); + _ocelotServer = new TestServer(_webHostBuilder); - _ocelotServer = new TestServer(_webHostBuilder); + _ocelotClient = _ocelotServer.CreateClient(); + } - _ocelotClient = _ocelotServer.CreateClient(); - } + public void ThenWarningShouldBeLogged(int howMany) + { + var loggerFactory = (MockLoggerFactory)_ocelotServer.Host.Services.GetService(); + loggerFactory.Verify(Times.Exactly(howMany)); + } - public void ThenWarningShouldBeLogged() - { - var loggerFactory = (MockLoggerFactory)_ocelotServer.Host.Services.GetService(); - loggerFactory.Verify(); - } + internal class MockLoggerFactory : IOcelotLoggerFactory + { + private Mock _logger; - internal class MockLoggerFactory : IOcelotLoggerFactory + public IOcelotLogger CreateLogger() { - private Mock _logger; - - public IOcelotLogger CreateLogger() + if (_logger != null) { - if (_logger == null) - { - _logger = new Mock(); - _logger.Setup(x => x.LogWarning(It.IsAny())).Verifiable(); - } - return _logger.Object; } - public void Verify() - { - _logger.Verify(x => x.LogWarning(It.IsAny()), Times.Once); - } + _logger = new Mock(); + _logger.Setup(x => x.LogWarning(It.IsAny())).Verifiable(); + _logger.Setup(x => x.LogWarning(It.IsAny>())).Verifiable(); + + return _logger.Object; } - /// - /// Public implementation of Dispose pattern callable by consumers. - /// - public void Dispose() + public void Verify(Times howMany) { - Dispose(true); - GC.SuppressFinalize(this); + _logger.Verify(x => x.LogWarning(It.IsAny>()), howMany); } + } - private bool _disposedValue; + /// + /// Public implementation of Dispose pattern callable by consumers. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - /// - /// Protected implementation of Dispose pattern. - /// - /// Flag to trigger actual disposing operation. - protected virtual void Dispose(bool disposing) - { - if (_disposedValue) - { - return; - } + private bool _disposedValue; - if (disposing) - { - _ocelotClient?.Dispose(); - _ocelotServer?.Dispose(); - _ocelotHost?.Dispose(); - DeleteOcelotConfig(); - } + /// + /// Protected implementation of Dispose pattern. + /// + /// Flag to trigger actual disposing operation. + protected virtual void Dispose(bool disposing) + { + if (_disposedValue) + { + return; + } - _disposedValue = true; + if (disposing) + { + _ocelotClient?.Dispose(); + _ocelotServer?.Dispose(); + _ocelotHost?.Dispose(); + DeleteOcelotConfig(); } + + _disposedValue = true; } } diff --git a/test/Ocelot.AcceptanceTests/StickySessionsTests.cs b/test/Ocelot.AcceptanceTests/StickySessionsTests.cs index ad10984fe..6592ec83b 100644 --- a/test/Ocelot.AcceptanceTests/StickySessionsTests.cs +++ b/test/Ocelot.AcceptanceTests/StickySessionsTests.cs @@ -20,8 +20,8 @@ public StickySessionsTests() [Fact] public void should_use_same_downstream_host() { - var downstreamPortOne = RandomPortFinder.GetRandomPort(); - var downstreamPortTwo = RandomPortFinder.GetRandomPort(); + var downstreamPortOne = PortFinder.GetRandomPort(); + var downstreamPortTwo = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; @@ -71,8 +71,8 @@ public void should_use_same_downstream_host() [Fact] public void should_use_different_downstream_host_for_different_re_route() { - var downstreamPortOne = RandomPortFinder.GetRandomPort(); - var downstreamPortTwo = RandomPortFinder.GetRandomPort(); + var downstreamPortOne = PortFinder.GetRandomPort(); + var downstreamPortTwo = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; @@ -149,8 +149,8 @@ public void should_use_different_downstream_host_for_different_re_route() [Fact] public void should_use_same_downstream_host_for_different_re_route() { - var downstreamPortOne = RandomPortFinder.GetRandomPort(); - var downstreamPortTwo = RandomPortFinder.GetRandomPort(); + var downstreamPortOne = PortFinder.GetRandomPort(); + var downstreamPortTwo = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; diff --git a/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs b/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs index 3a695bb9c..d0be68c64 100644 --- a/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs +++ b/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs @@ -23,9 +23,9 @@ public TwoDownstreamServicesTests() [Fact] public void should_fix_issue_194() { - var consulPort = RandomPortFinder.GetRandomPort(); - var servicePort1 = RandomPortFinder.GetRandomPort(); - var servicePort2 = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); + var servicePort1 = PortFinder.GetRandomPort(); + var servicePort2 = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; diff --git a/test/Ocelot.AcceptanceTests/UpstreamHostTests.cs b/test/Ocelot.AcceptanceTests/UpstreamHostTests.cs index 2d649e1ce..054ff5c77 100644 --- a/test/Ocelot.AcceptanceTests/UpstreamHostTests.cs +++ b/test/Ocelot.AcceptanceTests/UpstreamHostTests.cs @@ -18,7 +18,7 @@ public UpstreamHostTests() [Fact] public void should_return_response_200_with_simple_url_and_hosts_match() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -55,7 +55,7 @@ public void should_return_response_200_with_simple_url_and_hosts_match() [Fact] public void should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -108,7 +108,7 @@ public void should_return_response_200_with_simple_url_and_hosts_match_multiple_ [Fact] public void should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes_reversed() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -161,7 +161,7 @@ public void should_return_response_200_with_simple_url_and_hosts_match_multiple_ [Fact] public void should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes_reversed_with_no_host_first() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -213,7 +213,7 @@ public void should_return_response_200_with_simple_url_and_hosts_match_multiple_ [Fact] public void should_return_response_404_with_simple_url_and_hosts_dont_match() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/Usings.cs b/test/Ocelot.AcceptanceTests/Usings.cs index 6eb9a3d7b..9648f6280 100644 --- a/test/Ocelot.AcceptanceTests/Usings.cs +++ b/test/Ocelot.AcceptanceTests/Usings.cs @@ -10,6 +10,7 @@ // Project extra global namespaces global using Moq; global using Ocelot; +global using Ocelot.Testing; global using Shouldly; global using System.Net; global using TestStack.BDDfy; diff --git a/test/Ocelot.AcceptanceTests/WebSocketTests.cs b/test/Ocelot.AcceptanceTests/WebSocketTests.cs index 0ede6baea..ad9e7ec68 100644 --- a/test/Ocelot.AcceptanceTests/WebSocketTests.cs +++ b/test/Ocelot.AcceptanceTests/WebSocketTests.cs @@ -23,7 +23,7 @@ public WebSocketTests() [Fact] public void ShouldProxyWebsocketInputToDownstreamService() { - var downstreamPort = RandomPortFinder.GetRandomPort(); + var downstreamPort = PortFinder.GetRandomPort(); var downstreamHost = "localhost"; var config = new FileConfiguration @@ -58,9 +58,9 @@ public void ShouldProxyWebsocketInputToDownstreamService() [Fact] public void ShouldProxyWebsocketInputToDownstreamServiceAndUseLoadBalancer() { - var downstreamPort = RandomPortFinder.GetRandomPort(); + var downstreamPort = PortFinder.GetRandomPort(); var downstreamHost = "localhost"; - var secondDownstreamPort = RandomPortFinder.GetRandomPort(); + var secondDownstreamPort = PortFinder.GetRandomPort(); var secondDownstreamHost = "localhost"; var config = new FileConfiguration diff --git a/test/Ocelot.Benchmarks/MsLoggerBenchmarks.cs b/test/Ocelot.Benchmarks/MsLoggerBenchmarks.cs new file mode 100644 index 000000000..c54693300 --- /dev/null +++ b/test/Ocelot.Benchmarks/MsLoggerBenchmarks.cs @@ -0,0 +1,195 @@ +using BenchmarkDotNet.Order; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.Benchmarks; + +[Config(typeof(MsLoggerBenchmarks))] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +[MaxIterationCount(16)] +public class MsLoggerBenchmarks : ManualConfig +{ + private IWebHost _service; + private IWebHost _webHost; + private HttpClient _httpClient; + + public MsLoggerBenchmarks() + { + AddColumn(StatisticColumn.AllStatistics); + AddDiagnoser(MemoryDiagnoser.Default); + AddValidator(BaselineValidator.FailOnError); + } + + private async Task SendRequest() + { + _httpClient ??= new HttpClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:5000"); + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + } + + [Benchmark(Baseline = true)] + public async Task LogLevelCritical() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelCritical))] + public void SetUpCritical() => OcelotFactory(LogLevel.Critical); + + [Benchmark] + public async Task LogLevelError() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelError))] + public void SetupError() => OcelotFactory(LogLevel.Error); + + [Benchmark] + public async Task LogLevelWarning() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelWarning))] + public void SetUpWarning() => OcelotFactory(LogLevel.Warning); + + [Benchmark] + public async Task LogLevelInformation() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelInformation))] + public void SetUpInformation() => OcelotFactory(LogLevel.Information); + + [Benchmark] + public async Task LogLevelTrace() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelTrace))] + public void SetUpTrace() => OcelotFactory(LogLevel.Trace); + + [GlobalCleanup(Targets = new[] + { + nameof(LogLevelCritical), nameof(LogLevelError), nameof(LogLevelWarning), nameof(LogLevelInformation), + nameof(LogLevelTrace), + })] + public void OcelotCleanup() + { + _webHost?.Dispose(); + _service?.Dispose(); + } + + [GlobalCleanup] + public void Cleanup() + { + _httpClient?.Dispose(); + } + + private void GivenOcelotIsRunning(string url, LogLevel minLogLevel) + { + _webHost = new WebHostBuilder() + .UseKestrel() + .UseUrls(url) + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("ocelot.json", false, false) + .AddEnvironmentVariables(); + }) + .ConfigureServices(s => { s.AddOcelot(); }) + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.SetMinimumLevel(minLogLevel); + logging.AddConsole(); + }) + .Configure(app => + { + app.Use(async (context, next) => + { + var loggerFactory = context.RequestServices.GetService(); + var ocelotLogger = loggerFactory.CreateLogger(); + ocelotLogger.LogDebug(() => $"DEBUG: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogTrace(() => $"TRACE: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogInformation(() => $"INFORMATION: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogWarning(() => $"WARNING: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogError(() => $"ERROR: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", + new Exception("test")); + ocelotLogger.LogCritical(() => $"CRITICAL: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", + new Exception("test")); + + await next.Invoke(); + }); + app.UseOcelot().Wait(); + }) + .Build(); + + _webHost.Start(); + } + + private void OcelotFactory(LogLevel minLogLevel) + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51879, + }, + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + GivenThereIsAConfiguration(configuration); + GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201, string.Empty); + GivenOcelotIsRunning("http://localhost:5000", minLogLevel); + } + + public static void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = Path.Combine(AppContext.BaseDirectory, "ocelot.json"); + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _service = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _service.Start(); + } +} diff --git a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj index ffbd2d8a4..e2cb80062 100644 --- a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj +++ b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj @@ -26,6 +26,18 @@ all + + + + + + + + + + + + diff --git a/test/Ocelot.Benchmarks/Program.cs b/test/Ocelot.Benchmarks/Program.cs index a42519606..4d750e0a5 100644 --- a/test/Ocelot.Benchmarks/Program.cs +++ b/test/Ocelot.Benchmarks/Program.cs @@ -12,6 +12,8 @@ public static void Main(string[] args) typeof(AllTheThingsBenchmarks), typeof(ExceptionHandlerMiddlewareBenchmarks), typeof(DownstreamRouteFinderMiddlewareBenchmarks), + typeof(SerilogBenchmarks), + typeof(MsLoggerBenchmarks), }); switcher.Run(args); diff --git a/test/Ocelot.Benchmarks/SerilogBenchmarks.cs b/test/Ocelot.Benchmarks/SerilogBenchmarks.cs new file mode 100644 index 000000000..f3af7e814 --- /dev/null +++ b/test/Ocelot.Benchmarks/SerilogBenchmarks.cs @@ -0,0 +1,226 @@ +using BenchmarkDotNet.Order; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.Logging; +using Ocelot.Middleware; +using Serilog; +using Serilog.Core; + +namespace Ocelot.Benchmarks; + +[Config(typeof(SerilogBenchmarks))] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +public class SerilogBenchmarks : ManualConfig +{ + private IWebHost _service; + private Logger _logger; + private IWebHost _webHost; + private HttpClient _httpClient; + + public SerilogBenchmarks() + { + AddColumn(StatisticColumn.AllStatistics); + AddDiagnoser(MemoryDiagnoser.Default); + AddValidator(BaselineValidator.FailOnError); + } + + private async Task SendRequest() + { + _httpClient ??= new HttpClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:5000"); + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + } + + [Benchmark(Baseline = true)] + public async Task LogLevelCritical() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelCritical))] + public void SetUpCritical() => OcelotFactory(LogLevel.Critical); + + [Benchmark] + public async Task LogLevelError() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelError))] + public void SetupError() => OcelotFactory(LogLevel.Error); + + [Benchmark] + public async Task LogLevelWarning() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelWarning))] + public void SetUpWarning() => OcelotFactory(LogLevel.Warning); + + [Benchmark] + public async Task LogLevelInformation() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelInformation))] + public void SetUpInformation() => OcelotFactory(LogLevel.Information); + + [Benchmark] + public async Task LogLevelTrace() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelTrace))] + public void SetUpTrace() => OcelotFactory(LogLevel.Trace); + + [GlobalCleanup(Targets = new[] + { + nameof(LogLevelCritical), nameof(LogLevelError), nameof(LogLevelWarning), nameof(LogLevelInformation), + nameof(LogLevelTrace), + })] + public void OcelotCleanup() + { + _webHost?.Dispose(); + _service?.Dispose(); + } + + [GlobalCleanup] + public void Cleanup() + { + _httpClient?.Dispose(); + } + + private void GivenOcelotIsRunning(string url, LogLevel minLogLevel) + { + _logger = minLogLevel switch + { + LogLevel.Information => new LoggerConfiguration().MinimumLevel.Information() + .WriteTo.File( + $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") + .CreateLogger(), + LogLevel.Warning => new LoggerConfiguration().MinimumLevel.Warning() + .WriteTo.File( + $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") + .CreateLogger(), + LogLevel.Error => new LoggerConfiguration().MinimumLevel.Error() + .WriteTo.File( + $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") + .CreateLogger(), + LogLevel.Critical => new LoggerConfiguration().MinimumLevel.Fatal() + .WriteTo.File( + $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") + .CreateLogger(), + LogLevel.Trace => new LoggerConfiguration().MinimumLevel.Verbose() + .WriteTo.File( + $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") + .CreateLogger(), + LogLevel.None => new LoggerConfiguration() + .WriteTo.File( + $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") + .CreateLogger(), + _ => throw new ArgumentOutOfRangeException(nameof(minLogLevel), minLogLevel, null), + }; + + _webHost = new WebHostBuilder() + .UseKestrel() + .UseUrls(url) + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("ocelot.json", false, false) + .AddEnvironmentVariables(); + }) + .ConfigureServices(s => { s.AddOcelot(); }) + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.SetMinimumLevel(minLogLevel); + logging.AddSerilog(_logger); + }) + .Configure(app => + { + app.Use(async (context, next) => + { + var loggerFactory = context.RequestServices.GetService(); + var ocelotLogger = loggerFactory.CreateLogger(); + ocelotLogger.LogDebug(() => $"DEBUG: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogTrace(() => $"TRACE: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogInformation(() => $"INFORMATION: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogWarning(() => $"WARNING: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogError(() => $"ERROR: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", + new Exception("test")); + ocelotLogger.LogCritical(() => $"CRITICAL: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", + new Exception("test")); + + await next.Invoke(); + }); + app.UseOcelot().Wait(); + }) + .Build(); + + _webHost.Start(); + } + + private void OcelotFactory(LogLevel minLogLevel) + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51879, + }, + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + GivenThereIsAConfiguration(configuration); + GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201, string.Empty); + GivenOcelotIsRunning("http://localhost:5000", minLogLevel); + } + + public static void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = Path.Combine(AppContext.BaseDirectory, "ocelot.json"); + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _service = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _service.Start(); + } +} diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs index 374634598..34298a03a 100644 --- a/test/Ocelot.IntegrationTests/AdministrationTests.cs +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -75,7 +75,8 @@ public void Should_return_response_200_with_call_re_routes_controller() public void Should_return_response_200_with_call_re_routes_controller_using_base_url_added_in_file_config() { _httpClient = new HttpClient(); - _ocelotBaseUrl = "http://localhost:5011"; + var port = PortFinder.GetRandomPort(); + _ocelotBaseUrl = $"http://localhost:{port}"; _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); var configuration = new FileConfiguration @@ -122,12 +123,13 @@ public void Should_return_OK_status_and_multiline_indented_json_response_with_js public void Should_be_able_to_use_token_from_ocelot_a_on_ocelot_b() { var configuration = new FileConfiguration(); + var port = PortFinder.GetRandomPort(); this.Given(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenIdentityServerSigningEnvironmentalVariablesAreSet()) .And(x => GivenOcelotIsRunning()) .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenAnotherOcelotIsRunning("http://localhost:5017")) + .And(x => GivenAnotherOcelotIsRunning($"http://localhost:{port}")) .When(x => WhenIGetUrlOnTheSecondOcelot("/administration/configuration")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); @@ -356,8 +358,8 @@ private static void ThenTheConfigurationIsSavedCorrectly(FileConfiguration expec [Fact] public void Should_get_file_configuration_edit_and_post_updated_version_redirecting_route() { - var fooPort = 47689; - var barPort = 27654; + var fooPort = PortFinder.GetRandomPort(); + var barPort = PortFinder.GetRandomPort(); var initialConfiguration = new FileConfiguration { @@ -490,7 +492,8 @@ public void Should_return_response_200_with_call_re_routes_controller_when_using { var configuration = new FileConfiguration(); - var identityServerRootUrl = "http://localhost:5123"; + var port = PortFinder.GetRandomPort(); + var identityServerRootUrl = $"http://localhost:{port}"; Action options = o => { @@ -525,13 +528,11 @@ private void GivenIHaveAToken(string url) }; var content = new FormUrlEncodedContent(formData); - using (var httpClient = new HttpClient()) - { - var response = httpClient.PostAsync($"{url}/connect/token", content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } + using var httpClient = new HttpClient(); + var response = httpClient.PostAsync($"{url}/connect/token", content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); } private void GivenThereIsAnIdentityServerOn(string url, string apiName) @@ -593,11 +594,9 @@ private void GivenThereIsAnIdentityServerOn(string url, string apiName) _identityServerBuilder.Start(); - using (var httpClient = new HttpClient()) - { - var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result; - response.EnsureSuccessStatusCode(); - } + using var httpClient = new HttpClient(); + var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result; + response.EnsureSuccessStatusCode(); } private void GivenAnotherOcelotIsRunning(string baseUrl) @@ -851,7 +850,7 @@ private static void GivenThereIsAConfiguration(FileConfiguration fileConfigurati File.WriteAllText(configurationPath, jsonConfiguration); - var text = File.ReadAllText(configurationPath); + _ = File.ReadAllText(configurationPath); configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; @@ -862,7 +861,7 @@ private static void GivenThereIsAConfiguration(FileConfiguration fileConfigurati File.WriteAllText(configurationPath, jsonConfiguration); - text = File.ReadAllText(configurationPath); + _ = File.ReadAllText(configurationPath); } private void WhenIGetUrlOnTheApiGateway(string url) @@ -901,6 +900,7 @@ public void Dispose() _builder?.Dispose(); _httpClient?.Dispose(); _identityServerBuilder?.Dispose(); + GC.SuppressFinalize(this); } private void GivenThereIsAFooServiceRunningOn(string baseUrl) diff --git a/test/Ocelot.IntegrationTests/HeaderTests.cs b/test/Ocelot.IntegrationTests/HeaderTests.cs index 928b6180a..9072f3189 100644 --- a/test/Ocelot.IntegrationTests/HeaderTests.cs +++ b/test/Ocelot.IntegrationTests/HeaderTests.cs @@ -24,13 +24,15 @@ public class HeaderTests : IDisposable public HeaderTests() { _httpClient = new HttpClient(); - _ocelotBaseUrl = "http://localhost:5010"; + var port = PortFinder.GetRandomPort(); + _ocelotBaseUrl = $"http://localhost:{port}"; _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); } [Fact] - public void should_pass_remote_ip_address_if_as_x_forwarded_for_header() + public void Should_pass_remote_ip_address_if_as_x_forwarded_for_header() { + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -44,7 +46,7 @@ public void should_pass_remote_ip_address_if_as_x_forwarded_for_header() new() { Host = "localhost", - Port = 6773, + Port = port, }, }, UpstreamPathTemplate = "/", @@ -61,7 +63,7 @@ public void should_pass_remote_ip_address_if_as_x_forwarded_for_header() }, }; - this.Given(x => GivenThereIsAServiceRunningOn("http://localhost:6773", 200, "X-Forwarded-For")) + this.Given(x => GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "X-Forwarded-For")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) @@ -136,8 +138,8 @@ private static void GivenThereIsAConfiguration(FileConfiguration fileConfigurati } File.WriteAllText(configurationPath, jsonConfiguration); - - var text = File.ReadAllText(configurationPath); + + _ = File.ReadAllText(configurationPath); configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; @@ -146,9 +148,9 @@ private static void GivenThereIsAConfiguration(FileConfiguration fileConfigurati File.Delete(configurationPath); } - File.WriteAllText(configurationPath, jsonConfiguration); - - text = File.ReadAllText(configurationPath); + File.WriteAllText(configurationPath, jsonConfiguration); + + _ = File.ReadAllText(configurationPath); } private async Task WhenIGetUrlOnTheApiGateway(string url) @@ -178,7 +180,8 @@ public void Dispose() { _builder?.Dispose(); _httpClient?.Dispose(); - _downstreamBuilder?.Dispose(); + _downstreamBuilder?.Dispose(); + GC.SuppressFinalize(this); } } } diff --git a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj index dbd821b49..996603c55 100644 --- a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj +++ b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj @@ -33,6 +33,7 @@ + diff --git a/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs b/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs index 1e6a6f127..48eac3686 100644 --- a/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs +++ b/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs @@ -30,8 +30,9 @@ public ThreadSafeHeadersTests() } [Fact] - public void should_return_same_response_for_each_different_header_under_load_to_downsteam_service() - { + public void Should_return_same_response_for_each_different_header_under_load_to_downsteam_service() + { + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -45,7 +46,7 @@ public void should_return_same_response_for_each_different_header_under_load_to_ new() { Host = "localhost", - Port = 51611, + Port = port, }, }, UpstreamPathTemplate = "/", @@ -55,7 +56,7 @@ public void should_return_same_response_for_each_different_header_under_load_to_ }; this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenThereIsAServiceRunningOn("http://localhost:51611")) + .And(x => GivenThereIsAServiceRunningOn($"http://localhost:{port}")) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesWithDifferentHeaderValues("/", 300)) .Then(x => ThenTheSameHeaderValuesAreReturnedByTheDownstreamService()) @@ -126,8 +127,8 @@ private static void GivenThereIsAConfiguration(FileConfiguration fileConfigurati } File.WriteAllText(configurationPath, jsonConfiguration); - - var text = File.ReadAllText(configurationPath); + + _ = File.ReadAllText(configurationPath); configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; @@ -138,7 +139,7 @@ private static void GivenThereIsAConfiguration(FileConfiguration fileConfigurati File.WriteAllText(configurationPath, jsonConfiguration); - text = File.ReadAllText(configurationPath); + _ = File.ReadAllText(configurationPath); } private void WhenIGetUrlOnTheApiGatewayMultipleTimesWithDifferentHeaderValues(string url, int times) @@ -178,7 +179,8 @@ public void Dispose() { _builder?.Dispose(); _httpClient?.Dispose(); - _downstreamBuilder?.Dispose(); + _downstreamBuilder?.Dispose(); + GC.SuppressFinalize(this); } private class ThreadSafeHeadersTestResult diff --git a/test/Ocelot.IntegrationTests/Usings.cs b/test/Ocelot.IntegrationTests/Usings.cs index ec84c76eb..504bb7314 100644 --- a/test/Ocelot.IntegrationTests/Usings.cs +++ b/test/Ocelot.IntegrationTests/Usings.cs @@ -9,6 +9,7 @@ // Project extra global namespaces global using Ocelot; +global using Ocelot.Testing; global using Shouldly; global using TestStack.BDDfy; global using Xunit; diff --git a/test/Ocelot.ManualTest/Program.cs b/test/Ocelot.ManualTest/Program.cs index 5337f6b7f..135e7e2c7 100644 --- a/test/Ocelot.ManualTest/Program.cs +++ b/test/Ocelot.ManualTest/Program.cs @@ -33,7 +33,7 @@ public static void Main(string[] args) x.Audience = "test"; });*/ - s.AddSingleton((x, t) => new FakeHandler()); + s.AddSingleton((x, t, z) => new FakeHandler()); s.AddOcelot() .AddDelegatingHandler(true); /*.AddCacheManager(x => diff --git a/test/Ocelot.Testing/Ocelot.Testing.csproj b/test/Ocelot.Testing/Ocelot.Testing.csproj new file mode 100644 index 000000000..fa27745f4 --- /dev/null +++ b/test/Ocelot.Testing/Ocelot.Testing.csproj @@ -0,0 +1,10 @@ + + + + 0.0.0-dev + net6.0;net7.0;net8.0 + enable + enable + + + diff --git a/test/Ocelot.Testing/PortFinder.cs b/test/Ocelot.Testing/PortFinder.cs new file mode 100644 index 000000000..4b661ada7 --- /dev/null +++ b/test/Ocelot.Testing/PortFinder.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; + +namespace Ocelot.Testing; + +public static class PortFinder +{ + private const int EndPortRange = 45000; + private static int CurrentPort = 20000; + private static readonly object LockObj = new(); + private static readonly ConcurrentBag UsedPorts = new(); + + /// + /// Gets a pseudo-random port from the range [, ]. + /// + /// New allocated port for testing scenario. + /// Critical situation where available ports range has been exceeded. + public static int GetRandomPort() + { + lock (LockObj) + { + if (CurrentPort > EndPortRange) + { + throw new ExceedingPortRangeException(); + } + + return UsePort(CurrentPort++); + } + } + + private static int UsePort(int port) + { + UsedPorts.Add(port); + + var ipe = new IPEndPoint(IPAddress.Loopback, port); + + using var socket = new Socket(ipe.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + socket.Bind(ipe); + socket.Close(); + return port; + } +} + +public class ExceedingPortRangeException : Exception +{ + public ExceedingPortRangeException() + : base("Cannot find available port to bind to!") { } +} diff --git a/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs index f9f2420f8..70438aed8 100644 --- a/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs @@ -66,7 +66,7 @@ public void should_log_error_if_cannot_parse_claim_to_thing() private void ThenTheLoggerIsCalledCorrectly() { _logger - .Verify(x => x.LogDebug(It.IsAny()), Times.Once); + .Verify(x => x.LogDebug(It.IsAny), Times.Once); } private void ThenClaimsToThingsAreReturned() diff --git a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs index 95e4cd91b..854f2abea 100644 --- a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs @@ -136,7 +136,7 @@ public void should_log_errors_and_not_add_headers() private void ThenTheLoggerIsCalledCorrectly(string message) { - _logger.Verify(x => x.LogWarning(message), Times.Once); + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == message)), Times.Once); } [Fact] diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs index b36029232..a6943b4ca 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs @@ -1,9 +1,12 @@ using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.Configuration.Validator; +using Ocelot.Logging; using Ocelot.Requester; using Ocelot.Responses; using Ocelot.ServiceDiscovery; @@ -11,7 +14,7 @@ using Ocelot.UnitTests.Requester; using Ocelot.Values; using System.Security.Claims; -using System.Text.Encodings.Web; +using System.Text.Encodings.Web; namespace Ocelot.UnitTests.Configuration.Validation { @@ -1536,8 +1539,8 @@ private void GivenTheAuthSchemeExists(string name) private void GivenAQoSHandler() { var collection = new ServiceCollection(); - QosDelegatingHandlerDelegate del = (a, b) => new FakeDelegatingHandler(); - collection.AddSingleton(del); + DelegatingHandler Del(DownstreamRoute a, IHttpContextAccessor b, IOcelotLoggerFactory c) => new FakeDelegatingHandler(); + collection.AddSingleton((QosDelegatingHandlerDelegate)Del); var provider = collection.BuildServiceProvider(); _configurationValidator = new FileConfigurationFluentValidator(provider, new RouteFluentValidator(_authProvider.Object, new HostAndPortValidator(), new FileQoSOptionsFluentValidator(provider)), new FileGlobalConfigurationFluentValidator(new FileQoSOptionsFluentValidator(provider))); } diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs index 9cc3a2d0e..69a7401e9 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs @@ -1,7 +1,10 @@ -using FluentValidation.Results; -using Microsoft.Extensions.DependencyInjection; +using FluentValidation.Results; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.Configuration.Validator; +using Ocelot.Logging; using Ocelot.Requester; namespace Ocelot.UnitTests.Configuration.Validation @@ -73,11 +76,8 @@ private void ThenTheResultIsInValid() private void GivenAQosDelegate() { - QosDelegatingHandlerDelegate fake = (a, b) => - { - return null; - }; - _services.AddSingleton(fake); + DelegatingHandler Fake(DownstreamRoute a, IHttpContextAccessor b, IOcelotLoggerFactory c) => null; + _services.AddSingleton((QosDelegatingHandlerDelegate)Fake); var provider = _services.BuildServiceProvider(); _validator = new FileQoSOptionsFluentValidator(provider); } diff --git a/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs index 7f1ee7ddd..accc5a94d 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs @@ -358,7 +358,7 @@ public void should_be_invalid_re_route_using_downstream_http_version(string vers this.Given(_ => GivenThe(fileRoute)) .When(_ => WhenIValidate()) .Then(_ => ThenTheResultIsInvalid()) - .And(_ => ThenTheErrorsContains("'Downstream Http Version' is not in the correct format.")) + .And(_ => ThenTheErrorsContains("'Downstream Http Version'")) // this error message changes depending on the OS language .BDDfy(); } @@ -396,7 +396,7 @@ private void ThenTheResultIsInvalid() private void ThenTheErrorsContains(string expected) { - _result.Errors.ShouldContain(x => x.ErrorMessage == expected); + _result.Errors.ShouldContain(x => x.ErrorMessage.Contains(expected)); } private class FakeAutheHandler : IAuthenticationHandler diff --git a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs index e00c8a0af..bf570142c 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs @@ -158,7 +158,7 @@ private void ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(params Ser { var service = entry.Service; var expected = $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."; - _logger.Verify(x => x.LogWarning(expected), Times.Once); + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == expected)), Times.Once); } } diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs similarity index 96% rename from test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs rename to test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs index da6b64e6f..b7ff203dd 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs @@ -1,21 +1,21 @@ using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; +using Ocelot.DownstreamUrlCreator; using Ocelot.Responses; using Ocelot.Values; -namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer +namespace Ocelot.UnitTests.DownstreamUrlCreator { - public class UpstreamUrlPathTemplateVariableReplacerTests + public class DownstreamPathPlaceholderReplacerTests { private DownstreamRouteHolder _downstreamRoute; private Response _result; private readonly IDownstreamPathPlaceholderReplacer _downstreamPathReplacer; - public UpstreamUrlPathTemplateVariableReplacerTests() + public DownstreamPathPlaceholderReplacerTests() { - _downstreamPathReplacer = new DownstreamTemplatePathPlaceholderReplacer(); + _downstreamPathReplacer = new DownstreamPathPlaceholderReplacer(); } [Fact] diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs index dafb3564b..febe8748c 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs @@ -3,8 +3,8 @@ using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.DownstreamUrlCreator; using Ocelot.DownstreamUrlCreator.Middleware; -using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; @@ -24,7 +24,7 @@ public class DownstreamUrlCreatorMiddlewareTests private readonly RequestDelegate _next; private readonly HttpRequestMessage _request; private readonly HttpContext _httpContext; - private Mock _repo; + private readonly Mock _repo; public DownstreamUrlCreatorMiddlewareTests() { @@ -39,7 +39,7 @@ public DownstreamUrlCreatorMiddlewareTests() } [Fact] - public void should_replace_scheme_and_path() + public void Should_replace_scheme_and_path() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") @@ -67,7 +67,7 @@ public void should_replace_scheme_and_path() } [Fact] - public void should_replace_query_string() + public void Should_replace_query_string() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") @@ -99,7 +99,7 @@ public void should_replace_query_string() } [Fact] - public void should_replace_query_string_but_leave_non_placeholder_queries() + public void Should_replace_query_string_but_leave_non_placeholder_queries() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") @@ -131,7 +131,7 @@ public void should_replace_query_string_but_leave_non_placeholder_queries() } [Fact] - public void should_replace_query_string_but_leave_non_placeholder_queries_2() + public void Should_replace_query_string_but_leave_non_placeholder_queries_2() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") @@ -163,7 +163,7 @@ public void should_replace_query_string_but_leave_non_placeholder_queries_2() } [Fact] - public void should_replace_query_string_exact_match() + public void Should_replace_query_string_exact_match() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates/{unitIdIty}") @@ -196,7 +196,7 @@ public void should_replace_query_string_exact_match() } [Fact] - public void should_not_create_service_fabric_url() + public void Should_not_create_service_fabric_url() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") @@ -226,7 +226,7 @@ public void should_not_create_service_fabric_url() } [Fact] - public void should_create_service_fabric_url() + public void Should_create_service_fabric_url() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamScheme("http") @@ -256,7 +256,7 @@ public void should_create_service_fabric_url() } [Fact] - public void should_create_service_fabric_url_with_query_string_for_stateless_service() + public void Should_create_service_fabric_url_with_query_string_for_stateless_service() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamScheme("http") @@ -286,7 +286,7 @@ public void should_create_service_fabric_url_with_query_string_for_stateless_ser } [Fact] - public void should_create_service_fabric_url_with_query_string_for_stateful_service() + public void Should_create_service_fabric_url_with_query_string_for_stateful_service() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamScheme("http") @@ -316,7 +316,7 @@ public void should_create_service_fabric_url_with_query_string_for_stateful_serv } [Fact] - public void should_create_service_fabric_url_with_version_from_upstream_path_template() + public void Should_create_service_fabric_url_with_version_from_upstream_path_template() { var downstreamRoute = new DownstreamRouteHolder( new List(), @@ -343,14 +343,16 @@ public void should_create_service_fabric_url_with_version_from_upstream_path_tem .BDDfy(); } - [Fact] - public void issue_473_should_not_remove_additional_query_string() + [Fact(DisplayName = "473: " + nameof(Should_not_remove_additional_query_parameter_when_placeholder_and_parameter_names_are_different))] + public void Should_not_remove_additional_query_parameter_when_placeholder_and_parameter_names_are_different() { + var methods = new List { "Post", "Get" }; var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("/Authorized/{action}?server={server}") - .WithUpstreamHttpMethod(new List { "Post", "Get" }) - .WithDownstreamScheme("http") - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("/uc/Authorized/{server}/{action}").Build()) + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/uc/Authorized/{servak}/{action}").Build()) + .WithDownstreamPathTemplate("/Authorized/{action}?server={servak}") + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) .Build(); var config = new ServiceProviderConfigurationBuilder() @@ -361,23 +363,22 @@ public void issue_473_should_not_remove_additional_query_string() new List { new("{action}", "1"), - new("{server}", "2"), + new("{servak}", "2"), }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Post", "Get" }) + new RouteBuilder().WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(methods) .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/uc/Authorized/2/1/refresh?refreshToken=2288356cfb1338fdc5ff4ca558ec785118dfe1ff2864340937da8226863ff66d")) + .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/uc/Authorized/2/1/refresh?refreshToken=123456789")) .And(x => GivenTheServiceProviderConfigIs(config)) .And(x => x.GivenTheUrlReplacerWillReturn("/Authorized/1?server=2")) .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:5000/Authorized/1?refreshToken=2288356cfb1338fdc5ff4ca558ec785118dfe1ff2864340937da8226863ff66d&server=2")) - .And(x => ThenTheQueryStringIs("?refreshToken=2288356cfb1338fdc5ff4ca558ec785118dfe1ff2864340937da8226863ff66d&server=2")) + .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:5000/Authorized/1?refreshToken=123456789&server=2")) + .And(x => ThenTheQueryStringIs("?refreshToken=123456789&server=2")) .BDDfy(); } [Fact] - public void should_not_replace_by_empty_scheme() + public void Should_not_replace_by_empty_scheme() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamScheme(string.Empty) @@ -406,6 +407,100 @@ public void should_not_replace_by_empty_scheme() .BDDfy(); } + [Fact(DisplayName = "952: " + nameof(Should_map_query_parameters_with_different_names))] + public void Should_map_query_parameters_with_different_names() + { + var methods = new List { "Post", "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/users?userId={userId}").Build()) + .WithDownstreamPathTemplate("/persons?personId={userId}") + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + var config = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenTheDownStreamRouteIs( + new DownstreamRouteHolder( + new List + { + new("{userId}", "webley"), + }, + new RouteBuilder().WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(methods) + .Build()))) + .And(x => x.GivenTheDownstreamRequestUriIs($"http://localhost:5000/users?userId=webley")) + .And(x => GivenTheServiceProviderConfigIs(config)) + .And(x => x.GivenTheUrlReplacerWillReturn("/persons?personId=webley")) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheDownstreamRequestUriIs($"http://localhost:5000/persons?personId=webley")) + .And(x => ThenTheQueryStringIs($"?personId=webley")) + .BDDfy(); + } + + [Fact(DisplayName = "952: " + nameof(Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ))] + public void Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ() + { + var methods = new List { "Post", "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/users?userId={uid}").Build()) + .WithDownstreamPathTemplate("/persons?personId={uid}") + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + var config = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenTheDownStreamRouteIs( + new DownstreamRouteHolder( + new List + { + new("{uid}", "webley"), + }, + new RouteBuilder().WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(methods) + .Build()))) + .And(x => x.GivenTheDownstreamRequestUriIs($"http://localhost:5000/users?userId=webley")) + .And(x => GivenTheServiceProviderConfigIs(config)) + .And(x => x.GivenTheUrlReplacerWillReturn("/persons?personId=webley")) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheDownstreamRequestUriIs($"http://localhost:5000/persons?personId=webley&userId=webley")) + .And(x => ThenTheQueryStringIs($"?personId=webley&userId=webley")) + .BDDfy(); + } + + [Theory(DisplayName = "1174: " + nameof(Should_forward_query_parameters_without_duplicates))] + [InlineData("projectNumber=45&startDate=2019-12-12&endDate=2019-12-12", "endDate=2019-12-12&projectNumber=45&startDate=2019-12-12")] + [InlineData("$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z", "$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z")] + public void Should_forward_query_parameters_without_duplicates(string everythingelse, string expectedOrdered) + { + var methods = new List { "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/contracts?{everythingelse}").Build()) + .WithDownstreamPathTemplate("/api/contracts?{everythingelse}") + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + var config = new ServiceProviderConfigurationBuilder().Build(); + this.Given(x => x.GivenTheDownStreamRouteIs( + new DownstreamRouteHolder( + new List + { + new("{everythingelse}", everythingelse), + }, + new RouteBuilder().WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(methods) + .Build()))) + .And(x => x.GivenTheDownstreamRequestUriIs($"http://localhost:5000//contracts?{everythingelse}")) + .And(x => GivenTheServiceProviderConfigIs(config)) + .And(x => x.GivenTheUrlReplacerWillReturn($"/api/contracts?{everythingelse}")) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheDownstreamRequestUriIs($"http://localhost:5000/api/contracts?{expectedOrdered}")) + .And(x => ThenTheQueryStringIs($"?{expectedOrdered}")) + .BDDfy(); + } + private void GivenTheServiceProviderConfigIs(ServiceProviderConfiguration config) { var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null); diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs index 86eec01f0..d4c75ed7e 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs @@ -70,7 +70,7 @@ public void should_overwrite_existing_header_with_added_header() private void ThenAnErrorIsLogged(string key, string value) { - _logger.Verify(x => x.LogWarning($"Unable to add header to response {key}: {value}"), Times.Once); + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == $"Unable to add header to response {key}: {value}")), Times.Once); } private void GivenHttpRequestWithoutHeaders() diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs index 824c270fa..4563965bb 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs @@ -98,7 +98,7 @@ public void should_do_nothing_and_log_error() private void ThenTheErrorIsLogged() { - _logger.Verify(x => x.LogWarning("Unable to add header to response Trace-Id: {TraceId}"), Times.Once); + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == "Unable to add header to response Trace-Id: {TraceId}")), Times.Once); } private void ThenTheHeaderIsNotAdded(string key) diff --git a/test/Ocelot.UnitTests/Logging/AspDotNetLoggerTests.cs b/test/Ocelot.UnitTests/Logging/AspDotNetLoggerTests.cs deleted file mode 100644 index 61a941b70..000000000 --- a/test/Ocelot.UnitTests/Logging/AspDotNetLoggerTests.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Microsoft.Extensions.Logging; -using Ocelot.Infrastructure.RequestData; -using Ocelot.Logging; - -namespace Ocelot.UnitTests.Logging -{ - public class AspDotNetLoggerTests - { - private readonly Mock> _coreLogger; - private readonly Mock _repo; - private readonly AspDotNetLogger _logger; - private readonly string _b; - private readonly string _a; - private readonly Exception _ex; - - public AspDotNetLoggerTests() - { - _a = "tom"; - _b = "laura"; - _ex = new Exception("oh no"); - _coreLogger = new Mock>(); - _repo = new Mock(); - _logger = new AspDotNetLogger(_coreLogger.Object, _repo.Object); - } - - [Fact] - public void should_log_trace() - { - _logger.LogTrace($"a message from {_a} to {_b}"); - - ThenLevelIsLogged("requestId: no request id, previousRequestId: no previous request id, message: a message from tom to laura", LogLevel.Trace); - } - - [Fact] - public void should_log_info() - { - _logger.LogInformation($"a message from {_a} to {_b}"); - - ThenLevelIsLogged("requestId: no request id, previousRequestId: no previous request id, message: a message from tom to laura", LogLevel.Information); - } - - [Fact] - public void should_log_warning() - { - _logger.LogWarning($"a message from {_a} to {_b}"); - - ThenLevelIsLogged("requestId: no request id, previousRequestId: no previous request id, message: a message from tom to laura", LogLevel.Warning); - } - - [Fact] - public void should_log_error() - { - _logger.LogError($"a message from {_a} to {_b}", _ex); - - ThenLevelIsLogged("requestId: no request id, previousRequestId: no previous request id, message: a message from tom to laura", LogLevel.Error, _ex); - } - - [Fact] - public void should_log_critical() - { - _logger.LogCritical($"a message from {_a} to {_b}", _ex); - - ThenLevelIsLogged("requestId: no request id, previousRequestId: no previous request id, message: a message from tom to laura", LogLevel.Critical, _ex); - } - - private void ThenLevelIsLogged(string expected, LogLevel expectedLogLevel, Exception ex = null) - { - _coreLogger.Verify( - x => x.Log( - expectedLogLevel, - default(EventId), - expected, - ex, - It.IsAny>()), Times.Once); - } - } -} diff --git a/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs b/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs index 274a102d1..d7ac54b64 100644 --- a/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs +++ b/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs @@ -82,7 +82,7 @@ private void GivenAMiddlewareName() private void ThenTheLogIs(string expected) { _logger.Verify( - x => x.LogTrace(expected)); + x => x.LogTrace(It.Is>(c => c.Invoke() == expected))); } } } diff --git a/test/Ocelot.UnitTests/Logging/OcelotLoggerTests.cs b/test/Ocelot.UnitTests/Logging/OcelotLoggerTests.cs new file mode 100644 index 000000000..0895e276e --- /dev/null +++ b/test/Ocelot.UnitTests/Logging/OcelotLoggerTests.cs @@ -0,0 +1,426 @@ +using Microsoft.Extensions.Logging; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; + +namespace Ocelot.UnitTests.Logging; + +public class OcelotLoggerTests +{ + private readonly Mock> _coreLogger; + private readonly OcelotLogger _logger; + private readonly string _b; + private readonly string _a; + private readonly Exception _ex; + + public OcelotLoggerTests() + { + _a = "tom"; + _b = "laura"; + _ex = new Exception("oh no"); + _coreLogger = new Mock>(); + _coreLogger.Setup(x => x.IsEnabled(It.IsAny())).Returns(true); + var repo = new Mock(); + _logger = new OcelotLogger(_coreLogger.Object, repo.Object); + } + + [Fact] + public void Should_log_trace() + { + _logger.LogTrace(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged( + "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'", + LogLevel.Trace); + } + + [Fact] + public void Should_log_info() + { + _logger.LogInformation(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged( + "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'", + LogLevel.Information); + } + + [Fact] + public void Should_log_warning() + { + _logger.LogWarning(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged( + "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'", + LogLevel.Warning); + } + + [Fact] + public void Should_log_error() + { + _logger.LogError(() => $"a message from {_a} to {_b}", _ex); + + ThenLevelIsLogged( + "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'", + LogLevel.Error, _ex); + } + + [Fact] + public void Should_log_critical() + { + _logger.LogCritical(() => $"a message from {_a} to {_b}", _ex); + + ThenLevelIsLogged( + "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'", + LogLevel.Critical, _ex); + } + + /// + /// Here mocking the original logger implementation to verify calls. + /// + /// The chosen minimum log level. + /// A mocked object. + private static Mock> MockLogger(LogLevel? minimumLevel) + { + var logger = LoggerFactory.Create(builder => + { + if (minimumLevel.HasValue) + { + builder + .AddSimpleConsole() + .SetMinimumLevel(minimumLevel.Value); + } + else + { + builder.AddSimpleConsole(); + } + }) + .CreateLogger>(); + + var mockedILogger = new Mock>(); + mockedILogger.Setup(x => x.IsEnabled(It.IsAny())) + .Returns(logger.IsEnabled) + .Verifiable(); + + return mockedILogger; + } + + [Fact] + public void If_minimum_log_level_not_set_then_log_is_called_for_information_and_above() + { + var mockedILogger = MockLogger(null); + var repo = new Mock(); + + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); + var expected = "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'"; + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug); + + currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); + + currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Information); + + currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Warning); + + var testException = new Exception("test"); + + currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException); + + currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); + } + + [Fact] + public void If_minimum_log_level_set_to_none_then_log_method_is_never_called() + { + var mockedILogger = MockLogger(LogLevel.None); + + var repo = new Mock(); + + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); + var expected = "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'"; + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug); + + currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); + + currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Information); + + currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Warning); + + var testException = new Exception("test"); + + currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Error, testException); + + currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Critical, testException); + } + + [Fact] + public void If_minimum_log_level_set_to_trace_then_log_is_called_for_trace_and_above() + { + var mockedILogger = MockLogger(LogLevel.Trace); + + var repo = new Mock(); + + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); + var expected = "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'"; + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Debug); + + currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Trace); + + currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Information); + + currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Warning); + + var testException = new Exception("test"); + + currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException); + + currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); + } + + [Fact] + public void String_func_is_never_called_when_log_level_is_disabled() + { + var mockedFunc = new Mock>(); + mockedFunc.Setup(x => x.Invoke()).Returns("test").Verifiable(); + var mockedILogger = MockLogger(LogLevel.None); + var repo = new Mock(); + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogTrace(mockedFunc.Object); + + mockedFunc.Verify(x => x.Invoke(), Times.Never); + } + + [Fact] + public void String_func_is_called_once_when_log_level_is_enabled() + { + var mockedFunc = new Mock>(); + mockedFunc.Setup(x => x.Invoke()).Returns("test").Verifiable(); + var mockedILogger = MockLogger(LogLevel.Information); + var repo = new Mock(); + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogInformation(mockedFunc.Object); + + mockedFunc.Verify(x => x.Invoke(), Times.Once); + } + + [Fact] + public void If_minimum_log_level_set_to_debug_then_log_is_called_for_debug_and_above() + { + var mockedILogger = MockLogger(LogLevel.Debug); + + var repo = new Mock(); + + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); + var expected = "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'"; + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Debug); + + currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); + + currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Information); + + currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Warning); + + var testException = new Exception("test"); + + currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException); + + currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); + } + + [Fact] + public void If_minimum_log_level_set_to_warning_then_log_is_called_for_warning_and_above() + { + var mockedILogger = MockLogger(LogLevel.Warning); + + var repo = new Mock(); + + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); + var expected = "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'"; + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug); + + currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); + + currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Information); + + currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Warning); + + var testException = new Exception("test"); + + currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException); + + currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); + } + + [Fact] + public void If_minimum_log_level_set_to_error_then_log_is_called_for_error_and_above() + { + var mockedILogger = MockLogger(LogLevel.Error); + + var repo = new Mock(); + + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); + var expected = "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'"; + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug); + + currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); + + currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Information); + + currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Warning); + + var testException = new Exception("test"); + + currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException); + + currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); + } + + [Fact] + public void If_minimum_log_level_set_to_critical_then_log_is_called_for_critical_and_above() + { + var mockedILogger = MockLogger(LogLevel.Critical); + + var repo = new Mock(); + + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); + var expected = "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'"; + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug); + + currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); + + currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Information); + + currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Warning); + + var testException = new Exception("test"); + + currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Error, testException); + + currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); + } + + private void ThenLevelIsLogged(string expected, LogLevel expectedLogLevel, Exception ex = null) + { + _coreLogger.Verify( + x => x.Log( + expectedLogLevel, + default, + expected, + ex, + It.IsAny>()), Times.Once); + } + + private static void ThenLevelIsLogged(Mock> logger, string expected, LogLevel expectedLogLevel, Exception ex = null) + { + logger.Verify( + x => x.Log( + expectedLogLevel, + default, + expected, + ex, + It.IsAny>()), Times.Once); + } + + private static void ThenLevelIsNotLogged(Mock> logger, string expected, LogLevel expectedLogLevel, Exception ex = null) + { + var result = logger.Object.IsEnabled(expectedLogLevel); + + logger.Verify( + x => x.Log( + expectedLogLevel, + default, + expected, + ex, + It.IsAny>()), Times.Never); + } +} diff --git a/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs index d3e142479..0cd2147c3 100644 --- a/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs +++ b/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs @@ -117,28 +117,28 @@ public Task Invoke(HttpContext context, IServiceProvider serviceProvider) internal class FakeLogger : IOcelotLogger { - public void LogCritical(string message, Exception exception) - { - } + public void LogCritical(string message, Exception exception) { } - public void LogDebug(string message) - { - } + public void LogCritical(Func messageFactory, Exception exception) { } - public void LogError(string message, Exception exception) - { - } + public void LogError(string message, Exception exception) { } - public void LogInformation(string message) - { - } + public void LogError(Func messageFactory, Exception exception) { } - public void LogTrace(string message) - { - } + public void LogDebug(string message) { } - public void LogWarning(string message) - { - } + public void LogDebug(Func messageFactory) { } + + public void LogInformation(string message) { } + + public void LogInformation(Func messageFactory) { } + + public void LogWarning(string message) { } + + public void LogTrace(string message) { } + + public void LogTrace(Func messageFactory) { } + + public void LogWarning(Func messageFactory) { } } } diff --git a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj index c54b547a4..cb2557562 100644 --- a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +++ b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj @@ -34,6 +34,7 @@ + diff --git a/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs index 4cdd930fa..81147b56a 100644 --- a/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration.Builder; using Ocelot.DependencyInjection; @@ -13,7 +14,8 @@ public class OcelotBuilderExtensionsTests [Fact] public void Should_build() { - var loggerFactory = new Mock(); + var loggerFactory = new Mock(); + var contextAccessor = new Mock(); var services = new ServiceCollection(); var options = new QoSOptionsBuilder() .WithTimeoutValue(100) @@ -34,7 +36,7 @@ public void Should_build() var handler = provider.GetService(); handler.ShouldNotBeNull(); - var delgatingHandler = handler(route, loggerFactory.Object); + var delgatingHandler = handler(route, contextAccessor.Object, loggerFactory.Object); delgatingHandler.ShouldNotBeNull(); } } diff --git a/test/Ocelot.UnitTests/Polly/PollyCircuitBreakingDelegatingHandlerTests.cs b/test/Ocelot.UnitTests/Polly/PollyCircuitBreakingDelegatingHandlerTests.cs deleted file mode 100644 index 421ee594d..000000000 --- a/test/Ocelot.UnitTests/Polly/PollyCircuitBreakingDelegatingHandlerTests.cs +++ /dev/null @@ -1,153 +0,0 @@ -using Moq; -using Ocelot.Logging; -using Ocelot.Provider.Polly; -using Ocelot.Provider.Polly.Interfaces; -using Polly; -using Polly.Wrap; -using Shouldly; -using System; -using System.Net; -using System.Net.Http; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace Ocelot.UnitTests.Polly; - -public class PollyCircuitBreakingDelegatingHandlerTests -{ - private readonly Mock pollyQoSProviderMock; - private readonly Mock loggerFactoryMock; - private readonly Mock loggerMock; - - private readonly PollyCircuitBreakingDelegatingHandler sut; - - public PollyCircuitBreakingDelegatingHandlerTests() - { - pollyQoSProviderMock = new Mock(); - - loggerFactoryMock = new Mock(); - loggerMock = new Mock(); - - loggerFactoryMock.Setup(x => x.CreateLogger()) - .Returns(loggerMock.Object); - loggerMock.Setup(x => x.LogError(It.IsAny(), It.IsAny())); - - sut = new PollyCircuitBreakingDelegatingHandler(pollyQoSProviderMock.Object, loggerFactoryMock.Object); - } - - [Fact] - public async void SendAsync_OnePolicy_NoWrapping() - { - // Arrange - var fakeResponse = new HttpResponseMessage(HttpStatusCode.NoContent); - fakeResponse.Headers.Add("X-Xunit", nameof(SendAsync_OnePolicy_NoWrapping)); - - MethodInfo method = null; - var onePolicy = new Mock(); - onePolicy.Setup(x => x.ExecuteAsync(It.IsAny>>())) - .Callback((IInvocation x) => method = x.Method) - .ReturnsAsync(fakeResponse); - - pollyQoSProviderMock.SetupGet(x => x.CircuitBreaker) - .Returns(new CircuitBreaker(onePolicy.Object)); - - // Act - var actual = await InvokeAsync("SendAsync"); - - // Assert - ShouldHaveXunitHeaderWithNoContent(actual, nameof(SendAsync_OnePolicy_NoWrapping)); - method.DeclaringType.Name.ShouldBe(nameof(IAsyncPolicy)); - method.DeclaringType.ShouldNotBeOfType(); - } - - [Fact] - public async void SendAsync_TwoPolicies_HaveWrapped() - { - // Arrange - var fakeResponse = new HttpResponseMessage(HttpStatusCode.NoContent); - fakeResponse.Headers.Add("X-Xunit", nameof(SendAsync_TwoPolicies_HaveWrapped)); - - var policy1 = new FakeAsyncPolicy("Policy1", fakeResponse); - var policy2 = new FakeAsyncPolicy("Policy2", fakeResponse) - { - IsLast = true, - }; - - pollyQoSProviderMock.SetupGet(x => x.CircuitBreaker) - .Returns(new CircuitBreaker(policy1, policy2)); - - // Act - var actual = await InvokeAsync("SendAsync"); - - // Assert - ShouldHaveXunitHeaderWithNoContent(actual, nameof(SendAsync_TwoPolicies_HaveWrapped)); - ShouldBeWrappedBy(policy1, typeof(AsyncPolicyWrap).FullName); - ShouldBeWrappedBy(policy2, typeof(AsyncPolicy).FullName); - } - - private static void ShouldHaveXunitHeaderWithNoContent(HttpResponseMessage actual, string headerName) - { - actual.ShouldNotBeNull(); - actual.StatusCode.ShouldBe(HttpStatusCode.NoContent); - actual.Headers.GetValues("X-Xunit").ShouldContain(headerName); - } - - private static void ShouldBeWrappedBy(FakeAsyncPolicy policy, string wrapperName) - { - policy.Called.ShouldBeTrue(); - policy.Times.ShouldBe(1); - policy.Method.ShouldNotBeNull(); - policy.Target.ShouldNotBeNull(); - policy.Method.DeclaringType?.DeclaringType.ShouldNotBeNull(); - policy.Method.DeclaringType.DeclaringType.FullName.ShouldContain(wrapperName); - policy.Target.ToString().ShouldContain(wrapperName); - } - - private async Task InvokeAsync(string methodName) - { - var m = typeof(PollyCircuitBreakingDelegatingHandler).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); - var task = (Task)m.Invoke(sut, new object[] { new HttpRequestMessage(), CancellationToken.None }); - var actual = await task; - return actual; - } - - internal class FakeAsyncPolicy : AsyncPolicy, IAsyncPolicy - { - public object Result { get; private set; } - public string Name { get; private set; } - - public int Times { get; protected set; } - public bool Called => Times > 0; - public MethodInfo Method { get; protected set; } - public object Target { get; protected set; } - - public bool IsLast { get; set; } - - public FakeAsyncPolicy(string name, object result) - { - Name = name; - Result = result; - } - - protected override async Task ImplementationAsync(Func> action, - Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) - { - Times++; - Method = action.Method; - Target = action.Target; - - if (IsLast) - { - TResult r = Result?.GetType() == typeof(TResult) - ? (TResult)Result - : Activator.CreateInstance(); - return r; - } - - TResult result = await action(context, cancellationToken); - return result; - } - } -} diff --git a/test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs b/test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs new file mode 100644 index 000000000..872fa635a --- /dev/null +++ b/test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs @@ -0,0 +1,255 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Provider.Polly; +using Ocelot.Provider.Polly.Interfaces; +using Polly; +using Polly.Wrap; +using System.Reflection; + +namespace Ocelot.UnitTests.Polly; + +public class PollyPoliciesDelegatingHandlerTests +{ + private readonly Mock> _pollyQoSProviderMock; + private readonly Mock _contextAccessorMock; + private readonly PollyPoliciesDelegatingHandler _sut; + + public PollyPoliciesDelegatingHandlerTests() + { + _pollyQoSProviderMock = new Mock>(); + + var loggerFactoryMock = new Mock(); + var loggerMock = new Mock(); + _contextAccessorMock = new Mock(); + + loggerFactoryMock.Setup(x => x.CreateLogger()) + .Returns(loggerMock.Object); + loggerMock.Setup(x => x.LogError(It.IsAny(), It.IsAny())); + + _sut = new PollyPoliciesDelegatingHandler(DownstreamRouteFactory(), _contextAccessorMock.Object, loggerFactoryMock.Object); + } + + [Fact] + public async void SendAsync_OnePolicy_NoWrapping() + { + // Arrange + var fakeResponse = new HttpResponseMessage(HttpStatusCode.NoContent); + fakeResponse.Headers.Add("X-Xunit", nameof(SendAsync_OnePolicy_NoWrapping)); + + MethodInfo method = null; + var onePolicy = new Mock>(); + onePolicy.Setup(x => x.ExecuteAsync(It.IsAny>>())) + .Callback((IInvocation x) => method = x.Method) + .ReturnsAsync(fakeResponse); + + _pollyQoSProviderMock.Setup(x => x.GetPollyPolicyWrapper(It.IsAny())) + .Returns(new PollyPolicyWrapper(onePolicy.Object)); + + var httpContext = new Mock(); + httpContext.Setup(x => x.RequestServices.GetService(typeof(IPollyQoSProvider))) + .Returns(_pollyQoSProviderMock.Object); + + _contextAccessorMock.Setup(x => x.HttpContext).Returns(httpContext.Object); + + // Act + var actual = await InvokeAsync("SendAsync"); + + // Assert + ShouldHaveXunitHeaderWithNoContent(actual, nameof(SendAsync_OnePolicy_NoWrapping)); + method.DeclaringType.Name.ShouldBe("IAsyncPolicy`1"); + method.DeclaringType.ShouldNotBeOfType(); + } + + [Fact] + public async void SendAsync_TwoPolicies_HaveWrapped() + { + // Arrange + var fakeResponse = new HttpResponseMessage(HttpStatusCode.NoContent); + fakeResponse.Headers.Add("X-Xunit", nameof(SendAsync_TwoPolicies_HaveWrapped)); + + var policy1 = new FakeAsyncPolicy("Policy1", fakeResponse); + var policy2 = new FakeAsyncPolicy("Policy2", fakeResponse) + { + IsLast = true, + }; + + _pollyQoSProviderMock.Setup(x => x.GetPollyPolicyWrapper(It.IsAny())) + .Returns(new PollyPolicyWrapper(policy1, policy2)); + + var httpContext = new Mock(); + httpContext.Setup(x => x.RequestServices.GetService(typeof(IPollyQoSProvider))) + .Returns(_pollyQoSProviderMock.Object); + + _contextAccessorMock.Setup(x => x.HttpContext).Returns(httpContext.Object); + + // Act + var actual = await InvokeAsync("SendAsync"); + + // Assert + ShouldHaveXunitHeaderWithNoContent(actual, nameof(SendAsync_TwoPolicies_HaveWrapped)); + ShouldBeWrappedBy(policy1, typeof(AsyncPolicyWrap).FullName); + ShouldBeWrappedBy(policy2, typeof(AsyncPolicy).FullName); + } + + private static DownstreamRoute DownstreamRouteFactory() + { + var options = new QoSOptionsBuilder() + .WithTimeoutValue(100) + .WithExceptionsAllowedBeforeBreaking(2) + .WithDurationOfBreak(200) + .Build(); + + var upstreamPath = new UpstreamPathTemplateBuilder() + .WithTemplate("/") + .WithContainsQueryString(false) + .WithPriority(1) + .WithOriginalValue("/").Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(options) + .WithUpstreamPathTemplate(upstreamPath).Build(); + + return route; + } + + private static void ShouldHaveXunitHeaderWithNoContent(HttpResponseMessage actual, string headerName) + { + actual.ShouldNotBeNull(); + actual.StatusCode.ShouldBe(HttpStatusCode.NoContent); + actual.Headers.GetValues("X-Xunit").ShouldContain(headerName); + } + + private static void ShouldBeWrappedBy(FakeAsyncPolicy policy, string wrapperName) + { + policy.Called.ShouldBeTrue(); + policy.Times.ShouldBe(1); + policy.Method.ShouldNotBeNull(); + policy.Target.ShouldNotBeNull(); + policy.Method.DeclaringType?.DeclaringType.ShouldNotBeNull(); + policy.Method.DeclaringType.DeclaringType.FullName.ShouldContain(wrapperName); + policy.Target.ToString().ShouldContain(wrapperName); + } + + private async Task InvokeAsync(string methodName) + { + var m = typeof(PollyPoliciesDelegatingHandler).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); + var task = (Task)m.Invoke(_sut, new object[] { new HttpRequestMessage(), CancellationToken.None }); + var actual = await task; + return actual; + } + + internal class FakeAsyncPolicy : AsyncPolicy, IAsyncPolicy + where TResult : class + { + public object Result { get; private set; } + public string Name { get; private set; } + + public int Times { get; protected set; } + public bool Called => Times > 0; + public MethodInfo Method { get; protected set; } + public object Target { get; protected set; } + + public bool IsLast { get; set; } + + public FakeAsyncPolicy(string name, object result) + { + Name = name; + Result = result; + } + + protected override async Task ImplementationAsync(Func> action, Context context, CancellationToken cancellationToken, + bool continueOnCapturedContext) + { + Times++; + Method = action.Method; + Target = action.Target; + + if (IsLast) + { + var r = Result?.GetType() == typeof(TResult) + ? (TResult)Result + : Activator.CreateInstance(); + return r; + } + + var result = await action(context, cancellationToken); + return result; + } + + public new IAsyncPolicy WithPolicyKey(string policyKey) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action, IDictionary contextData) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action, Context context) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action, IDictionary contextData, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action, Context context, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action, IDictionary contextData, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action, Context context) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action, IDictionary contextData) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action, IDictionary contextData, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action, Context context, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action, IDictionary contextData, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action, IDictionary contextData) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action, Context context) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action, IDictionary contextData, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action, Context context, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action, IDictionary contextData, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action, IDictionary contextData) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action, Context context) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action, IDictionary contextData, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action, Context context, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action, IDictionary contextData, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + } +} diff --git a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs index e04d56411..95b59757e 100644 --- a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs +++ b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs @@ -1,24 +1,243 @@ using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Provider.Polly; - -namespace Ocelot.UnitTests.Polly -{ - public class PollyQoSProviderTests - { - [Fact] - public void Should_build() - { - var options = new QoSOptionsBuilder() - .WithTimeoutValue(100) - .WithExceptionsAllowedBeforeBreaking(1) - .WithDurationOfBreak(200) - .Build(); - var route = new DownstreamRouteBuilder().WithQosOptions(options) - .Build(); - var factory = new Mock(); - var pollyQoSProvider = new PollyQoSProvider(route, factory.Object); - pollyQoSProvider.CircuitBreaker.ShouldNotBeNull(); - } - } +using Polly; +using Polly.CircuitBreaker; +using Polly.Timeout; +using Polly.Wrap; + +namespace Ocelot.UnitTests.Polly; + +public class PollyQoSProviderTests +{ + [Fact] + public void Should_build() + { + var options = new QoSOptionsBuilder() + .WithTimeoutValue(100) + .WithExceptionsAllowedBeforeBreaking(1) + .WithDurationOfBreak(200) + .Build(); + var route = new DownstreamRouteBuilder().WithQosOptions(options) + .Build(); + var factory = new Mock(); + var pollyQoSProvider = new PollyQoSProvider(factory.Object); + var policy = pollyQoSProvider.GetPollyPolicyWrapper(route).ShouldNotBeNull() + .AsyncPollyPolicy.ShouldNotBeNull(); + policy.ShouldNotBeNull(); + } + + [Fact] + public void should_build_and_wrap_contains_two_policies() + { + var pollyQosProvider = PollyQoSProviderFactory(); + var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider); + var policy = pollyPolicyWrapper.AsyncPollyPolicy; + + if (policy is AsyncPolicyWrap policyWrap) + { + policyWrap.ShouldNotBeNull(); + var policies = policyWrap.GetPolicies().ToList(); + + policies.Count.ShouldBe(2); + var circuitBreakerFound = false; + var timeoutPolicyFound = false; + + foreach(var currentPolicy in policies) + { + currentPolicy.ShouldNotBeNull(); + var convertedPolicy = (IAsyncPolicy)currentPolicy; + + switch (convertedPolicy) + { + case AsyncCircuitBreakerPolicy: + circuitBreakerFound = true; + continue; + case AsyncTimeoutPolicy: + timeoutPolicyFound = true; + break; + } + } + + Assert.True(circuitBreakerFound); + Assert.True(timeoutPolicyFound); + + return; + } + + Assert.Fail("policy is not AsyncPolicyWrap"); + } + + [Fact] + public void should_build_and_contains_one_policy_when_with_exceptions_allowed_before_breaking_is_zero() + { + var pollyQosProvider = PollyQoSProviderFactory(); + var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider, true); + var policy = pollyPolicyWrapper.AsyncPollyPolicy; + + if (policy is AsyncTimeoutPolicy convertedPolicy) + { + convertedPolicy.ShouldNotBeNull(); + return; + } + + Assert.Fail("policy is not AsyncTimeoutPolicy"); + } + + [Fact] + public void should_return_same_circuit_breaker_for_given_route() + { + var pollyQosProvider = PollyQoSProviderFactory(); + var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider); + var pollyPolicyWrapper2 = PolicyWrapperFactory("/", pollyQosProvider); + pollyPolicyWrapper.ShouldBe(pollyPolicyWrapper2); + } + + [Fact] + public void should_return_different_circuit_breaker_for_two_different_routes() + { + var pollyQosProvider = PollyQoSProviderFactory(); + var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider); + var pollyPolicyWrapper2 = PolicyWrapperFactory("/test", pollyQosProvider); + pollyPolicyWrapper.ShouldNotBe(pollyPolicyWrapper2); + } + + [Theory] + [InlineData(HttpStatusCode.InternalServerError)] + [InlineData(HttpStatusCode.NotImplemented)] + [InlineData(HttpStatusCode.BadGateway)] + [InlineData(HttpStatusCode.ServiceUnavailable)] + [InlineData(HttpStatusCode.GatewayTimeout)] + [InlineData(HttpStatusCode.HttpVersionNotSupported)] + [InlineData(HttpStatusCode.VariantAlsoNegotiates)] + [InlineData(HttpStatusCode.InsufficientStorage)] + [InlineData(HttpStatusCode.LoopDetected)] + public async Task should_throw_broken_circuit_exception_after_two_exceptions(HttpStatusCode errorCode) + { + var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); + + var response = new HttpResponseMessage(errorCode); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await Assert.ThrowsAsync>(async () => + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); + } + + [Fact] + public async Task should_not_throw_broken_circuit_exception_if_status_code_ok() + { + var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); + + var response = new HttpResponseMessage(HttpStatusCode.OK); + Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); + Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); + Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); + } + + [Fact] + public async Task should_throw_and_before_delay_should_not_allow_requests() + { + var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); + + var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await Assert.ThrowsAsync>(async () => + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); + + await Task.Delay(200); + + await Assert.ThrowsAsync>(async () => + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); + } + + [Fact] + public async Task should_throw_but_after_delay_should_allow_one_more_internal_server_error() + { + var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); + + var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await Assert.ThrowsAsync>(async () => + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); + + await Task.Delay(600); + + Assert.Equal(HttpStatusCode.InternalServerError, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); + } + + [Fact] + public async Task should_throw_but_after_delay_should_allow_one_more_internal_server_error_and_throw() + { + var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); + + var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await Assert.ThrowsAsync>(async () => + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); + + await Task.Delay(600); + + Assert.Equal(HttpStatusCode.InternalServerError, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); + await Assert.ThrowsAsync>(async () => + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); + } + + [Fact] + public async Task should_throw_but_after_delay_should_allow_one_more_ok_request_and_put_counter_back_to_zero() + { + var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); + + var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await Assert.ThrowsAsync>(async () => + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); + + await Task.Delay(600); + + var response2 = new HttpResponseMessage(HttpStatusCode.OK); + Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response2))).StatusCode); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await Assert.ThrowsAsync>(async () => + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); + } + + private PollyQoSProvider PollyQoSProviderFactory() + { + var factory = new Mock(); + factory.Setup(x => x.CreateLogger()) + .Returns(new Mock().Object); + + var pollyQoSProvider = new PollyQoSProvider(factory.Object); + return pollyQoSProvider; + } + + private static PollyPolicyWrapper PolicyWrapperFactory(string routeTemplate, PollyQoSProvider pollyQoSProvider, bool inactiveExceptionsAllowedBeforeBreaking = false) + { + var options = new QoSOptionsBuilder() + .WithTimeoutValue(5000) + .WithExceptionsAllowedBeforeBreaking(inactiveExceptionsAllowedBeforeBreaking ? 0 : 2) + .WithDurationOfBreak(300) + .Build(); + + var upstreamPath = new UpstreamPathTemplateBuilder() + .WithTemplate(routeTemplate) + .WithContainsQueryString(false) + .WithPriority(1) + .WithOriginalValue(routeTemplate).Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(options) + .WithUpstreamPathTemplate(upstreamPath).Build(); + + var pollyPolicyWrapper = pollyQoSProvider.GetPollyPolicyWrapper(route).ShouldNotBeNull(); + pollyPolicyWrapper.ShouldNotBeNull(); + pollyPolicyWrapper.AsyncPollyPolicy.ShouldNotBeNull(); + + return pollyPolicyWrapper; + } } diff --git a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs index de1adc2bf..d6f63f3b9 100644 --- a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs @@ -24,7 +24,7 @@ public class DelegatingHandlerHandlerProviderFactoryTests public DelegatingHandlerHandlerProviderFactoryTests() { - _qosDelegate = (a, b) => new FakeQoSHandler(); + _qosDelegate = (a, b, c) => new FakeQoSHandler(); _tracingFactory = new Mock(); _qosFactory = new Mock(); _loggerFactory = new Mock(); @@ -375,7 +375,7 @@ public void should_log_error_and_return_no_qos_provider_delegate_when_qos_factor private void ThenTheWarningIsLogged() { - _logger.Verify(x => x.LogWarning($"Route {_downstreamRoute.UpstreamPathTemplate} specifies use QoS but no QosHandler found in DI container. Will use not use a QosHandler, please check your setup!"), Times.Once); + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == $"Route {_downstreamRoute.UpstreamPathTemplate} specifies use QoS but no QosHandler found in DI container. Will use not use a QosHandler, please check your setup!")), Times.Once); } private void ThenHandlerAtPositionIs(int pos) diff --git a/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs b/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs index 505cbd70f..e352ee4a3 100644 --- a/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs @@ -284,7 +284,7 @@ private void GivenCacheIsCalledWithExpectedKey(string expectedKey) private void ThenTheDangerousAcceptAnyServerCertificateValidatorWarningIsLogged() { - _logger.Verify(x => x.LogWarning($"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamRoute, UpstreamPathTemplate: {_context.Items.DownstreamRoute().UpstreamPathTemplate}, DownstreamPathTemplate: {_context.Items.DownstreamRoute().DownstreamPathTemplate}"), Times.Once); + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == $"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamRoute, UpstreamPathTemplate: {_context.Items.DownstreamRoute().UpstreamPathTemplate}, DownstreamPathTemplate: {_context.Items.DownstreamRoute().DownstreamPathTemplate}")), Times.Once); } private void GivenTheClientIsCached() diff --git a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs index b4ed6535c..3dda1467c 100644 --- a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs @@ -104,7 +104,7 @@ private void WarningIsLogged() { _logger.Verify( x => x.LogWarning( - It.IsAny() + It.IsAny>() ), Times.Once); } @@ -113,7 +113,7 @@ private void InformationIsLogged() { _logger.Verify( x => x.LogInformation( - It.IsAny() + It.IsAny>() ), Times.Once); } diff --git a/test/Ocelot.UnitTests/Requester/QoSFactoryTests.cs b/test/Ocelot.UnitTests/Requester/QoSFactoryTests.cs index 4251befd4..a6a69c28f 100644 --- a/test/Ocelot.UnitTests/Requester/QoSFactoryTests.cs +++ b/test/Ocelot.UnitTests/Requester/QoSFactoryTests.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Logging; @@ -11,14 +12,16 @@ public class QoSFactoryTests { private QoSFactory _factory; private ServiceCollection _services; - private readonly Mock _loggerFactory; + private readonly Mock _loggerFactory; + private readonly Mock _contextAccessor; public QoSFactoryTests() { _services = new ServiceCollection(); - _loggerFactory = new Mock(); + _loggerFactory = new Mock(); + _contextAccessor = new Mock(); var provider = _services.BuildServiceProvider(); - _factory = new QoSFactory(provider, _loggerFactory.Object); + _factory = new QoSFactory(provider, _contextAccessor.Object, _loggerFactory.Object); } [Fact] @@ -34,10 +37,10 @@ public void should_return_error() public void should_return_handler() { _services = new ServiceCollection(); - DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute a, IOcelotLoggerFactory b) => new FakeDelegatingHandler(); + DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute a, IHttpContextAccessor b, IOcelotLoggerFactory c) => new FakeDelegatingHandler(); _services.AddSingleton(QosDelegatingHandlerDelegate); var provider = _services.BuildServiceProvider(); - _factory = new QoSFactory(provider, _loggerFactory.Object); + _factory = new QoSFactory(provider, _contextAccessor.Object, _loggerFactory.Object); var downstreamRoute = new DownstreamRouteBuilder().Build(); var handler = _factory.Get(downstreamRoute); handler.IsError.ShouldBeFalse(); diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs index fd4fb72e5..465e06a2e 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs @@ -152,12 +152,12 @@ private void ThenTheResultIsError() _logInformationMessages.ShouldNotBeNull() .Count.ShouldBe(2); - _logger.Verify(x => x.LogInformation(It.IsAny()), + _logger.Verify(x => x.LogInformation(It.IsAny>()), Times.Exactly(2)); _logWarningMessages.ShouldNotBeNull() .Count.ShouldBe(1); - _logger.Verify(x => x.LogWarning(It.IsAny()), + _logger.Verify(x => x.LogWarning(It.IsAny>()), Times.Once()); } @@ -187,10 +187,10 @@ private void GivenTheRoute(ServiceProviderConfiguration serviceConfig, Downstrea private void WhenIGetTheServiceProvider() { - _logger.Setup(x => x.LogInformation(It.IsAny())) - .Callback(message => _logInformationMessages.Add(message)); - _logger.Setup(x => x.LogWarning(It.IsAny())) - .Callback(message => _logWarningMessages.Add(message)); + _logger.Setup(x => x.LogInformation(It.IsAny>())) + .Callback>(myFunc => _logInformationMessages.Add(myFunc.Invoke())); + _logger.Setup(x => x.LogWarning(It.IsAny>())) + .Callback>(myFunc => _logWarningMessages.Add(myFunc.Invoke())); _result = _factory.Get(_serviceConfig, _route); } diff --git a/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs b/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs index 2afd41c2f..d5190ce01 100644 --- a/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs @@ -68,8 +68,8 @@ private void GivenPropertyDangerousAcceptAnyServerCertificateValidator(bool enab _client.SetupSet(x => x.Options.RemoteCertificateValidationCallback = It.IsAny()) .Callback(actual.Add); - _logger.Setup(x => x.LogWarning(It.IsAny())) - .Callback(actual.Add); + _logger.Setup(x => x.LogWarning(It.IsAny>())) + .Callback>(y => actual.Add(y.Invoke())); } private void AndDoNotSetupProtocolsAndHeaders() @@ -111,7 +111,7 @@ private void ThenIgnoredAllSslWarnings(List actual) var request = _context.Object.Items.DownstreamRequest(); route.DangerousAcceptAnyServerCertificateValidator.ShouldBeTrue(); - _logger.Verify(x => x.LogWarning(It.IsAny()), Times.Once()); + _logger.Verify(x => x.LogWarning(It.IsAny>()), Times.Once()); var warning = actual.Last() as string; warning.ShouldNotBeNullOrEmpty(); var expectedWarning = string.Format(WebSocketsProxyMiddleware.IgnoredSslWarningFormat, route.UpstreamPathTemplate, route.DownstreamPathTemplate); @@ -153,8 +153,8 @@ private void GivenNonWebsocketScheme(string scheme, List actual) }; _context.SetupGet(x => x.Items).Returns(items); - _logger.Setup(x => x.LogWarning(It.IsAny())) - .Callback(actual.Add); + _logger.Setup(x => x.LogWarning(It.IsAny>())) + .Callback>(myFunc => actual.Add(myFunc.Invoke())); } private void ThenNonWsSchemesAreReplaced(string scheme, string expectedScheme, List actual) @@ -163,7 +163,7 @@ private void ThenNonWsSchemesAreReplaced(string scheme, string expectedScheme, L var request = _context.Object.Items.DownstreamRequest(); route.DangerousAcceptAnyServerCertificateValidator.ShouldBeFalse(); - _logger.Verify(x => x.LogWarning(It.IsAny()), Times.Once()); + _logger.Verify(x => x.LogWarning(It.IsAny>()), Times.Once()); var warning = actual.First() as string; warning.ShouldNotBeNullOrEmpty(); warning.ShouldContain($"'{scheme}'"); diff --git a/test/Ocelot.UnitTests/appsettings.json b/test/Ocelot.UnitTests/appsettings.json index 57566b4e3..e18d6ebb9 100644 --- a/test/Ocelot.UnitTests/appsettings.json +++ b/test/Ocelot.UnitTests/appsettings.json @@ -1,10 +1,8 @@ { "Logging": { - "IncludeScopes": true, "LogLevel": { - "Default": "Error", - "System": "Error", - "Microsoft": "Error" + "Default": "Trace", + "Microsoft.AspNetCore": "Trace" } }, "spring": {