diff --git a/.circleci/config.yml b/.circleci/config.yml index e6944a970..4a5f1d936 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,29 +1,43 @@ version: 2.1 +orbs: + queue: eddiewebb/queue@2.2.1 jobs: build: docker: - - image: mijitt0m/ocelot-build:0.0.1 + - image: mijitt0m/ocelot-build:0.0.9 steps: - checkout - - run: make build + - run: dotnet tool restore && dotnet cake release: docker: - - image: mijitt0m/ocelot-build:0.0.1 + - image: mijitt0m/ocelot-build:0.0.9 steps: - checkout - - run: make release + - run: dotnet tool restore && dotnet cake --target=Release workflows: version: 2 - master: + main: jobs: + - queue/block_workflow: + time: "20" + only-on-branch: main - release: + requires: + - queue/block_workflow filters: branches: - only: master + only: main + develop: + jobs: + - build: + filters: + branches: + only: develop pr: jobs: - build: filters: branches: ignore: - - master + - main + - develop diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 000000000..ce3369c2e --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "cake.tool": { + "version": "3.0.0", + "commands": [ + "dotnet-cake" + ] + }, + "coveralls.net": { + "version": "4.0.1", + "commands": [ + "csmacnz.Coveralls" + ] + } + } +} \ No newline at end of file diff --git a/.dockerignore b/.dockerignore index 54fec4f0a..aee45d8a2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,5 @@ -*/*/bin -*/*/obj +*/*/bin +*/*/obj +artifacts/ +TestResults/ +tools/ \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index 824353039..6aa3d30f4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,6 +4,7 @@ root = true end_of_line = lf insert_final_newline = true -[*.cs] +[*.cs] +end_of_line = lf indent_style = space indent_size = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..167f8eda1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# 2010 +*.txt -crlf + +# 2020 +*.txt text eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4980a426c..220172830 100644 --- a/.gitignore +++ b/.gitignore @@ -183,7 +183,7 @@ ClientBin/ *.dbmdl *.dbproj.schemaview *.pfx -!idsrv3test.pfx +!mycert.pfx *.publishsettings node_modules/ orleans.codegen.cs diff --git a/Makefile b/Makefile deleted file mode 100644 index a24d2a923..000000000 --- a/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -NAME ?= ocelot - -build: - ./build.sh - -build_and_run_tests: - ./build.sh --target=RunTests - -release: - ./build.sh --target=Release - -run_acceptance_tests: - ./build.sh --target=RunAcceptanceTests - -run_benchmarks: - ./build.sh --target=RunBenchmarkTests - -run_unit_tests: - ./build.sh --target=RunUnitTests - \ No newline at end of file diff --git a/Ocelot.sln b/Ocelot.sln index 603690596..b59c188a2 100644 --- a/Ocelot.sln +++ b/Ocelot.sln @@ -1,21 +1,23 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29613.14 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32112.339 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5CFB79B7-C9DC-45A4-9A75-625D92471702}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3FA7C349-DBE8-4904-A2CE-015B8869CE6C}" ProjectSection(SolutionItems) = preProject .dockerignore = .dockerignore + .editorconfig = .editorconfig .gitignore = .gitignore build.cake = build.cake build.ps1 = build.ps1 codeanalysis.ruleset = codeanalysis.ruleset + .circleci\config.yml = .circleci\config.yml GitVersion.yml = GitVersion.yml LICENSE.md = LICENSE.md README.md = README.md - ReleaseNotes.md = ReleaseNotes.md + releasenotes.md = releasenotes.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5B401523-36DA-4491-B73A-7590A26E420B}" @@ -42,15 +44,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Eureka", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Polly", "src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj", "{1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Rafty", "src\Ocelot.Provider.Rafty\Ocelot.Provider.Rafty.csproj", "{AC153C67-EF18-47E6-A230-F0D3CF5F0A98}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.Butterfly", "src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj", "{6045E23D-669C-4F27-AF8E-8EEE6DB3557F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Provider.Kubernetes", "src\Ocelot.Provider.Kubernetes\Ocelot.Provider.Kubernetes.csproj", "{72C8E528-B4F5-45CE-8A06-CD3787364856}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{8FA0CBA0-0338-48EB-B37F-83CA5022237C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotBasic", "samples\OcelotBasic\OcelotBasic.csproj", "{ED0B3A09-112B-4BA4-82D6-11569BC7A99B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OcelotBasic.ApiGateway", "samples\OcelotBasic\Ocelot.Samples.OcelotBasic.ApiGateway.csproj", "{ED0B3A09-112B-4BA4-82D6-11569BC7A99B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdministrationApi", "samples\AdministrationApi\AdministrationApi.csproj", "{B180F8AE-2F8F-44F9-9E5D-FE65B84B742E}" EndProject @@ -64,9 +64,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DownstreamService", "sample EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "k8s", "k8s", "{4B706988-4817-43A8-ABE1-32A67998C2C8}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiGateway", "samples\OcelotKube\ApiGateway\ApiGateway.csproj", "{8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OcelotKube.ApiGateway", "samples\OcelotKube\ApiGateway\Ocelot.Samples.OcelotKube.ApiGateway.csproj", "{8500055B-2C51-4CF1-A6EE-F05BB3E9BF16}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DownstreamService", "samples\OcelotKube\DownstreamService\DownstreamService.csproj", "{7B319B8C-8155-4779-BD93-5ABD05CA2AB6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.OcelotKube.DownstreamService", "samples\OcelotKube\DownstreamService\Ocelot.Samples.OcelotKube.DownstreamService.csproj", "{7B319B8C-8155-4779-BD93-5ABD05CA2AB6}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "service-fabric", "service-fabric", "{B412628F-C325-47E1-A8D9-873DE04C8AF5}" EndProject @@ -80,6 +80,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "basic", "basic", "{ED066001 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "graphql", "graphql", "{C15CD120-5F8D-41DE-9B21-00E3EA77D6C1}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.OpenTracing", "src\Ocelot.Tracing.OpenTracing\Ocelot.Tracing.OpenTracing.csproj", "{11C622AD-8C0A-4CF4-811B-3DBB76550797}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "open-tracing", "open-tracing", "{731C6A8A-69ED-445C-A132-C638AA93F9C7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotOpenTracing", "samples\OcelotOpenTracing\OcelotOpenTracing.csproj", "{C9427E78-4281-4F59-A66E-17C0B66550E5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -130,10 +136,6 @@ Global {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Debug|Any CPU.Build.0 = Debug|Any CPU {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Release|Any CPU.ActiveCfg = Release|Any CPU {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E}.Release|Any CPU.Build.0 = Release|Any CPU - {AC153C67-EF18-47E6-A230-F0D3CF5F0A98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AC153C67-EF18-47E6-A230-F0D3CF5F0A98}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AC153C67-EF18-47E6-A230-F0D3CF5F0A98}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AC153C67-EF18-47E6-A230-F0D3CF5F0A98}.Release|Any CPU.Build.0 = Release|Any CPU {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Debug|Any CPU.Build.0 = Debug|Any CPU {6045E23D-669C-4F27-AF8E-8EEE6DB3557F}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -178,6 +180,14 @@ Global {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Debug|Any CPU.Build.0 = Debug|Any CPU {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Release|Any CPU.ActiveCfg = Release|Any CPU {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Release|Any CPU.Build.0 = Release|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Release|Any CPU.Build.0 = Release|Any CPU + {C9427E78-4281-4F59-A66E-17C0B66550E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9427E78-4281-4F59-A66E-17C0B66550E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9427E78-4281-4F59-A66E-17C0B66550E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9427E78-4281-4F59-A66E-17C0B66550E5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -194,7 +204,6 @@ Global {02F5AE4D-9C36-4E58-B7C6-012CBBDEFDE0} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {9BBD3586-145C-4FA0-91C5-9ED58287D753} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {1F6E5DCF-8A2E-4E24-A25D-064362DE8D0E} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} - {AC153C67-EF18-47E6-A230-F0D3CF5F0A98} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {6045E23D-669C-4F27-AF8E-8EEE6DB3557F} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {72C8E528-B4F5-45CE-8A06-CD3787364856} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {ED0B3A09-112B-4BA4-82D6-11569BC7A99B} = {ED066001-BAF7-4117-9884-DF591A56347D} @@ -212,6 +221,9 @@ Global {1F1F324D-6EA4-4E63-A6A7-C6053F412F1A} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} {ED066001-BAF7-4117-9884-DF591A56347D} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} {C15CD120-5F8D-41DE-9B21-00E3EA77D6C1} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {11C622AD-8C0A-4CF4-811B-3DBB76550797} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {731C6A8A-69ED-445C-A132-C638AA93F9C7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {C9427E78-4281-4F59-A66E-17C0B66550E5} = {731C6A8A-69ED-445C-A132-C638AA93F9C7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48} diff --git a/README.md b/README.md index a655d3d28..bfbafbb48 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,21 @@ -[](https://threemammals.com/ocelot) +![Ocelot Logo](/images/ocelot_logo.png) -[![CircleCI](https://circleci.com/gh/ThreeMammals/Ocelot/tree/master.svg?style=svg)](https://circleci.com/gh/ThreeMammals/Ocelot/tree/master) +[![CircleCI](https://circleci.com/gh/ThreeMammals/Ocelot/tree/main.svg?style=svg)](https://circleci.com/gh/ThreeMammals/Ocelot/tree/main) -[![Coverage Status](https://coveralls.io/repos/github/ThreeMammals/Ocelot/badge.svg?branch=master)](https://coveralls.io/github/ThreeMammals/Ocelot?branch=master) - -[Slack](threemammals.slack.com) +[![Coverage Status](https://coveralls.io/repos/github/ThreeMammals/Ocelot/badge.svg)](https://coveralls.io/github/ThreeMammals/Ocelot) # Ocelot -Ocelot is a .NET API Gateway. This project is aimed at people using .NET running -a micro services / service oriented architecture +Ocelot is a .NET API Gateway. This project is aimed at people using .NET running a micro services / service oriented architecture that need a unified point of entry into their system. However it will work with anything that speaks HTTP and run on any platform that ASP.NET Core supports. -In particular I want easy integration with -IdentityServer reference and bearer tokens. +In particular I want easy integration with IdentityServer reference and bearer tokens. -We have been unable to find this in my current workplace -without having to write our own Javascript middlewares -to handle the IdentityServer reference tokens. We would -rather use the IdentityServer code that already exists -to do this. +We have been unable to find this in my current workplace without having to write our own Javascript middlewares to handle the IdentityServer reference tokens. We would rather use the IdentityServer code that already exists to do this. Ocelot is a bunch of middlewares in a specific order. -Ocelot manipulates the HttpRequest object into a state specified by its configuration until -it reaches a request builder middleware where it creates a HttpRequestMessage object which is -used to make a request to a downstream service. The middleware that makes the request is -the last thing in the Ocelot pipeline. It does not call the next middleware. -The response from the downstream service is retrieved as the requests goes back up the Ocelot pipeline. -There is a piece of middleware that maps the HttpResponseMessage onto the HttpResponse object and that -is returned to the client. That is basically it with a bunch of other features! +Ocelot manipulates the HttpRequest object into a state specified by its configuration until it reaches a request builder middleware where it creates a HttpRequestMessage object which is used to make a request to a downstream service. The middleware that makes the request is the last thing in the Ocelot pipeline. It does not call the next middleware. The response from the downstream service is retrieved as the requests goes back up the Ocelot pipeline. There is a piece of middleware that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client. That is basically it with a bunch of other features! ## Features @@ -42,22 +28,22 @@ A quick list of Ocelot's capabilities for more information see the [documentatio * Kubernetes * WebSockets * Authentication -* Authorisation +* Authorization * Rate Limiting * Caching * Retry policies / QoS * Load Balancing * Logging / Tracing / Correlation -* Headers / Query String / Claims Transformation +* Headers / Method / Query String / Claims Transformation * Custom Middleware / Delegating Handlers * Configuration / Administration REST API * Platform / Cloud Agnostic ## How to install -Ocelot is designed to work with ASP.NET Core only and it targets `netstandard2.0`. This means it can be used anywhere `.NET Standard 2.0` is supported, including `.NET Core 3.1` and `.NET Framework 4.8` and up. [This](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) documentation may prove helpful when working out if Ocelot would be suitable for you. +Ocelot is designed to work with ASP.NET and it targets `net7.0`. -Install Ocelot and it's dependencies using NuGet. +Install Ocelot and it's dependencies using NuGet. `Install-Package Ocelot` @@ -81,18 +67,16 @@ We love to receive contributions from the community so please keep them coming : Pull requests, issues and commentary welcome! -Please complete the relevant template for issues and PRs. Sometimes it's worth getting in touch with us to discuss changes -before doing any work incase this is something we are already doing or it might not make sense. We can also give -advice on the easiest way to do things :) +Please complete the relevant template for issues and PRs. Sometimes it's worth getting in touch with us to discuss changes before doing any work incase this is something we are already doing or it might not make sense. We can also give advice on the easiest way to do things :) Finally we mark all existing issues as help wanted, small, medium and large effort. If you want to contribute for the first time I suggest looking at a help wanted & small effort issue :) ## Donate -If you think this project is worth supporting financially please make a contribution using the button below! +If you think this project is worth supporting financially please make a contribution using the button below! We use the money to run the https://threemammals.com website. [![Support via PayPal](https://cdn.rawgit.com/twolfson/paypal-github-button/1.0.0/dist/button.svg)](https://www.paypal.me/ThreeMammals/) ## Things that are currently annoying me -[![](https://codescene.io/projects/697/status.svg) Get more details at **codescene.io**.](https://codescene.io/projects/697/jobs/latest-successful/results) +[![](https://codescene.io/projects/697/status.svg) Get more details at **codescene.io**.](https://codescene.io/projects/697/jobs/latest-successful/results) \ No newline at end of file diff --git a/ReleaseNotes.md b/ReleaseNotes.md deleted file mode 100644 index a647c6c30..000000000 --- a/ReleaseNotes.md +++ /dev/null @@ -1 +0,0 @@ -No issues closed since last release \ No newline at end of file diff --git a/build.cake b/build.cake index 3d4626bdb..67d144808 100644 --- a/build.cake +++ b/build.cake @@ -1,500 +1,573 @@ -#tool "nuget:?package=GitVersion.CommandLine&version=5.0.1" -#tool "nuget:?package=GitReleaseNotes" -#addin nuget:?package=Cake.Json -#addin nuget:?package=Newtonsoft.Json -#addin nuget:?package=System.Net.Http -#tool "nuget:?package=ReportGenerator" -#tool "nuget:?package=coveralls.net&version=0.7.0" -#addin Cake.Coveralls&version=0.10.1 - -// compile -var compileConfig = Argument("configuration", "Release"); - -var slnFile = "./Ocelot.sln"; - -// build artifacts -var artifactsDir = Directory("artifacts"); - -// unit testing -var artifactsForUnitTestsDir = artifactsDir + Directory("UnitTests"); -var unitTestAssemblies = @"./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj"; -var minCodeCoverage = 80d; -var coverallsRepoToken = "OCELOT_COVERALLS_TOKEN"; -var coverallsRepo = "https://coveralls.io/github/ThreeMammals/Ocelot"; - -// acceptance testing -var artifactsForAcceptanceTestsDir = artifactsDir + Directory("AcceptanceTests"); -var acceptanceTestAssemblies = @"./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj"; - -// integration testing -var artifactsForIntegrationTestsDir = artifactsDir + Directory("IntegrationTests"); -var integrationTestAssemblies = @"./test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj"; - -// benchmark testing -var artifactsForBenchmarkTestsDir = artifactsDir + Directory("BenchmarkTests"); -var benchmarkTestAssemblies = @"./test/Ocelot.Benchmarks"; - -// packaging -var packagesDir = artifactsDir + Directory("Packages"); -var releaseNotesFile = packagesDir + File("releasenotes.md"); -var artifactsFile = packagesDir + File("artifacts.txt"); - -// stable releases -var tagsUrl = "https://api.github.com/repos/ThreeMammals/ocelot/releases/tags/"; -var nugetFeedStableKey = EnvironmentVariable("OCELOT_NUTGET_API_KEY"); -var nugetFeedStableUploadUrl = "https://www.nuget.org/api/v2/package"; -var nugetFeedStableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package"; - -// internal build variables - don't change these. -string committedVersion = "0.0.0-dev"; -GitVersion versioning = null; -int releaseId = 0; -string gitHubUsername = "TomPallister"; -string gitHubPassword = Environment.GetEnvironmentVariable("OCELOT_GITHUB_API_KEY"); - -var target = Argument("target", "Default"); - -Information("target is " + target); -Information("Build configuration is " + compileConfig); - -Task("Default") - .IsDependentOn("Build"); - -Task("Build") - .IsDependentOn("RunTests"); - -Task("RunTests") - .IsDependentOn("RunUnitTests") - .IsDependentOn("RunAcceptanceTests") - .IsDependentOn("RunIntegrationTests"); - -Task("Release") - .IsDependentOn("Build") - .IsDependentOn("CreateArtifacts") - .IsDependentOn("PublishGitHubRelease") - .IsDependentOn("PublishToNuget"); - -Task("Compile") - .IsDependentOn("Clean") - .IsDependentOn("Version") - .Does(() => - { - var settings = new DotNetCoreBuildSettings - { - Configuration = compileConfig, - }; - - DotNetCoreBuild(slnFile, settings); - }); - -Task("Clean") - .Does(() => - { - if (DirectoryExists(artifactsDir)) - { - DeleteDirectory(artifactsDir, recursive:true); - } - CreateDirectory(artifactsDir); - }); - -Task("Version") - .Does(() => - { - versioning = GetNuGetVersionForCommit(); - var nugetVersion = versioning.NuGetVersion; - Information("SemVer version number: " + nugetVersion); - - if (IsRunningOnCircleCI()) - { - Information("Persisting version number..."); - PersistVersion(committedVersion, nugetVersion); - } - else - { - Information("We are not running on build server, so we won't persist the version number."); - } - }); - -Task("RunUnitTests") - .IsDependentOn("Compile") - .Does(() => - { - var testSettings = new DotNetCoreTestSettings - { - Configuration = compileConfig, - ResultsDirectory = artifactsForUnitTestsDir, - ArgumentCustomization = args => args - // this create the code coverage report - .Append("--settings test/Ocelot.UnitTests/UnitTests.runsettings") - }; - - EnsureDirectoryExists(artifactsForUnitTestsDir); - DotNetCoreTest(unitTestAssemblies, testSettings); - - var coverageSummaryFile = GetSubDirectories(artifactsForUnitTestsDir).First().CombineWithFilePath(File("coverage.opencover.xml")); - Information(coverageSummaryFile); - Information(artifactsForUnitTestsDir); - // todo bring back report generator to get a friendly report - // ReportGenerator(coverageSummaryFile, artifactsForUnitTestsDir); - // https://github.com/danielpalme/ReportGenerator - - if (IsRunningOnCircleCI()) - { - var repoToken = EnvironmentVariable(coverallsRepoToken); - if (string.IsNullOrEmpty(repoToken)) - { - throw new Exception(string.Format("Coveralls repo token not found. Set environment variable '{0}'", coverallsRepoToken)); - } - - Information(string.Format("Uploading test coverage to {0}", coverallsRepo)); - CoverallsNet(coverageSummaryFile, CoverallsNetReportType.OpenCover, new CoverallsNetSettings() - { - RepoToken = repoToken - }); - } - else - { - Information("We are not running on the build server so we won't publish the coverage report to coveralls.io"); - } - - var sequenceCoverage = XmlPeek(coverageSummaryFile, "//CoverageSession/Summary/@sequenceCoverage"); - var branchCoverage = XmlPeek(coverageSummaryFile, "//CoverageSession/Summary/@branchCoverage"); - - Information("Sequence Coverage: " + sequenceCoverage); - - if(double.Parse(sequenceCoverage) < minCodeCoverage) - { - var whereToCheck = !IsRunningOnCircleCI() ? coverallsRepo : artifactsForUnitTestsDir; - throw new Exception(string.Format("Code coverage fell below the threshold of {0}%. You can find the code coverage report at {1}", minCodeCoverage, whereToCheck)); - }; - }); - -Task("RunAcceptanceTests") - .IsDependentOn("Compile") - .Does(() => - { - var settings = new DotNetCoreTestSettings - { - Configuration = compileConfig, - ArgumentCustomization = args => args - .Append("--no-restore") - .Append("--no-build") - }; - - EnsureDirectoryExists(artifactsForAcceptanceTestsDir); - DotNetCoreTest(acceptanceTestAssemblies, settings); - }); - -Task("RunIntegrationTests") - .IsDependentOn("Compile") - .Does(() => - { - var settings = new DotNetCoreTestSettings - { - Configuration = compileConfig, - ArgumentCustomization = args => args - .Append("--no-restore") - .Append("--no-build") - }; - - EnsureDirectoryExists(artifactsForIntegrationTestsDir); - DotNetCoreTest(integrationTestAssemblies, settings); - }); - -Task("CreateArtifacts") - .IsDependentOn("Compile") - .Does(() => - { - EnsureDirectoryExists(packagesDir); - - CopyFiles("./src/**/Release/Ocelot.*.nupkg", packagesDir); - - // todo fix this for docker build - //GenerateReleaseNotes(releaseNotesFile); - - var projectFiles = GetFiles("./src/**/Release/Ocelot.*.nupkg"); - - foreach(var projectFile in projectFiles) - { - System.IO.File.AppendAllLines(artifactsFile, new[]{ - projectFile.GetFilename().FullPath, - // todo fix this for docker build - //"releaseNotes:releasenotes.md" - }); - } - - var artifacts = System.IO.File - .ReadAllLines(artifactsFile) - .Distinct(); - - foreach(var artifact in artifacts) - { - var codePackage = packagesDir + File(artifact); - - Information("Created package " + codePackage); - } - }); - -Task("PublishGitHubRelease") - .IsDependentOn("CreateArtifacts") - .Does(() => - { - if (IsRunningOnCircleCI()) - { - var path = packagesDir.ToString() + @"/**/*"; - - CreateGitHubRelease(); - - foreach (var file in GetFiles(path)) - { - UploadFileToGitHubRelease(file); - } - - CompleteGitHubRelease(); - } - }); - -Task("EnsureStableReleaseRequirements") - .Does(() => - { - Information("Check if stable release..."); - - if (!IsRunningOnCircleCI()) - { - throw new Exception("Stable release should happen via circleci"); - } - - Information("Release is stable..."); - }); - -Task("DownloadGitHubReleaseArtifacts") - .Does(() => - { - - try - { - EnsureDirectoryExists(packagesDir); - - var releaseUrl = tagsUrl + versioning.NuGetVersion; - - var assets_url = Newtonsoft.Json.Linq.JObject.Parse(GetResource(releaseUrl)) - .Value("assets_url"); - - var assets = GetResource(assets_url); - - foreach(var asset in Newtonsoft.Json.JsonConvert.DeserializeObject(assets)) - { - var file = packagesDir + File(asset.Value("name")); - DownloadFile(asset.Value("browser_download_url"), file); - } - } - catch(Exception exception) - { - Information("There was an exception " + exception); - throw; - } - }); - -Task("PublishToNuget") - .IsDependentOn("DownloadGitHubReleaseArtifacts") - .Does(() => - { - if (IsRunningOnCircleCI()) - { - PublishPackages(packagesDir, artifactsFile, nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl); - } - }); - -RunTarget(target); - -/// Gets nuique nuget version for this commit -private GitVersion GetNuGetVersionForCommit() -{ - GitVersion(new GitVersionSettings{ - UpdateAssemblyInfo = false, - OutputType = GitVersionOutput.BuildServer - }); - - return GitVersion(new GitVersionSettings{ OutputType = GitVersionOutput.Json }); -} - -/// Updates project version in all of our projects -private void PersistVersion(string committedVersion, string newVersion) -{ - Information(string.Format("We'll search all csproj files for {0} and replace with {1}...", committedVersion, newVersion)); - - var projectFiles = GetFiles("./**/*.csproj"); - - foreach(var projectFile in projectFiles) - { - var file = projectFile.ToString(); - - Information(string.Format("Updating {0}...", file)); - - var updatedProjectFile = System.IO.File.ReadAllText(file) - .Replace(committedVersion, newVersion); - - System.IO.File.WriteAllText(file, updatedProjectFile); - } -} - -/// generates release notes based on issues closed in GitHub since the last release -private void GenerateReleaseNotes(ConvertableFilePath file) -{ - if(!IsRunningOnWindows()) - { - Warning("We are not running on Windows so we cannot generate release notes."); - return; - } - - Information("Generating release notes at " + file); - - var releaseNotesExitCode = StartProcess( - @"tools/GitReleaseNotes/tools/gitreleasenotes.exe", - new ProcessSettings { Arguments = ". /o " + file }); - - if (string.IsNullOrEmpty(System.IO.File.ReadAllText(file))) - { - System.IO.File.WriteAllText(file, "No issues closed since last release"); - } - - if (releaseNotesExitCode != 0) - { - throw new Exception("Failed to generate release notes"); - } -} - -/// Publishes code and symbols packages to nuget feed, based on contents of artifacts file -private void PublishPackages(ConvertableDirectoryPath packagesDir, ConvertableFilePath artifactsFile, string feedApiKey, string codeFeedUrl, string symbolFeedUrl) -{ - Information("PublishPackages"); - var artifacts = System.IO.File - .ReadAllLines(artifactsFile) - .Distinct(); - - foreach(var artifact in artifacts) - { - var codePackage = packagesDir + File(artifact); - - Information("Pushing package " + codePackage); - - Information("Calling NuGetPush"); - - NuGetPush( - codePackage, - new NuGetPushSettings { - ApiKey = feedApiKey, - Source = codeFeedUrl - }); - } -} - -private void CreateGitHubRelease() -{ - var json = $"{{ \"tag_name\": \"{versioning.NuGetVersion}\", \"target_commitish\": \"master\", \"name\": \"{versioning.NuGetVersion}\", \"body\": \"todo: notes coming\", \"draft\": true, \"prerelease\": true }}"; - var content = new System.Net.Http.StringContent(json, System.Text.Encoding.UTF8, "application/json"); - - using(var client = new System.Net.Http.HttpClient()) - { - client.DefaultRequestHeaders.Authorization = - new System.Net.Http.Headers.AuthenticationHeaderValue( - "Basic", Convert.ToBase64String( - System.Text.ASCIIEncoding.ASCII.GetBytes( - $"{gitHubUsername}:{gitHubPassword}"))); - - client.DefaultRequestHeaders.Add("User-Agent", "Ocelot Release"); - - var result = client.PostAsync("https://api.github.com/repos/ThreeMammals/Ocelot/releases", content).Result; - if(result.StatusCode != System.Net.HttpStatusCode.Created) - { - throw new Exception("CreateGitHubRelease result.StatusCode = " + result.StatusCode); - } - var returnValue = result.Content.ReadAsStringAsync().Result; - dynamic test = Newtonsoft.Json.JsonConvert.DeserializeObject(returnValue); - releaseId = test.id; - } -} - -private void UploadFileToGitHubRelease(FilePath file) -{ - var data = System.IO.File.ReadAllBytes(file.FullPath); - var content = new System.Net.Http.ByteArrayContent(data); - content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream"); - - using(var client = new System.Net.Http.HttpClient()) - { - client.DefaultRequestHeaders.Authorization = - new System.Net.Http.Headers.AuthenticationHeaderValue( - "Basic", Convert.ToBase64String( - System.Text.ASCIIEncoding.ASCII.GetBytes( - $"{gitHubUsername}:{gitHubPassword}"))); - - client.DefaultRequestHeaders.Add("User-Agent", "Ocelot Release"); - - var result = client.PostAsync($"https://uploads.github.com/repos/ThreeMammals/Ocelot/releases/{releaseId}/assets?name={file.GetFilename()}", content).Result; - if(result.StatusCode != System.Net.HttpStatusCode.Created) - { - throw new Exception("UploadFileToGitHubRelease result.StatusCode = " + result.StatusCode); - } - } -} - -private void CompleteGitHubRelease() -{ - var json = $"{{ \"tag_name\": \"{versioning.NuGetVersion}\", \"target_commitish\": \"master\", \"name\": \"{versioning.NuGetVersion}\", \"body\": \"todo: notes coming\", \"draft\": false, \"prerelease\": false }}"; - var request = new System.Net.Http.HttpRequestMessage(new System.Net.Http.HttpMethod("Patch"), $"https://api.github.com/repos/ThreeMammals/Ocelot/releases/{releaseId}"); - request.Content = new System.Net.Http.StringContent(json, System.Text.Encoding.UTF8, "application/json"); - - using(var client = new System.Net.Http.HttpClient()) - { - client.DefaultRequestHeaders.Authorization = - new System.Net.Http.Headers.AuthenticationHeaderValue( - "Basic", Convert.ToBase64String( - System.Text.ASCIIEncoding.ASCII.GetBytes( - $"{gitHubUsername}:{gitHubPassword}"))); - - client.DefaultRequestHeaders.Add("User-Agent", "Ocelot Release"); - - var result = client.SendAsync(request).Result; - if(result.StatusCode != System.Net.HttpStatusCode.OK) - { - throw new Exception("CompleteGitHubRelease result.StatusCode = " + result.StatusCode); - } - } -} - - -/// gets the resource from the specified url -private string GetResource(string url) -{ - try - { - Information("Getting resource from " + url); - - var assetsRequest = System.Net.WebRequest.CreateHttp(url); - assetsRequest.Method = "GET"; - assetsRequest.Accept = "application/vnd.github.v3+json"; - assetsRequest.UserAgent = "BuildScript"; - - using (var assetsResponse = assetsRequest.GetResponse()) - { - var assetsStream = assetsResponse.GetResponseStream(); - var assetsReader = new StreamReader(assetsStream); - var response = assetsReader.ReadToEnd(); - - Information("Response is " + response); - - return response; - } - } - catch(Exception exception) - { - Information("There was an exception " + exception); - throw; - } -} - -private bool IsRunningOnCircleCI() -{ - return !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CIRCLECI")); +#tool "dotnet:?package=GitVersion.Tool&version=5.8.1" +#tool "dotnet:?package=coveralls.net&version=4.0.1" +#addin nuget:?package=Newtonsoft.Json +#addin nuget:?package=System.Text.Encodings.Web&version=4.7.1 +#tool "nuget:?package=ReportGenerator&version=5.1.19" +#addin Cake.Coveralls&version=1.1.0 + +// compile +var compileConfig = Argument("configuration", "Release"); + +var slnFile = "./Ocelot.sln"; + +// build artifacts +var artifactsDir = Directory("artifacts"); + +// unit testing +var artifactsForUnitTestsDir = artifactsDir + Directory("UnitTests"); +var unitTestAssemblies = @"./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj"; +var minCodeCoverage = 0.80d; +var coverallsRepoToken = "OCELOT_COVERALLS_TOKEN"; +var coverallsRepo = "https://coveralls.io/github/ThreeMammals/Ocelot"; + +// acceptance testing +var artifactsForAcceptanceTestsDir = artifactsDir + Directory("AcceptanceTests"); +var acceptanceTestAssemblies = @"./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj"; + +// integration testing +var artifactsForIntegrationTestsDir = artifactsDir + Directory("IntegrationTests"); +var integrationTestAssemblies = @"./test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj"; + +// benchmark testing +var artifactsForBenchmarkTestsDir = artifactsDir + Directory("BenchmarkTests"); +var benchmarkTestAssemblies = @"./test/Ocelot.Benchmarks"; + +// packaging +var packagesDir = artifactsDir + Directory("Packages"); +var releaseNotesFile = packagesDir + File("releasenotes.md"); +var artifactsFile = packagesDir + File("artifacts.txt"); + +// stable releases +var tagsUrl = "https://api.github.com/repos/ThreeMammals/ocelot/releases/tags/"; +var nugetFeedStableKey = EnvironmentVariable("OCELOT_NUTGET_API_KEY"); +var nugetFeedStableUploadUrl = "https://www.nuget.org/api/v2/package"; +var nugetFeedStableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package"; + +// internal build variables - don't change these. +string committedVersion = "0.0.0-dev"; +GitVersion versioning = null; +int releaseId = 0; +string gitHubUsername = "TomPallister"; +string gitHubPassword = Environment.GetEnvironmentVariable("OCELOT_GITHUB_API_KEY"); + +var target = Argument("target", "Default"); + +Information("target is " + target); +Information("Build configuration is " + compileConfig); + +Task("Default") + .IsDependentOn("Build"); + +Task("Build") + .IsDependentOn("RunTests"); + +Task("ReleaseNotes") + .IsDependentOn("CreateReleaseNotes"); + +Task("RunTests") + .IsDependentOn("RunUnitTests") + .IsDependentOn("RunAcceptanceTests") + .IsDependentOn("RunIntegrationTests"); + +Task("Release") + .IsDependentOn("Build") + .IsDependentOn("CreateArtifacts") + .IsDependentOn("PublishGitHubRelease") + .IsDependentOn("PublishToNuget"); + +Task("Compile") + .IsDependentOn("Clean") + .IsDependentOn("Version") + .Does(() => + { + var settings = new DotNetBuildSettings + { + Configuration = compileConfig, + }; + + DotNetBuild(slnFile, settings); + }); + +Task("Clean") + .Does(() => + { + if (DirectoryExists(artifactsDir)) + { + DeleteDirectory(artifactsDir, new DeleteDirectorySettings { + Recursive = true, + Force = true + }); + } + CreateDirectory(artifactsDir); + }); + +Task("CreateReleaseNotes") + .Does(() => + { + Information("Generating release notes at " + releaseNotesFile); + + IEnumerable lastReleaseTag; + + var lastReleaseTagExitCode = StartProcess( + "git", + new ProcessSettings { + Arguments = "describe --tags --abbrev=0", + RedirectStandardOutput = true + }, + out lastReleaseTag + ); + + if (lastReleaseTagExitCode != 0) + { + throw new Exception("Failed to get latest release tag"); + } + + var lastRelease = lastReleaseTag.First(); + + Information("Last release tag is " + lastRelease); + + IEnumerable releaseNotes; + + var releaseNotesExitCode = StartProcess( + "git", + new ProcessSettings { + Arguments = $"log --pretty=format:\"%h - %an - %s\" {lastRelease}..HEAD", + RedirectStandardOutput = true + }, + out releaseNotes + ); + + if (releaseNotesExitCode != 0) + { + throw new Exception("Failed to generate release notes"); + } + + EnsureDirectoryExists(packagesDir); + + System.IO.File.WriteAllLines(releaseNotesFile, releaseNotes); + + if (string.IsNullOrEmpty(System.IO.File.ReadAllText(releaseNotesFile))) + { + System.IO.File.WriteAllText(releaseNotesFile, "No commits since last release"); + } + + Information("Release notes are\r\n" + System.IO.File.ReadAllText(releaseNotesFile)); + }); + +Task("Version") + .IsDependentOn("CreateReleaseNotes") + .Does(() => + { + versioning = GetNuGetVersionForCommit(); + var nugetVersion = versioning.NuGetVersion; + Information("SemVer version number: " + nugetVersion); + + if (IsRunningOnCircleCI()) + { + Information("Persisting version number..."); + PersistVersion(committedVersion, nugetVersion); + } + else + { + Information("We are not running on build server, so we won't persist the version number."); + } + }); + +Task("RunUnitTests") + .IsDependentOn("Compile") + .Does(() => + { + var testSettings = new DotNetTestSettings + { + Configuration = compileConfig, + ResultsDirectory = artifactsForUnitTestsDir, + ArgumentCustomization = args => args + // this create the code coverage report + .Append("--collect:\"XPlat Code Coverage\"") + }; + + EnsureDirectoryExists(artifactsForUnitTestsDir); + DotNetTest(unitTestAssemblies, testSettings); + + var coverageSummaryFile = GetSubDirectories(artifactsForUnitTestsDir).First().CombineWithFilePath(File("coverage.cobertura.xml")); + Information(coverageSummaryFile); + Information(artifactsForUnitTestsDir); + + GenerateReport(coverageSummaryFile); + + if (IsRunningOnCircleCI() && IsMainOrDevelop()) + { + var repoToken = EnvironmentVariable(coverallsRepoToken); + if (string.IsNullOrEmpty(repoToken)) + { + throw new Exception(string.Format("Coveralls repo token not found. Set environment variable '{0}'", coverallsRepoToken)); + } + + Information(string.Format("Uploading test coverage to {0}", coverallsRepo)); + CoverallsNet(coverageSummaryFile, CoverallsNetReportType.OpenCover, new CoverallsNetSettings() + { + RepoToken = repoToken + }); + } + else + { + Information("We are not running on the build server so we won't publish the coverage report to coveralls.io"); + } + + var sequenceCoverage = XmlPeek(coverageSummaryFile, "//coverage/@line-rate"); + var branchCoverage = XmlPeek(coverageSummaryFile, "//coverage/@line-rate"); + + Information("Sequence Coverage: " + sequenceCoverage); + + if(double.Parse(sequenceCoverage) < minCodeCoverage) + { + var whereToCheck = !IsRunningOnCircleCI() ? coverallsRepo : artifactsForUnitTestsDir; + throw new Exception(string.Format("Code coverage fell below the threshold of {0}%. You can find the code coverage report at {1}", minCodeCoverage, whereToCheck)); + }; + }); + +Task("RunAcceptanceTests") + .IsDependentOn("Compile") + .Does(() => + { + var settings = new DotNetTestSettings + { + Configuration = compileConfig, + ArgumentCustomization = args => args + .Append("--no-restore") + .Append("--no-build") + }; + + EnsureDirectoryExists(artifactsForAcceptanceTestsDir); + DotNetTest(acceptanceTestAssemblies, settings); + }); + +Task("RunIntegrationTests") + .IsDependentOn("Compile") + .Does(() => + { + var settings = new DotNetTestSettings + { + Configuration = compileConfig, + ArgumentCustomization = args => args + .Append("--no-restore") + .Append("--no-build") + }; + + EnsureDirectoryExists(artifactsForIntegrationTestsDir); + DotNetTest(integrationTestAssemblies, settings); + }); + +Task("CreateArtifacts") + .IsDependentOn("Compile") + .Does(() => + { + EnsureDirectoryExists(packagesDir); + + CopyFiles("./src/**/Release/Ocelot.*.nupkg", packagesDir); + + var projectFiles = GetFiles("./src/**/Release/Ocelot.*.nupkg"); + + foreach(var projectFile in projectFiles) + { + System.IO.File.AppendAllLines(artifactsFile, new[]{ + projectFile.GetFilename().FullPath, + "releasenotes.md" + }); + } + + var artifacts = System.IO.File + .ReadAllLines(artifactsFile) + .Distinct(); + + foreach(var artifact in artifacts) + { + var codePackage = packagesDir + File(artifact); + + Information("Created package " + codePackage); + } + }); + +Task("PublishGitHubRelease") + .IsDependentOn("CreateArtifacts") + .Does(() => + { + if (IsRunningOnCircleCI()) + { + var path = packagesDir.ToString() + @"/**/*"; + + CreateGitHubRelease(); + + foreach (var file in GetFiles(path)) + { + UploadFileToGitHubRelease(file); + } + + CompleteGitHubRelease(); + } + }); + +Task("EnsureStableReleaseRequirements") + .Does(() => + { + Information("Check if stable release..."); + + if (!IsRunningOnCircleCI()) + { + throw new Exception("Stable release should happen via circleci"); + } + + Information("Release is stable..."); + }); + +Task("DownloadGitHubReleaseArtifacts") + .Does(() => + { + + try + { + // hack to let GitHub catch up, todo - refactor to poll + System.Threading.Thread.Sleep(5000); + + EnsureDirectoryExists(packagesDir); + + var releaseUrl = tagsUrl + versioning.NuGetVersion; + + var assets_url = Newtonsoft.Json.Linq.JObject.Parse(GetResource(releaseUrl)) + .Value("assets_url"); + + var assets = GetResource(assets_url); + + foreach(var asset in Newtonsoft.Json.JsonConvert.DeserializeObject(assets)) + { + var file = packagesDir + File(asset.Value("name")); + DownloadFile(asset.Value("browser_download_url"), file); + } + } + catch(Exception exception) + { + Information("There was an exception " + exception); + throw; + } + }); + +Task("PublishToNuget") + .IsDependentOn("DownloadGitHubReleaseArtifacts") + .Does(() => + { + if (IsRunningOnCircleCI()) + { + PublishPackages(packagesDir, artifactsFile, nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl); + } + }); + +RunTarget(target); + +private void GenerateReport(Cake.Core.IO.FilePath coverageSummaryFile) +{ + var dir = System.IO.Directory.GetCurrentDirectory(); + Information(dir); + + var reportSettings = new ProcessArgumentBuilder(); + reportSettings.Append($"-targetdir:" + $"{dir}/{artifactsForUnitTestsDir}"); + reportSettings.Append($"-reports:" + coverageSummaryFile); + + var toolpath = Context.Tools.Resolve("net7.0/ReportGenerator.dll"); + Information($"Tool Path : {toolpath.ToString()}"); + + DotNetExecute(toolpath, reportSettings); +} + +/// Gets unique nuget version for this commit +private GitVersion GetNuGetVersionForCommit() +{ + GitVersion(new GitVersionSettings{ + UpdateAssemblyInfo = false, + OutputType = GitVersionOutput.BuildServer + }); + + return GitVersion(new GitVersionSettings{ OutputType = GitVersionOutput.Json }); +} + +/// Updates project version in all of our projects +private void PersistVersion(string committedVersion, string newVersion) +{ + Information(string.Format("We'll search all csproj files for {0} and replace with {1}...", committedVersion, newVersion)); + + var projectFiles = GetFiles("./**/*.csproj"); + + foreach(var projectFile in projectFiles) + { + var file = projectFile.ToString(); + + Information(string.Format("Updating {0}...", file)); + + var updatedProjectFile = System.IO.File.ReadAllText(file) + .Replace(committedVersion, newVersion); + + System.IO.File.WriteAllText(file, updatedProjectFile); + } +} + +/// Publishes code and symbols packages to nuget feed, based on contents of artifacts file +private void PublishPackages(ConvertableDirectoryPath packagesDir, ConvertableFilePath artifactsFile, string feedApiKey, string codeFeedUrl, string symbolFeedUrl) +{ + Information("PublishPackages"); + var artifacts = System.IO.File + .ReadAllLines(artifactsFile) + .Distinct(); + + foreach(var artifact in artifacts) + { + if (artifact == "releasenotes.md") + { + continue; + } + + var codePackage = packagesDir + File(artifact); + + Information("Pushing package " + codePackage); + + Information("Calling NuGetPush"); + + DotNetNuGetPush( + codePackage, + new DotNetNuGetPushSettings { + ApiKey = feedApiKey, + Source = codeFeedUrl + }); + } +} + +private void CreateGitHubRelease() +{ + var json = $"{{ \"tag_name\": \"{versioning.NuGetVersion}\", \"target_commitish\": \"main\", \"name\": \"{versioning.NuGetVersion}\", \"body\": \"{ReleaseNotesAsJson()}\", \"draft\": true, \"prerelease\": true }}"; + + var content = new System.Net.Http.StringContent(json, System.Text.Encoding.UTF8, "application/json"); + + using(var client = new System.Net.Http.HttpClient()) + { + client.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue( + "Basic", Convert.ToBase64String( + System.Text.ASCIIEncoding.ASCII.GetBytes( + $"{gitHubUsername}:{gitHubPassword}"))); + + client.DefaultRequestHeaders.Add("User-Agent", "Ocelot Release"); + + var result = client.PostAsync("https://api.github.com/repos/ThreeMammals/Ocelot/releases", content).Result; + if(result.StatusCode != System.Net.HttpStatusCode.Created) + { + throw new Exception("CreateGitHubRelease result.StatusCode = " + result.StatusCode); + } + var returnValue = result.Content.ReadAsStringAsync().Result; + dynamic test = Newtonsoft.Json.JsonConvert.DeserializeObject(returnValue); + releaseId = test.id; + } +} + +private string ReleaseNotesAsJson() +{ + return System.Text.Encodings.Web.JavaScriptEncoder.Default.Encode(System.IO.File.ReadAllText(releaseNotesFile)); +} + +private void UploadFileToGitHubRelease(FilePath file) +{ + var data = System.IO.File.ReadAllBytes(file.FullPath); + var content = new System.Net.Http.ByteArrayContent(data); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream"); + + using(var client = new System.Net.Http.HttpClient()) + { + client.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue( + "Basic", Convert.ToBase64String( + System.Text.ASCIIEncoding.ASCII.GetBytes( + $"{gitHubUsername}:{gitHubPassword}"))); + + client.DefaultRequestHeaders.Add("User-Agent", "Ocelot Release"); + + var result = client.PostAsync($"https://uploads.github.com/repos/ThreeMammals/Ocelot/releases/{releaseId}/assets?name={file.GetFilename()}", content).Result; + if(result.StatusCode != System.Net.HttpStatusCode.Created) + { + throw new Exception("UploadFileToGitHubRelease result.StatusCode = " + result.StatusCode); + } + } +} + +private void CompleteGitHubRelease() +{ + var json = $"{{ \"tag_name\": \"{versioning.NuGetVersion}\", \"target_commitish\": \"main\", \"name\": \"{versioning.NuGetVersion}\", \"body\": \"{ReleaseNotesAsJson()}\", \"draft\": false, \"prerelease\": false }}"; + var request = new System.Net.Http.HttpRequestMessage(new System.Net.Http.HttpMethod("Patch"), $"https://api.github.com/repos/ThreeMammals/Ocelot/releases/{releaseId}"); + request.Content = new System.Net.Http.StringContent(json, System.Text.Encoding.UTF8, "application/json"); + + using(var client = new System.Net.Http.HttpClient()) + { + client.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue( + "Basic", Convert.ToBase64String( + System.Text.ASCIIEncoding.ASCII.GetBytes( + $"{gitHubUsername}:{gitHubPassword}"))); + + client.DefaultRequestHeaders.Add("User-Agent", "Ocelot Release"); + + var result = client.SendAsync(request).Result; + if(result.StatusCode != System.Net.HttpStatusCode.OK) + { + throw new Exception("CompleteGitHubRelease result.StatusCode = " + result.StatusCode); + } + } +} + + +/// gets the resource from the specified url +private string GetResource(string url) +{ + try + { + Information("Getting resource from " + url); + + var assetsRequest = System.Net.WebRequest.CreateHttp(url); + assetsRequest.Method = "GET"; + assetsRequest.Accept = "application/vnd.github.v3+json"; + assetsRequest.UserAgent = "BuildScript"; + + using (var assetsResponse = assetsRequest.GetResponse()) + { + var assetsStream = assetsResponse.GetResponseStream(); + var assetsReader = new StreamReader(assetsStream); + var response = assetsReader.ReadToEnd(); + + Information("Response is " + response); + + return response; + } + } + catch(Exception exception) + { + Information("There was an exception " + exception); + throw; + } +} + +private bool IsRunningOnCircleCI() +{ + return !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CIRCLECI")); +} + +private bool IsMainOrDevelop() +{ + var env = Environment.GetEnvironmentVariable("CIRCLE_BRANCH").ToLower(); + + if(env == "main") + { + return true; + } + + if(env == "develop") + { + return true; + } + + return false; } \ No newline at end of file diff --git a/build.ps1 b/build.ps1 deleted file mode 100644 index a336e2985..000000000 --- a/build.ps1 +++ /dev/null @@ -1,256 +0,0 @@ -########################################################################## -# This is the Cake bootstrapper script for PowerShell. -# This file was downloaded from https://github.com/cake-build/resources -# Feel free to change this file to fit your needs. -########################################################################## - -<# - -.SYNOPSIS -This is a Powershell script to bootstrap a Cake build. - -.DESCRIPTION -This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) -and execute your Cake build script with the parameters you provide. - -.PARAMETER Script -The build script to execute. -.PARAMETER Target -The build script target to run. -.PARAMETER Configuration -The build configuration to use. -.PARAMETER Verbosity -Specifies the amount of information to be displayed. -.PARAMETER ShowDescription -Shows description about tasks. -.PARAMETER DryRun -Performs a dry run. -.PARAMETER SkipToolPackageRestore -Skips restoring of packages. -.PARAMETER ScriptArgs -Remaining arguments are added here. - -.LINK -https://cakebuild.net - -#> - -[CmdletBinding()] -Param( - [string]$Script = "build.cake", - [string]$Target, - [string]$Configuration, - [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] - [string]$Verbosity, - [switch]$ShowDescription, - [Alias("WhatIf", "Noop")] - [switch]$DryRun, - [switch]$SkipToolPackageRestore, - [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] - [string[]]$ScriptArgs -) - -# Attempt to set highest encryption available for SecurityProtocol. -# PowerShell will not set this by default (until maybe .NET 4.6.x). This -# will typically produce a message for PowerShell v2 (just an info -# message though) -try { - # Set TLS 1.2 (3072), then TLS 1.1 (768), then TLS 1.0 (192), finally SSL 3.0 (48) - # Use integers because the enumeration values for TLS 1.2 and TLS 1.1 won't - # exist in .NET 4.0, even though they are addressable if .NET 4.5+ is - # installed (.NET 4.5 is an in-place upgrade). - # PowerShell Core already has support for TLS 1.2 so we can skip this if running in that. - if (-not $IsCoreCLR) { - [System.Net.ServicePointManager]::SecurityProtocol = 3072 -bor 768 -bor 192 -bor 48 - } - } catch { - Write-Output 'Unable to set PowerShell to use TLS 1.2 and TLS 1.1 due to old .NET Framework installed. If you see underlying connection closed or trust errors, you may need to upgrade to .NET Framework 4.5+ and PowerShell v3' - } - -[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null -function MD5HashFile([string] $filePath) -{ - if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) - { - return $null - } - - [System.IO.Stream] $file = $null; - [System.Security.Cryptography.MD5] $md5 = $null; - try - { - $md5 = [System.Security.Cryptography.MD5]::Create() - $file = [System.IO.File]::OpenRead($filePath) - return [System.BitConverter]::ToString($md5.ComputeHash($file)) - } - finally - { - if ($file -ne $null) - { - $file.Dispose() - } - } -} - -function GetProxyEnabledWebClient -{ - $wc = New-Object System.Net.WebClient - $proxy = [System.Net.WebRequest]::GetSystemWebProxy() - $proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials - $wc.Proxy = $proxy - return $wc -} - -Write-Host "Preparing to run build script..." - -if(!$PSScriptRoot){ - $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent -} - -$TOOLS_DIR = Join-Path $PSScriptRoot "tools" -$ADDINS_DIR = Join-Path $TOOLS_DIR "Addins" -$MODULES_DIR = Join-Path $TOOLS_DIR "Modules" -$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" -$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" -$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" -$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" -$ADDINS_PACKAGES_CONFIG = Join-Path $ADDINS_DIR "packages.config" -$MODULES_PACKAGES_CONFIG = Join-Path $MODULES_DIR "packages.config" - -# Make sure tools folder exists -if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { - Write-Verbose -Message "Creating tools directory..." - New-Item -Path $TOOLS_DIR -Type Directory | Out-Null -} - -# Make sure that packages.config exist. -if (!(Test-Path $PACKAGES_CONFIG)) { - Write-Verbose -Message "Downloading packages.config..." - try { - $wc = GetProxyEnabledWebClient - $wc.DownloadFile("https://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) - } catch { - Throw "Could not download packages.config." - } -} - -# Try find NuGet.exe in path if not exists -if (!(Test-Path $NUGET_EXE)) { - Write-Verbose -Message "Trying to find nuget.exe in PATH..." - $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_ -PathType Container) } - $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 - if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { - Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." - $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName - } -} - -# Try download NuGet.exe if not exists -if (!(Test-Path $NUGET_EXE)) { - Write-Verbose -Message "Downloading NuGet.exe..." - try { - $wc = GetProxyEnabledWebClient - $wc.DownloadFile($NUGET_URL, $NUGET_EXE) - } catch { - Throw "Could not download NuGet.exe." - } -} - -# Save nuget.exe path to environment to be available to child processed -$env:NUGET_EXE = $NUGET_EXE -$env:NUGET_EXE_INVOCATION = if ($IsLinux -or $IsMacOS) { - "mono `"$NUGET_EXE`"" -} else { - "`"$NUGET_EXE`"" -} - -# Restore tools from NuGet? -if(-Not $SkipToolPackageRestore.IsPresent) { - Push-Location - Set-Location $TOOLS_DIR - - # Check for changes in packages.config and remove installed tools if true. - [string] $md5Hash = MD5HashFile $PACKAGES_CONFIG - if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or - ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { - Write-Verbose -Message "Missing or changed package.config hash..." - Get-ChildItem -Exclude packages.config,nuget.exe,Cake.Bakery | - Remove-Item -Recurse -Force - } - - Write-Verbose -Message "Restoring tools from NuGet..." - - $NuGetOutput = Invoke-Expression "& $env:NUGET_EXE_INVOCATION install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" - - if ($LASTEXITCODE -ne 0) { - Throw "An error occurred while restoring NuGet tools." - } - else - { - $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" - } - Write-Verbose -Message ($NuGetOutput | Out-String) - - Pop-Location -} - -# Restore addins from NuGet -if (Test-Path $ADDINS_PACKAGES_CONFIG) { - Push-Location - Set-Location $ADDINS_DIR - - Write-Verbose -Message "Restoring addins from NuGet..." - $NuGetOutput = Invoke-Expression "& $env:NUGET_EXE_INVOCATION install -ExcludeVersion -OutputDirectory `"$ADDINS_DIR`"" - - if ($LASTEXITCODE -ne 0) { - Throw "An error occurred while restoring NuGet addins." - } - - Write-Verbose -Message ($NuGetOutput | Out-String) - - Pop-Location -} - -# Restore modules from NuGet -if (Test-Path $MODULES_PACKAGES_CONFIG) { - Push-Location - Set-Location $MODULES_DIR - - Write-Verbose -Message "Restoring modules from NuGet..." - $NuGetOutput = Invoke-Expression "& $env:NUGET_EXE_INVOCATION install -ExcludeVersion -OutputDirectory `"$MODULES_DIR`"" - - if ($LASTEXITCODE -ne 0) { - Throw "An error occurred while restoring NuGet modules." - } - - Write-Verbose -Message ($NuGetOutput | Out-String) - - Pop-Location -} - -# Make sure that Cake has been installed. -if (!(Test-Path $CAKE_EXE)) { - Throw "Could not find Cake.exe at $CAKE_EXE" -} - -$CAKE_EXE_INVOCATION = if ($IsLinux -or $IsMacOS) { - "mono `"$CAKE_EXE`"" -} else { - "`"$CAKE_EXE`"" -} - - # Build an array (not a string) of Cake arguments to be joined later -$cakeArguments = @() -if ($Script) { $cakeArguments += "`"$Script`"" } -if ($Target) { $cakeArguments += "-target=`"$Target`"" } -if ($Configuration) { $cakeArguments += "-configuration=$Configuration" } -if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" } -if ($ShowDescription) { $cakeArguments += "-showdescription" } -if ($DryRun) { $cakeArguments += "-dryrun" } -$cakeArguments += $ScriptArgs - -# Start Cake -Write-Host "Running build script..." -Invoke-Expression "& $CAKE_EXE_INVOCATION $($cakeArguments -join " ")" -exit $LASTEXITCODE diff --git a/build.sh b/build.sh deleted file mode 100755 index b9e12527f..000000000 --- a/build.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env bash - -########################################################################## -# This is the Cake bootstrapper script for Linux and OS X. -# This file was downloaded from https://github.com/cake-build/resources -# Feel free to change this file to fit your needs. -########################################################################## - -# Define directories. -SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) -TOOLS_DIR=$SCRIPT_DIR/tools -ADDINS_DIR=$TOOLS_DIR/Addins -MODULES_DIR=$TOOLS_DIR/Modules -NUGET_EXE=$TOOLS_DIR/nuget.exe -CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe -PACKAGES_CONFIG=$TOOLS_DIR/packages.config -PACKAGES_CONFIG_MD5=$TOOLS_DIR/packages.config.md5sum -ADDINS_PACKAGES_CONFIG=$ADDINS_DIR/packages.config -MODULES_PACKAGES_CONFIG=$MODULES_DIR/packages.config - -# Define md5sum or md5 depending on Linux/OSX -MD5_EXE= -if [[ "$(uname -s)" == "Darwin" ]]; then - MD5_EXE="md5 -r" -else - MD5_EXE="md5sum" -fi - -# Define default arguments. -SCRIPT="build.cake" -CAKE_ARGUMENTS=() - -# Parse arguments. -for i in "$@"; do - case $1 in - -s|--script) SCRIPT="$2"; shift ;; - --) shift; CAKE_ARGUMENTS+=("$@"); break ;; - *) CAKE_ARGUMENTS+=("$1") ;; - esac - shift -done - -# Make sure the tools folder exist. -if [ ! -d "$TOOLS_DIR" ]; then - mkdir "$TOOLS_DIR" -fi - -# Make sure that packages.config exist. -if [ ! -f "$TOOLS_DIR/packages.config" ]; then - echo "Downloading packages.config..." - curl -Lsfo "$TOOLS_DIR/packages.config" https://cakebuild.net/download/bootstrapper/packages - if [ $? -ne 0 ]; then - echo "An error occurred while downloading packages.config." - exit 1 - fi -fi - -# Download NuGet if it does not exist. -if [ ! -f "$NUGET_EXE" ]; then - echo "Downloading NuGet..." - curl -Lsfo "$NUGET_EXE" https://dist.nuget.org/win-x86-commandline/latest/nuget.exe - if [ $? -ne 0 ]; then - echo "An error occurred while downloading nuget.exe." - exit 1 - fi -fi - -# Restore tools from NuGet. -pushd "$TOOLS_DIR" >/dev/null -if [ ! -f "$PACKAGES_CONFIG_MD5" ] || [ "$( cat "$PACKAGES_CONFIG_MD5" | sed 's/\r$//' )" != "$( $MD5_EXE "$PACKAGES_CONFIG" | awk '{ print $1 }' )" ]; then - find . -type d ! -name . ! -name 'Cake.Bakery' | xargs rm -rf -fi - -mono "$NUGET_EXE" install -ExcludeVersion -if [ $? -ne 0 ]; then - echo "Could not restore NuGet tools." - exit 1 -fi - -$MD5_EXE "$PACKAGES_CONFIG" | awk '{ print $1 }' >| "$PACKAGES_CONFIG_MD5" - -popd >/dev/null - -# Restore addins from NuGet. -if [ -f "$ADDINS_PACKAGES_CONFIG" ]; then - pushd "$ADDINS_DIR" >/dev/null - - mono "$NUGET_EXE" install -ExcludeVersion - if [ $? -ne 0 ]; then - echo "Could not restore NuGet addins." - exit 1 - fi - - popd >/dev/null -fi - -# Restore modules from NuGet. -if [ -f "$MODULES_PACKAGES_CONFIG" ]; then - pushd "$MODULES_DIR" >/dev/null - - mono "$NUGET_EXE" install -ExcludeVersion - if [ $? -ne 0 ]; then - echo "Could not restore NuGet modules." - exit 1 - fi - - popd >/dev/null -fi - -# Make sure that Cake has been installed. -if [ ! -f "$CAKE_EXE" ]; then - echo "Could not find Cake.exe at '$CAKE_EXE'." - exit 1 -fi - -# Start Cake -exec mono "$CAKE_EXE" $SCRIPT "${CAKE_ARGUMENTS[@]}" diff --git a/codeanalysis.ruleset b/codeanalysis.ruleset index 4b278ba80..0198bd195 100644 --- a/codeanalysis.ruleset +++ b/codeanalysis.ruleset @@ -1,5 +1,5 @@  - + @@ -50,12 +50,11 @@ - + - @@ -70,14 +69,14 @@ - + - + diff --git a/docker/Dockerfile.base b/docker/Dockerfile.base index 02d91f53a..691339490 100644 --- a/docker/Dockerfile.base +++ b/docker/Dockerfile.base @@ -1,9 +1,9 @@ -# this is the dockerfile that create the ocelot build container -# build with the docker-build.sh file in this folder -FROM mcr.microsoft.com/dotnet/core/sdk:3.1-bionic AS build - -RUN apt install gnupg ca-certificates -RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF -RUN echo "deb https://download.mono-project.com/repo/ubuntu stable-bionic main" | tee /etc/apt/sources.list.d/mono-official-stable.list -RUN apt update -RUN apt-get -y install mono-devel \ No newline at end of file +FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine + +RUN apk add bash icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib git openssh-client + +RUN curl -L --output ./dotnet-install.sh https://dot.net/v1/dotnet-install.sh + +RUN chmod u+x ./dotnet-install.sh + +RUN ./dotnet-install.sh -c 6.0 -i /usr/share/dotnet diff --git a/docker/Dockerfile.build b/docker/Dockerfile.build index 0478da417..5498c6106 100644 --- a/docker/Dockerfile.build +++ b/docker/Dockerfile.build @@ -1,15 +1,16 @@ -# call from ocelot repo root with -# docker build --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . -FROM mijitt0m/ocelot-build:0.0.1 - -ARG OCELOT_COVERALLS_TOKEN - -ENV OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN - -WORKDIR /src - -COPY ./. . - -RUN chmod u+x build.sh - -RUN make build \ No newline at end of file +# call from ocelot repo root with +# docker build --platform linux/arm64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . +# docker build --platform linux/amd64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . +FROM mijitt0m/ocelot-build:0.0.9 + +ARG OCELOT_COVERALLS_TOKEN + +ENV OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN + +WORKDIR /build + +COPY ./. . + +RUN dotnet tool restore + +RUN dotnet cake \ No newline at end of file diff --git a/docker/Dockerfile.release b/docker/Dockerfile.release index 5d63816d2..e2659c035 100644 --- a/docker/Dockerfile.release +++ b/docker/Dockerfile.release @@ -1,20 +1,20 @@ -# call from ocelot repo root with -# docker build --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN --build-arg OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.release . - -FROM mijitt0m/ocelot-build:0.0.1 - -ARG OCELOT_COVERALLS_TOKEN -ARG OCELOT_NUTGET_API_KEY -ARG OCELOT_GITHUB_API_KEY - -ENV OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -ENV OCELOT_NUTGET_API_KEY=$OCELOT_NUTGET_API_KEY -ENV OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY - -WORKDIR /src - -COPY ./. . - -RUN chmod u+x build.sh - -RUN make release \ No newline at end of file +# call from ocelot repo root with +# docker build --platform linux/arm64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN --build-arg OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . +# docker build --platform linux/amd64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN --build-arg OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . +FROM mijitt0m/ocelot-build:0.0.9 + +ARG OCELOT_COVERALLS_TOKEN +ARG OCELOT_NUTGET_API_KEY +ARG OCELOT_GITHUB_API_KEY + +ENV OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN +ENV OCELOT_NUTGET_API_KEY=$OCELOT_NUTGET_API_KEY +ENV OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY + +WORKDIR /build + +COPY ./. . + +RUN dotnet tool restore + +RUN dotnet cake \ No newline at end of file diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 000000000..bf2cee9b5 --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,7 @@ +# this script build the ocelot docker file +version=0.0.9 +docker build --platform linux/amd64 -t mijitt0m/ocelot-build -f Dockerfile.base . +echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin +docker tag mijitt0m/ocelot-build mijitt0m/ocelot-build:$version +docker push mijitt0m/ocelot-build:latest +docker push mijitt0m/ocelot-build:$version \ No newline at end of file diff --git a/docker/docker-build.sh b/docker/docker-build.sh deleted file mode 100755 index 8bb4e2065..000000000 --- a/docker/docker-build.sh +++ /dev/null @@ -1,6 +0,0 @@ -# this script build the ocelot docker file -docker build -t mijitt0m/ocelot-build -f Dockerfile.base . -echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin -docker tag mijitt0m/ocelot-build mijitt0m/ocelot-build:0.0.1 -docker push mijitt0m/ocelot-build:latest -docker push mijitt0m/ocelot-build:0.0.1 diff --git a/docs/building/building.rst b/docs/building/building.rst index 0a494f9f0..4544eccd9 100644 --- a/docs/building/building.rst +++ b/docs/building/building.rst @@ -1,10 +1,10 @@ Building ======== -* The best way to build Ocelot is using the Dockerfile.build file which can be found in the docker folder in Ocelot root. Use the following command `docker build -f ./docker/Dockerfile.build .`. +* You can also just run `dotnet tool restore && dotnet cake` locally!. Output will got to the `./artifacts` directory. -* You'll can run the `./build.ps1` or `./build.sh` script depending on your OS. This will compile, run unit and acceptance tests and build the output packages locally. Output will got to the `./artifacts` directory. +* The best way to replicate the CI process is to build Ocelot locally is using the Dockerfile.build file which can be found in the docker folder in Ocelot root. Use the following command `docker build --platform linux/amd64 -f ./docker/Dockerfile.build .` for example. You will need to change the platform flag depending on your platform. * There is a Makefile to make it easier to call the various targers in `build.cake`. The scripts are called with .sh but can be easily changed to ps1 if you are using Windows. -* Alternatively you can build the project in VS2019 with the latest .NET Core SDK. \ No newline at end of file +* Alternatively you can build the project in VS2022 with the latest .NET 7.0 SDK. \ No newline at end of file diff --git a/docs/building/releaseprocess.rst b/docs/building/releaseprocess.rst index 43cb5f61a..40da81567 100644 --- a/docs/building/releaseprocess.rst +++ b/docs/building/releaseprocess.rst @@ -1,8 +1,8 @@ Release process =============== -* The release process works best with GitHubFlow branching. -* Contributors can do whatever they want on PRs and merges to master will result in packages being released to GitHub and NuGet. +* The release process works best with Git Flow branching. +* Contributors can do whatever they want on PRs and merges to main will result in packages being released to GitHub and NuGet. Ocelot uses the following process to accept work into the NuGet packages. @@ -10,7 +10,7 @@ Ocelot uses the following process to accept work into the NuGet packages. 2. User creates a fork and branches from this (unless a member of core team, they can just create a branch on the main repo) e.g. feat/xxx, fix/xxx etc. It doesn't really matter what the xxx is. It might make sense to use the issue number and maybe a short description. I don't care as long as it has (feat, fix, refactor)/xxx :) -3. When the user is happy with their work they can create a pull request against master in GitHub with their changes. The user must follow the `SemVer `_ support for this is provided by `GitVersion `_. So if you need to make breaking changes please make sure you use the correct commit message so GitVersion uses the correct semver tags. Do not manually tag the Ocelot repo this will break things. +3. When the user is happy with their work they can create a pull request against develop in GitHub with their changes. The user must follow the `SemVer `_ support for this is provided by `GitVersion `_. So if you need to make breaking changes please make sure you use the correct commit message so GitVersion uses the correct semver tags. Do not manually tag the Ocelot repo this will break things. 4. The Ocelot team will review the PR and if all is good merge it, else they will suggest feedback that the user will need to act on. In order to speed up getting a PR the user should think about the following. - Have I covered all my changes with tests at unit and acceptance level? @@ -23,13 +23,15 @@ In order for a PR to be merged the following must have occured. - Build must not have slowed down dramatically. - The main Ocelot package must not have taken on any non MS dependencies. -5. After the PR is merged to master the release process will begin which builds the code, versions it, pushes artifacts to GitHub and NuGet packages to NuGet. +5. After the PR is merged to develop the Ocelot NuGet packages will not be updated until a release is created. -6. The final step is to go back to GitHub and close any issues that are now fixed. You should see something like this in`GitHub `_ and this in `NuGet `_. +6. When enough work has been completed to justify a new release. Develop will be merged into main the release process will begin which builds the code, versions it, pushes artifacts to GitHub and NuGet packages to NuGet. + +7. The final step is to go back to GitHub and close any issues that are now fixed. You should see something like this in`GitHub `_ and this in `NuGet `_. Notes ----- All NuGet package builds & releases are done with CircleCI `here _` and all releases are done from `here _`. -Only TomPallister can merge releases into master at the moment. This is to ensure there is a final quality gate in place. Tom is mainly looking for security issues on the final merge. +Only TomPallister can merge releases into main at the moment. This is to ensure there is a final quality gate in place. Tom is mainly looking for security issues on the final merge. diff --git a/docs/building/tests.rst b/docs/building/tests.rst index d1fc8aba7..c21e91cfd 100644 --- a/docs/building/tests.rst +++ b/docs/building/tests.rst @@ -1,24 +1,18 @@ Tests ===== -The tests should all just run and work apart from the integration tests which need the following -environmental variables setting. This is a manual step at the moment. +The tests should all just run and work as part of the build process. You can of course also run them in visual studio. - ``OCELOT_USERNAME=admin`` - ``OCELOT_HASH=kE/mxd1hO9h9Sl2VhGhwJUd9xZEv4NP6qXoN39nIqM4=`` +Create SSL Cert for Testing +^^^^^^^^^^^^^^^^^^^^^^^^^^^ - ``OCELOT_SALT=zzWITpnDximUNKYLiUam/w==`` +You can do this via openssl: -On windows you can use.. - - ``SETX OCELOT_USERNAME admin`` - -On mac.. - - ``export OCELOT_USERNAME=admin`` - -I need to work out a nicer way of doing this in the future. +Install openssl package (if you are using Windows, download binaries here). +Generate private key: `openssl genrsa 2048 > private.pem` +Generate the self signed certificate: `openssl req -x509 -days 1000 -new -key private.pem -out public.pem` +If needed, create PFX: `openssl pkcs12 -export -in public.pem -inkey private.pem -out mycert.pfx` \ No newline at end of file diff --git a/docs/features/administration.rst b/docs/features/administration.rst index 4d8919947..6449f583a 100644 --- a/docs/features/administration.rst +++ b/docs/features/administration.rst @@ -1,140 +1,131 @@ -Administration -============== - -Ocelot supports changing configuration during runtime via an authenticated HTTP API. This can be authenticated in two ways either using Ocelot's -internal IdentityServer (for authenticating requests to the administration API only) or hooking the administration API authentication into your own -IdentityServer. - -The first thing you need to do if you want to use the administration API is bring in the relavent NuGet package.. - -``Install-Package Ocelot.Administration`` - -This will bring down everything needed by the admin API. - -Providing your own IdentityServer -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -All you need to do to hook into your own IdentityServer is add the following to your ConfigureServices method. - -.. code-block:: csharp - - public virtual void ConfigureServices(IServiceCollection services) - { - Action options = o => { - // o.Authority = ; - // o.ApiName = ; - // etc.... - }; - - services - .AddOcelot() - .AddAdministration("/administration", options); - } - -You now need to get a token from your IdentityServer and use in subsequent requests to Ocelot's administration API. - -This feature was implemented for `issue 228 `_. It is useful because the IdentityServer authentication -middleware needs the URL of the IdentityServer. If you are using the internal IdentityServer it might not alaways be possible to have the Ocelot URL. - -Internal IdentityServer -^^^^^^^^^^^^^^^^^^^^^^^ - -The API is authenticated using bearer tokens that you request from Ocelot iteself. This is provided by the amazing -`Identity Server `_ project that I have been using for a few years now. Check them out. - -In order to enable the administration section you need to do a few things. First of all add this to your -initial Startup.cs. - -The path can be anything you want and it is obviously reccomended don't use -a url you would like to route through with Ocelot as this will not work. The administration uses the -MapWhen functionality of asp.net core and all requests to {root}/administration will be sent there not -to the Ocelot middleware. - -The secret is the client secret that Ocelot's internal IdentityServer will use to authenticate requests to the administration API. This can be whatever you want it to be! - -.. code-block:: csharp - - public virtual void ConfigureServices(IServiceCollection services) - { - services - .AddOcelot() - .AddAdministration("/administration", "secret"); - } - -In order for the administration API to work, Ocelot / IdentityServer must be able to call itself for validation. This means that you need to add the base url of Ocelot -to global configuration if it is not default (http://localhost:5000). Please note if you are using something like docker to host Ocelot it might not be able to -call back to localhost etc and you need to know what you are doing with docker networking in this scenario. Anyway this can be done as follows.. - -If you want to run on a different host and port locally.. - -.. code-block:: json - - "GlobalConfiguration": { - "BaseUrl": "http://localhost:55580" - } - -or if Ocelot is exposed via dns - -.. code-block:: json - - "GlobalConfiguration": { - "BaseUrl": "http://mydns.com" - } - -Now if you went with the configuration options above and want to access the API you can use the postman scripts -called ocelot.postman_collection.json in the solution to change the Ocelot configuration. Obviously these -will need to be changed if you are running Ocelot on a different url to http://localhost:5000. - - -The scripts show you how to request a bearer token from ocelot and then use it to GET the existing configuration and POST -a configuration. - -If you are running multiple Ocelot instances in a cluster then you need to use a certificate to sign the bearer tokens used to access the administration API. - -In order to do this you need to add two more environmental variables for each Ocelot in the cluster. - -``OCELOT_CERTIFICATE`` - The path to a certificate that can be used to sign the tokens. The certificate needs to be of the type X509 and obviously Ocelot needs to be able to access it. -``OCELOT_CERTIFICATE_PASSWORD`` - The password for the certificate. - -Normally Ocelot just uses temporary signing credentials but if you set these environmental variables then it will use the certificate. If all the other Ocelot instances in the cluster have the same certificate then you are good! - - -Administration API -^^^^^^^^^^^^^^^^^^ - -**POST {adminPath}/connect/token** - -This gets a token for use with the admin area using the client credentials we talk about setting above. Under the hood this calls into an IdentityServer hosted within Ocelot. - -The body of the request is form-data as follows - -``client_id`` set as admin - -``client_secret`` set as whatever you used when setting up the administration services. - -``scope`` set as admin - -``grant_type`` set as client_credentials - -**GET {adminPath}/configuration** - - -This gets the current Ocelot configuration. It is exactly the same JSON we use to set Ocelot up with in the first place. - -**POST {adminPath}/configuration** - -This overrwrites the existing configuration (should probably be a put!). I reccomend getting your config from the GET endpoint, making any changes and posting it back...simples. - -The body of the request is JSON and it is the same format as the FileConfiguration.cs that we use to set up -Ocelot on a file system. - -Please note that if you want to use this API then the process running Ocelot must have permission to write to the disk -where your ocelot.json or ocelot.{environment}.json is located. This is because Ocelot will overwrite them on save. - -**DELETE {adminPath}/outputcache/{region}** - -This clears a region of the cache. If you are using a backplane it will clear all instances of the cache! Giving your the ability to run a cluster of Ocelots and cache over all of them in memory and clear them all at the same time / just use a distributed cache. - -The region is whatever you set against the Region field in the FileCacheOptions section of the Ocelot configuration. +Administration +============== + +Ocelot supports changing configuration during runtime via an authenticated HTTP API. This can be authenticated in two ways either using Ocelot's internal IdentityServer (for authenticating requests to the administration API only) or hooking the administration API authentication into your own IdentityServer. + +The first thing you need to do if you want to use the administration API is bring in the relavent NuGet package.. + +``Install-Package Ocelot.Administration`` + +This will bring down everything needed by the admin API. + +Providing your own IdentityServer +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +All you need to do to hook into your own IdentityServer is add the following to your ConfigureServices method. + +.. code-block:: csharp + + public virtual void ConfigureServices(IServiceCollection services) + { + Action options = o => + { + o.Authority = identityServerRootUrl; + o.RequireHttpsMetadata = false; + o.TokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false, + }; + // etc.... + }; + + services + .AddOcelot() + .AddAdministration("/administration", options); + } + +You now need to get a token from your IdentityServer and use in subsequent requests to Ocelot's administration API. + +This feature was implemented for `issue 228 `_. It is useful because the IdentityServer authentication middleware needs the URL of the IdentityServer. If you are using the internal IdentityServer it might not alaways be possible to have the Ocelot URL. + +Internal IdentityServer +^^^^^^^^^^^^^^^^^^^^^^^ + +The API is authenticated using bearer tokens that you request from Ocelot iteself. This is provided by the amazing `Identity Server `_ project that I have been using for a few years now. Check them out. + +In order to enable the administration section you need to do a few things. First of all add this to yourinitial Startup.cs. + +The path can be anything you want and it is obviously reccomended don't usea url you would like to route through with Ocelot as this will not work. The administration uses theMapWhen functionality of asp.net core and all requests to {root}/administration will be sent there not to the Ocelot middleware. + +The secret is the client secret that Ocelot's internal IdentityServer will use to authenticate requests to the administration API. This can be whatever you want it to be! + +.. code-block:: csharp + + public virtual void ConfigureServices(IServiceCollection services) + { + services + .AddOcelot() + .AddAdministration("/administration", "secret"); + } + +In order for the administration API to work, Ocelot / IdentityServer must be able to call itself for validation. This means that you need to add the base url of Ocelot to global configuration if it is not default (http://localhost:5000). Please note if you are using something like docker to host Ocelot it might not be able to call back to localhost etc and you need to know what you are doing with docker networking in this scenario. Anyway this can be done as follows.. + +If you want to run on a different host and port locally.. + +.. code-block:: json + + "GlobalConfiguration": { + "BaseUrl": "http://localhost:55580" + } + +or if Ocelot is exposed via dns + +.. code-block:: json + + "GlobalConfiguration": { + "BaseUrl": "http://mydns.com" + } + +Now if you went with the configuration options above and want to access the API you can use the postman scriptscalled ocelot.postman_collection.json in the solution to change the Ocelot configuration. Obviously these will need to be changed if you are running Ocelot on a different url to http://localhost:5000. + + +The scripts show you how to request a bearer token from ocelot and then use it to GET the existing configuration and POST a configuration. + +If you are running multiple Ocelot instances in a cluster then you need to use a certificate to sign the bearer tokens used to access the administration API. + +In order to do this you need to add two more environmental variables for each Ocelot in the cluster. + +``OCELOT_CERTIFICATE`` + The path to a certificate that can be used to sign the tokens. The certificate needs to be of the type X509 and obviously Ocelot needs to be able to access it. +``OCELOT_CERTIFICATE_PASSWORD`` + The password for the certificate. + +Normally Ocelot just uses temporary signing credentials but if you set these environmental variables then it will use the certificate. If all the other Ocelot instances in thecluster have the same certificate then you are good! + + +Administration API +^^^^^^^^^^^^^^^^^^ + +**POST {adminPath}/connect/token** + +This gets a token for use with the admin area using the client credentials we talk about setting above. Under the hood this calls into an IdentityServer hosted within Ocelot. + +The body of the request is form-data as follows + +``client_id`` set as admin + +``client_secret`` set as whatever you used when setting up the administration services. + +``scope`` set as admin + +``grant_type`` set as client_credentials + +**GET {adminPath}/configuration** + + +This gets the current Ocelot configuration. It is exactly the same JSON we use to set Ocelot up with in the first place. + +**POST {adminPath}/configuration** + +This overrwrites the existing configuration (should probably be a put!). I reccomend getting your config from the GET endpoint, making any changes and posting it back...simples. + +The body of the request is JSON and it is the same format as the FileConfiguration.cs that we use to set up +Ocelot on a file system. + +Please note that if you want to use this API then the process running Ocelot must have permission to write to the disk where your ocelot.json or ocelot.{environment}.json is located. This is because Ocelot will overwrite them on save. + +**DELETE {adminPath}/outputcache/{region}** + +This clears a region of the cache. If you are using a backplane it will clear all instances of the cache! Giving your the ability to run a cluster of Ocelots and cache over all of them in memory and clear them all at the same time / just use a distributed cache. + +The region is whatever you set against the Region field in the FileCacheOptions section of the Ocelot configuration. diff --git a/docs/features/authentication.rst b/docs/features/authentication.rst index 641ca2ed7..e41c0033a 100644 --- a/docs/features/authentication.rst +++ b/docs/features/authentication.rst @@ -1,182 +1,177 @@ -Authentication -============== - -In order to authenticate ReRoutes and subsequently use any of Ocelot's claims based features such as authorisation or modifying the request with values from the token. Users must register authentication services in their Startup.cs as usual but they provide a scheme (authentication provider key) with each registration e.g. - -.. code-block:: csharp - - public void ConfigureServices(IServiceCollection services) - { - var authenticationProviderKey = "TestKey"; - - services.AddAuthentication() - .AddJwtBearer(authenticationProviderKey, x => - { - }); - } - - -In this example TestKey is the scheme that this provider has been registered with. -We then map this to a ReRoute in the configuration e.g. - -.. code-block:: json - - "ReRoutes": [{ - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 51876, - } - ], - "DownstreamPathTemplate": "/", - "UpstreamPathTemplate": "/", - "UpstreamHttpMethod": ["Post"], - "ReRouteIsCaseSensitive": false, - "DownstreamScheme": "http", - "AuthenticationOptions": { - "AuthenticationProviderKey": "TestKey", - "AllowedScopes": [] - } - }] - -When Ocelot runs it will look at this ReRoutes AuthenticationOptions.AuthenticationProviderKey -and check that there is an Authentication provider registered with the given key. If there isn't then Ocelot -will not start up, if there is then the ReRoute will use that provider when it executes. - -If a ReRoute is authenticated Ocelot will invoke whatever scheme is associated with it while executing the authentication middleware. If the request fails authentication Ocelot returns a http status code 401. - -JWT Tokens -^^^^^^^^^^ - -If you want to authenticate using JWT tokens maybe from a provider like Auth0 you can register your authentication middleware as normal e.g. - -.. code-block:: csharp - - public void ConfigureServices(IServiceCollection services) - { - var authenticationProviderKey = "TestKey"; - - services.AddAuthentication() - .AddJwtBearer(authenticationProviderKey, x => - { - x.Authority = "test"; - x.Audience = "test"; - }); - - services.AddOcelot(); - } - -Then map the authentication provider key to a ReRoute in your configuration e.g. - -.. code-block:: json - - "ReRoutes": [{ - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 51876, - } - ], - "DownstreamPathTemplate": "/", - "UpstreamPathTemplate": "/", - "UpstreamHttpMethod": ["Post"], - "ReRouteIsCaseSensitive": false, - "DownstreamScheme": "http", - "AuthenticationOptions": { - "AuthenticationProviderKey": "TestKey", - "AllowedScopes": [] - } - }] - - - -Identity Server Bearer Tokens -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In order to use IdentityServer bearer tokens, register your IdentityServer services as usual in ConfigureServices with a scheme (key). If you don't understand how to do this please consult the IdentityServer documentation. - -.. code-block:: csharp - - public void ConfigureServices(IServiceCollection services) - { - var authenticationProviderKey = "TestKey"; - Action options = o => - { - o.Authority = "https://whereyouridentityserverlives.com"; - o.ApiName = "api"; - o.SupportedTokens = SupportedTokens.Both; - o.ApiSecret = "secret"; - }; - - services.AddAuthentication() - .AddIdentityServerAuthentication(authenticationProviderKey, options); - - services.AddOcelot(); - } - -Then map the authentication provider key to a ReRoute in your configuration e.g. - -.. code-block:: json - - "ReRoutes": [{ - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 51876, - } - ], - "DownstreamPathTemplate": "/", - "UpstreamPathTemplate": "/", - "UpstreamHttpMethod": ["Post"], - "ReRouteIsCaseSensitive": false, - "DownstreamScheme": "http", - "AuthenticationOptions": { - "AuthenticationProviderKey": "TestKey", - "AllowedScopes": [] - } - }] - -Okta -^^^^ -Add the following to your startup Configure method: - -.. code-block:: csharp - - app - .UseAuthentication() - .UseOcelot() - .Wait(); - - -Add the following, at minimum, to your startup ConfigureServices method: - -.. code-block:: csharp - - services - .AddAuthentication() - .AddJwtBearer(oktaProviderKey, options => - { - options.Audience = configuration["Authentication:Okta:Audience"]; // Okta Authorization server Audience - options.Authority = configuration["Authentication:Okta:Server"]; // Okta Authorization Issuer URI URL e.g. https://{subdomain}.okta.com/oauth2/{authidentifier} - }); - services.AddOcelot(configuration); - - -NOTE: In order to get Ocelot to view the scope claim from Okta properly, you have to add the following to map the default Okta "scp" claim to "scope" - - -.. code-block:: csharp - - // Map Okta scp to scope claims instead of http://schemas.microsoft.com/identity/claims/scope to allow ocelot to read/verify them - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("scp"); - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("scp", "scope"); - - -`Issue 446 `_ that contains some code and examples that might help with Okta integration. - -Allowed Scopes -^^^^^^^^^^^^^ - -If you add scopes to AllowedScopes Ocelot will get all the user claims (from the token) of the type scope and make sure that the user has all of the scopes in the list. - -This is a way to restrict access to a ReRoute on a per scope basis. +Authentication +============== + +In order to authenticate Routes and subsequently use any of Ocelot's claims based features such as authorization or modifying the request with values from the token. Users must register authentication services in their Startup.cs as usual but they provide a scheme (authentication provider key) with each registration e.g. + +.. code-block:: csharp + + public void ConfigureServices(IServiceCollection services) + { + var authenticationProviderKey = "TestKey"; + + services.AddAuthentication() + .AddJwtBearer(authenticationProviderKey, x => + { + }); + } + + +In this example TestKey is the scheme that this provider has been registered with. We then map this to a Route in the configuration e.g. + +.. code-block:: json + + "Routes": [{ + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 51876, + } + ], + "DownstreamPathTemplate": "/", + "UpstreamPathTemplate": "/", + "UpstreamHttpMethod": ["Post"], + "RouteIsCaseSensitive": false, + "DownstreamScheme": "http", + "AuthenticationOptions": { + "AuthenticationProviderKey": "TestKey", + "AllowedScopes": [] + } + }] + +When Ocelot runs it will look at this Routes AuthenticationOptions.AuthenticationProviderKey and check that there is an Authentication provider registered with the given key. If there isn't then Ocelot will not start up, if there is then the Route will use that provider when it executes. + +If a Route is authenticated Ocelot will invoke whatever scheme is associated with it while executing the authentication middleware. If the request fails authentication Ocelot returns a http status code 401. + +JWT Tokens +^^^^^^^^^^ + +If you want to authenticate using JWT tokens maybe from a provider like Auth0 you can register your authentication middleware as normal e.g. + +.. code-block:: csharp + + public void ConfigureServices(IServiceCollection services) + { + var authenticationProviderKey = "TestKey"; + + services.AddAuthentication() + .AddJwtBearer(authenticationProviderKey, x => + { + x.Authority = "test"; + x.Audience = "test"; + }); + + services.AddOcelot(); + } + +Then map the authentication provider key to a Route in your configuration e.g. + +.. code-block:: json + + "Routes": [{ + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 51876, + } + ], + "DownstreamPathTemplate": "/", + "UpstreamPathTemplate": "/", + "UpstreamHttpMethod": ["Post"], + "RouteIsCaseSensitive": false, + "DownstreamScheme": "http", + "AuthenticationOptions": { + "AuthenticationProviderKey": "TestKey", + "AllowedScopes": [] + } + }] + + + +Identity Server Bearer Tokens +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In order to use IdentityServer bearer tokens, register your IdentityServer services as usual in ConfigureServices with a scheme (key). If you don't understand how to do this please consult the IdentityServer documentation. + +.. code-block:: csharp + + public void ConfigureServices(IServiceCollection services) + { + var authenticationProviderKey = "TestKey"; + Action options = o => + { + o.Authority = "https://whereyouridentityserverlives.com"; + // etc + }; + + services.AddAuthentication() + .AddJwtBearer(authenticationProviderKey, options); + + services.AddOcelot(); + } + +Then map the authentication provider key to a Route in your configuration e.g. + +.. code-block:: json + + "Routes": [{ + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 51876, + } + ], + "DownstreamPathTemplate": "/", + "UpstreamPathTemplate": "/", + "UpstreamHttpMethod": ["Post"], + "RouteIsCaseSensitive": false, + "DownstreamScheme": "http", + "AuthenticationOptions": { + "AuthenticationProviderKey": "TestKey", + "AllowedScopes": [] + } + }] + +Okta +^^^^ +Add the following to your startup Configure method: + +.. code-block:: csharp + + app + .UseAuthentication() + .UseOcelot() + .Wait(); + + +Add the following, at minimum, to your startup ConfigureServices method: + +.. code-block:: csharp + + services + .AddAuthentication() + .AddJwtBearer(oktaProviderKey, options => + { + options.Audience = configuration["Authentication:Okta:Audience"]; // Okta Authorization server Audience + options.Authority = configuration["Authentication:Okta:Server"]; // Okta Authorization Issuer URI URL e.g. https://{subdomain}.okta.com/oauth2/{authidentifier} + }); + services.AddOcelot(configuration); + + +NOTE: In order to get Ocelot to view the scope claim from Okta properly, you have to add the following to map the default Okta "scp" claim to "scope" + + +.. code-block:: csharp + + // Map Okta scp to scope claims instead of http://schemas.microsoft.com/identity/claims/scope to allow ocelot to read/verify them + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("scp"); + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("scp", "scope"); + + +`Issue 446 `_ that contains some code and examples that might help with Okta integration. + +Allowed Scopes +^^^^^^^^^^^^^ + +If you add scopes to AllowedScopes Ocelot will get all the user claims (from the token) of the type scope and make sure that the user has all of the scopes in the list. + +This is a way to restrict access to a Route on a per scope basis. diff --git a/docs/features/authorisation.rst b/docs/features/authorisation.rst deleted file mode 100644 index b7a4ec1ee..000000000 --- a/docs/features/authorisation.rst +++ /dev/null @@ -1,18 +0,0 @@ -Authorisation -============= - -Ocelot supports claims based authorisation which is run post authentication. This means if -you have a route you want to authorise you can add the following to you ReRoute configuration. - -.. code-block:: json - - "RouteClaimsRequirement": { - "UserType": "registered" - } - -In this example when the authorisation middleware is called Ocelot will check to see -if the user has the claim type UserType and if the value of that claim is registered. -If it isn't then the user will not be authorised and the response will be 403 forbidden. - - - diff --git a/docs/features/authorization.rst b/docs/features/authorization.rst new file mode 100644 index 000000000..c890c14c0 --- /dev/null +++ b/docs/features/authorization.rst @@ -0,0 +1,15 @@ +Authorization +============= + +Ocelot supports claims based authorization which is run post authentication. This means if you have a route you want to authorize you can add the following to you Route configuration. + +.. code-block:: json + + "RouteClaimsRequirement": { + "UserType": "registered" + } + +In this example when the authorization middleware is called Ocelot will check to seeif the user has the claim type UserType and if the value of that claim is registered. If it isn't then the user will not be authorized and the response will be 403 forbidden. + + + diff --git a/docs/features/caching.rst b/docs/features/caching.rst index c4be74f42..f6c2a7fd2 100644 --- a/docs/features/caching.rst +++ b/docs/features/caching.rst @@ -1,10 +1,7 @@ Caching ======= -Ocelot supports some very rudimentary caching at the moment provider by -the `CacheManager `_ project. This is an amazing project -that is solving a lot of caching problems. I would reccomend using this package to -cache with Ocelot. +Ocelot supports some very rudimentary caching at the moment provider by the `CacheManager `_ project. This is an amazing project that is solving a lot of caching problems. I would reccomend using this package to cache with Ocelot. The following example shows how to add CacheManager to Ocelot so that you can do output caching. @@ -24,7 +21,7 @@ The second thing you need to do something like the following to your ConfigureSe x.WithDictionaryHandle(); }) -Finally in order to use caching on a route in your ReRoute configuration add this setting. +Finally in order to use caching on a route in your Route configuration add this setting. .. code-block:: json @@ -32,19 +29,14 @@ Finally in order to use caching on a route in your ReRoute configuration add thi In this example ttl seconds is set to 15 which means the cache will expire after 15 seconds. -If you look at the example `here `_ you can see how the cache manager -is setup and then passed into the Ocelot AddCacheManager configuration method. You can use any settings supported by -the CacheManager package and just pass them in. +If you look at the example `here `_ you can see how the cache manager is setup and then passed into the Ocelot AddCacheManager configuration method. You can use any settings supported by the CacheManager package and just pass them in. -Anyway Ocelot currently supports caching on the URL of the downstream service -and setting a TTL in seconds to expire the cache. You can also clear the cache for a region -by calling Ocelot's administration API. +Anyway Ocelot currently supports caching on the URL of the downstream service and setting a TTL in seconds to expire the cache. You can also clear the cache for a region by calling Ocelot's administration API. Your own caching ^^^^^^^^^^^^^^^^ -If you want to add your own caching method implement the following interfaces and register them in DI -e.g. ``services.AddSingleton, MyCache>()`` +If you want to add your own caching method implement the following interfaces and register them in DI e.g. ``services.AddSingleton, MyCache>()`` ``IOcelotCache`` this is for output caching. diff --git a/docs/features/claimstransformation.rst b/docs/features/claimstransformation.rst index 8d4013176..c82657a4c 100644 --- a/docs/features/claimstransformation.rst +++ b/docs/features/claimstransformation.rst @@ -1,35 +1,17 @@ Claims Transformation ===================== -Ocelot allows the user to access claims and transform them into headers, query string -parameters, other claims and change downstream paths. This is only available once a user -has been authenticated. +Ocelot allows the user to access claims and transform them into headers, query string parameters, other claims and change downstream paths. This is only available once a user has been authenticated. -After the user is authenticated we run the claims to claims transformation middleware. -This allows the user to transform claims before the authorisation middleware is called. -After the user is authorised first we call the claims to headers middleware, then -the claims to query string parameters middleware, and Finally the claims to downstream path -middleware. +After the user is authenticated we run the claims to claims transformation middleware. This allows the user to transform claims before the authorization middleware is called. After the user is authorized first we call the claims to headers middleware, thenthe claims to query string parameters middleware, and Finally the claims to downstream pathmiddleware. -The syntax for performing the transforms is the same for each process. In the ReRoute -configuration a json dictionary is added with a specific name either AddClaimsToRequest, -AddHeadersToRequest, AddQueriesToRequest, or ChangeDownstreamPathTemplate. +The syntax for performing the transforms is the same for each process. In the Route configuration a json dictionary is added with a specific name either AddClaimsToRequest, AddHeadersToRequest, AddQueriesToRequest, or ChangeDownstreamPathTemplate. Note: I'm not a hotshot programmer so have no idea if this syntax is good... -Within this dictionary the entries specify how Ocelot should transform things! -The key to the dictionary is going to become the key of either a claim, header -or query parameter. In the case of ChangeDownstreamPathTemplate, the key must be -also specified in the DownstreamPathTemplate, in order to do the transformation. +Within this dictionary the entries specify how Ocelot should transform things! The key to the dictionary is going to become the key of either a claim, header or query parameter. In the case of ChangeDownstreamPathTemplate, the key must be also specified in the DownstreamPathTemplate, in order to do the transformation. -The value of the entry is parsed to logic that will perform the transform. First of -all a dictionary accessor is specified e.g. Claims[CustomerId]. This means we want -to access the claims and get the CustomerId claim type. Next is a greater than (>) -symbol which is just used to split the string. The next entry is either value or value with -and indexer. If value is specified Ocelot will just take the value and add it to the -transform. If the value has an indexer Ocelot will look for a delimiter which is provided -after another greater than symbol. Ocelot will then split the value on the delimiter -and add whatever was at the index requested to the transform. +The value of the entry is parsed to logic that will perform the transform. First ofall a dictionary accessor is specified e.g. Claims[CustomerId]. This means we want to access the claims and get the CustomerId claim type. Next is a greater than (>)symbol which is just used to split the string. The next entry is either value or value with an indexer. If value is specified Ocelot will just take the value and add it to the transform. If the value has an indexer Ocelot will look for a delimiter which is provided after another greater than symbol. Ocelot will then split the value on the delimiter and add whatever was at the index requested to the transform. Claims to Claims Transformation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -43,8 +25,7 @@ Below is an example configuration that will transforms claims to claims "UserId": "Claims[sub] > value[1] > |" } -This shows a transforms where Ocelot looks at the users sub claim and transforms it into -UserType and UserId claims. Assuming the sub looks like this "usertypevalue|useridvalue". +This shows a transforms where Ocelot looks at the users sub claim and transforms it into UserType and UserId claims. Assuming the sub looks like this "usertypevalue|useridvalue". Claims to Headers Tranformation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -57,8 +38,7 @@ Below is an example configuration that will transforms claims to headers "CustomerId": "Claims[sub] > value[1] > |" } -This shows a transform where Ocelot looks at the users sub claim and transforms it into a -CustomerId header. Assuming the sub looks like this "usertypevalue|useridvalue". +This shows a transform where Ocelot looks at the users sub claim and transforms it into a CustomerId header. Assuming the sub looks like this "usertypevalue|useridvalue". Claims to Query String Parameters Transformation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -71,8 +51,7 @@ Below is an example configuration that will transforms claims to query string pa "LocationId": "Claims[LocationId] > value", } -This shows a transform where Ocelot looks at the users LocationId claim and add it as -a query string parameter to be forwarded onto the downstream service. +This shows a transform where Ocelot looks at the users LocationId claim and add it as a query string parameter to be forwarded onto the downstream service. Claims to Downstream Path Transformation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -87,10 +66,7 @@ Below is an example configuration that will transform claims to downstream path "userId": "Claims[sub] > value[1] > |", } -This shows a transform where Ocelot looks at the users userId claim and substitutes the value -to the "{userId}" placeholder specified in the DownstreamPathTemplate. Take into account that the -key specified in the ChangeDownstreamPathTemplate must be the same than the placeholder specified in +This shows a transform where Ocelot looks at the users userId claim and substitutes the value to the "{userId}" placeholder specified in the DownstreamPathTemplate. Take into account that the key specified in the ChangeDownstreamPathTemplate must be the same than the placeholder specified in the DownstreamPathTemplate. -Note: if a key specified in the ChangeDownstreamPathTemplate does not exist as a placeholder in DownstreamPathTemplate -it will fail at runtime returning an error in the response. \ No newline at end of file +Note: if a key specified in the ChangeDownstreamPathTemplate does not exist as a placeholder in DownstreamPathTemplate it will fail at runtime returning an error in the response. \ No newline at end of file diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 7983af397..d41b05a88 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -1,20 +1,16 @@ Configuration ============ -An example configuration can be found `here `_. -There are two sections to the configuration. An array of ReRoutes and a GlobalConfiguration. -The ReRoutes are the objects that tell Ocelot how to treat an upstream request. The Global -configuration is a bit hacky and allows overrides of ReRoute specific settings. It's useful -if you don't want to manage lots of ReRoute specific settings. +An example configuration can be found `here `_. There are two sections to the configuration. An array of Routes and a GlobalConfiguration. The Routes are the objects that tell Ocelot how to treat an upstream request. The Global configuration is a bit hacky and allows overrides of Route specific settings. It's useful if you don't want to manage lots of Route specific settings. .. code-block:: json { - "ReRoutes": [], + "Routes": [], "GlobalConfiguration": {} } -Here is an example ReRoute configuration, You don't need to set all of these things but this is everything that is available at the moment: +Here is an example Route configuration, You don't need to set all of these things but this is everything that is available at the moment: .. code-block:: json @@ -24,6 +20,8 @@ Here is an example ReRoute configuration, You don't need to set all of these thi "UpstreamHttpMethod": [ "Get" ], + "DownstreamHttpMethod": "", + "DownstreamHttpVersion": "", "AddHeadersToRequest": {}, "AddClaimsToRequest": {}, "RouteClaimsRequirement": {}, @@ -33,7 +31,7 @@ Here is an example ReRoute configuration, You don't need to set all of these thi "TtlSeconds": 0, "Region": "" }, - "ReRouteIsCaseSensitive": false, + "RouteIsCaseSensitive": false, "ServiceName": "", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ @@ -114,7 +112,7 @@ Instead of adding the configuration directly e.g. AddJsonFile("ocelot.json") you In this scenario Ocelot will look for any files that match the pattern (?i)ocelot.([a-zA-Z0-9]*).json and then merge these together. If you want to set the GlobalConfiguration property you must have a file called ocelot.global.json. -The way Ocelot merges the files is basically load them, loop over them, add any ReRoutes, add any AggregateReRoutes and if the file is called ocelot.global.json add the GlobalConfiguration aswell as any ReRoutes or AggregateReRoutes. Ocelot will then save the merged configuration to a file called ocelot.json and this will be used as the source of truth while ocelot is running. +The way Ocelot merges the files is basically load them, loop over them, add any Routes, add any AggregateRoutes and if the file is called ocelot.global.json add the GlobalConfiguration aswell as any Routes or AggregateRoutes. Ocelot will then save the merged configuration to a file called ocelot.json and this will be used as the source of truth while ocelot is running. At the moment there is no validation at this stage it only happens when Ocelot validates the final merged configuration. This is something to be aware of when you are investigating problems. I would advise always checking what is in ocelot.json if you have any problems. @@ -150,8 +148,7 @@ Then you add the following when you register your services Ocelot will attempt t .AddConsul() .AddConfigStoredInConsul(); -You also need to add the following to your ocelot.json. This is how Ocelot -finds your Consul agent and interacts to load and store the configuration from Consul. +You also need to add the following to your ocelot.json. This is how Ocelot finds your Consul agent and interacts to load and store the configuration from Consul. .. code-block:: json @@ -162,8 +159,7 @@ finds your Consul agent and interacts to load and store the configuration from C } } -I decided to create this feature after working on the Raft consensus algorithm and finding out its super hard. Why not take advantage of the fact Consul already gives you this! -I guess it means if you want to use Ocelot to its fullest you take on Consul as a dependency for now. +I decided to create this feature after working on the Raft consensus algorithm and finding out its super hard. Why not take advantage of the fact Consul already gives you this! I guess it means if you want to use Ocelot to its fullest you take on Consul as a dependency for now. This feature has a 3 second ttl cache before making a new request to your local consul agent. @@ -199,24 +195,16 @@ If you do not set the ConfigurationKey Ocelot will use the string InternalConfig Follow Redirects / Use CookieContainer ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Use HttpHandlerOptions in ReRoute configuration to set up HttpHandler behavior: +Use HttpHandlerOptions in Route configuration to set up HttpHandler behavior: -1. AllowAutoRedirect is a value that indicates whether the request should follow redirection responses. Set it true if the request should automatically -follow redirection responses from the Downstream resource; otherwise false. The default value is false. +1. AllowAutoRedirect is a value that indicates whether the request should follow redirection responses. Set it true if the request should automatically follow redirection responses from the Downstream resource; otherwise false. The default value is false. -2. UseCookieContainer is a value that indicates whether the handler uses the CookieContainer -property to store server cookies and uses these cookies when sending requests. The default value is false. Please note -that if you are using the CookieContainer Ocelot caches the HttpClient for each downstream service. This means that all requests -to that DownstreamService will share the same cookies. `Issue 274 `_ was created because a user -noticed that the cookies were being shared. I tried to think of a nice way to handle this but I think it is impossible. If you don't cache the clients -that means each request gets a new client and therefore a new cookie container. If you clear the cookies from the cached client container you get race conditions due to inflight -requests. This would also mean that subsequent requests don't use the cookies from the previous response! All in all not a great situation. I would avoid setting -UseCookieContainer to true unless you have a really really good reason. Just look at your response headers and forward the cookies back with your next request! +2. UseCookieContainer is a value that indicates whether the handler uses the CookieContainer property to store server cookies and uses these cookies when sending requests. The default value is false. Please note that if you are using the CookieContainer Ocelot caches the HttpClient for each downstream service. This means that all requests to that DownstreamService will share the same cookies. `Issue 274 `_ was created because a user noticed that the cookies were being shared. I tried to think of a nice way to handle this but I think it is impossible. If you don't cache the clients that means each request gets a new client and therefore a new cookie container. If you clear the cookies from the cached client container you get race conditions due to inflight requests. This would also mean that subsequent requests don't use the cookies from the previous response! All in all not a great situation. I would avoid setting UseCookieContainer to true unless you have a really really good reason. Just look at your response headers and forward the cookies back with your next request! SSL Errors ^^^^^^^^^^ -If you want to ignore SSL warnings / errors set the following in your ReRoute config. +If you want to ignore SSL warnings / errors set the following in your Route config. .. code-block:: json @@ -227,4 +215,59 @@ I don't recommend doing this, I suggest creating your own certificate and then g MaxConnectionsPerServer ^^^^^^^^^^^^^^^^^^^^^^^ -This controls how many connections the internal HttpClient will open. This can be set at ReRoute or global level. \ No newline at end of file +This controls how many connections the internal HttpClient will open. This can be set at Route or global level. + +React to Configuration Changes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Resolve IOcelotConfigurationChangeTokenSource from the DI container if you wish to react to changes to the Ocelot configuration via the Ocelot.Administration API or ocelot.json being reloaded from the disk. You may either poll the change token's HasChanged property, or register a callback with the RegisterChangeCallback method. + +Polling the HasChanged property +------------------------------- + +.. code-block:: csharp + public class ConfigurationNotifyingService : BackgroundService + { + private readonly IOcelotConfigurationChangeTokenSource _tokenSource; + private readonly ILogger _logger; + public ConfigurationNotifyingService(IOcelotConfigurationChangeTokenSource tokenSource, ILogger logger) + { + _tokenSource = tokenSource; + _logger = logger; + } + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + if (_tokenSource.ChangeToken.HasChanged) + { + _logger.LogInformation("Configuration updated"); + } + await Task.Delay(1000, stoppingToken); + } + } + } + +Registering a callback +---------------------- + +.. code-block:: csharp + public class MyDependencyInjectedClass : IDisposable + { + private readonly IOcelotConfigurationChangeTokenSource _tokenSource; + private readonly IDisposable _callbackHolder; + public MyClass(IOcelotConfigurationChangeTokenSource tokenSource) + { + _tokenSource = tokenSource; + _callbackHolder = tokenSource.ChangeToken.RegisterChangeCallback(_ => Console.WriteLine("Configuration changed"), null); + } + public void Dispose() + { + _callbackHolder.Dispose(); + } + } + +DownstreamHttpVersion +--------------------- + +Ocelot allows you to choose the HTTP version it will use to make the proxy request. It can be set as "1.0", "1.1" or "2.0". \ No newline at end of file diff --git a/docs/features/delegatinghandlers.rst b/docs/features/delegatinghandlers.rst index 1da13c5f6..aaf325b78 100644 --- a/docs/features/delegatinghandlers.rst +++ b/docs/features/delegatinghandlers.rst @@ -1,66 +1,64 @@ -Delegating Handlers -=================== - -Ocelot allows the user to add delegating handlers to the HttpClient transport. This feature was requested `GitHub #208 `_ -and I decided that it was going to be useful in various ways. Since then we extended it in `GitHub #264 `_. - -Usage -^^^^^ - -In order to add delegating handlers to the HttpClient transport you need to do two main things. - -First in order to create a class that can be used a delegating handler it must look as follows. We are going to register these handlers in the -asp.net core container so you can inject any other services you have registered into the constructor of your handler. - -.. code-block:: csharp - - public class FakeHandler : DelegatingHandler - { - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - //do stuff and optionally call the base handler.. - return await base.SendAsync(request, cancellationToken); - } - } - -Next you must add the handlers to Ocelot's container like below... - -.. code-block:: csharp - - services.AddOcelot() - .AddDelegatingHandler() - .AddDelegatingHandler() - -Both of these Add methods have a default parameter called global which is set to false. If it is false then the intent of -the DelegatingHandler is to be applied to specific ReRoutes via ocelot.json (more on that later). If it is set to true -then it becomes a global handler and will be applied to all ReRoutes. - -e.g. - -As below... - -.. code-block:: csharp - - services.AddOcelot() - .AddDelegatingHandler(true) - -Finally if you want ReRoute specific DelegatingHandlers or to order your specific and / or global (more on this later) DelegatingHandlers -then you must add the following json to the specific ReRoute in ocelot.json. The names in the array must match the class names of your -DelegatingHandlers for Ocelot to match them together. - -.. code-block:: json - - "DelegatingHandlers": [ - "FakeHandlerTwo", - "FakeHandler" - ] - -You can have as many DelegatingHandlers as you want and they are run in the following order: - -1. Any globals that are left in the order they were added to services and are not in the DelegatingHandlers array from ocelot.json. -2. Any non global DelegatingHandlers plus any globals that were in the DelegatingHandlers array from ocelot.json ordered as they are in the DelegatingHandlers array. -3. Tracing DelegatingHandler if enabled (see tracing docs). -4. QoS DelegatingHandler if enabled (see QoS docs). -5. The HttpClient sends the HttpRequestMessage. - -Hopefully other people will find this feature useful! +Delegating Handlers +=================== + +Ocelot allows the user to add delegating handlers to the HttpClient transport. This feature was requested `GitHub #208 `_ +and I decided that it was going to be useful in various ways. Since then we extended it in `GitHub #264 `_. + +Usage +^^^^^ + +In order to add delegating handlers to the HttpClient transport you need to do two main things. + +First in order to create a class that can be used a delegating handler it must look as follows. We are going to register these handlers in the +asp.net core container so you can inject any other services you have registered into the constructor of your handler. + +.. code-block:: csharp + + public class FakeHandler : DelegatingHandler + { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + //do stuff and optionally call the base handler.. + return await base.SendAsync(request, cancellationToken); + } + } + +Next you must add the handlers to Ocelot's container like below... + +.. code-block:: csharp + + services.AddOcelot() + .AddDelegatingHandler() + .AddDelegatingHandler() + +Both of these Add methods have a default parameter called global which is set to false. If it is false then the intent of the DelegatingHandler is to be applied to specific Routes via ocelot.json (more on that later). If it is set to true +then it becomes a global handler and will be applied to all Routes. + +e.g. + +As below... + +.. code-block:: csharp + + services.AddOcelot() + .AddDelegatingHandler(true) + +Finally if you want Route specific DelegatingHandlers or to order your specific and / or global (more on this later) DelegatingHandlers then you must add the following json to the specific Route in ocelot.json. The names in the array must match the class names of your +DelegatingHandlers for Ocelot to match them together. + +.. code-block:: json + + "DelegatingHandlers": [ + "FakeHandlerTwo", + "FakeHandler" + ] + +You can have as many DelegatingHandlers as you want and they are run in the following order: + +1. Any globals that are left in the order they were added to services and are not in the DelegatingHandlers array from ocelot.json. +2. Any non global DelegatingHandlers plus any globals that were in the DelegatingHandlers array from ocelot.json ordered as they are in the DelegatingHandlers array. +3. Tracing DelegatingHandler if enabled (see tracing docs). +4. QoS DelegatingHandler if enabled (see QoS docs). +5. The HttpClient sends the HttpRequestMessage. + +Hopefully other people will find this feature useful! diff --git a/docs/features/errorcodes.rst b/docs/features/errorcodes.rst new file mode 100644 index 000000000..d07eb3a49 --- /dev/null +++ b/docs/features/errorcodes.rst @@ -0,0 +1,13 @@ +Http Error Status Codes +======================= + +Ocelot will return HTTP status error codes based on internal logic in certain siturations: +- 401 if the authentication middleware runs and the user is not authenticated. +- 403 if the authorization middleware runs and the user is unauthenticated, claim value not authroised, scope not authorized, user doesnt have required claim or cannot find claim. +- 503 if the downstream request times out. +- 499 if the request is cancelled by the client. +- 404 if unable to find a downstream route. +- 502 if unable to connect to downstream service. +- 500 if unable to complete the HTTP request downstream and the exception is not OperationCanceledException or HttpRequestException. +- 404 if Ocelot is unable to map an internal error code to a HTTP status code. + diff --git a/docs/features/graphql.rst b/docs/features/graphql.rst index 36006fae1..5c74c0564 100644 --- a/docs/features/graphql.rst +++ b/docs/features/graphql.rst @@ -1,14 +1,11 @@ GraphQL ======= -OK you got me Ocelot doesn't directly support GraphQL but so many people have asked about it I wanted to show how easy it is to integrate -the `graphql-dotnet `_ library. +OK you got me Ocelot doesn't directly support GraphQL but so many people have asked about it I wanted to show how easy it is to integrate the `graphql-dotnet `_ library. -Please see the sample project `OcelotGraphQL `_. -Using a combination of the graphql-dotnet project and Ocelot's DelegatingHandler features this is pretty easy to do. -However I do not intend to integrate more closely with GraphQL at the moment. Check out the samples readme and that should give -you enough instruction on how to do this! +Please see the sample project `OcelotGraphQL `_. Using a combination of the graphql-dotnet project and Ocelot's DelegatingHandler features this is pretty easy to do. +However I do not intend to integrate more closely with GraphQL at the moment. Check out the samples readme and that should give you enough instruction on how to do this! Good luck and have fun :> diff --git a/docs/features/headerstransformation.rst b/docs/features/headerstransformation.rst index ed772d612..a7f9d50a0 100644 --- a/docs/features/headerstransformation.rst +++ b/docs/features/headerstransformation.rst @@ -1,150 +1,150 @@ -Headers Transformation -====================== - -Ocelot allows the user to transform headers pre and post downstream request. At the moment Ocelot only supports find and replace. This feature was requested `GitHub #190 `_ and I decided that it was going to be useful in various ways. - -Add to Request -^^^^^^^^^^^^^^ - -This feature was requestes in `GitHub #313 `_. - -If you want to add a header to your upstream request please add the following to a ReRoute in your ocelot.json: - -.. code-block:: json - - "UpstreamHeaderTransform": { - "Uncle": "Bob" - } - -In the example above a header with the key Uncle and value Bob would be send to to the upstream service. - -Placeholders are supported too (see below). - -Add to Response -^^^^^^^^^^^^^^^ - -This feature was requested in `GitHub #280 `_. - -If you want to add a header to your downstream response please add the following to a ReRoute in ocelot.json.. - -.. code-block:: json - - "DownstreamHeaderTransform": { - "Uncle": "Bob" - }, - -In the example above a header with the key Uncle and value Bob would be returned by Ocelot when requesting the specific ReRoute. - -If you want to return the Butterfly APM trace id then do something like the following.. - -.. code-block:: json - - "DownstreamHeaderTransform": { - "AnyKey": "{TraceId}" - }, - -Find and Replace -^^^^^^^^^^^^^^^^ - -In order to transform a header first we specify the header key and then the type of transform we want e.g. - -.. code-block:: json - - "Test": "http://www.bbc.co.uk/, http://ocelot.com/" - -The key is "Test" and the value is "http://www.bbc.co.uk/, http://ocelot.com/". The value is saying replace http://www.bbc.co.uk/ with http://ocelot.com/. The syntax is {find}, {replace}. Hopefully pretty simple. There are examples below that explain more. - -Pre Downstream Request -^^^^^^^^^^^^^^^^^^^^^^ - -Add the following to a ReRoute in ocelot.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This header will be changed before the request downstream and will be sent to the downstream server. - -.. code-block:: json - - "UpstreamHeaderTransform": { - "Test": "http://www.bbc.co.uk/, http://ocelot.com/" - }, - -Post Downstream Request -^^^^^^^^^^^^^^^^^^^^^^^ - -Add the following to a ReRoute in ocelot.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This transformation will take place after Ocelot has received the response from the downstream service. - -.. code-block:: json - - "DownstreamHeaderTransform": { - "Test": "http://www.bbc.co.uk/, http://ocelot.com/" - }, - -Placeholders -^^^^^^^^^^^^ - -Ocelot allows placeholders that can be used in header transformation. - -{RemoteIpAddress} - This will find the clients IP address using _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString() so you will get back some IP. -{BaseUrl} - This will use Ocelot's base url e.g. http://localhost:5000 as its value. -{DownstreamBaseUrl} - This will use the downstream services base url e.g. http://localhost:5000 as its value. This only works for DownstreamHeaderTransform at the moment. -{TraceId} - This will use the Butterfly APM Trace Id. This only works for DownstreamHeaderTransform at the moment. - -Handling 302 Redirects -^^^^^^^^^^^^^^^^^^^^^^ -Ocelot will by default automatically follow redirects however if you want to return the location header to the client you might want to change the location to be Ocelot not the downstream service. Ocelot allows this with the following configuration. - -.. code-block:: json - - "DownstreamHeaderTransform": { - "Location": "http://www.bbc.co.uk/, http://ocelot.com/" - }, - "HttpHandlerOptions": { - "AllowAutoRedirect": false, - }, - -or you could use the BaseUrl placeholder. - -.. code-block:: json - - "DownstreamHeaderTransform": { - "Location": "http://localhost:6773, {BaseUrl}" - }, - "HttpHandlerOptions": { - "AllowAutoRedirect": false, - }, - -finally if you are using a load balancer with Ocelot you will get multiple downstream base urls so the above would not work. In this case you can do the following. - -.. code-block:: json - - "DownstreamHeaderTransform": { - "Location": "{DownstreamBaseUrl}, {BaseUrl}" - }, - "HttpHandlerOptions": { - "AllowAutoRedirect": false, - }, - -X-Forwarded-For -^^^^^^^^^^^^^^^ - -An example of using {RemoteIpAddress} placeholder... - -.. code-block:: json - - "UpstreamHeaderTransform": { - "X-Forwarded-For": "{RemoteIpAddress}" - } - -Future -^^^^^^ - -Ideally this feature would be able to support the fact that a header can have multiple values. At the moment it just assumes one. -It would also be nice if it could multi find and replace e.g. - -.. code-block:: json - - "DownstreamHeaderTransform": { - "Location": "[{one,one},{two,two}" - }, - "HttpHandlerOptions": { - "AllowAutoRedirect": false, - }, - -If anyone wants to have a go at this please help yourself!! +Headers Transformation +====================== + +Ocelot allows the user to transform headers pre and post downstream request. At the moment Ocelot only supports find and replace. This feature was requested `GitHub #190 `_ and I decided that it was going to be useful in various ways. + +Add to Request +^^^^^^^^^^^^^^ + +This feature was requestes in `GitHub #313 `_. + +If you want to add a header to your upstream request please add the following to a Route in your ocelot.json: + +.. code-block:: json + + "UpstreamHeaderTransform": { + "Uncle": "Bob" + } + +In the example above a header with the key Uncle and value Bob would be send to to the upstream service. + +Placeholders are supported too (see below). + +Add to Response +^^^^^^^^^^^^^^^ + +This feature was requested in `GitHub #280 `_. + +If you want to add a header to your downstream response please add the following to a Route in ocelot.json.. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Uncle": "Bob" + }, + +In the example above a header with the key Uncle and value Bob would be returned by Ocelot when requesting the specific Route. + +If you want to return the Butterfly APM trace id then do something like the following.. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "AnyKey": "{TraceId}" + }, + +Find and Replace +^^^^^^^^^^^^^^^^ + +In order to transform a header first we specify the header key and then the type of transform we want e.g. + +.. code-block:: json + + "Test": "http://www.bbc.co.uk/, http://ocelot.com/" + +The key is "Test" and the value is "http://www.bbc.co.uk/, http://ocelot.com/". The value is saying replace http://www.bbc.co.uk/ with http://ocelot.com/. The syntax is {find}, {replace}. Hopefully pretty simple. There are examples below that explain more. + +Pre Downstream Request +^^^^^^^^^^^^^^^^^^^^^^ + +Add the following to a Route in ocelot.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This header will be changed before the request downstream and will be sent to the downstream server. + +.. code-block:: json + + "UpstreamHeaderTransform": { + "Test": "http://www.bbc.co.uk/, http://ocelot.com/" + }, + +Post Downstream Request +^^^^^^^^^^^^^^^^^^^^^^^ + +Add the following to a Route in ocelot.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This transformation will take place after Ocelot has received the response from the downstream service. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Test": "http://www.bbc.co.uk/, http://ocelot.com/" + }, + +Placeholders +^^^^^^^^^^^^ + +Ocelot allows placeholders that can be used in header transformation. + +{RemoteIpAddress} - This will find the clients IP address using _httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString() so you will get back some IP. +{BaseUrl} - This will use Ocelot's base url e.g. http://localhost:5000 as its value. +{DownstreamBaseUrl} - This will use the downstream services base url e.g. http://localhost:5000 as its value. This only works for DownstreamHeaderTransform at the moment. +{TraceId} - This will use the Butterfly APM Trace Id. This only works for DownstreamHeaderTransform at the moment. +{UpstreamHost} - This will look for the incoming Host header. + +Handling 302 Redirects +^^^^^^^^^^^^^^^^^^^^^^ +Ocelot will by default automatically follow redirects however if you want to return the location header to the client you might want to change the location to be Ocelot not the downstream service. Ocelot allows this with the following configuration. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Location": "http://www.bbc.co.uk/, http://ocelot.com/" + }, + "HttpHandlerOptions": { + "AllowAutoRedirect": false, + }, + +or you could use the BaseUrl placeholder. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Location": "http://localhost:6773, {BaseUrl}" + }, + "HttpHandlerOptions": { + "AllowAutoRedirect": false, + }, + +finally if you are using a load balancer with Ocelot you will get multiple downstream base urls so the above would not work. In this case you can do the following. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Location": "{DownstreamBaseUrl}, {BaseUrl}" + }, + "HttpHandlerOptions": { + "AllowAutoRedirect": false, + }, + +X-Forwarded-For +^^^^^^^^^^^^^^^ + +An example of using {RemoteIpAddress} placeholder... + +.. code-block:: json + + "UpstreamHeaderTransform": { + "X-Forwarded-For": "{RemoteIpAddress}" + } + +Future +^^^^^^ + +Ideally this feature would be able to support the fact that a header can have multiple values. At the moment it just assumes one. It would also be nice if it could multi find and replace e.g. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Location": "[{one,one},{two,two}" + }, + "HttpHandlerOptions": { + "AllowAutoRedirect": false, + }, + +If anyone wants to have a go at this please help yourself!! diff --git a/docs/features/kubernetes.rst b/docs/features/kubernetes.rst index 75568ec41..abab44aef 100644 --- a/docs/features/kubernetes.rst +++ b/docs/features/kubernetes.rst @@ -1,95 +1,95 @@ -Kubernetes -============== - -This feature was requested as part of `Issue 345 `_ . to add support for kubernetes's service discovery provider. - -The first thing you need to do is install the NuGet package that provides kubernetes support in Ocelot. - -``Install-Package Ocelot.Provider.Kubernetes`` - -Then add the following to your ConfigureServices method. - -.. code-block:: csharp - - s.AddOcelot() - .AddKubernetes(); - -If you have services deployed in kubernetes you will normally use the naming service to access them. Default usePodServiceAccount = True, which means that ServiceAccount using Pod to access the service of the k8s cluster needs to be ServiceAccount based on RABC authorization - -.. code-block::csharp - public static class OcelotBuilderExtensions - { - public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder, bool usePodServiceAccount = true); - } - -You can replicate a Permissive. Using RBAC role bindings. -`Permissive RBAC Permissions `_, k8s api server and token will read from pod . - -.. code-block::bash -kubectl create clusterrolebinding permissive-binding --clusterrole=cluster-admin --user=admin --user=kubelet --group=system:serviceaccounts - -The following example shows how to set up a ReRoute that will work in kubernetes. The most important thing is the ServiceName which is made up of the -kubernetes service name. We also need to set up the ServiceDiscoveryProvider in GlobalConfiguration. The example here shows a typical configuration. - - -.. code-block:: json - - { - "ReRoutes": [ - { - "DownstreamPathTemplate": "/api/values", - "DownstreamScheme": "http", - "UpstreamPathTemplate": "/values", - "ServiceName": "downstreamservice", - "UpstreamHttpMethod": [ "Get" ] - } - ], - "GlobalConfiguration": { - "ServiceDiscoveryProvider": { - "Host": "192.168.0.13", - "Port": 443, - "Token": "txpc696iUhbVoudg164r93CxDTrKRVWG", - "Namespace": "dev", - "Type": "kube" - } - } -} - -Service deployment in Namespace Dev , ServiceDiscoveryProvider type is kube, you also can set pollkube ServiceDiscoveryProvider type. - Note: Host、 Port and Token are no longer in use。 - -You use Ocelot to poll kubernetes for latest service information rather than per request. If you want to poll kubernetes for the latest services rather than per request (default behaviour) then you need to set the following configuration. - -.. code-block:: json - - "ServiceDiscoveryProvider": { - "Host": "192.168.0.13", - "Port": 443, - "Token": "txpc696iUhbVoudg164r93CxDTrKRVWG", - "Namespace": "dev", - "Type": "pollkube" - "PollingInterval": 100 - } - -The polling interval is in milliseconds and tells Ocelot how often to call kubernetes for changes in service configuration. - -Please note there are tradeoffs here. If you poll kubernetes it is possible Ocelot will not know if a service is down depending on your polling interval and you might get more errors than if you get the latest services per request. This really depends on how volatile your services are. I doubt it will matter for most people and polling may give a tiny performance improvement over calling kubernetes per request. -There is no way for Ocelot to work these out for you. - -If your downstream service resides in a different namespace you can override the global setting at the ReRoute level by specifying a ServiceNamespace - - -.. code-block:: json - -{ - "ReRoutes": [ - { - "DownstreamPathTemplate": "/api/values", - "DownstreamScheme": "http", - "UpstreamPathTemplate": "/values", - "ServiceName": "downstreamservice", - "ServiceNamespace": "downstream-namespace", - "UpstreamHttpMethod": [ "Get" ] - } - ] -} +Kubernetes +============== + +This feature was requested as part of `Issue 345 `_ . to add support for kubernetes's provider. + +Ocelot will call the k8s endpoints API in a given namespace to get all of the endpoints for a pod and then load balance across them. Ocelot used to use the services api to send requests to the k8s service but this was changed in `PR 1134 `_ because the service did not load balance as expected. + +The first thing you need to do is install the NuGet package that provides kubernetes support in Ocelot. + +``Install-Package Ocelot.Provider.Kubernetes`` + +Then add the following to your ConfigureServices method. + +.. code-block:: csharp + + s.AddOcelot() + .AddKubernetes(); + +If you have services deployed in kubernetes you will normally use the naming service to access them. Default usePodServiceAccount = True, which means that ServiceAccount using Pod to access the service of the k8s cluster needs to be ServiceAccount based on RBAC authorization + +.. code-block::csharp + public static class OcelotBuilderExtensions + { + public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder, bool usePodServiceAccount = true); + } + +You can replicate a Permissive. Using RBAC role bindings. +`Permissive RBAC Permissions `_, k8s api server and token will read from pod. + +.. code-block::bash +kubectl create clusterrolebinding permissive-binding --clusterrole=cluster-admin --user=admin --user=kubelet --group=system:serviceaccounts + +The following example shows how to set up a Route that will work in kubernetes. The most important thing is the ServiceName which is made up of the kubernetes service name. We also need to set up the ServiceDiscoveryProvider in GlobalConfiguration. The example here shows a typical configuration. + + +.. code-block:: json + + { + "Routes": [ + { + "DownstreamPathTemplate": "/api/values", + "DownstreamScheme": "http", + "UpstreamPathTemplate": "/values", + "ServiceName": "downstreamservice", + "UpstreamHttpMethod": [ "Get" ] + } + ], + "GlobalConfiguration": { + "ServiceDiscoveryProvider": { + "Host": "192.168.0.13", + "Port": 443, + "Token": "txpc696iUhbVoudg164r93CxDTrKRVWG", + "Namespace": "dev", + "Type": "kube" + } + } +} + +Service deployment in Namespace Dev , ServiceDiscoveryProvider type is kube, you also can set pollkube ServiceDiscoveryProvider type. + Note: Host、 Port and Token are no longer in use。 + +You use Ocelot to poll kubernetes for latest service information rather than per request. If you want to poll kubernetes for the latest services rather than per request (default behaviour) then you need to set the following configuration. + +.. code-block:: json + + "ServiceDiscoveryProvider": { + "Host": "192.168.0.13", + "Port": 443, + "Token": "txpc696iUhbVoudg164r93CxDTrKRVWG", + "Namespace": "dev", + "Type": "pollkube", + "PollingInterval": 100 + } + +The polling interval is in milliseconds and tells Ocelot how often to call kubernetes for changes in service configuration. + +Please note there are tradeoffs here. If you poll kubernetes it is possible Ocelot will not know if a service is down depending on your polling interval and you might get more errors than if you get the latest services per request. This really depends on how volatile your services are. I doubt it will matter for most people and polling may give a tiny performance improvement over calling kubernetes per request. There is no way for Ocelot to work these out for you. + +If your downstream service resides in a different namespace you can override the global setting at the Route level by specifying a ServiceNamespace. + + +.. code-block:: json + + { + "Routes": [ + { + "DownstreamPathTemplate": "/api/values", + "DownstreamScheme": "http", + "UpstreamPathTemplate": "/values", + "ServiceName": "downstreamservice", + "ServiceNamespace": "downstream-namespace", + "UpstreamHttpMethod": [ "Get" ] + } + ] + } diff --git a/docs/features/loadbalancer.rst b/docs/features/loadbalancer.rst index 707060338..a4ec4ad91 100644 --- a/docs/features/loadbalancer.rst +++ b/docs/features/loadbalancer.rst @@ -1,110 +1,210 @@ -Load Balancer -============= - -Ocelot can load balance across available downstream services for each ReRoute. This means you can scale your downstream services and Ocelot can use them effectively. - -The type of load balancer available are: - - LeastConnection - tracks which services are dealing with requests and sends new requests to service with least existing requests. The algorythm state is not distributed across a cluster of Ocelot's. - - RoundRobin - loops through available services and sends requests. The algorythm state is not distributed across a cluster of Ocelot's. - - NoLoadBalancer - takes the first available service from config or service discovery. - - CookieStickySessions - uses a cookie to stick all requests to a specific server. More info below. - -You must choose in your configuration which load balancer to use. - -Configuration -^^^^^^^^^^^^^ - -The following shows how to set up multiple downstream services for a ReRoute using ocelot.json and then select the LeastConnection load balancer. This is the simplest way to get load balancing set up. - -.. code-block:: json - - { - "DownstreamPathTemplate": "/api/posts/{postId}", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "10.0.1.10", - "Port": 5000, - }, - { - "Host": "10.0.1.11", - "Port": 5000, - } - ], - "UpstreamPathTemplate": "/posts/{postId}", - "LoadBalancerOptions": { - "Type": "LeastConnection" - }, - "UpstreamHttpMethod": [ "Put", "Delete" ] - } - - -Service Discovery -^^^^^^^^^^^^^^^^^ - -The following shows how to set up a ReRoute using service discovery then select the LeastConnection load balancer. - -.. code-block:: json - - { - "DownstreamPathTemplate": "/api/posts/{postId}", - "DownstreamScheme": "https", - "UpstreamPathTemplate": "/posts/{postId}", - "UpstreamHttpMethod": [ "Put" ], - "ServiceName": "product", - "LoadBalancerOptions": { - "Type": "LeastConnection" - }, - } - -When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balance requests across any available services. If you add and remove services from the -service discovery provider (consul) then Ocelot should respect this and stop calling services that have been removed and start calling services that have been added. - -CookieStickySessions -^^^^^^^^^^^^^^^^^^^^ - -I've implemented a really basic sticky session type of load balancer. The scenario it is meant to support is you have a bunch of downstream -servers that don't share session state so if you get more than one request for one of these servers then it should go to the same box each -time or the session state might be incorrect for the given user. This feature was requested in `Issue #322 `_ -though what the user wants is more complicated than just sticky sessions :) anyway I thought this would be a nice feature to have! - -In order to set up CookieStickySessions load balancer you need to do something like the following. - -.. code-block:: json - - { - "DownstreamPathTemplate": "/api/posts/{postId}", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "10.0.1.10", - "Port": 5000, - }, - { - "Host": "10.0.1.11", - "Port": 5000, - } - ], - "UpstreamPathTemplate": "/posts/{postId}", - "LoadBalancerOptions": { - "Type": "CookieStickySessions", - "Key": "ASP.NET_SessionId", - "Expiry": 1800000 - }, - "UpstreamHttpMethod": [ "Put", "Delete" ] - } - -The LoadBalancerOptions are Type this needs to be CookieStickySessions, Key this is the key of the cookie you -wish to use for the sticky sessions, Expiry this is how long in milliseconds you want to the session to be stuck for. Remember this -refreshes on every request which is meant to mimick how sessions work usually. - -If you have multiple ReRoutes with the same LoadBalancerOptions then all of those ReRoutes will use the same load balancer for there -subsequent requests. This means the sessions will be stuck across ReRoutes. - -Please note that if you give more than one DownstreamHostAndPort or you are using a Service Discovery provider such as Consul -and this returns more than one service then CookieStickySessions uses round robin to select the next server. This is hard coded at the -moment but could be changed. +Load Balancer +============= + +Ocelot can load balance across available downstream services for each Route. This means you can scale your downstream services and Ocelot can use them effectively. + +The type of load balancer available are: + + LeastConnection - tracks which services are dealing with requests and sends new requests to service with least existing requests. The algorythm state is not distributed across a cluster of Ocelot's. + + RoundRobin - loops through available services and sends requests. The algorythm state is not distributed across a cluster of Ocelot's. + + NoLoadBalancer - takes the first available service from config or service discovery. + + CookieStickySessions - uses a cookie to stick all requests to a specific server. More info below. + +You must choose in your configuration which load balancer to use. + +Configuration +^^^^^^^^^^^^^ + +The following shows how to set up multiple downstream services for a Route using ocelot.json and then select the LeastConnection load balancer. This is the simplest way to get load balancing set up. + +.. code-block:: json + + { + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "10.0.1.10", + "Port": 5000, + }, + { + "Host": "10.0.1.11", + "Port": 5000, + } + ], + "UpstreamPathTemplate": "/posts/{postId}", + "LoadBalancerOptions": { + "Type": "LeastConnection" + }, + "UpstreamHttpMethod": [ "Put", "Delete" ] + } + + +Service Discovery +^^^^^^^^^^^^^^^^^ + +The following shows how to set up a Route using service discovery then select the LeastConnection load balancer. + +.. code-block:: json + + { + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamScheme": "https", + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": [ "Put" ], + "ServiceName": "product", + "LoadBalancerOptions": { + "Type": "LeastConnection" + }, + } + +When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balance requests across any available services. If you add and remove services from the +service discovery provider (consul) then Ocelot should respect this and stop calling services that have been removed and start calling services that have been added. + +CookieStickySessions +^^^^^^^^^^^^^^^^^^^^ + +I've implemented a really basic sticky session type of load balancer. The scenario it is meant to support is you have a bunch of downstream servers that don't share session state so if you get more than one request for one of these servers then it should go to the same box each time or the session state might be incorrect for the given user. This feature was requested in `Issue #322 `_ though what the user wants is more complicated than just sticky sessions :) anyway I thought this would be a nice feature to have! + +In order to set up CookieStickySessions load balancer you need to do something like the following. + +.. code-block:: json + + { + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "10.0.1.10", + "Port": 5000, + }, + { + "Host": "10.0.1.11", + "Port": 5000, + } + ], + "UpstreamPathTemplate": "/posts/{postId}", + "LoadBalancerOptions": { + "Type": "CookieStickySessions", + "Key": "ASP.NET_SessionId", + "Expiry": 1800000 + }, + "UpstreamHttpMethod": [ "Put", "Delete" ] + } + +The LoadBalancerOptions are Type this needs to be CookieStickySessions, Key this is the key of the cookie you wish to use for the sticky sessions, Expiry this is how long in milliseconds you want to the session to be stuck for. Remember this refreshes on every request which is meant to mimick how sessions work usually. + +If you have multiple Routes with the same LoadBalancerOptions then all of those Routes will use the same load balancer for there subsequent requests. This means the sessions will be stuck across Routes. + +Please note that if you give more than one DownstreamHostAndPort or you are using a Service Discovery provider such as Consul and this returns more than one service then CookieStickySessions uses round robin to select the next server. This is hard coded at the moment but could be changed. + +Custom Load Balancers +^^^^^^^^^^^^^^^^^^^^ + +`DavidLievrouw >> _services; + private readonly object _lock = new object(); + + private int _last; + + public CustomLoadBalancer(Func>> services) + { + _services = services; + } + + public async Task> Lease(DownstreamContext downstreamContext, HttpContext httpContext) + { + var services = await _services(); + lock (_lock) + { + if (_last >= services.Count) + { + _last = 0; + } + + var next = services[_last]; + _last++; + return new OkResponse(next.HostAndPort); + } + } + + public void Release(ServiceHostAndPort hostAndPort) + { + } + } + +Finally you need to register this class with Ocelot. I have used the most complex example below to show all of the data / types that can be passed into the factory that creates load balancers. + +.. code-block:: csharp + + Func loadBalancerFactoryFunc = (serviceProvider, Route, serviceDiscoveryProvider) => new CustomLoadBalancer(serviceDiscoveryProvider.Get); + + s.AddOcelot() + .AddCustomLoadBalancer(loadBalancerFactoryFunc); + +However there is a much simpler example that will work the same. + +.. code-block:: csharp + + s.AddOcelot() + .AddCustomLoadBalancer(); + +There are numerous extension methods to add a custom load balancer and the interface is as follows. + +.. code-block:: csharp + + IOcelotBuilder AddCustomLoadBalancer() + where T : ILoadBalancer, new(); + + IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) + where T : ILoadBalancer; + + IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) + where T : ILoadBalancer; + + IOcelotBuilder AddCustomLoadBalancer( + Func loadBalancerFactoryFunc) + where T : ILoadBalancer; + + IOcelotBuilder AddCustomLoadBalancer( + Func loadBalancerFactoryFunc) + where T : ILoadBalancer; + +When you enable custom load balancers Ocelot looks up your load balancer by its class name when it decides if it should do load balancing. If it finds a match it will use your load balaner to load balance. If Ocelot cannot match the load balancer type in your configuration with the name of registered load balancer class then you will receive a HTTP 500 internal server error. If your load balancer factory throw an exception when Ocelot calls it you will receive a HTTP 500 internal server error. + +Remember if you specify no load balancer in your config Ocelot will not try and load balance. \ No newline at end of file diff --git a/docs/features/logging.rst b/docs/features/logging.rst index b09a26cfb..c79c7743d 100644 --- a/docs/features/logging.rst +++ b/docs/features/logging.rst @@ -1,20 +1,17 @@ Logging ======= -Ocelot uses the standard logging interfaces ILoggerFactory / ILogger at the moment. -This is encapsulated in IOcelotLogger / IOcelotLoggerFactory with an implementation +Ocelot uses the standard logging interfaces ILoggerFactory / ILogger at the moment. This is encapsulated in IOcelotLogger / IOcelotLoggerFactory with an implementation for the standard asp.net core logging stuff at the moment. This is because Ocelot add's some extra info to the logs such as request id if it is configured. There is a global error handler 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. -The reason for not just using bog standard framework logging is that I could not -work out how to override the request id that get's logged when setting IncludeScopes +The reason for not just using bog standard framework logging is that I could not work out how to override the request id that get's logged when setting IncludeScopes to true for logging settings. Nicely onto the next feature. Warning ^^^^^^^ -If you are logging to Console you will get terrible performance. I have had so many issues about performance issues with Ocelot -and it is always logging level Debug, logging to Console :) Make sure you are logging to something proper in production :) +If you are logging to Console you will get terrible performance. I have had so many issues about performance issues with Ocelot and it is always logging level Debug, logging to Console :) Make sure you are logging to something proper in production :) diff --git a/docs/features/methodtransformation.rst b/docs/features/methodtransformation.rst new file mode 100644 index 000000000..b1fc8cd32 --- /dev/null +++ b/docs/features/methodtransformation.rst @@ -0,0 +1,28 @@ +HTTP Method Transformation +========================== + +Ocelot allows the user to change the HTTP request method that will be used when making a request to a downstream service. + +This achieved by setting the following Route configuration: + +.. code-block:: json + + { + "DownstreamPathTemplate": "/{url}", + "UpstreamPathTemplate": "/{url}", + "UpstreamHttpMethod": [ + "Get" + ], + "DownstreamHttpMethod": "POST", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 53271 + } + ], + } + +The key property here is DownstreamHttpMethod which is set as POST and the Route will only match on GET as set by UpstreamHttpMethod. + +This feature can be useful when interacting with downstream apis that only support POST and you want to present some kind of RESTful interface. \ No newline at end of file diff --git a/docs/features/middlewareinjection.rst b/docs/features/middlewareinjection.rst index 4b6636582..7fccc599b 100644 --- a/docs/features/middlewareinjection.rst +++ b/docs/features/middlewareinjection.rst @@ -31,9 +31,9 @@ The user can set functions against the following. * AuthenticationMiddleware - This overrides Ocelots authentication middleware. -* PreAuthorisationMiddleware - This allows the user to run pre authorisation logic and then call Ocelot's authorisation middleware. +* PreAuthorizationMiddleware - This allows the user to run pre authorization logic and then call Ocelot's authorization middleware. -* AuthorisationMiddleware - This overrides Ocelots authorisation middleware. +* AuthorizationMiddleware - This overrides Ocelots authorization middleware. * PreQueryStringBuilderMiddleware - This allows the user to manipulate the query string on the http request before it is passed to Ocelots request creator. diff --git a/docs/features/qualityofservice.rst b/docs/features/qualityofservice.rst index 9839b2f76..ddd4d6892 100644 --- a/docs/features/qualityofservice.rst +++ b/docs/features/qualityofservice.rst @@ -1,8 +1,7 @@ Quality of Service ================== -Ocelot supports one QoS capability at the current time. You can set on a per ReRoute basis if you -want to use a circuit breaker when making requests to a downstream service. This uses an awesome +Ocelot supports one QoS capability at the current time. You can set on a per Route basis if you want to use a circuit breaker when making requests to a downstream service. This uses an awesome .NET library called Polly check them out `here `_. The first thing you need to do if you want to use the administration API is bring in the relevant NuGet package.. @@ -20,7 +19,7 @@ Then in your ConfigureServices method .AddPolly(); } -Then add the following section to a ReRoute configuration. +Then add the following section to a Route configuration. .. code-block:: json @@ -30,8 +29,7 @@ Then add the following section to a ReRoute configuration. "TimeoutValue":5000 } -You must set a number greater than 0 against ExceptionsAllowedBeforeBreaking for this rule to be -implemented. Duration of break means the circuit breaker will stay open for 1 second after it is tripped. +You must set a number greater than 0 against ExceptionsAllowedBeforeBreaking for this rule to be implemented. Duration of break means the circuit breaker will stay open for 1 second after it is tripped. TimeoutValue means if a request takes more than 5 seconds it will automatically be timed out. You can set the TimeoutValue in isolation of the ExceptionsAllowedBeforeBreaking and DurationOfBreak options. @@ -44,5 +42,4 @@ You can set the TimeoutValue in isolation of the ExceptionsAllowedBeforeBreaking There is no point setting the other two in isolation as they affect each other :) -If you do not add a QoS section QoS will not be used however Ocelot will default to a 90 second timeout -on all downstream requests. If someone needs this to be configurable open an issue. +If you do not add a QoS section QoS will not be used however Ocelot will default to a 90 second timeout on all downstream requests. If someone needs this to be configurable open an issue. diff --git a/docs/features/raft.rst b/docs/features/raft.rst deleted file mode 100644 index fdbc1ea24..000000000 --- a/docs/features/raft.rst +++ /dev/null @@ -1,49 +0,0 @@ -Raft (EXPERIMENTAL DO NOT USE IN PRODUCTION) -============================================ - -Ocelot has recently integrated `Rafty `_ which is an implementation of Raft that I have also been working on over the last year. This project is very experimental so please do not use this feature of Ocelot in production until I think it's OK. - -Raft is a distributed concensus algorithm that allows a cluster of servers (Ocelots) to maintain local state without having a centralised database for storing state (e.g. SQL Server). - -To get Raft support you must first install the Ocelot Rafty package. - -``Install-Package Ocelot.Provider.Rafty`` - -Then you must make the following changes to your Startup.cs / Program.cs. - -.. code-block:: csharp - - public virtual void ConfigureServices(IServiceCollection services) - { - services - .AddOcelot() - .AddAdministration("/administration", "secret") - .AddRafty(); - } - -In addition to this you must add a file called peers.json to your main project and it will look as follows - -.. code-block:: json - - { - "Peers": [{ - "HostAndPort": "http://localhost:5000" - }, - { - "HostAndPort": "http://localhost:5002" - }, - { - "HostAndPort": "http://localhost:5003" - }, - { - "HostAndPort": "http://localhost:5004" - }, - { - "HostAndPort": "http://localhost:5001" - } - ] - } - -Each instance of Ocelot must have it's address in the array so that they can communicate using Rafty. - -Once you have made these configuration changes you must deploy and start each instance of Ocelot using the addresses in the peers.json file. The servers should then start communicating with each other! You can test if everything is working by posting a configuration update and checking it has replicated to all servers by getting their configuration. diff --git a/docs/features/ratelimiting.rst b/docs/features/ratelimiting.rst index 4dd0354f7..895b3e419 100644 --- a/docs/features/ratelimiting.rst +++ b/docs/features/ratelimiting.rst @@ -1,47 +1,47 @@ -Rate Limiting -============= - -Thanks to `@catcherwong article `_ for inspiring me to finally write this documentation. - -Ocelot supports rate limiting of upstream requests so that your downstream services do not become overloaded. This feature was added by @geffzhang on GitHub! Thanks very much. - -OK so to get rate limiting working for a ReRoute you need to add the following json to it. - -.. code-block:: json - - "RateLimitOptions": { - "ClientWhitelist": [], - "EnableRateLimiting": true, - "Period": "1s", - "PeriodTimespan": 1, - "Limit": 1 - } - -ClientWhitelist - This is an array that contains the whitelist of the client. It means that the client in this array will not be affected by the rate limiting. - -EnableRateLimiting - This value specifies enable endpoint rate limiting. - -Period - This value specifies the period that the limit applies to, such as 1s, 5m, 1h,1d and so on. If you make more requests in the period than the limit allows then you need to wait for PeriodTimespan to elapse before you make another request. - -PeriodTimespan - This value specifies that we can retry after a certain number of seconds. - -Limit - This value specifies the maximum number of requests that a client can make in a defined period. - -You can also set the following in the GlobalConfiguration part of ocelot.json - -.. code-block:: json - - "RateLimitOptions": { - "DisableRateLimitHeaders": false, - "QuotaExceededMessage": "Customize Tips!", - "HttpStatusCode": 999, - "ClientIdHeader" : "Test" - } - -DisableRateLimitHeaders - This value specifies whether X-Rate-Limit and Retry-After headers are disabled. - -QuotaExceededMessage - This value specifies the exceeded message. - -HttpStatusCode - This value specifies the returned HTTP Status code when rate limiting occurs. - -ClientIdHeader - Allows you to specifiy the header that should be used to identify clients. By default it is "ClientId" +Rate Limiting +============= + +Thanks to `@catcherwong article `_ for inspiring me to finally write this documentation. + +Ocelot supports rate limiting of upstream requests so that your downstream services do not become overloaded. This feature was added by @geffzhang on GitHub! Thanks very much. + +OK so to get rate limiting working for a Route you need to add the following json to it. + +.. code-block:: json + + "RateLimitOptions": { + "ClientWhitelist": [], + "EnableRateLimiting": true, + "Period": "1s", + "PeriodTimespan": 1, + "Limit": 1 + } + +ClientWhitelist - This is an array that contains the whitelist of the client. It means that the client in this array will not be affected by the rate limiting. + +EnableRateLimiting - This value specifies enable endpoint rate limiting. + +Period - This value specifies the period that the limit applies to, such as 1s, 5m, 1h,1d and so on. If you make more requests in the period than the limit allows then you need to wait for PeriodTimespan to elapse before you make another request. + +PeriodTimespan - This value specifies that we can retry after a certain number of seconds. + +Limit - This value specifies the maximum number of requests that a client can make in a defined period. + +You can also set the following in the GlobalConfiguration part of ocelot.json + +.. code-block:: json + + "RateLimitOptions": { + "DisableRateLimitHeaders": false, + "QuotaExceededMessage": "Customize Tips!", + "HttpStatusCode": 999, + "ClientIdHeader" : "Test" + } + +DisableRateLimitHeaders - This value specifies whether X-Rate-Limit and Retry-After headers are disabled. + +QuotaExceededMessage - This value specifies the exceeded message. + +HttpStatusCode - This value specifies the returned HTTP Status code when rate limiting occurs. + +ClientIdHeader - Allows you to specifiy the header that should be used to identify clients. By default it is "ClientId" diff --git a/docs/features/requestaggregation.rst b/docs/features/requestaggregation.rst index 6281728cd..3c84d0da5 100644 --- a/docs/features/requestaggregation.rst +++ b/docs/features/requestaggregation.rst @@ -1,187 +1,185 @@ -Request Aggregation -=================== - -Ocelot allows you to specify Aggregate ReRoutes that compose multiple normal ReRoutes and map their responses into one object. This is usually where you have -a client that is making multiple requests to a server where it could just be one. This feature allows you to start implementing back end for a front end type -architecture with Ocelot. - -This feature was requested as part of `Issue 79 `_ and further improvements were made as part of `Issue 298 `_. - -In order to set this up you must do something like the following in your ocelot.json. Here we have specified two normal ReRoutes and each one has a Key property. -We then specify an Aggregate that composes the two ReRoutes using their keys in the ReRouteKeys list and says then we have the UpstreamPathTemplate which works like a normal ReRoute. -Obviously you cannot have duplicate UpstreamPathTemplates between ReRoutes and Aggregates. You can use all of Ocelot's normal ReRoute options apart from RequestIdKey (explained in gotchas below). - -Advanced register your own Aggregators -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Ocelot started with just the basic request aggregation and since then we have added a more advanced method that let's the user take in the responses from the -downstream services and then aggregate them into a response object. - -The ocelot.json setup is pretty much the same as the basic aggregation approach apart from you need to add an Aggregator property like below. - -.. code-block:: json - - { - "ReRoutes": [ - { - "DownstreamPathTemplate": "/", - "UpstreamPathTemplate": "/laura", - "UpstreamHttpMethod": [ - "Get" - ], - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 51881 - } - ], - "Key": "Laura" - }, - { - "DownstreamPathTemplate": "/", - "UpstreamPathTemplate": "/tom", - "UpstreamHttpMethod": [ - "Get" - ], - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 51882 - } - ], - "Key": "Tom" - } - ], - "Aggregates": [ - { - "ReRouteKeys": [ - "Tom", - "Laura" - ], - "UpstreamPathTemplate": "/", - "Aggregator": "FakeDefinedAggregator" - } - ] - } - -Here we have added an aggregator called FakeDefinedAggregator. Ocelot is going to look for this aggregator when it tries to aggregate this ReRoute. - -In order to make the aggregator available we must add the FakeDefinedAggregator to the OcelotBuilder like below. - -.. code-block:: csharp - - services - .AddOcelot() - .AddSingletonDefinedAggregator(); - -Now when Ocelot tries to aggregate the ReRoute above it will find the FakeDefinedAggregator in the container and use it to aggregate the ReRoute. -Because the FakeDefinedAggregator is registered in the container you can add any dependencies it needs into the container like below. - -.. code-block:: csharp - - services.AddSingleton(); - - services - .AddOcelot() - .AddSingletonDefinedAggregator(); - -In this example FooAggregator takes a dependency on FooDependency and it will be resolved by the container. - -In addition to this Ocelot lets you add transient aggregators like below. - -.. code-block:: csharp - - services - .AddOcelot() - .AddTransientDefinedAggregator(); - -In order to make an Aggregator you must implement this interface. - -.. code-block:: csharp - - public interface IDefinedAggregator - { - Task Aggregate(List responses); - } - -With this feature you can pretty much do whatever you want because DownstreamResponse contains Content, Headers and Status Code. We can add extra things if needed -just raise an issue on GitHub. Please note if the HttpClient throws an exception when making a request to a ReRoute in the aggregate then you will not get a DownstreamResponse for -it but you would for any that succeed. If it does throw an exception this will be logged. - -Basic expecting JSON from Downstream Services -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: json - - { - "ReRoutes": [ - { - "DownstreamPathTemplate": "/", - "UpstreamPathTemplate": "/laura", - "UpstreamHttpMethod": [ - "Get" - ], - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 51881 - } - ], - "Key": "Laura" - }, - { - "DownstreamPathTemplate": "/", - "UpstreamPathTemplate": "/tom", - "UpstreamHttpMethod": [ - "Get" - ], - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 51882 - } - ], - "Key": "Tom" - } - ], - "Aggregates": [ - { - "ReRouteKeys": [ - "Tom", - "Laura" - ], - "UpstreamPathTemplate": "/" - } - ] - } - -You can also set UpstreamHost and ReRouteIsCaseSensitive in the Aggregate configuration. These behave the same as any other ReRoutes. - -If the ReRoute /tom returned a body of {"Age": 19} and /laura returned {"Age": 25} the the response after aggregation would be as follows. - -.. code-block:: json - - {"Tom":{"Age": 19},"Laura":{"Age": 25}} - -At the moment the aggregation is very simple. Ocelot just gets the response from your downstream service and sticks it into a json dictionary -as above. With the ReRoute key being the key of the dictionary and the value the response body from your downstream service. You can see that the object is just -JSON without any pretty spaces etc. - -All headers will be lost from the downstream services response. - -Ocelot will always return content type application/json with an aggregate request. - -If you downstream services return a 404 the aggregate will just return nothing for that downstream service. -It will not change the aggregate response into a 404 even if all the downstreams return a 404. - -Gotcha's / Further info ------------------------ - -You cannot use ReRoutes with specific RequestIdKeys as this would be crazy complicated to track. - -Aggregation only supports the GET HTTP Verb. - +Request Aggregation +=================== + +Ocelot allows you to specify Aggregate Routes that compose multiple normal Routes and map their responses into one object. This is usually where you have +a client that is making multiple requests to a server where it could just be one. This feature allows you to start implementing back end for a front end type +architecture with Ocelot. + +This feature was requested as part of `Issue 79 `_ and further improvements were made as part of `Issue 298 `_. + +In order to set this up you must do something like the following in your ocelot.json. Here we have specified two normal Routes and each one has a Key property. +We then specify an Aggregate that composes the two Routes using their keys in the RouteKeys list and says then we have the UpstreamPathTemplate which works like a normal Route. +Obviously you cannot have duplicate UpstreamPathTemplates between Routes and Aggregates. You can use all of Ocelot's normal Route options apart from RequestIdKey (explained in gotchas below). + +Advanced register your own Aggregators +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Ocelot started with just the basic request aggregation and since then we have added a more advanced method that let's the user take in the responses from the +downstream services and then aggregate them into a response object. + +The ocelot.json setup is pretty much the same as the basic aggregation approach apart from you need to add an Aggregator property like below. + +.. code-block:: json + + { + "Routes": [ + { + "DownstreamPathTemplate": "/", + "UpstreamPathTemplate": "/laura", + "UpstreamHttpMethod": [ + "Get" + ], + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 51881 + } + ], + "Key": "Laura" + }, + { + "DownstreamPathTemplate": "/", + "UpstreamPathTemplate": "/tom", + "UpstreamHttpMethod": [ + "Get" + ], + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 51882 + } + ], + "Key": "Tom" + } + ], + "Aggregates": [ + { + "RouteKeys": [ + "Tom", + "Laura" + ], + "UpstreamPathTemplate": "/", + "Aggregator": "FakeDefinedAggregator" + } + ] + } + +Here we have added an aggregator called FakeDefinedAggregator. Ocelot is going to look for this aggregator when it tries to aggregate this Route. + +In order to make the aggregator available we must add the FakeDefinedAggregator to the OcelotBuilder like below. + +.. code-block:: csharp + + services + .AddOcelot() + .AddSingletonDefinedAggregator(); + +Now when Ocelot tries to aggregate the Route above it will find the FakeDefinedAggregator in the container and use it to aggregate the Route. +Because the FakeDefinedAggregator is registered in the container you can add any dependencies it needs into the container like below. + +.. code-block:: csharp + + services.AddSingleton(); + + services + .AddOcelot() + .AddSingletonDefinedAggregator(); + +In this example FooAggregator takes a dependency on FooDependency and it will be resolved by the container. + +In addition to this Ocelot lets you add transient aggregators like below. + +.. code-block:: csharp + + services + .AddOcelot() + .AddTransientDefinedAggregator(); + +In order to make an Aggregator you must implement this interface. + +.. code-block:: csharp + + public interface IDefinedAggregator + { + Task Aggregate(List responses); + } + +With this feature you can pretty much do whatever you want because the HttpContext objects contain the results of all the aggregate requests. Please note if the HttpClient throws an exception when making a request to a Route in the aggregate then you will not get a HttpContext for it but you would for any that succeed. If it does throw an exception this will be logged. + +Basic expecting JSON from Downstream Services +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: json + + { + "Routes": [ + { + "DownstreamPathTemplate": "/", + "UpstreamPathTemplate": "/laura", + "UpstreamHttpMethod": [ + "Get" + ], + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 51881 + } + ], + "Key": "Laura" + }, + { + "DownstreamPathTemplate": "/", + "UpstreamPathTemplate": "/tom", + "UpstreamHttpMethod": [ + "Get" + ], + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 51882 + } + ], + "Key": "Tom" + } + ], + "Aggregates": [ + { + "RouteKeys": [ + "Tom", + "Laura" + ], + "UpstreamPathTemplate": "/" + } + ] + } + +You can also set UpstreamHost and RouteIsCaseSensitive in the Aggregate configuration. These behave the same as any other Routes. + +If the Route /tom returned a body of {"Age": 19} and /laura returned {"Age": 25} the the response after aggregation would be as follows. + +.. code-block:: json + + {"Tom":{"Age": 19},"Laura":{"Age": 25}} + +At the moment the aggregation is very simple. Ocelot just gets the response from your downstream service and sticks it into a json dictionary +as above. With the Route key being the key of the dictionary and the value the response body from your downstream service. You can see that the object is just +JSON without any pretty spaces etc. + +All headers will be lost from the downstream services response. + +Ocelot will always return content type application/json with an aggregate request. + +If you downstream services return a 404 the aggregate will just return nothing for that downstream service. +It will not change the aggregate response into a 404 even if all the downstreams return a 404. + +Gotcha's / Further info +----------------------- + +You cannot use Routes with specific RequestIdKeys as this would be crazy complicated to track. + +Aggregation only supports the GET HTTP Verb. + diff --git a/docs/features/requestid.rst b/docs/features/requestid.rst index fb3f74cf5..d36734026 100644 --- a/docs/features/requestid.rst +++ b/docs/features/requestid.rst @@ -1,12 +1,10 @@ Request Id / Correlation Id =========================== -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 supports a client sending a request id in the form of a header. If set Ocelot willuse 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. -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. @@ -20,19 +18,19 @@ In your ocelot.json set the following in the GlobalConfiguration section. This w "RequestIdKey": "OcRequestId" } -I recommend using the GlobalConfiguration unless you really need it to be ReRoute specific. +I recommend using the GlobalConfiguration unless you really need it to be Route specific. -*ReRoute* +*Route* -If you want to override this for a specific ReRoute add the following to ocelot.json for the specific ReRoute. +If you want to override this for a specific Route add the following to ocelot.json for the specific Route. .. code-block:: json "RequestIdKey": "OcRequestId" -Once Ocelot has identified the incoming requests matching ReRoute object it will set the request id based on the ReRoute configuration. +Once Ocelot has identified the incoming requests matching Route object it will set the request id based on the Route configuration. -This can lead to a small gotcha. If you set a GlobalConfiguration it is possible to get one request id until the ReRoute is identified and then another after that because the request id key can change. This is by design and is the best solution I can think of at the moment. In this case the OcelotLogger will show the request id and previous request id in the logs. +This can lead to a small gotcha. If you set a GlobalConfiguration it is possible to get one request id until the Route is identified and then another after that because the request id key can change. This is by design and is the best solution I can think of at the moment. In this case the OcelotLogger will show the request id and previous request id in the logs. Below is an example of the logging when set at Debug level for a normal request.. @@ -43,11 +41,11 @@ Below is an example of the logging when set at Debug level for a normal request. dbug: Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware[0] requestId: asdf, previousRequestId: no previous request id, message: upstream url path is {upstreamUrlPath}, dbug: Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware[0] - requestId: asdf, previousRequestId: no previous request id, message: downstream template is {downstreamRoute.Data.ReRoute.DownstreamPath}, + requestId: asdf, previousRequestId: no previous request id, message: downstream template is {downstreamRoute.Data.Route.DownstreamPath}, dbug: Ocelot.RateLimit.Middleware.ClientRateLimitMiddleware[0] requestId: asdf, previousRequestId: no previous request id, message: EndpointRateLimiting is not enabled for Ocelot.Values.PathTemplate, - dbug: Ocelot.Authorisation.Middleware.AuthorisationMiddleware[0] - requestId: 1234, previousRequestId: asdf, message: /posts/{postId} route does not require user to be authorised, + dbug: Ocelot.Authorization.Middleware.AuthorizationMiddleware[0] + requestId: 1234, previousRequestId: asdf, message: /posts/{postId} route does not require user to be authorized, dbug: Ocelot.DownstreamUrlCreator.Middleware.DownstreamUrlCreatorMiddleware[0] requestId: 1234, previousRequestId: asdf, message: downstream url is {downstreamUrl.Data.Value}, dbug: Ocelot.Request.Middleware.HttpRequestBuilderMiddleware[0] diff --git a/docs/features/routing.rst b/docs/features/routing.rst index 18cc60c18..30e42a82b 100644 --- a/docs/features/routing.rst +++ b/docs/features/routing.rst @@ -1,245 +1,238 @@ -Routing -======= - -Ocelot's primary functionality is to take incoming http requests and forward them on -to a downstream service. Ocelot currently only supports this in the form of another http request (in the future -this could be any transport mechanism). - -Ocelot's describes the routing of one request to another as a ReRoute. In order to get -anything working in Ocelot you need to set up a ReRoute in the configuration. - -.. code-block:: json - - { - "ReRoutes": [ - ] - } - -To configure a ReRoute you need to add one to the ReRoutes json array. - -.. code-block:: json - - { - "DownstreamPathTemplate": "/api/posts/{postId}", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 80, - } - ], - "UpstreamPathTemplate": "/posts/{postId}", - "UpstreamHttpMethod": [ "Put", "Delete" ] - } - -The DownstreamPathTemplate, DownstreamScheme and DownstreamHostAndPorts define the URL that a request will be forwarded to. - -DownstreamHostAndPorts is a collection that defines the host and port of any downstream services that you wish to forward requests to. -Usually this will just contain a single entry but sometimes you might want to load balance requests to your downstream services and Ocelot allows you add more than one entry and then select a load balancer. - -The UpstreamPathTemplate is the URL that Ocelot will use to identify which DownstreamPathTemplate to use for a given request. -The UpstreamHttpMethod is used so Ocelot can distinguish between requests with different HTTP verbs to the same URL. You can set a specific list of HTTP Methods or set an empty list to allow any of them. - -In Ocelot you can add placeholders for variables to your Templates in the form of {something}. -The placeholder variable needs to be present in both the DownstreamPathTemplate and UpstreamPathTemplate properties. When it is Ocelot will attempt to substitute the value in the UpstreamPathTemplate placeholder into the DownstreamPathTemplate for each request Ocelot processes. - -You can also do a catch all type of ReRoute e.g. - -.. code-block:: json - - { - "DownstreamPathTemplate": "/api/{everything}", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 80, - } - ], - "UpstreamPathTemplate": "/{everything}", - "UpstreamHttpMethod": [ "Get", "Post" ] - } - -This will forward any path + query string combinations to the downstream service after the path /api. - - -The default ReRouting configuration is case insensitive! - -In order to change this you can specify on a per ReRoute basis the following setting. - -.. code-block:: json - - "ReRouteIsCaseSensitive": true - -This means that when Ocelot tries to match the incoming upstream url with an upstream template the -evaluation will be case sensitive. - -Catch All -^^^^^^^^^ - -Ocelot's routing also supports a catch all style routing where the user can specify that they want to match all traffic. - -If you set up your config like below, all requests will be proxied straight through. The placeholder {url} name is not significant, any name will work. - -.. code-block:: json - - { - "DownstreamPathTemplate": "/{url}", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 80, - } - ], - "UpstreamPathTemplate": "/{url}", - "UpstreamHttpMethod": [ "Get" ] - } - -The catch all has a lower priority than any other ReRoute. If you also have the ReRoute below in your config then Ocelot would match it before the catch all. - -.. code-block:: json - - { - "DownstreamPathTemplate": "/", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "10.0.10.1", - "Port": 80, - } - ], - "UpstreamPathTemplate": "/", - "UpstreamHttpMethod": [ "Get" ] - } - -Upstream Host -^^^^^^^^^^^^^ - -This feature allows you to have ReRoutes based on the upstream host. This works by looking at the host header the client has used and then using this as part of the information we use to identify a ReRoute. - -In order to use this feature please add the following to your config. - -.. code-block:: json - - { - "DownstreamPathTemplate": "/", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "10.0.10.1", - "Port": 80, - } - ], - "UpstreamPathTemplate": "/", - "UpstreamHttpMethod": [ "Get" ], - "UpstreamHost": "somedomain.com" - } - -The ReRoute above will only be matched when the host header value is somedomain.com. - -If you do not set UpstreamHost on a ReRoute then any host header will match it. This means that if you have two ReRoutes that are the same, apart from the UpstreamHost, where one is null and the other set Ocelot will favour the one that has been set. - -This feature was requested as part of `Issue 216 `_ . - -Priority -^^^^^^^^ - -You can define the order you want your ReRoutes to match the Upstream HttpRequest by including a "Priority" property in ocelot.json -See `Issue 270 `_ for reference - -.. code-block:: json - - { - "Priority": 0 - } - -0 is the lowest priority, Ocelot will always use 0 for /{catchAll} ReRoutes and this is still hardcoded. After that you are free -to set any priority you wish. - -e.g. you could have - -.. code-block:: json - - { - "UpstreamPathTemplate": "/goods/{catchAll}" - "Priority": 0 - } - -and - -.. code-block:: json - - { - "UpstreamPathTemplate": "/goods/delete" - "Priority": 1 - } - -In the example above if you make a request into Ocelot on /goods/delete Ocelot will match /goods/delete ReRoute. Previously it would have -matched /goods/{catchAll} (because this is the first ReRoute in the list!). - -Dynamic Routing -^^^^^^^^^^^^^^^ - -This feature was requested in `issue 340 `_. - -The idea is to enable dynamic routing when using a service discovery provider so you don't have to provide the ReRoute config. See the docs :ref:`service-discovery` if -this sounds interesting to you. - -Query Strings -^^^^^^^^^^^^^ - -Ocelot allows you to specify a query string as part of the DownstreamPathTemplate like the example below. - -.. code-block:: json - - { - "ReRoutes": [ - { - "DownstreamPathTemplate": "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - "UpstreamPathTemplate": "/api/units/{subscriptionId}/{unitId}/updates", - "UpstreamHttpMethod": [ - "Get" - ], - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 50110 - } - ] - } - ], - "GlobalConfiguration": { - } - } - -In this example Ocelot will use the value from the {unitId} in the upstream path template and add it to the downstream request as a query string parameter called unitId! - -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 - - { - "ReRoutes": [ - { - "DownstreamPathTemplate": "/api/units/{subscriptionId}/{unitId}/updates", - "UpstreamPathTemplate": "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - "UpstreamHttpMethod": [ - "Get" - ], - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 50110 - } - ] - } - ], - "GlobalConfiguration": { - } - } - -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. +Routing +======= + +Ocelot's primary functionality is to take incoming http requests and forward them on to a downstream service. Ocelot currently only supports this in the form of another http request (in the future +this could be any transport mechanism). + +Ocelot's describes the routing of one request to another as a Route. In order to get anything working in Ocelot you need to set up a Route in the configuration. + +.. code-block:: json + + { + "Routes": [ + ] + } + +To configure a Route you need to add one to the Routes json array. + +.. code-block:: json + + { + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 80, + } + ], + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": [ "Put", "Delete" ] + } + +The DownstreamPathTemplate, DownstreamScheme and DownstreamHostAndPorts define the URL that a request will be forwarded to. + +DownstreamHostAndPorts is a collection that defines the host and port of any downstream services that you wish to forward requests to. Usually this will just contain a single entry but sometimes you might want to load balance requests to your downstream services and Ocelot allows you add more than one entry and then select a load balancer. + +The UpstreamPathTemplate is the URL that Ocelot will use to identify which DownstreamPathTemplate to use for a given request. The UpstreamHttpMethod is used so Ocelot can distinguish between requests with different HTTP verbs to the same URL. You can set a specific list of HTTP Methods or set an empty list to allow any of them. + +In Ocelot you can add placeholders for variables to your Templates in the form of {something}. The placeholder variable needs to be present in both the DownstreamPathTemplate and UpstreamPathTemplate properties. When it is Ocelot will attempt to substitute the value in the UpstreamPathTemplate placeholder into the DownstreamPathTemplate for each request Ocelot processes. + +You can also do a catch all type of Route e.g. + +.. code-block:: json + + { + "DownstreamPathTemplate": "/api/{everything}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 80, + } + ], + "UpstreamPathTemplate": "/{everything}", + "UpstreamHttpMethod": [ "Get", "Post" ] + } + +This will forward any path + query string combinations to the downstream service after the path /api. + + +The default ReRouting configuration is case insensitive! + +In order to change this you can specify on a per Route basis the following setting. + +.. code-block:: json + + "RouteIsCaseSensitive": true + +This means that when Ocelot tries to match the incoming upstream url with an upstream template the +evaluation will be case sensitive. + +Catch All +^^^^^^^^^ + +Ocelot's routing also supports a catch all style routing where the user can specify that they want to match all traffic. + +If you set up your config like below, all requests will be proxied straight through. The placeholder {url} name is not significant, any name will work. + +.. code-block:: json + + { + "DownstreamPathTemplate": "/{url}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 80, + } + ], + "UpstreamPathTemplate": "/{url}", + "UpstreamHttpMethod": [ "Get" ] + } + +The catch all has a lower priority than any other Route. If you also have the Route below in your config then Ocelot would match it before the catch all. + +.. code-block:: json + + { + "DownstreamPathTemplate": "/", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "10.0.10.1", + "Port": 80, + } + ], + "UpstreamPathTemplate": "/", + "UpstreamHttpMethod": [ "Get" ] + } + +Upstream Host +^^^^^^^^^^^^^ + +This feature allows you to have Routes based on the upstream host. This works by looking at the host header the client has used and then using this as part of the information we use to identify a Route. + +In order to use this feature please add the following to your config. + +.. code-block:: json + + { + "DownstreamPathTemplate": "/", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "10.0.10.1", + "Port": 80, + } + ], + "UpstreamPathTemplate": "/", + "UpstreamHttpMethod": [ "Get" ], + "UpstreamHost": "somedomain.com" + } + +The Route above will only be matched when the host header value is somedomain.com. + +If you do not set UpstreamHost on a Route then any host header will match it. This means that if you have two Routes that are the same, apart from the UpstreamHost, where one is null and the other set Ocelot will favour the one that has been set. + +This feature was requested as part of `Issue 216 `_ . + +Priority +^^^^^^^^ + +You can define the order you want your Routes to match the Upstream HttpRequest by including a "Priority" property in ocelot.json +See `Issue 270 `_ for reference + +.. code-block:: json + + { + "Priority": 0 + } + +0 is the lowest priority, Ocelot will always use 0 for /{catchAll} Routes and this is still hardcoded. After that you are free to set any priority you wish. + +e.g. you could have + +.. code-block:: json + + { + "UpstreamPathTemplate": "/goods/{catchAll}" + "Priority": 0 + } + +and + +.. code-block:: json + + { + "UpstreamPathTemplate": "/goods/delete" + "Priority": 1 + } + +In the example above if you make a request into Ocelot on /goods/delete Ocelot will match /goods/delete Route. Previously it would have matched /goods/{catchAll} (because this is the first Route in the list!). + +Dynamic Routing +^^^^^^^^^^^^^^^ + +This feature was requested in `issue 340 `_. + +The idea is to enable dynamic routing when using a service discovery provider so you don't have to provide the Route config. See the docs :ref:`service-discovery` if +this sounds interesting to you. + +Query Strings +^^^^^^^^^^^^^ + +Ocelot allows you to specify a query string as part of the DownstreamPathTemplate like the example below. + +.. code-block:: json + + { + "Routes": [ + { + "DownstreamPathTemplate": "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", + "UpstreamPathTemplate": "/api/units/{subscriptionId}/{unitId}/updates", + "UpstreamHttpMethod": [ + "Get" + ], + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 50110 + } + ] + } + ], + "GlobalConfiguration": { + } + } + +In this example Ocelot will use the value from the {unitId} in the upstream path template and add it to the downstream request as a query string parameter called unitId! + +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 + + { + "Routes": [ + { + "DownstreamPathTemplate": "/api/units/{subscriptionId}/{unitId}/updates", + "UpstreamPathTemplate": "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", + "UpstreamHttpMethod": [ + "Get" + ], + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 50110 + } + ] + } + ], + "GlobalConfiguration": { + } + } + +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. diff --git a/docs/features/servicediscovery.rst b/docs/features/servicediscovery.rst index f23d2a4a9..94b993cdf 100644 --- a/docs/features/servicediscovery.rst +++ b/docs/features/servicediscovery.rst @@ -1,252 +1,244 @@ -.. service-discovery: - -Service Discovery -================= - -Ocelot allows you to specify a service discovery provider and will use this to find the host and port -for the downstream service Ocelot is forwarding a request to. At the moment this is only supported in the -GlobalConfiguration section which means the same service discovery provider will be used for all ReRoutes -you specify a ServiceName for at ReRoute level. - -Consul -^^^^^^ - -The first thing you need to do is install the NuGet package that provides Consul support in Ocelot. - -``Install-Package Ocelot.Provider.Consul`` - -Then add the following to your ConfigureServices method. - -.. code-block:: csharp - - s.AddOcelot() - .AddConsul(); - -The following is required in the GlobalConfiguration. The Provider is required and if you do not specify a host and port the Consul default -will be used. - -.. code-block:: json - - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 8500, - "Type": "Consul" - } - -In the future we can add a feature that allows ReRoute specfic configuration. - -In order to tell Ocelot a ReRoute is to use the service discovery provider for its host and port you must add the -ServiceName and load balancer you wish to use when making requests downstream. At the moment Ocelot has a RoundRobin -and LeastConnection algorithm you can use. If no load balancer is specified Ocelot will not load balance requests. - -.. code-block:: json - - { - "DownstreamPathTemplate": "/api/posts/{postId}", - "DownstreamScheme": "https", - "UpstreamPathTemplate": "/posts/{postId}", - "UpstreamHttpMethod": [ "Put" ], - "ServiceName": "product", - "LoadBalancerOptions": { - "Type": "LeastConnection" - }, - } - -When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balance requests across any available services. - -A lot of people have asked me to implement a feature where Ocelot polls Consul for latest service information rather than per request. If you want to poll Consul for the latest services rather than per request (default behaviour) then you need to set the following configuration. - -.. code-block:: json - - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 8500, - "Type": "PollConsul", - "PollingInterval": 100 - } - -The polling interval is in milliseconds and tells Ocelot how often to call Consul for changes in service configuration. - -Please note there are tradeoffs here. If you poll Consul it is possible Ocelot will not know if a service is down depending on your polling interval and you might get more errors than if you get the latest services per request. This really depends on how volatile your services are. I doubt it will matter for most people and polling may give a tiny performance improvement over calling Consul per request (as sidecar agent). If you are calling a remote Consul agent then polling will be a good performance improvement. - -Your services need to be added to Consul something like below (C# style but hopefully this make sense)...The only important thing to note -is not to add http or https to the Address field. I have been contacted before about not accepting scheme in Address and accepting scheme -in address. After reading `this `_ I don't think the scheme should be in there. - -.. code-block: csharp - - new AgentService() - { - Service = "some-service-name", - Address = "localhost", - Port = 8080, - ID = "some-id", - } - -Or - -.. code-block:: json - - "Service": { - "ID": "some-id", - "Service": "some-service-name", - "Address": "localhost", - "Port": 8080 - } - -ACL Token ---------- - -If you are using ACL with Consul Ocelot supports adding the X-Consul-Token header. In order so this to work you must add the additional property below. - -.. code-block:: json - - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 8500, - "Token": "footoken", - "Type": "Consul" - } - -Ocelot will add this token to the Consul client that it uses to make requests and that is then used for every request. - -Eureka -^^^^^^ - -This feature was requested as part of `Issue 262 `_ . to add support for Netflix's -Eureka service discovery provider. The main reason for this is it is a key part of `Steeltoe `_ which is something -to do with `Pivotal `_! Anyway enough of the background. - -The first thing you need to do is install the NuGet package that provides Eureka support in Ocelot. - -``Install-Package Ocelot.Provider.Eureka`` - -Then add the following to your ConfigureServices method. - -.. code-block:: csharp - - s.AddOcelot() - .AddEureka(); - -Then in order to get this working add the following to ocelot.json.. - -.. code-block:: json - - "ServiceDiscoveryProvider": { - "Type": "Eureka" - } - -And following the guide `Here `_ you may also need to add some stuff to appsettings.json. For example the json below tells the steeltoe / pivotal services where to look for the service discovery server and if the service should register with it. - -.. code-block:: json - - "eureka": { - "client": { - "serviceUrl": "http://localhost:8761/eureka/", - "shouldRegisterWithEureka": false, - "shouldFetchRegistry": true - } - } - -I am told that if shouldRegisterWithEureka is false then shouldFetchRegistry will defaut to true so you don't need it explicitly but left it in there. - -Ocelot will now register all the necessary services when it starts up and if you have the json above will register itself with -Eureka. One of the services polls Eureka every 30 seconds (default) and gets the latest service state and persists this in memory. -When Ocelot asks for a given service it is retrieved from memory so performance is not a big problem. Please note that this code -is provided by the Pivotal.Discovery.Client NuGet package so big thanks to them for all the hard work. - -Ocelot will use the scheme (http/https) set in Eureka if these values are not provided in ocelot.json - -Dynamic Routing -^^^^^^^^^^^^^^^ - -This feature was requested in `issue 340 `_. The idea is to enable dynamic routing when using a service discovery provider (see that section of the docs for more info). In this mode Ocelot will use the first segment of the upstream path to lookup the downstream service with the service discovery provider. - -An example of this would be calling Ocelot with a url like https://api.mywebsite.com/product/products. Ocelot will take the first segment of -the path which is product and use it as a key to look up the service in Consul. If Consul returns a service Ocelot will request it on whatever host and port comes back from Consul plus the remaining path segments in this case products thus making the downstream call http://hostfromconsul:portfromconsul/products. Ocelot will apprend any query string to the downstream url as normal. - -In order to enable dynamic routing you need to have 0 ReRoutes in your config. At the moment you cannot mix dynamic and configuration ReRoutes. In addition to this you need to specify the Service Discovery provider details as outlined above and the downstream http/https scheme as DownstreamScheme. - -In addition to that you can set RateLimitOptions, QoSOptions, LoadBalancerOptions and HttpHandlerOptions, DownstreamScheme (You might want to call Ocelot on https but talk to private services over http) that will be applied to all of the dynamic ReRoutes. - -The config might look something like - -.. code-block:: json - - { - "ReRoutes": [], - "Aggregates": [], - "GlobalConfiguration": { - "RequestIdKey": null, - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 8500, - "Type": "Consul", - "Token": null, - "ConfigurationKey": null - }, - "RateLimitOptions": { - "ClientIdHeader": "ClientId", - "QuotaExceededMessage": null, - "RateLimitCounterPrefix": "ocelot", - "DisableRateLimitHeaders": false, - "HttpStatusCode": 429 - }, - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 0, - "DurationOfBreak": 0, - "TimeoutValue": 0 - }, - "BaseUrl": null, - "LoadBalancerOptions": { - "Type": "LeastConnection", - "Key": null, - "Expiry": 0 - }, - "DownstreamScheme": "http", - "HttpHandlerOptions": { - "AllowAutoRedirect": false, - "UseCookieContainer": false, - "UseTracing": false - } - } - } - -Ocelot also allows you to set DynamicReRoutes which lets you set rate limiting rules per downstream service. This is useful if you have for example a product and search service and you want to rate limit one more than the other. An example of this would be as follows. - -.. code-block:: json - - { - "DynamicReRoutes": [ - { - "ServiceName": "product", - "RateLimitRule": { - "ClientWhitelist": [], - "EnableRateLimiting": true, - "Period": "1s", - "PeriodTimespan": 1000.0, - "Limit": 3 - } - } - ], - "GlobalConfiguration": { - "RequestIdKey": null, - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 8523, - "Type": "Consul" - }, - "RateLimitOptions": { - "ClientIdHeader": "ClientId", - "QuotaExceededMessage": "", - "RateLimitCounterPrefix": "", - "DisableRateLimitHeaders": false, - "HttpStatusCode": 428 - } - "DownstreamScheme": "http", - } - } - -This configuration means that if you have a request come into Ocelot on /product/* then dynamic routing will kick in and ocelot will use the rate limiting set against the product service in the DynamicReRoutes section. - -Please take a look through all of the docs to understand these options. +.. service-discovery: + +Service Discovery +================= + +Ocelot allows you to specify a service discovery provider and will use this to find the host and port for the downstream service Ocelot is forwarding a request to. At the moment this is only supported in the +GlobalConfiguration section which means the same service discovery provider will be used for all Routes you specify a ServiceName for at Route level. + +Consul +^^^^^^ + +The first thing you need to do is install the NuGet package that provides Consul support in Ocelot. + +``Install-Package Ocelot.Provider.Consul`` + +Then add the following to your ConfigureServices method. + +.. code-block:: csharp + + s.AddOcelot() + .AddConsul(); + +The following is required in the GlobalConfiguration. The Provider is required and if you do not specify a host and port the Consul default +will be used. + +Please note the Scheme option defauls to HTTP. It was added in this `PR `_. It defaults to HTTP to not introduce a breaking change. + +.. code-block:: json + + "ServiceDiscoveryProvider": { + "Scheme": "https", + "Host": "localhost", + "Port": 8500, + "Type": "Consul" + } + +In the future we can add a feature that allows Route specfic configuration. + +In order to tell Ocelot a Route is to use the service discovery provider for its host and port you must add the ServiceName and load balancer you wish to use when making requests downstream. At the moment Ocelot has a RoundRobin and LeastConnection algorithm you can use. If no load balancer is specified Ocelot will not load balance requests. + +.. code-block:: json + + { + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamScheme": "https", + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": [ "Put" ], + "ServiceName": "product", + "LoadBalancerOptions": { + "Type": "LeastConnection" + }, + } + +When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balance requests across any available services. + +A lot of people have asked me to implement a feature where Ocelot polls Consul for latest service information rather than per request. If you want to poll Consul for the latest services rather than per request (default behaviour) then you need to set the following configuration. + +.. code-block:: json + + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 8500, + "Type": "PollConsul", + "PollingInterval": 100 + } + +The polling interval is in milliseconds and tells Ocelot how often to call Consul for changes in service configuration. + +Please note there are tradeoffs here. If you poll Consul it is possible Ocelot will not know if a service is down depending on your polling interval and you might get more errors than if you get the latest services per request. This really depends on how volatile your services are. I doubt it will matter for most people and polling may give a tiny performance improvement over calling Consul per request (as sidecar agent). If you are calling a remote Consul agent then polling will be a good performance improvement. + +Your services need to be added to Consul something like below (C# style but hopefully this make sense)...The only important thing to note is not to add http or https to the Address field. I have been contacted before about not accepting scheme in Address and accepting scheme in address. After reading `this `_ I don't think the scheme should be in there. + +.. code-block: csharp + + new AgentService() + { + Service = "some-service-name", + Address = "localhost", + Port = 8080, + ID = "some-id", + } + +Or + +.. code-block:: json + + "Service": { + "ID": "some-id", + "Service": "some-service-name", + "Address": "localhost", + "Port": 8080 + } + +ACL Token +--------- + +If you are using ACL with Consul Ocelot supports adding the X-Consul-Token header. In order so this to work you must add the additional property below. + +.. code-block:: json + + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 8500, + "Token": "footoken", + "Type": "Consul" + } + +Ocelot will add this token to the Consul client that it uses to make requests and that is then used for every request. + +Eureka +^^^^^^ + +This feature was requested as part of `Issue 262 `_ . to add support for Netflix's Eureka service discovery provider. The main reason for this is it is a key part of `Steeltoe `_ which is something to do with `Pivotal `_! Anyway enough of the background. + +The first thing you need to do is install the NuGet package that provides Eureka support in Ocelot. + +``Install-Package Ocelot.Provider.Eureka`` + +Then add the following to your ConfigureServices method. + +.. code-block:: csharp + + s.AddOcelot() + .AddEureka(); + +Then in order to get this working add the following to ocelot.json.. + +.. code-block:: json + + "ServiceDiscoveryProvider": { + "Type": "Eureka" + } + +And following the guide `Here `_ you may also need to add some stuff to appsettings.json. For example the json below tells the steeltoe / pivotal services where to look for the service discovery server and if the service should register with it. + +.. code-block:: json + + "eureka": { + "client": { + "serviceUrl": "http://localhost:8761/eureka/", + "shouldRegisterWithEureka": false, + "shouldFetchRegistry": true + } + } + +I am told that if shouldRegisterWithEureka is false then shouldFetchRegistry will defaut to true so you don't need it explicitly but left it in there. + +Ocelot will now register all the necessary services when it starts up and if you have the json above will register itself with Eureka. One of the services polls Eureka every 30 seconds (default) and gets the latest service state and persists this in memory. When Ocelot asks for a given service it is retrieved from memory so performance is not a big problem. Please note that this code is provided by the Pivotal.Discovery.Client NuGet package so big thanks to them for all the hard work. + +Ocelot will use the scheme (http/https) set in Eureka if these values are not provided in ocelot.json + +Dynamic Routing +^^^^^^^^^^^^^^^ + +This feature was requested in `issue 340 `_. The idea is to enable dynamic routing when using a service discovery provider (see that section of the docs for more info). In this mode Ocelot will use the first segment of the upstream path to lookup the downstream service with the service discovery provider. + +An example of this would be calling Ocelot with a url like https://api.mywebsite.com/product/products. Ocelot will take the first segment of +the path which is product and use it as a key to look up the service in Consul. If Consul returns a service Ocelot will request it on whatever host and port comes back from Consul plus the remaining path segments in this case products thus making the downstream call http://hostfromconsul:portfromconsul/products. Ocelot will apprend any query string to the downstream url as normal. + +In order to enable dynamic routing you need to have 0 Routes in your config. At the moment you cannot mix dynamic and configuration Routes. In addition to this you need to specify the Service Discovery provider details as outlined above and the downstream http/https scheme as DownstreamScheme. + +In addition to that you can set RateLimitOptions, QoSOptions, LoadBalancerOptions and HttpHandlerOptions, DownstreamScheme (You might want to call Ocelot on https but talk to private services over http) that will be applied to all of the dynamic Routes. + +The config might look something like + +.. code-block:: json + + { + "Routes": [], + "Aggregates": [], + "GlobalConfiguration": { + "RequestIdKey": null, + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 8500, + "Type": "Consul", + "Token": null, + "ConfigurationKey": null + }, + "RateLimitOptions": { + "ClientIdHeader": "ClientId", + "QuotaExceededMessage": null, + "RateLimitCounterPrefix": "ocelot", + "DisableRateLimitHeaders": false, + "HttpStatusCode": 429 + }, + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 0, + "DurationOfBreak": 0, + "TimeoutValue": 0 + }, + "BaseUrl": null, + "LoadBalancerOptions": { + "Type": "LeastConnection", + "Key": null, + "Expiry": 0 + }, + "DownstreamScheme": "http", + "HttpHandlerOptions": { + "AllowAutoRedirect": false, + "UseCookieContainer": false, + "UseTracing": false + } + } + } + +Ocelot also allows you to set DynamicRoutes which lets you set rate limiting rules per downstream service. This is useful if you have for example a product and search service and you want to rate limit one more than the other. An example of this would be as follows. + +.. code-block:: json + + { + "DynamicRoutes": [ + { + "ServiceName": "product", + "RateLimitRule": { + "ClientWhitelist": [], + "EnableRateLimiting": true, + "Period": "1s", + "PeriodTimespan": 1000.0, + "Limit": 3 + } + } + ], + "GlobalConfiguration": { + "RequestIdKey": null, + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 8523, + "Type": "Consul" + }, + "RateLimitOptions": { + "ClientIdHeader": "ClientId", + "QuotaExceededMessage": "", + "RateLimitCounterPrefix": "", + "DisableRateLimitHeaders": false, + "HttpStatusCode": 428 + } + "DownstreamScheme": "http", + } + } + +This configuration means that if you have a request come into Ocelot on /product/* then dynamic routing will kick in and ocelot will use the rate limiting set against the product service in the DynamicRoutes section. + +Please take a look through all of the docs to understand these options. diff --git a/docs/features/servicefabric.rst b/docs/features/servicefabric.rst index 0bc3ee1b5..64e62d191 100644 --- a/docs/features/servicefabric.rst +++ b/docs/features/servicefabric.rst @@ -1,42 +1,40 @@ -Service Fabric -============== - -If you have services deployed in Service Fabric you will normally use the naming service to access them. - -The following example shows how to set up a ReRoute that will work in Service Fabric. The most important thing is the ServiceName which is made up of the -Service Fabric application name then the specific service name. We also need to set up the ServiceDiscoveryProvider in -GlobalConfiguration. The example here shows a typical configuration. It assumes service fabric is running on localhost and that the naming service is on port 19081. - -The example below is taken from the samples folder so please check it if this doesnt make sense! - -.. code-block:: json - - { - "ReRoutes": [ - { - "DownstreamPathTemplate": "/api/values", - "UpstreamPathTemplate": "/EquipmentInterfaces", - "UpstreamHttpMethod": [ - "Get" - ], - "DownstreamScheme": "http", - "ServiceName": "OcelotServiceApplication/OcelotApplicationService", - } - ], - "GlobalConfiguration": { - "RequestIdKey": "OcRequestId", - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 19081, - "Type": "ServiceFabric" - } - } - } - -If you are using stateless / guest exe services ocelot will be able to proxy through the naming service without anything else. However -if you are using statefull / actor services you must send the PartitionKind and PartitionKey query string values with the client -request e.g. - -GET http://ocelot.com/EquipmentInterfaces?PartitionKind=xxx&PartitionKey=xxx - -There is no way for Ocelot to work these out for you. +Service Fabric +============== + +If you have services deployed in Service Fabric you will normally use the naming service to access them. + +The following example shows how to set up a Route that will work in Service Fabric. The most important thing is the ServiceName which is made up of the Service Fabric application name then the specific service name. We also need to set up the ServiceDiscoveryProvider in GlobalConfiguration. The example here shows a typical configuration. It assumes service fabric is running on localhost and that the naming service is on port 19081. + +The example below is taken from the samples folder so please check it if this doesnt make sense! + +.. code-block:: json + + { + "Routes": [ + { + "DownstreamPathTemplate": "/api/values", + "UpstreamPathTemplate": "/EquipmentInterfaces", + "UpstreamHttpMethod": [ + "Get" + ], + "DownstreamScheme": "http", + "ServiceName": "OcelotServiceApplication/OcelotApplicationService", + } + ], + "GlobalConfiguration": { + "RequestIdKey": "OcRequestId", + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 19081, + "Type": "ServiceFabric" + } + } + } + +If you are using stateless / guest exe services ocelot will be able to proxy through the naming service without anything else. However +if you are using statefull / actor services you must send the PartitionKind and PartitionKey query string values with the client +request e.g. + +GET http://ocelot.com/EquipmentInterfaces?PartitionKind=xxx&PartitionKey=xxx + +There is no way for Ocelot to work these out for you. diff --git a/docs/features/tracing.rst b/docs/features/tracing.rst index bda436275..a400a8091 100644 --- a/docs/features/tracing.rst +++ b/docs/features/tracing.rst @@ -1,41 +1,74 @@ -Tracing -======= - -This page details how to perform distributed tracing with Ocelot. At the moment we only support Butterfly but other tracers might just work without -anything Ocelot specific. - -Butterfly -^^^^^^^^^ - -Ocelot providers tracing functionality from the excellent `Butterfly `_ project. The code for the Ocelot integration -can be found `here `_. - -In order to use the tracing please read the Butterfly documentation. - -In ocelot you need to do the following if you wish to trace a ReRoute. - - ``Install-Package Ocelot.Tracing.Butterfly`` - -In your ConfigureServices method - -.. code-block:: csharp - - services - .AddOcelot() - // this comes from Ocelot.Tracing.Butterfly package - .AddButterfly(option => - { - //this is the url that the butterfly collector server is running on... - option.CollectorUrl = "http://localhost:9618"; - option.Service = "Ocelot"; - }); - -Then in your ocelot.json add the following to the ReRoute you want to trace.. - -.. code-block:: json - - "HttpHandlerOptions": { - "UseTracing": true - }, - -Ocelot will now send tracing information to Butterfly when this ReRoute is called. +Tracing +======= + +This page details how to perform distributed tracing with Ocelot. + +OpenTracing +^^^^^^^^^^^ + +Ocelot providers tracing functionality from the excellent `OpenTracing C# `_ project. The code for the Ocelot integration +can be found `here `_. + +The example below uses `Jaeger C# `_ client to provide the tracer used in Ocelot. + +.. code-block:: csharp + + services.AddSingleton(sp => + { + var loggerFactory = sp.GetService(); + Configuration config = new Configuration(context.HostingEnvironment.ApplicationName, loggerFactory); + + var tracer = config.GetTracer(); + GlobalTracer.Register(tracer); + return tracer; + }); + + services + .AddOcelot() + .AddOpenTracing(); + +Then in your ocelot.json add the following to the Route you want to trace.. + +.. code-block:: json + + "HttpHandlerOptions": { + "UseTracing": true + }, + +Ocelot will now send tracing information to Jaeger when this Route is called. + +Butterfly +^^^^^^^^^ + +Ocelot providers tracing functionality from the excellent `Butterfly `_ project. The code for the Ocelot integration +can be found `here `_. + +In order to use the tracing please read the Butterfly documentation. + +In ocelot you need to do the following if you wish to trace a Route. + + ``Install-Package Ocelot.Tracing.Butterfly`` + +In your ConfigureServices method + +.. code-block:: csharp + + services + .AddOcelot() + // this comes from Ocelot.Tracing.Butterfly package + .AddButterfly(option => + { + //this is the url that the butterfly collector server is running on... + option.CollectorUrl = "http://localhost:9618"; + option.Service = "Ocelot"; + }); + +Then in your ocelot.json add the following to the Route you want to trace.. + +.. code-block:: json + + "HttpHandlerOptions": { + "UseTracing": true + }, + +Ocelot will now send tracing information to Butterfly when this Route is called. diff --git a/docs/features/websockets.rst b/docs/features/websockets.rst index 5b5879193..ad185f297 100644 --- a/docs/features/websockets.rst +++ b/docs/features/websockets.rst @@ -1,114 +1,109 @@ -Websockets -========== - -Ocelot supports proxying websockets with some extra bits. This functionality was requested in `Issue 212 `_. - -In order to get websocket proxying working with Ocelot you need to do the following. - -In your Configure method you need to tell your application to use WebSockets. - -.. code-block:: csharp - - Configure(app => - { - app.UseWebSockets(); - app.UseOcelot().Wait(); - }) - -Then in your ocelot.json add the following to proxy a ReRoute using websockets. - -.. code-block:: json - - { - "DownstreamPathTemplate": "/ws", - "UpstreamPathTemplate": "/", - "DownstreamScheme": "ws", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 5001 - } - ], - } - -With this configuration set Ocelot will match any websocket traffic that comes in on / and proxy it to localhost:5001/ws. To make this clearer -Ocelot will receive messages from the upstream client, proxy these to the downstream service, receive messages from the downstream service and -proxy these to the upstream client. - -SignalR -^^^^^^^ - -Ocelot supports proxying SignalR. This functionality was requested in `Issue 344 `_. - -In order to get websocket proxying working with Ocelot you need to do the following. - -Install Microsoft.AspNetCore.SignalR.Client 1.0.2 you can try other packages but this one is tested. - -Do not run it in IISExpress or install the websockets feature in the IIS features - -In your Configure method you need to tell your application to use SignalR. - -.. code-block:: csharp - - Configure(app => - { - app.UseWebSockets(); - app.UseOcelot().Wait(); - }) - -Then in your ocelot.json add the following to proxy a ReRoute using SignalR. Note normal Ocelot routing rules apply the main thing is the scheme which is set to "ws". - -.. code-block:: json - - { - "ReRoutes": [ - { - "DownstreamPathTemplate": "/{catchAll}", - "DownstreamScheme": "ws", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 50000 - } - ], - "UpstreamPathTemplate": "/gateway/{catchAll}", - "UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE", "OPTIONS" ] - } - ] -} - -With this configuration set Ocelot will match any SignalR traffic that comes in on / and proxy it to localhost:5001/ws. To make this clearer -Ocelot will receive messages from the upstream client, proxy these to the downstream service, receive messages from the downstream service and -proxy these to the upstream client. - -Supported -^^^^^^^^^ - -1. Load Balancer -2. Routing -3. Service Discovery - -This means that you can set up your downstream services running websockets and either have multiple DownstreamHostAndPorts in your ReRoute -config or hook your ReRoute into a service discovery provider and then load balance requests...Which I think is pretty cool :) - -Not Supported -^^^^^^^^^^^^^ - -Unfortunately a lot of Ocelot's features are non websocket specific such as header and http client stuff. I've listed what won't work below. - -1. Tracing -2. RequestId -3. Request Aggregation -4. Rate Limiting -5. Quality of Service -6. Middleware Injection -7. Header Transformation -8. Delegating Handlers -9. Claims Transformation -10. Caching -11. Authentication - If anyone requests it we might be able to do something with basic authentication. -12. Authorisation - -I'm not 100% sure what will happen with this feature when it get's into the wild so please make sure you test thoroughly! - - +Websockets +========== + +Ocelot supports proxying websockets with some extra bits. This functionality was requested in `Issue 212 `_. + +In order to get websocket proxying working with Ocelot you need to do the following. + +In your Configure method you need to tell your application to use WebSockets. + +.. code-block:: csharp + + Configure(app => + { + app.UseWebSockets(); + app.UseOcelot().Wait(); + }) + +Then in your ocelot.json add the following to proxy a Route using websockets. + +.. code-block:: json + + { + "DownstreamPathTemplate": "/ws", + "UpstreamPathTemplate": "/", + "DownstreamScheme": "ws", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 5001 + } + ], + } + +With this configuration set Ocelot will match any websocket traffic that comes in on / and proxy it to localhost:5001/ws. To make this clearer Ocelot will receive messages from the upstream client, proxy these to the downstream service, receive messages from the downstream service and proxy these to the upstream client. + +SignalR +^^^^^^^ + +Ocelot supports proxying SignalR. This functionality was requested in `Issue 344 `_. + +In order to get websocket proxying working with Ocelot you need to do the following. + +Install Microsoft.AspNetCore.SignalR.Client 1.0.2 you can try other packages but this one is tested. + +Do not run it in IISExpress or install the websockets feature in the IIS features + +In your Configure method you need to tell your application to use SignalR. + +.. code-block:: csharp + + Configure(app => + { + app.UseWebSockets(); + app.UseOcelot().Wait(); + }) + +Then in your ocelot.json add the following to proxy a Route using SignalR. Note normal Ocelot routing rules apply the main thing is the scheme which is set to "ws". + +.. code-block:: json + + { + "Routes": [ + { + "DownstreamPathTemplate": "/{catchAll}", + "DownstreamScheme": "ws", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 50000 + } + ], + "UpstreamPathTemplate": "/gateway/{catchAll}", + "UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE", "OPTIONS" ] + } + ] +} + +With this configuration set Ocelot will match any SignalR traffic that comes in on / and proxy it to localhost:5001/ws. To make this clearer Ocelot will receive messages from the upstream client, proxy these to the downstream service, receive messages from the downstream service and proxy these to the upstream client. + +Supported +^^^^^^^^^ + +1. Load Balancer +2. Routing +3. Service Discovery + +This means that you can set up your downstream services running websockets and either have multiple DownstreamHostAndPorts in your Route config or hook your Route into a service discovery provider and then load balance requests...Which I think is pretty cool :) + +Not Supported +^^^^^^^^^^^^^ + +Unfortunately a lot of Ocelot's features are non websocket specific such as header and http client stuff. I've listed what won't work below. + +1. Tracing +2. RequestId +3. Request Aggregation +4. Rate Limiting +5. Quality of Service +6. Middleware Injection +7. Header Transformation +8. Delegating Handlers +9. Claims Transformation +10. Caching +11. Authentication - If anyone requests it we might be able to do something with basic authentication. +12. Authorization + +I'm not 100% sure what will happen with this feature when it get's into the wild so please make sure you test thoroughly! + + diff --git a/docs/index.rst b/docs/index.rst index 4aba33fd9..8df5b21c4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,7 +12,8 @@ Thanks for taking a look at the Ocelot documentation. Please use the left hand n introduction/gettingstarted introduction/contributing introduction/notsupported - + introduction/gotchas + .. toctree:: :maxdepth: 2 :hidden: @@ -23,16 +24,17 @@ Thanks for taking a look at the Ocelot documentation. Please use the left hand n features/requestaggregation features/graphql features/servicediscovery - features/servicefabric + features/servicefabric features/kubernetes features/authentication - features/authorisation + features/authorization features/websockets features/administration features/ratelimiting features/caching features/qualityofservice features/headerstransformation + features/methodtransformation features/claimstransformation features/logging features/tracing @@ -40,7 +42,7 @@ Thanks for taking a look at the Ocelot documentation. Please use the left hand n features/middlewareinjection features/loadbalancer features/delegatinghandlers - features/raft + features/errorcodes .. toctree:: :maxdepth: 2 diff --git a/docs/introduction/bigpicture.rst b/docs/introduction/bigpicture.rst index 0ea39e172..ee56f0efa 100644 --- a/docs/introduction/bigpicture.rst +++ b/docs/introduction/bigpicture.rst @@ -1,23 +1,13 @@ Big Picture =========== -Ocelot is aimed at people using .NET running -a micro services / service orientated architecture -that need a unified point of entry into their system. +Ocelot is aimed at people using .NET running a micro services / service orientated architecture that need a unified point of entry into their system. -In particular I want easy integration with -IdentityServer reference and bearer tokens. +In particular I want easy integration with IdentityServer reference and bearer tokens. Ocelot is a bunch of middlewares in a specific order. -Ocelot manipulates the HttpRequest object into a state specified by its configuration until -it reaches a request builder middleware where it creates a HttpRequestMessage object which is -used to make a request to a downstream service. The middleware that makes the request is -the last thing in the Ocelot pipeline. It does not call the next middleware. -The response from the downstream service is stored in a per request scoped repository -and retrieved as the requests goes back up the Ocelot pipeline. There is a piece of middleware -that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client. -That is basically it with a bunch of other features. +Ocelot manipulates the HttpRequest object into a state specified by its configuration until it reaches a request builder middleware where it creates a HttpRequestMessage object which is used to make a request to a downstream service. The middleware that makes the request is the last thing in the Ocelot pipeline. It does not call the next middleware. There is a piece of middleware that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client. That is basically it with a bunch of other features. The following are configurations that you use when deploying Ocelot. diff --git a/docs/introduction/contributing.rst b/docs/introduction/contributing.rst index 535e22275..db05d5d92 100644 --- a/docs/introduction/contributing.rst +++ b/docs/introduction/contributing.rst @@ -1,5 +1,4 @@ Contributing ============ -Pull requests, issues and commentary welcome! No special process just create a request and get in -touch either via gitter or create an issue. \ No newline at end of file +Pull requests, issues and commentary welcome! No special process just create a request and get in touch either via gitter or create an issue. \ No newline at end of file diff --git a/docs/introduction/gettingstarted.rst b/docs/introduction/gettingstarted.rst index e2178c984..e29983b33 100644 --- a/docs/introduction/gettingstarted.rst +++ b/docs/introduction/gettingstarted.rst @@ -1,15 +1,14 @@ Getting Started =============== -Ocelot is designed to work with .NET Core only and is currently -built to netstandard2.0. `This `_ documentation may prove helpful when working out if Ocelot would be suitable for you. +Ocelot is designed to work with ASP.NET and is currently on net6.0. -.NET Core 3.1 -^^^^^^^^^^^^^ +.NET 7.0 +^^^^^^^^ **Install NuGet package** -Install Ocelot and it's dependencies using nuget. You will need to create a netstandard2.0 project and bring the package into it. Then follow the Startup below and :doc:`../features/configuration` sections +Install Ocelot and it's dependencies using nuget. You will need to create a net6.0 project and bring the package into it. Then follow the Startup below and :doc:`../features/configuration` sections to get up and running. ``Install-Package Ocelot`` @@ -23,7 +22,7 @@ The following is a very basic ocelot.json. It won't do anything but should get O .. code-block:: json { - "ReRoutes": [], + "Routes": [], "GlobalConfiguration": { "BaseUrl": "https://api.mybusiness.com" } @@ -34,7 +33,7 @@ If you want some example that actually does something use the following: .. code-block:: json { - "ReRoutes": [ + "Routes": [ { "DownstreamPathTemplate": "/todos/{id}", "DownstreamScheme": "https", @@ -53,16 +52,13 @@ If you want some example that actually does something use the following: } } -The most important thing to note here is BaseUrl. Ocelot needs to know the URL it is running under -in order to do Header find & replace and for certain administration configurations. When setting this URL it should be the external URL that clients will see Ocelot running on e.g. If you are running containers Ocelot might run on the url http://123.12.1.1:6543 but has something like nginx in front of it responding on https://api.mybusiness.com. In this case the Ocelot base url should be https://api.mybusiness.com. +The most important thing to note here is BaseUrl. Ocelot needs to know the URL it is running under in order to do Header find & replace and for certain administration configurations. When setting this URL it should be the external URL that clients will see Ocelot running on e.g. If you are running containers Ocelot might run on the url http://123.12.1.1:6543 but has something like nginx in front of it responding on https://api.mybusiness.com. In this case the Ocelot base url should be https://api.mybusiness.com. -If you are using containers and require Ocelot to respond to clients on http://123.12.1.1:6543 -then you can do this, however if you are deploying multiple Ocelot's you will probably want to pass this on the command line in some kind of script. Hopefully whatever scheduler you are using can pass the IP. +If you are using containers and require Ocelot to respond to clients on http://123.12.1.1:6543 then you can do this, however if you are deploying multiple Ocelot's you will probably want to pass this on the command line in some kind of script. Hopefully whatever scheduler you are using can pass the IP. **Program** -Then in your Program.cs you will want to have the following. The main things to note are -AddOcelot() (adds ocelot services), UseOcelot().Wait() (sets up all the Ocelot middleware). +Then in your Program.cs you will want to have the following. The main things to note are AddOcelot() (adds ocelot services), UseOcelot().Wait() (sets up all the Ocelot middleware). .. code-block:: csharp @@ -107,88 +103,4 @@ AddOcelot() (adds ocelot services), UseOcelot().Wait() (sets up all the Ocelot m .Run(); } } - } - - - **Note:** When using ASP.NET Core 2.2 and you want to use In-Process hosting, replace **.UseIISIntegration()** with **.UseIIS()**, otherwise you'll get startup errors. - -.NET Core 1.0 -^^^^^^^^^^^^^ - -**Install NuGet package** - -Install Ocelot and it's dependecies using nuget. You will need to create a netcoreapp1.0+ projct and bring the package into it. Then follow the Startup below and :doc:`../features/configuration` sections -to get up and running. Please note you will need to choose one of the Ocelot packages from the NuGet feed. - -All versions can be found `here `_. - -**Configuration** - -The following is a very basic ocelot.json. It won't do anything but should get Ocelot starting. - -.. code-block:: json - - { - "ReRoutes": [], - "GlobalConfiguration": {} - } - -**Program** - -Then in your Program.cs you will want to have the following. - -.. code-block:: csharp - - public class Program - { - public static void Main(string[] args) - { - IWebHostBuilder builder = new WebHostBuilder(); - - builder.ConfigureServices(s => { - }); - - builder.UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseStartup(); - - var host = builder.Build(); - - host.Run(); - } - } - -**Startup** - -An example startup using a json file for configuration can be seen below. - -.. code-block:: csharp - - public class Startup - { - public Startup(IHostingEnvironment env) - { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) - .AddJsonFile("ocelot.json") - .AddEnvironmentVariables(); - - Configuration = builder.Build(); - } - - public IConfigurationRoot Configuration { get; } - - public void ConfigureServices(IServiceCollection services) - { - services.AddOcelot(Configuration); - } - - public void Configure(IApplicationBuilder app) - { - app.UseOcelot().Wait(); - } - } - -This is pretty much all you need to get going. + } \ No newline at end of file diff --git a/docs/introduction/gotchas.rst b/docs/introduction/gotchas.rst new file mode 100644 index 000000000..e980e0370 --- /dev/null +++ b/docs/introduction/gotchas.rst @@ -0,0 +1,4 @@ +Gotchas +============= + +**Note:** When using ASP.NET Core 2.2 and you want to use In-Process hosting, replace **.UseIISIntegration()** with **.UseIIS()**, otherwise you'll get startup errors. \ No newline at end of file diff --git a/docs/introduction/notsupported.rst b/docs/introduction/notsupported.rst index 404b8c70f..4d86759a0 100644 --- a/docs/introduction/notsupported.rst +++ b/docs/introduction/notsupported.rst @@ -7,10 +7,7 @@ Ocelot does not support... * Forwarding a host header - The host header that you send to Ocelot will not be forwarded to the downstream service. Obviously this would break everything :( -* Swagger - I have looked multiple times at building swagger.json out of the Ocelot ocelot.json but it doesnt fit into the vision - I have for Ocelot. If you would like to have Swagger in Ocelot then you must roll your own swagger.json and do the following in your - Startup.cs or Program.cs. The code sample below registers a piece of middleware that loads your hand rolled swagger.json and returns - it on /swagger/v1/swagger.json. It then registers the SwaggerUI middleware from Swashbuckle.AspNetCore +* Swagger - I have looked multiple times at building swagger.json out of the Ocelot ocelot.json but it doesnt fit into the vision I have for Ocelot. If you would like to have Swagger in Ocelot then you must roll your own swagger.json and do the following in your Startup.cs or Program.cs. The code sample below registers a piece of middleware that loads your hand rolled swagger.json and returns it on /swagger/v1/swagger.json. It then registers the SwaggerUI middleware from Swashbuckle.AspNetCore .. code-block:: csharp @@ -28,16 +25,8 @@ Ocelot does not support... app.UseOcelot().Wait(); -The main reasons why I don't think Swagger makes sense is we already hand roll our definition in ocelot.json. -If we want people developing against Ocelot to be able to see what routes are available then either share the ocelot.json -with them (This should be as easy as granting access to a repo etc) or use the Ocelot administration API so that they can query Ocelot for the configuration. +The main reasons why I don't think Swagger makes sense is we already hand roll our definition in ocelot.json. If we want people developing against Ocelot to be able to see what routes are available then either share the ocelot.json with them (This should be as easy as granting access to a repo etc) or use the Ocelot administration API so that they can query Ocelot for the configuration. -In addition to this many people will configure Ocelot to proxy all traffic like /products/{everything} to their product service -and you would not be describing what is actually available if you parsed this and turned it into a Swagger path. Also Ocelot has -no concept of the models that the downstream services can return and linking to the above problem the same endpoint can return -multiple models. Ocelot does not know what models might be used in POST, PUT etc so it all gets a bit messy and finally the Swashbuckle -package doesnt reload swagger.json if it changes during runtime. Ocelot's configuration can change during runtime so the Swagger and Ocelot -information would not match. Unless I rolled my own Swagger implementation. +In addition to this many people will configure Ocelot to proxy all traffic like /products/{everything} to their product service and you would not be describing what is actually available if you parsed this and turned it into a Swagger path. Also Ocelot has no concept of the models that the downstream services can return and linking to the above problem the same endpoint can return multiple models. Ocelot does not know what models might be used in POST, PUT etc so it all gets a bit messy and finally the Swashbuckle package doesnt reload swagger.json if it changes during runtime. Ocelot's configuration can change during runtime so the Swagger and Ocelot information would not match. Unless I rolled my own Swagger implementation. -If the user wants something to easily test against the Ocelot API then I suggest using Postman as a simple way to do this. It might -even be possible to write something that maps ocelot.json to the postman json spec. However I don't intend to do this. +If the user wants something to easily test against the Ocelot API then I suggest using Postman as a simple way to do this. It might even be possible to write something that maps ocelot.json to the postman json spec. However I don't intend to do this. diff --git a/images/ocelot_logo.png b/images/ocelot_logo.png new file mode 100644 index 000000000..78cae888b Binary files /dev/null and b/images/ocelot_logo.png differ diff --git a/releasenotes.md b/releasenotes.md new file mode 100644 index 000000000..e69de29bb diff --git a/samples/AdministrationApi/AdministrationApi.csproj b/samples/AdministrationApi/AdministrationApi.csproj index 3fcf5dc8e..59739bdee 100644 --- a/samples/AdministrationApi/AdministrationApi.csproj +++ b/samples/AdministrationApi/AdministrationApi.csproj @@ -1,13 +1,13 @@ - - - netcoreapp3.1 - - - - - - - - - - \ No newline at end of file + + + net7.0 + + + + + + + + + + diff --git a/samples/AdministrationApi/Issue645.postman_collection.json b/samples/AdministrationApi/Issue645.postman_collection.json index 580419a0b..c9bac89f5 100644 --- a/samples/AdministrationApi/Issue645.postman_collection.json +++ b/samples/AdministrationApi/Issue645.postman_collection.json @@ -1,150 +1,150 @@ -{ - "info": { - "_postman_id": "6234b40a-e363-4c73-8577-1c9074abb951", - "name": "Issue645", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "1. GET http://localhost: 55580/administration/.well-known/openid-configuration", - "request": { - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{AccessToken}}" - } - ], - "body": {}, - "url": { - "raw": "http://localhost:5000/administration/.well-known/openid-configuration", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "5000", - "path": [ - "administration", - ".well-known", - "openid-configuration" - ] - } - }, - "response": [] - }, - { - "name": "3. GET http://localhost: 55580/administration/configuration", - "request": { - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{AccessToken}}" - }, - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\r\n \"reRoutes\": [\r\n {\r\n \"downstreamPathTemplate\": \"/{everything}\",\r\n \"upstreamPathTemplate\": \"/templates/{everything}\",\r\n \"upstreamHttpMethod\": [\r\n \"GET\"\r\n ],\r\n \"addHeadersToRequest\": {},\r\n \"upstreamHeaderTransform\": {},\r\n \"downstreamHeaderTransform\": {},\r\n \"addClaimsToRequest\": {},\r\n \"routeClaimsRequirement\": {},\r\n \"addQueriesToRequest\": {},\r\n \"requestIdKey\": null,\r\n \"fileCacheOptions\": {\r\n \"ttlSeconds\": 0,\r\n \"region\": null\r\n },\r\n \"reRouteIsCaseSensitive\": false,\r\n \"downstreamScheme\": \"http\",\r\n \"qoSOptions\": {\r\n \"exceptionsAllowedBeforeBreaking\": 0,\r\n \"durationOfBreak\": 0,\r\n \"timeoutValue\": 0\r\n },\r\n \"loadBalancerOptions\": {\r\n \"type\": null,\r\n \"key\": null,\r\n \"expiry\": 0\r\n },\r\n \"rateLimitOptions\": {\r\n \"clientWhitelist\": [],\r\n \"enableRateLimiting\": false,\r\n \"period\": null,\r\n \"periodTimespan\": 0,\r\n \"limit\": 0\r\n },\r\n \"authenticationOptions\": {\r\n \"authenticationProviderKey\": null,\r\n \"allowedScopes\": []\r\n },\r\n \"httpHandlerOptions\": {\r\n \"allowAutoRedirect\": false,\r\n \"useCookieContainer\": false,\r\n \"useTracing\": false,\r\n \"useProxy\": true\r\n },\r\n \"downstreamHostAndPorts\": [\r\n {\r\n \"host\": \"localhost\",\r\n \"port\": 50689\r\n }\r\n ],\r\n \"upstreamHost\": null,\r\n \"key\": null,\r\n \"delegatingHandlers\": [],\r\n \"priority\": 1,\r\n \"timeout\": 0,\r\n \"dangerousAcceptAnyServerCertificateValidator\": false\r\n }\r\n ],\r\n \"aggregates\": [],\r\n \"globalConfiguration\": {\r\n \"requestIdKey\": \"Request-Id\",\r\n \"rateLimitOptions\": {\r\n \"clientIdHeader\": \"ClientId\",\r\n \"quotaExceededMessage\": null,\r\n \"rateLimitCounterPrefix\": \"ocelot\",\r\n \"disableRateLimitHeaders\": false,\r\n \"httpStatusCode\": 429\r\n },\r\n \"qoSOptions\": {\r\n \"exceptionsAllowedBeforeBreaking\": 0,\r\n \"durationOfBreak\": 0,\r\n \"timeoutValue\": 0\r\n },\r\n \"baseUrl\": \"http://localhost:55580\",\r\n \"loadBalancerOptions\": {\r\n \"type\": null,\r\n \"key\": null,\r\n \"expiry\": 0\r\n },\r\n \"downstreamScheme\": null,\r\n \"httpHandlerOptions\": {\r\n \"allowAutoRedirect\": false,\r\n \"useCookieContainer\": false,\r\n \"useTracing\": false,\r\n \"useProxy\": true\r\n }\r\n }\r\n}" - }, - "url": { - "raw": "http://localhost:5000/administration/configuration", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "5000", - "path": [ - "administration", - "configuration" - ] - } - }, - "response": [] - }, - { - "name": "2. POST http://localhost: 55580/administration/connect/token", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var jsonData = JSON.parse(responseBody);", - "postman.setGlobalVariable(\"AccessToken\", jsonData.access_token);", - "postman.setGlobalVariable(\"RefreshToken\", jsonData.refresh_token);" - ] - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "client_id", - "value": "admin", - "type": "text" - }, - { - "key": "client_secret", - "value": "secret", - "type": "text" - }, - { - "key": "scope", - "value": "admin", - "type": "text" - }, - { - "key": "grant_type", - "value": "client_credentials", - "type": "text" - } - ] - }, - "url": { - "raw": "http://localhost:5000/administration/connect/token", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "5000", - "path": [ - "administration", - "connect", - "token" - ] - } - }, - "response": [] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "id": "0f60e7b3-e4f1-4458-bbc4-fc4809e86b2d", - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "id": "1279a2cf-b771-4a86-9dfa-302b240fac62", - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ] +{ + "info": { + "_postman_id": "6234b40a-e363-4c73-8577-1c9074abb951", + "name": "Issue645", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "1. GET http://localhost: 55580/administration/.well-known/openid-configuration", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{AccessToken}}" + } + ], + "body": {}, + "url": { + "raw": "http://localhost:5000/administration/.well-known/openid-configuration", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "administration", + ".well-known", + "openid-configuration" + ] + } + }, + "response": [] + }, + { + "name": "3. GET http://localhost: 55580/administration/configuration", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{AccessToken}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"routes\": [\r\n {\r\n \"downstreamPathTemplate\": \"/{everything}\",\r\n \"upstreamPathTemplate\": \"/templates/{everything}\",\r\n \"upstreamHttpMethod\": [\r\n \"GET\"\r\n ],\r\n \"addHeadersToRequest\": {},\r\n \"upstreamHeaderTransform\": {},\r\n \"downstreamHeaderTransform\": {},\r\n \"addClaimsToRequest\": {},\r\n \"routeClaimsRequirement\": {},\r\n \"addQueriesToRequest\": {},\r\n \"requestIdKey\": null,\r\n \"fileCacheOptions\": {\r\n \"ttlSeconds\": 0,\r\n \"region\": null\r\n },\r\n \"routeIsCaseSensitive\": false,\r\n \"downstreamScheme\": \"http\",\r\n \"qoSOptions\": {\r\n \"exceptionsAllowedBeforeBreaking\": 0,\r\n \"durationOfBreak\": 0,\r\n \"timeoutValue\": 0\r\n },\r\n \"loadBalancerOptions\": {\r\n \"type\": null,\r\n \"key\": null,\r\n \"expiry\": 0\r\n },\r\n \"rateLimitOptions\": {\r\n \"clientWhitelist\": [],\r\n \"enableRateLimiting\": false,\r\n \"period\": null,\r\n \"periodTimespan\": 0,\r\n \"limit\": 0\r\n },\r\n \"authenticationOptions\": {\r\n \"authenticationProviderKey\": null,\r\n \"allowedScopes\": []\r\n },\r\n \"httpHandlerOptions\": {\r\n \"allowAutoRedirect\": false,\r\n \"useCookieContainer\": false,\r\n \"useTracing\": false,\r\n \"useProxy\": true\r\n },\r\n \"downstreamHostAndPorts\": [\r\n {\r\n \"host\": \"localhost\",\r\n \"port\": 50689\r\n }\r\n ],\r\n \"upstreamHost\": null,\r\n \"key\": null,\r\n \"delegatingHandlers\": [],\r\n \"priority\": 1,\r\n \"timeout\": 0,\r\n \"dangerousAcceptAnyServerCertificateValidator\": false\r\n }\r\n ],\r\n \"aggregates\": [],\r\n \"globalConfiguration\": {\r\n \"requestIdKey\": \"Request-Id\",\r\n \"rateLimitOptions\": {\r\n \"clientIdHeader\": \"ClientId\",\r\n \"quotaExceededMessage\": null,\r\n \"rateLimitCounterPrefix\": \"ocelot\",\r\n \"disableRateLimitHeaders\": false,\r\n \"httpStatusCode\": 429\r\n },\r\n \"qoSOptions\": {\r\n \"exceptionsAllowedBeforeBreaking\": 0,\r\n \"durationOfBreak\": 0,\r\n \"timeoutValue\": 0\r\n },\r\n \"baseUrl\": \"http://localhost:55580\",\r\n \"loadBalancerOptions\": {\r\n \"type\": null,\r\n \"key\": null,\r\n \"expiry\": 0\r\n },\r\n \"downstreamScheme\": null,\r\n \"httpHandlerOptions\": {\r\n \"allowAutoRedirect\": false,\r\n \"useCookieContainer\": false,\r\n \"useTracing\": false,\r\n \"useProxy\": true\r\n }\r\n }\r\n}" + }, + "url": { + "raw": "http://localhost:5000/administration/configuration", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "administration", + "configuration" + ] + } + }, + "response": [] + }, + { + "name": "2. POST http://localhost: 55580/administration/connect/token", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "var jsonData = JSON.parse(responseBody);", + "postman.setGlobalVariable(\"AccessToken\", jsonData.access_token);", + "postman.setGlobalVariable(\"RefreshToken\", jsonData.refresh_token);" + ] + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "client_id", + "value": "admin", + "type": "text" + }, + { + "key": "client_secret", + "value": "secret", + "type": "text" + }, + { + "key": "scope", + "value": "admin", + "type": "text" + }, + { + "key": "grant_type", + "value": "client_credentials", + "type": "text" + } + ] + }, + "url": { + "raw": "http://localhost:5000/administration/connect/token", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5000", + "path": [ + "administration", + "connect", + "token" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "id": "0f60e7b3-e4f1-4458-bbc4-fc4809e86b2d", + "type": "text/javascript", + "exec": [ + string.Empty + ] + } + }, + { + "listen": "test", + "script": { + "id": "1279a2cf-b771-4a86-9dfa-302b240fac62", + "type": "text/javascript", + "exec": [ + string.Empty + ] + } + } + ] } \ No newline at end of file diff --git a/samples/AdministrationApi/Program.cs b/samples/AdministrationApi/Program.cs index a5e46fda0..36695bf9a 100644 --- a/samples/AdministrationApi/Program.cs +++ b/samples/AdministrationApi/Program.cs @@ -1,50 +1,48 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore; +using System.IO; + using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; + +using Ocelot.Administration; using Ocelot.DependencyInjection; using Ocelot.Middleware; -using Ocelot.Administration; namespace AdministrationApi { - public class Program -{ - public static void Main(string[] args) + public class Program { - new WebHostBuilder() - .UseKestrel() - .UseUrls("http://localhost:5000") - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("ocelot.json") - .AddEnvironmentVariables(); - }) - .ConfigureServices(s => { - s.AddOcelot() - .AddAdministration("/administration", "secret"); - }) - .ConfigureLogging((hostingContext, logging) => - { - logging.AddConsole(); - }) - .UseIISIntegration() - .Configure(app => - { - app.UseOcelot().Wait(); - }) - .Build() - .Run(); + public static void Main(string[] args) + { + new WebHostBuilder() + .UseKestrel() + .UseUrls("http://localhost:5000") + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json") + .AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot() + .AddAdministration("/administration", "secret"); + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConsole(); + }) + .UseIISIntegration() + .Configure(app => + { + app.UseOcelot().Wait(); + }) + .Build() + .Run(); + } } } -} diff --git a/samples/AdministrationApi/README.md b/samples/AdministrationApi/README.md index 36ed2a677..42a01b2f4 100644 --- a/samples/AdministrationApi/README.md +++ b/samples/AdministrationApi/README.md @@ -1,94 +1,94 @@ -```json -{ - "reRoutes": [ - { - "downstreamPathTemplate": "/{everything}", - "upstreamPathTemplate": "/templates/{everything}", - "upstreamHttpMethod": [ - "GET" - ], - "addHeadersToRequest": {}, - "upstreamHeaderTransform": {}, - "downstreamHeaderTransform": {}, - "addClaimsToRequest": {}, - "routeClaimsRequirement": {}, - "addQueriesToRequest": {}, - "requestIdKey": null, - "fileCacheOptions": { - "ttlSeconds": 0, - "region": null - }, - "reRouteIsCaseSensitive": false, - "downstreamScheme": "http", - "qoSOptions": { - "exceptionsAllowedBeforeBreaking": 0, - "durationOfBreak": 0, - "timeoutValue": 0 - }, - "loadBalancerOptions": { - "type": null, - "key": null, - "expiry": 0 - }, - "rateLimitOptions": { - "clientWhitelist": [], - "enableRateLimiting": false, - "period": null, - "periodTimespan": 0, - "limit": 0 - }, - "authenticationOptions": { - "authenticationProviderKey": null, - "allowedScopes": [] - }, - "httpHandlerOptions": { - "allowAutoRedirect": false, - "useCookieContainer": false, - "useTracing": false, - "useProxy": true - }, - "downstreamHostAndPorts": [ - { - "host": "localhost", - "port": 50689 - } - ], - "upstreamHost": null, - "key": null, - "delegatingHandlers": [], - "priority": 1, - "timeout": 0, - "dangerousAcceptAnyServerCertificateValidator": false - } - ], - "aggregates": [], - "globalConfiguration": { - "requestIdKey": "Request-Id", - "rateLimitOptions": { - "clientIdHeader": "ClientId", - "quotaExceededMessage": null, - "rateLimitCounterPrefix": "ocelot", - "disableRateLimitHeaders": false, - "httpStatusCode": 429 - }, - "qoSOptions": { - "exceptionsAllowedBeforeBreaking": 0, - "durationOfBreak": 0, - "timeoutValue": 0 - }, - "baseUrl": "http://localhost:55580", - "loadBalancerOptions": { - "type": null, - "key": null, - "expiry": 0 - }, - "downstreamScheme": null, - "httpHandlerOptions": { - "allowAutoRedirect": false, - "useCookieContainer": false, - "useTracing": false, - "useProxy": true - } - } -} -``` +```json +{ + "routes": [ + { + "downstreamPathTemplate": "/{everything}", + "upstreamPathTemplate": "/templates/{everything}", + "upstreamHttpMethod": [ + "GET" + ], + "addHeadersToRequest": {}, + "upstreamHeaderTransform": {}, + "downstreamHeaderTransform": {}, + "addClaimsToRequest": {}, + "routeClaimsRequirement": {}, + "addQueriesToRequest": {}, + "requestIdKey": null, + "fileCacheOptions": { + "ttlSeconds": 0, + "region": null + }, + "routeIsCaseSensitive": false, + "downstreamScheme": "http", + "qoSOptions": { + "exceptionsAllowedBeforeBreaking": 0, + "durationOfBreak": 0, + "timeoutValue": 0 + }, + "loadBalancerOptions": { + "type": null, + "key": null, + "expiry": 0 + }, + "rateLimitOptions": { + "clientWhitelist": [], + "enableRateLimiting": false, + "period": null, + "periodTimespan": 0, + "limit": 0 + }, + "authenticationOptions": { + "authenticationProviderKey": null, + "allowedScopes": [] + }, + "httpHandlerOptions": { + "allowAutoRedirect": false, + "useCookieContainer": false, + "useTracing": false, + "useProxy": true + }, + "downstreamHostAndPorts": [ + { + "host": "localhost", + "port": 50689 + } + ], + "upstreamHost": null, + "key": null, + "delegatingHandlers": [], + "priority": 1, + "timeout": 0, + "dangerousAcceptAnyServerCertificateValidator": false + } + ], + "aggregates": [], + "globalConfiguration": { + "requestIdKey": "Request-Id", + "rateLimitOptions": { + "clientIdHeader": "ClientId", + "quotaExceededMessage": null, + "rateLimitCounterPrefix": "ocelot", + "disableRateLimitHeaders": false, + "httpStatusCode": 429 + }, + "qoSOptions": { + "exceptionsAllowedBeforeBreaking": 0, + "durationOfBreak": 0, + "timeoutValue": 0 + }, + "baseUrl": "http://localhost:55580", + "loadBalancerOptions": { + "type": null, + "key": null, + "expiry": 0 + }, + "downstreamScheme": null, + "httpHandlerOptions": { + "allowAutoRedirect": false, + "useCookieContainer": false, + "useTracing": false, + "useProxy": true + } + } +} +``` diff --git a/samples/AdministrationApi/ocelot.json b/samples/AdministrationApi/ocelot.json index d16dcebfc..0fa4143ff 100644 --- a/samples/AdministrationApi/ocelot.json +++ b/samples/AdministrationApi/ocelot.json @@ -1,18 +1,18 @@ -{ - "ReRoutes": [ - { - "DownstreamPathTemplate": "/service/stats/collected", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 5100 - } - ], - "UpstreamPathTemplate": "/api/stats/collected" - } - ], - "GlobalConfiguration": { - "BaseUrl": "http://localhost:5000" - } +{ + "Routes": [ + { + "DownstreamPathTemplate": "/service/stats/collected", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 5100 + } + ], + "UpstreamPathTemplate": "/api/stats/collected" + } + ], + "GlobalConfiguration": { + "BaseUrl": "http://localhost:5000" + } } \ No newline at end of file diff --git a/samples/Docker/Dockerfile b/samples/Docker/Dockerfile index 86b12ab01..dad54b20f 100644 --- a/samples/Docker/Dockerfile +++ b/samples/Docker/Dockerfile @@ -17,7 +17,6 @@ COPY src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj src/Ocelot.C COPY src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj COPY src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj COPY src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj -COPY src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj COPY src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj COPY src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj COPY test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj diff --git a/samples/OcelotBasic/OcelotBasic.csproj b/samples/OcelotBasic/Ocelot.Samples.OcelotBasic.ApiGateway.csproj similarity index 51% rename from samples/OcelotBasic/OcelotBasic.csproj rename to samples/OcelotBasic/Ocelot.Samples.OcelotBasic.ApiGateway.csproj index 5e2902064..a6e612a78 100644 --- a/samples/OcelotBasic/OcelotBasic.csproj +++ b/samples/OcelotBasic/Ocelot.Samples.OcelotBasic.ApiGateway.csproj @@ -1,16 +1,16 @@ - - - - netcoreapp3.1 - - + + + + net7.0 + InProcess + + - - - - - - - - - + + + + + + + + diff --git a/samples/OcelotBasic/Program.cs b/samples/OcelotBasic/Program.cs index c0d8bed6a..b5856d857 100644 --- a/samples/OcelotBasic/Program.cs +++ b/samples/OcelotBasic/Program.cs @@ -1,42 +1,40 @@ -using System.IO; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.IO; + +namespace Ocelot.Samples.OcelotBasic.ApiGateway; -namespace OcelotBasic +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("ocelot.json") - .AddEnvironmentVariables(); - }) - .ConfigureServices(s => { - s.AddOcelot(); - }) - .ConfigureLogging((hostingContext, logging) => - { - //add your logging - }) - .UseIISIntegration() - .Configure(app => - { - app.UseOcelot().Wait(); - }) - .Build() - .Run(); - } + new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json") + .AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, logging) => + { + if (hostingContext.HostingEnvironment.IsDevelopment()) + { + logging.ClearProviders(); + logging.AddConsole(); + } + //add your logging + }) + .UseIISIntegration() + .UseStartup() + .Build() + .Run(); } } diff --git a/samples/OcelotBasic/Properties/launchSettings.json b/samples/OcelotBasic/Properties/launchSettings.json index b500ae576..924e292df 100644 --- a/samples/OcelotBasic/Properties/launchSettings.json +++ b/samples/OcelotBasic/Properties/launchSettings.json @@ -11,17 +11,19 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + "launchUrl": "posts/1", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, - "OcelotBasic": { + "ApiGateway": { "commandName": "Project", "launchBrowser": true, + "launchUrl": "posts/1", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:5001;http://localhost:5000" } } -} \ No newline at end of file +} diff --git a/samples/OcelotBasic/Startup.cs b/samples/OcelotBasic/Startup.cs new file mode 100644 index 000000000..62b8b422b --- /dev/null +++ b/samples/OcelotBasic/Startup.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; + +namespace Ocelot.Samples.OcelotBasic.ApiGateway; + +public class Startup +{ + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddOcelot(); + services.AddLogging(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseOcelot().Wait(); + } +} diff --git a/samples/OcelotBasic/appsettings.Development.json b/samples/OcelotBasic/appsettings.Development.json index dba68eb12..07053a355 100644 --- a/samples/OcelotBasic/appsettings.Development.json +++ b/samples/OcelotBasic/appsettings.Development.json @@ -1,9 +1,11 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Debug", + "Microsoft": "Information", + "Microsoft.AspNetCore": "Information", + "Microsoft.Hosting.Lifetime": "Information", + "System": "Information" } } } diff --git a/samples/OcelotBasic/appsettings.json b/samples/OcelotBasic/appsettings.json index 81ff87771..7376aada1 100644 --- a/samples/OcelotBasic/appsettings.json +++ b/samples/OcelotBasic/appsettings.json @@ -1,9 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Warning" } }, "AllowedHosts": "*" diff --git a/samples/OcelotBasic/ocelot.json b/samples/OcelotBasic/ocelot.json index 7a1759ffa..2864550cd 100644 --- a/samples/OcelotBasic/ocelot.json +++ b/samples/OcelotBasic/ocelot.json @@ -1,5 +1,5 @@ { - "ReRoutes": [ + "Routes": [ { "DownstreamPathTemplate": "/todos/{id}", "DownstreamScheme": "https", @@ -18,4 +18,4 @@ "GlobalConfiguration": { "BaseUrl": "https://localhost:5000" } -} \ No newline at end of file +} diff --git a/samples/OcelotEureka/ApiGateway/ApiGateway.csproj b/samples/OcelotEureka/ApiGateway/ApiGateway.csproj index a257c9801..a286c8a16 100644 --- a/samples/OcelotEureka/ApiGateway/ApiGateway.csproj +++ b/samples/OcelotEureka/ApiGateway/ApiGateway.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net7.0 @@ -15,9 +15,9 @@ - - - + + + diff --git a/samples/OcelotEureka/ApiGateway/Program.cs b/samples/OcelotEureka/ApiGateway/Program.cs index f2cd87f92..f80237b4c 100644 --- a/samples/OcelotEureka/ApiGateway/Program.cs +++ b/samples/OcelotEureka/ApiGateway/Program.cs @@ -1,14 +1,14 @@ using Ocelot.Provider.Eureka; using Ocelot.Provider.Polly; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; + +using Ocelot.DependencyInjection; +using Ocelot.Middleware; namespace ApiGateway { - using Microsoft.AspNetCore; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Ocelot.DependencyInjection; - using Ocelot.Middleware; - public class Program { public static void Main(string[] args) diff --git a/samples/OcelotEureka/ApiGateway/ocelot.json b/samples/OcelotEureka/ApiGateway/ocelot.json index 963160f9b..5a69973de 100644 --- a/samples/OcelotEureka/ApiGateway/ocelot.json +++ b/samples/OcelotEureka/ApiGateway/ocelot.json @@ -1,22 +1,22 @@ -{ - "ReRoutes": [ - { - "DownstreamPathTemplate": "/api/Category", - "DownstreamScheme": "http", - "UpstreamPathTemplate": "/Category", - "ServiceName": "ncore-rat", - "UpstreamHttpMethod": [ "Get" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10000, - "TimeoutValue": 5000 - }, - "FileCacheOptions": { "TtlSeconds": 15 } - } - ], - "GlobalConfiguration": { - "RequestIdKey": "OcRequestId", - "AdministrationPath": "/administration", - "ServiceDiscoveryProvider": { "Type": "Eureka" } - } -} +{ + "Routes": [ + { + "DownstreamPathTemplate": "/api/Category", + "DownstreamScheme": "http", + "UpstreamPathTemplate": "/Category", + "ServiceName": "ncore-rat", + "UpstreamHttpMethod": [ "Get" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10000, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + } + ], + "GlobalConfiguration": { + "RequestIdKey": "OcRequestId", + "AdministrationPath": "/administration", + "ServiceDiscoveryProvider": { "Type": "Eureka" } + } +} diff --git a/samples/OcelotEureka/DownstreamService/Controllers/CategoryController.cs b/samples/OcelotEureka/DownstreamService/Controllers/CategoryController.cs index 53350be74..5ce38f475 100644 --- a/samples/OcelotEureka/DownstreamService/Controllers/CategoryController.cs +++ b/samples/OcelotEureka/DownstreamService/Controllers/CategoryController.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; + using Microsoft.AspNetCore.Mvc; namespace DownstreamService.Controllers diff --git a/samples/OcelotEureka/DownstreamService/DownstreamService.csproj b/samples/OcelotEureka/DownstreamService/DownstreamService.csproj index 209fa6d21..748249c00 100644 --- a/samples/OcelotEureka/DownstreamService/DownstreamService.csproj +++ b/samples/OcelotEureka/DownstreamService/DownstreamService.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net7.0 diff --git a/samples/OcelotEureka/DownstreamService/Program.cs b/samples/OcelotEureka/DownstreamService/Program.cs index ddce0c286..130b46f0e 100644 --- a/samples/OcelotEureka/DownstreamService/Program.cs +++ b/samples/OcelotEureka/DownstreamService/Program.cs @@ -1,4 +1,5 @@ using System; + using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; diff --git a/samples/OcelotEureka/DownstreamService/Startup.cs b/samples/OcelotEureka/DownstreamService/Startup.cs index b3924be64..56a187c2c 100644 --- a/samples/OcelotEureka/DownstreamService/Startup.cs +++ b/samples/OcelotEureka/DownstreamService/Startup.cs @@ -1,18 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Hosting; + +using Steeltoe.Discovery.Client; namespace DownstreamService { - using Steeltoe.Discovery.Client; - public class Startup { public Startup(IConfiguration configuration) @@ -30,7 +25,7 @@ public void ConfigureServices(IServiceCollection services) } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { @@ -38,7 +33,6 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) } app.UseDiscoveryClient(); - app.UseMvc(); } } } diff --git a/samples/OcelotGraphQL/OcelotGraphQL.csproj b/samples/OcelotGraphQL/OcelotGraphQL.csproj index 408d8543e..06956a7f8 100644 --- a/samples/OcelotGraphQL/OcelotGraphQL.csproj +++ b/samples/OcelotGraphQL/OcelotGraphQL.csproj @@ -1,6 +1,6 @@ - netcoreapp3.1 + net7.0 @@ -11,7 +11,7 @@ - - + + - \ No newline at end of file + diff --git a/samples/OcelotGraphQL/Program.cs b/samples/OcelotGraphQL/Program.cs index 83a48fda1..bbf5eff48 100644 --- a/samples/OcelotGraphQL/Program.cs +++ b/samples/OcelotGraphQL/Program.cs @@ -1,131 +1,137 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Ocelot.Middleware; -using Ocelot.DependencyInjection; -using GraphQL.Types; -using GraphQL; -using Ocelot.Requester; -using Ocelot.Responses; -using System.Net.Http; -using System.Net; -using Microsoft.Extensions.DependencyInjection; -using System.Threading; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; -namespace OcelotGraphQL -{ - public class Hero - { - public int Id { get; set; } - public string Name { get; set; } - } +using GraphQL; +using GraphQL.Types; - public class Query - { - private List _heroes = new List - { - new Hero { Id = 1, Name = "R2-D2" }, - new Hero { Id = 2, Name = "Batman" }, - new Hero { Id = 3, Name = "Wonder Woman" }, - new Hero { Id = 4, Name = "Tom Pallister" } - }; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; - [GraphQLMetadata("hero")] - public Hero GetHero(int id) - { - return _heroes.FirstOrDefault(x => x.Id == id); - } - } - - public class GraphQlDelegatingHandler : DelegatingHandler - { - private readonly ISchema _schema; - - public GraphQlDelegatingHandler(ISchema schema) - { - _schema = schema; - } - - protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - //try get query from body, could check http method :) - var query = await request.Content.ReadAsStringAsync(); - - //if not body try query string, dont hack like this in real world.. - if(query.Length == 0) - { - var decoded = WebUtility.UrlDecode(request.RequestUri.Query); - query = decoded.Replace("?query=", ""); - } - - var result = _schema.Execute(_ => - { - _.Query = query; - }); - - //maybe check for errors and headers etc in real world? - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(result) - }; - - //ocelot will treat this like any other http request... - return response; - } - } - - public class Program - { - public static void Main(string[] args) - { - var schema = Schema.For(@" - type Hero { - id: Int - name: String - } - - type Query { - hero(id: Int): Hero - } - ", _ => { - _.Types.Include(); - }); - - new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("ocelot.json", false, false) - .AddEnvironmentVariables(); - }) - .ConfigureServices(s => { - s.AddSingleton(schema); - s.AddOcelot() - .AddSingletonDelegatingHandler(); - }) - .ConfigureLogging((hostingContext, logging) => - { - logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); - logging.AddConsole(); - }) - .UseIISIntegration() - .Configure(app => - { - app.UseOcelot().Wait(); - }) - .Build() - .Run(); - } - } -} +using Ocelot.DependencyInjection; +using Ocelot.Middleware; + +namespace OcelotGraphQL +{ + public class Hero + { + public int Id { get; set; } + public string Name { get; set; } + } + + public class Query + { + private readonly List _heroes = new() + { + new Hero { Id = 1, Name = "R2-D2" }, + new Hero { Id = 2, Name = "Batman" }, + new Hero { Id = 3, Name = "Wonder Woman" }, + new Hero { Id = 4, Name = "Tom Pallister" } + }; + + [GraphQLMetadata("hero")] + public Hero GetHero(int id) + { + return _heroes.FirstOrDefault(x => x.Id == id); + } + } + + public class GraphQlDelegatingHandler : DelegatingHandler + { + //private readonly ISchema _schema; + private readonly IDocumentExecuter _executer; + private readonly IDocumentWriter _writer; + + public GraphQlDelegatingHandler(IDocumentExecuter executer, IDocumentWriter writer) + { + _executer = executer; + _writer = writer; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + //try get query from body, could check http method :) + var query = await request.Content.ReadAsStringAsync(cancellationToken); + + //if not body try query string, dont hack like this in real world.. + if (query.Length == 0) + { + var decoded = WebUtility.UrlDecode(request.RequestUri.Query); + query = decoded.Replace("?query=", string.Empty); + } + + var result = await _executer.ExecuteAsync(_ => + { + _.Query = query; + }); + + var responseBody = await _writer.WriteToStringAsync(result); + + //maybe check for errors and headers etc in real world? + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responseBody) + }; + + //ocelot will treat this like any other http request... + return response; + } + } + + public class Program + { + public static void Main() + { + var schema = Schema.For(@" + type Hero { + id: Int + name: String + } + + type Query { + hero(id: Int): Hero + } + ", _ => + { + _.Types.Include(); + }); + + new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json", false, false) + .AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(schema); + s.AddOcelot() + .AddDelegatingHandler(); + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + logging.AddConsole(); + }) + .UseIISIntegration() + .Configure(app => + { + app.UseOcelot().Wait(); + }) + .Build() + .Run(); + } + } +} diff --git a/samples/OcelotGraphQL/README.md b/samples/OcelotGraphQL/README.md index b2101d568..7f16ed985 100644 --- a/samples/OcelotGraphQL/README.md +++ b/samples/OcelotGraphQL/README.md @@ -1,71 +1,71 @@ -# Ocelot using GraphQL example - -Loads of people keep asking me if Ocelot will every support GraphQL, in my mind Ocelot and GraphQL are two different things that can work together. -I would not try and implement GraphQL in Ocelot instead I would either have Ocelot in front of GraphQL to handle things like authorisation / authentication or I would -bring in the awesome [graphql-dotnet](https://github.com/graphql-dotnet/graphql-dotnet) library and use it in a [DelegatingHandler](http://ocelot.readthedocs.io/en/latest/features/delegatinghandlers.html). This way you could have Ocelot and GraphQL without the extra hop to GraphQL. This same is an example of how to do that. - -## Example - -If you run this project with - -$ dotnet run - -Use postman or something to make the following requests and you can see Ocelot and GraphQL in action together... - -GET http://localhost:5000/graphql?query={ hero(id: 4) { id name } } - -RESPONSE -```json - { - "data": { - "hero": { - "id": 4, - "name": "Tom Pallister" - } - } - } -``` - -POST http://localhost:5000/graphql - -BODY -```json - { hero(id: 4) { id name } } -``` - -RESPONSE -```json - { - "data": { - "hero": { - "id": 4, - "name": "Tom Pallister" - } - } - } -``` - -## Notes - -Please note this project never goes out to another service, it just gets the data for GraphQL in memory. You would need to add the details of your GraphQL server in ocelot.json e.g. - -```json -{ - "ReRoutes": [ - { - "DownstreamPathTemplate": "/graphql", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "yourgraphqlhost.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/graphql", - "DelegatingHandlers": [ - "GraphQlDelegatingHandler" - ] - } - ] - } +# Ocelot using GraphQL example + +Loads of people keep asking me if Ocelot will every support GraphQL, in my mind Ocelot and GraphQL are two different things that can work together. +I would not try and implement GraphQL in Ocelot instead I would either have Ocelot in front of GraphQL to handle things like authorization / authentication or I would +bring in the awesome [graphql-dotnet](https://github.com/graphql-dotnet/graphql-dotnet) library and use it in a [DelegatingHandler](http://ocelot.readthedocs.io/en/latest/features/delegatinghandlers.html). This way you could have Ocelot and GraphQL without the extra hop to GraphQL. This same is an example of how to do that. + +## Example + +If you run this project with + +$ dotnet run + +Use postman or something to make the following requests and you can see Ocelot and GraphQL in action together... + +GET http://localhost:5000/graphql?query={ hero(id: 4) { id name } } + +RESPONSE +```json + { + "data": { + "hero": { + "id": 4, + "name": "Tom Pallister" + } + } + } +``` + +POST http://localhost:5000/graphql + +BODY +```json + { hero(id: 4) { id name } } +``` + +RESPONSE +```json + { + "data": { + "hero": { + "id": 4, + "name": "Tom Pallister" + } + } + } +``` + +## Notes + +Please note this project never goes out to another service, it just gets the data for GraphQL in memory. You would need to add the details of your GraphQL server in ocelot.json e.g. + +```json +{ + "Routes": [ + { + "DownstreamPathTemplate": "/graphql", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "yourgraphqlhost.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/graphql", + "DelegatingHandlers": [ + "GraphQlDelegatingHandler" + ] + } + ] + } ``` \ No newline at end of file diff --git a/samples/OcelotGraphQL/ocelot.json b/samples/OcelotGraphQL/ocelot.json index 115006f9f..c716bf258 100644 --- a/samples/OcelotGraphQL/ocelot.json +++ b/samples/OcelotGraphQL/ocelot.json @@ -1,19 +1,19 @@ -{ - "ReRoutes": [ - { - "DownstreamPathTemplate": "/", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/graphql", - "DelegatingHandlers": [ - "GraphQlDelegatingHandler" - ] - } - ] - } +{ + "Routes": [ + { + "DownstreamPathTemplate": "/", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/graphql", + "DelegatingHandlers": [ + "GraphQlDelegatingHandler" + ] + } + ] + } \ No newline at end of file diff --git a/samples/OcelotKube/ApiGateway/ApiGateway.csproj b/samples/OcelotKube/ApiGateway/ApiGateway.csproj deleted file mode 100644 index 05f462ab7..000000000 --- a/samples/OcelotKube/ApiGateway/ApiGateway.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - netcoreapp3.1 - InProcess - Linux - - - - - - - - - - - diff --git a/samples/OcelotKube/ApiGateway/Dockerfile b/samples/OcelotKube/ApiGateway/Dockerfile index 1e3fcd116..990742e4a 100644 --- a/samples/OcelotKube/ApiGateway/Dockerfile +++ b/samples/OcelotKube/ApiGateway/Dockerfile @@ -1,19 +1,20 @@ -FROM mcr.microsoft.com/dotnet/core/aspnet:2.1-stretch-slim AS base +FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base WORKDIR /app EXPOSE 80 +EXPOSE 443 -FROM mcr.microsoft.com/dotnet/core/sdk:2.1-stretch AS build +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build WORKDIR /src -COPY ["ApiGateway/ApiGateway.csproj", "ApiGateway/"] -RUN dotnet restore "ApiGateway/ApiGateway.csproj" +COPY ["ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj", "ApiGateway/"] +RUN dotnet restore "ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj" COPY . . WORKDIR "/src/ApiGateway" -RUN dotnet build "ApiGateway.csproj" -c Release -o /app +RUN dotnet build "Ocelot.Samples.OcelotKube.ApiGateway.csproj" -c Release -o /app/build FROM build AS publish -RUN dotnet publish "ApiGateway.csproj" -c Release -o /app +RUN dotnet publish "Ocelot.Samples.OcelotKube.ApiGateway.csproj" -c Release -o /app/publish /p:UseAppHost=false FROM base AS final WORKDIR /app -COPY --from=publish /app . -ENTRYPOINT ["dotnet", "ApiGateway.dll"] \ No newline at end of file +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Ocelot.Samples.OcelotKube.ApiGateway.dll"] diff --git a/samples/OcelotKube/ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj b/samples/OcelotKube/ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj new file mode 100644 index 000000000..5d08a3b8a --- /dev/null +++ b/samples/OcelotKube/ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + InProcess + Linux + + + + + + + + + + + + diff --git a/samples/OcelotKube/ApiGateway/Program.cs b/samples/OcelotKube/ApiGateway/Program.cs index 978c8ccc9..36f5fe49c 100644 --- a/samples/OcelotKube/ApiGateway/Program.cs +++ b/samples/OcelotKube/ApiGateway/Program.cs @@ -1,32 +1,27 @@ using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; -using Ocelot.Provider.Kubernetes; -namespace ApiGateway +namespace Ocelot.Samples.OcelotKube.ApiGateway; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - BuildWebHost(args).Run(); - } - - public static IWebHost BuildWebHost(string[] args) => - WebHost.CreateDefaultBuilder(args) - .ConfigureAppConfiguration((hostingContext, config) => - { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("ocelot.json", false, false) - .AddEnvironmentVariables(); - }) - .UseStartup() - .Build(); + BuildWebHost(args).Run(); } -} + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json", false, false) + .AddEnvironmentVariables(); + }) + .UseStartup() + .Build(); +} diff --git a/samples/OcelotKube/ApiGateway/Properties/launchSettings.json b/samples/OcelotKube/ApiGateway/Properties/launchSettings.json index 5315ab2c8..5684b7657 100644 --- a/samples/OcelotKube/ApiGateway/Properties/launchSettings.json +++ b/samples/OcelotKube/ApiGateway/Properties/launchSettings.json @@ -11,6 +11,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + "launchUrl": "values", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -18,6 +19,7 @@ "ApiGateway": { "commandName": "Project", "launchBrowser": true, + "launchUrl": "values", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, @@ -26,7 +28,7 @@ "Docker": { "commandName": "Docker", "launchBrowser": true, - "launchUrl": "{Scheme}://localhost:{ServicePort}" + "launchUrl": "{Scheme}://localhost:{ServicePort}/values" } } -} \ No newline at end of file +} diff --git a/samples/OcelotKube/ApiGateway/Startup.cs b/samples/OcelotKube/ApiGateway/Startup.cs index d7b2473c4..2afcde95c 100644 --- a/samples/OcelotKube/ApiGateway/Startup.cs +++ b/samples/OcelotKube/ApiGateway/Startup.cs @@ -1,31 +1,32 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + using Ocelot.DependencyInjection; using Ocelot.Middleware; using Ocelot.Provider.Kubernetes; -namespace ApiGateway +namespace Ocelot.Samples.OcelotKube.ApiGateway; + +public class Startup { - public class Startup + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) { - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - services.AddOcelot() - .AddKubernetes(); - } + services.AddOcelot() + .AddKubernetes(); + } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseOcelot().Wait(); + app.UseDeveloperExceptionPage(); } + + app.UseOcelot().Wait(); } } diff --git a/samples/OcelotKube/ApiGateway/ocelot.json b/samples/OcelotKube/ApiGateway/ocelot.json index ec70503e9..6a28b9eec 100644 --- a/samples/OcelotKube/ApiGateway/ocelot.json +++ b/samples/OcelotKube/ApiGateway/ocelot.json @@ -1,20 +1,20 @@ -{ - "ReRoutes": [ - { - "DownstreamPathTemplate": "/api/values", - "DownstreamScheme": "http", - "UpstreamPathTemplate": "/values", - "ServiceName": "downstreamservice", - "UpstreamHttpMethod": [ "Get" ] - } - ], - "GlobalConfiguration": { - "ServiceDiscoveryProvider": { - "Host": "192.168.0.13", - "Port": 443, - "Token": "txpc696iUhbVoudg164r93CxDTrKRVWG", - "Namespace": "dev", - "Type": "kube" - } - } -} +{ + "Routes": [ + { + "DownstreamPathTemplate": "/api/values", + "DownstreamScheme": "http", + "UpstreamPathTemplate": "/values", + "ServiceName": "downstreamservice", + "UpstreamHttpMethod": [ "Get" ] + } + ], + "GlobalConfiguration": { + "ServiceDiscoveryProvider": { + "Host": "192.168.0.13", + "Port": 443, + "Token": "txpc696iUhbVoudg164r93CxDTrKRVWG", + "Namespace": "dev", + "Type": "kube" + } + } +} diff --git a/samples/OcelotKube/DownstreamService/Controllers/ValuesController.cs b/samples/OcelotKube/DownstreamService/Controllers/ValuesController.cs index 425cf18de..48ebcb7d2 100644 --- a/samples/OcelotKube/DownstreamService/Controllers/ValuesController.cs +++ b/samples/OcelotKube/DownstreamService/Controllers/ValuesController.cs @@ -1,45 +1,41 @@ -using System; +using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -namespace DownstreamService.Controllers +namespace Ocelot.Samples.OcelotKube.DownstreamService.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ValuesController : ControllerBase { - [Route("api/[controller]")] - [ApiController] - public class ValuesController : ControllerBase + // GET api/values + [HttpGet] + public ActionResult> Get() { - // GET api/values - [HttpGet] - public ActionResult> Get() - { - return new string[] { "value1", "value2" }; - } + return new[] { "value1", "value2" }; + } - // GET api/values/5 - [HttpGet("{id}")] - public ActionResult Get(int id) - { - return "value"; - } + // GET api/values/5 + [HttpGet("{id}")] + public ActionResult Get(int id) + { + return "value"; + } - // POST api/values - [HttpPost] - public void Post([FromBody] string value) - { - } + // POST api/values + [HttpPost] + public void Post([FromBody] string value) + { + } - // PUT api/values/5 - [HttpPut("{id}")] - public void Put(int id, [FromBody] string value) - { - } + // PUT api/values/5 + [HttpPut("{id}")] + public void Put(int id, [FromBody] string value) + { + } - // DELETE api/values/5 - [HttpDelete("{id}")] - public void Delete(int id) - { - } + // DELETE api/values/5 + [HttpDelete("{id}")] + public void Delete(int id) + { } } diff --git a/samples/OcelotKube/DownstreamService/Controllers/WeatherForecastController.cs b/samples/OcelotKube/DownstreamService/Controllers/WeatherForecastController.cs new file mode 100644 index 000000000..33222b1f1 --- /dev/null +++ b/samples/OcelotKube/DownstreamService/Controllers/WeatherForecastController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ocelot.Samples.OcelotKube.DownstreamService.Controllers; + +using Models; + +[ApiController] +[Route("api/[controller]")] +public class WeatherForecastController : ControllerBase +{ + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5) + .Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } +} diff --git a/samples/OcelotKube/DownstreamService/Dockerfile b/samples/OcelotKube/DownstreamService/Dockerfile index a96955150..985096fcc 100644 --- a/samples/OcelotKube/DownstreamService/Dockerfile +++ b/samples/OcelotKube/DownstreamService/Dockerfile @@ -1,19 +1,20 @@ -FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base +FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base WORKDIR /app EXPOSE 80 +EXPOSE 443 -FROM microsoft/dotnet:2.1-sdk AS build +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build WORKDIR /src -COPY ["DownstreamService/DownstreamService.csproj", "DownstreamService/"] -RUN dotnet restore "DownstreamService/DownstreamService.csproj" +COPY ["DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj", "DownstreamService/"] +RUN dotnet restore "DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj" COPY . . WORKDIR "/src/DownstreamService" -RUN dotnet build "DownstreamService.csproj" -c Release -o /app +RUN dotnet build "Ocelot.Samples.OcelotKube.DownstreamService.csproj" -c Release -o /app/build FROM build AS publish -RUN dotnet publish "DownstreamService.csproj" -c Release -o /app +RUN dotnet publish "Ocelot.Samples.OcelotKube.DownstreamService.csproj" -c Release -o /app/publish /p:UseAppHost=false FROM base AS final WORKDIR /app -COPY --from=publish /app . -ENTRYPOINT ["dotnet", "DownstreamService.dll"] +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Ocelot.Samples.OcelotKube.DownstreamService.dll"] diff --git a/samples/OcelotKube/DownstreamService/Dockerfile.develop b/samples/OcelotKube/DownstreamService/Dockerfile.develop deleted file mode 100644 index 6f49a2909..000000000 --- a/samples/OcelotKube/DownstreamService/Dockerfile.develop +++ /dev/null @@ -1,15 +0,0 @@ -FROM microsoft/dotnet:2.1-sdk -ARG BUILD_CONFIGURATION=Debug -ENV ASPNETCORE_ENVIRONMENT=Development -ENV DOTNET_USE_POLLING_FILE_WATCHER=true -EXPOSE 80 - -WORKDIR /src -COPY ["DownstreamService/DownstreamService.csproj", "DownstreamService/"] - -RUN dotnet restore "DownstreamService/DownstreamService.csproj" -COPY . . -WORKDIR "/src/DownstreamService" -RUN dotnet build --no-restore "DownstreamService.csproj" -c $BUILD_CONFIGURATION - -ENTRYPOINT ["dotnet", "run", "--no-build", "--no-launch-profile", "-c", "$BUILD_CONFIGURATION", "--"] \ No newline at end of file diff --git a/samples/OcelotKube/DownstreamService/Models/WeatherForecast.cs b/samples/OcelotKube/DownstreamService/Models/WeatherForecast.cs new file mode 100644 index 000000000..66e01aee6 --- /dev/null +++ b/samples/OcelotKube/DownstreamService/Models/WeatherForecast.cs @@ -0,0 +1,14 @@ +using System; + +namespace Ocelot.Samples.OcelotKube.DownstreamService.Models; + +public class WeatherForecast +{ + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string Summary { get; set; } +} diff --git a/samples/OcelotKube/DownstreamService/DownstreamService.csproj b/samples/OcelotKube/DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj similarity index 52% rename from samples/OcelotKube/DownstreamService/DownstreamService.csproj rename to samples/OcelotKube/DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj index d430c524e..56b2d53e7 100644 --- a/samples/OcelotKube/DownstreamService/DownstreamService.csproj +++ b/samples/OcelotKube/DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj @@ -1,15 +1,14 @@ - - - - netcoreapp3.1 - InProcess - Linux - - - - - - - - - + + + + net7.0 + InProcess + Linux + + + + + + + + diff --git a/samples/OcelotKube/DownstreamService/Program.cs b/samples/OcelotKube/DownstreamService/Program.cs index 03e1b8aea..94f139c01 100644 --- a/samples/OcelotKube/DownstreamService/Program.cs +++ b/samples/OcelotKube/DownstreamService/Program.cs @@ -1,24 +1,57 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.Text.Json; +using System.Text.Json.Serialization; -namespace DownstreamService +namespace Ocelot.Samples.OcelotKube.DownstreamService; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) + var builder = WebApplication.CreateBuilder(args); + + builder.Services + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + .AddEndpointsApiExplorer() + .AddSwaggerGen() + + .AddControllers() + .AddJsonOptions(options => + { + options.AllowInputFormatterExceptionMessages = true; + + var jOptions = options.JsonSerializerOptions; + jOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true)); + jOptions.PropertyNameCaseInsensitive = true; + jOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + }); + + AddApplicationServices(builder.Services); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) { - CreateWebHostBuilder(args).Build().Run(); + app.UseSwagger(); + app.UseSwaggerUI(); } - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup(); + app.UseHttpsRedirection(); + app.UseAuthorization(); + + app.MapControllers(); + app.Run(); + } + + private static void AddApplicationServices(IServiceCollection services) + { + services.AddHttpClient(); // to keep performance of HTTP Client high + //services.AddSingleton + //services.AddScoped + //services.AddTransient } } diff --git a/samples/OcelotKube/DownstreamService/Properties/launchSettings.json b/samples/OcelotKube/DownstreamService/Properties/launchSettings.json index 30d6118c6..6f9313866 100644 --- a/samples/OcelotKube/DownstreamService/Properties/launchSettings.json +++ b/samples/OcelotKube/DownstreamService/Properties/launchSettings.json @@ -12,7 +12,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - "launchUrl": "api/values", + "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -20,7 +20,7 @@ "DownstreamService": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "api/values", + "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, @@ -29,7 +29,7 @@ "Docker": { "commandName": "Docker", "launchBrowser": true, - "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/api/values" + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger" } } -} \ No newline at end of file +} diff --git a/samples/OcelotKube/DownstreamService/Startup.cs b/samples/OcelotKube/DownstreamService/Startup.cs deleted file mode 100644 index 9a927a37b..000000000 --- a/samples/OcelotKube/DownstreamService/Startup.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace DownstreamService -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseMvc(); - } - } -} diff --git a/samples/OcelotOpenTracing/OcelotOpenTracing.csproj b/samples/OcelotOpenTracing/OcelotOpenTracing.csproj new file mode 100644 index 000000000..0384c8c01 --- /dev/null +++ b/samples/OcelotOpenTracing/OcelotOpenTracing.csproj @@ -0,0 +1,30 @@ + + + + Exe + net7.0 + + + + + + + + + + + + + + + true + + + true + + + true + + + + diff --git a/samples/OcelotOpenTracing/Program.cs b/samples/OcelotOpenTracing/Program.cs new file mode 100644 index 000000000..0c472ac23 --- /dev/null +++ b/samples/OcelotOpenTracing/Program.cs @@ -0,0 +1,68 @@ +using System.IO; + +using Jaeger; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +using Ocelot.DependencyInjection; +using Ocelot.Middleware; +using Ocelot.Tracing.OpenTracing; + +using OpenTracing.Util; + +namespace OcelotOpenTracing +{ + internal static class Program + { + private static void Main(string[] args) + { + Host.CreateDefaultBuilder() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseKestrel() + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", + optional: true, reloadOnChange: false) + .AddJsonFile("ocelot.json", optional: false, reloadOnChange: true) + .AddEnvironmentVariables(); + }) + .ConfigureServices((context, services) => + { + services.AddSingleton(sp => + { + var loggerFactory = sp.GetService(); + var config = new Configuration(context.HostingEnvironment.ApplicationName, loggerFactory); + + var tracer = config.GetTracer(); + GlobalTracer.Register(tracer); + return tracer; + }); + + services + .AddOcelot() + .AddOpenTracing(); + }) + .ConfigureLogging(logging => + { + logging.AddConsole(); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + }) + .Build() + .Run(); + } + } +} diff --git a/samples/OcelotOpenTracing/appsettings.Development.json b/samples/OcelotOpenTracing/appsettings.Development.json new file mode 100644 index 000000000..8983e0fc1 --- /dev/null +++ b/samples/OcelotOpenTracing/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/samples/OcelotOpenTracing/appsettings.json b/samples/OcelotOpenTracing/appsettings.json new file mode 100644 index 000000000..d9d9a9bff --- /dev/null +++ b/samples/OcelotOpenTracing/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/OcelotOpenTracing/ocelot.json b/samples/OcelotOpenTracing/ocelot.json new file mode 100644 index 000000000..a5a670321 --- /dev/null +++ b/samples/OcelotOpenTracing/ocelot.json @@ -0,0 +1,24 @@ +{ + "ReRoutes": [ + { + "HttpHandlerOptions": { + "UseTracing": true + }, + "DownstreamPathTemplate": "/todos/{id}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 443 + } + ], + "UpstreamPathTemplate": "/posts/{id}", + "UpstreamHttpMethod": [ + "Get" + ] + } + ], + "GlobalConfiguration": { + "BaseUrl": "https://localhost:5000" + } +} diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.cs b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.cs index 28ea7abbb..a017ee028 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.cs +++ b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.cs @@ -3,13 +3,14 @@ // Licensed under the MIT License (MIT). See License.txt in the repo root for license information. // ------------------------------------------------------------ +using System.Collections.Generic; +using System.Fabric; + +using Microsoft.ServiceFabric.Services.Communication.Runtime; +using Microsoft.ServiceFabric.Services.Runtime; + namespace OcelotApplicationApiGateway { - using System.Fabric; - using Microsoft.ServiceFabric.Services.Communication.Runtime; - using Microsoft.ServiceFabric.Services.Runtime; - using System.Collections.Generic; - /// Service that handles front-end web requests and acts as a proxy to the back-end data for the UI web page. /// It is a stateless service that hosts a Web API application on OWIN. internal sealed class OcelotServiceWebService : StatelessService diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj index 186b43f36..416ea124b 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj +++ b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj @@ -1,11 +1,10 @@ - + Stateless Web Service for Stateful OcelotApplicationApiGateway App - - netcoreapp3.1 + net7.0 OcelotApplicationApiGateway - Exe OcelotApplicationApiGateway + x64 @@ -13,10 +12,10 @@ - - + + - \ No newline at end of file + diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/Program.cs b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/Program.cs index 7da30a50a..9a4dd524e 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/Program.cs +++ b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/Program.cs @@ -3,16 +3,15 @@ // Licensed under the MIT License (MIT). See License.txt in the repo root for license information. // ------------------------------------------------------------ -namespace OcelotApplicationApiGateway +using System; +using System.Diagnostics.Tracing; +using System.Threading; -{ - using System; - using System.Fabric; - using System.Threading; - using Microsoft.ServiceFabric.Services.Runtime; - using System.Diagnostics.Tracing; +using Microsoft.ServiceFabric.Services.Runtime; +namespace OcelotApplicationApiGateway +{ /// /// The service host is the executable that hosts the Service instances. /// @@ -25,7 +24,7 @@ public static void Main(string[] args) { //Creating a new event listener to redirect the traces to a file - ServiceEventListener listener = new ServiceEventListener("OcelotApplicationApiGateway"); + var listener = new ServiceEventListener("OcelotApplicationApiGateway"); listener.EnableEvents(ServiceEventSource.Current, EventLevel.LogAlways, EventKeywords.All); // The ServiceManifest.XML file defines one or more service type names. @@ -33,7 +32,7 @@ public static void Main(string[] args) // When Service Fabric creates an instance of this service type, // an instance of the class is created in this host process. ServiceRuntime - .RegisterServiceAsync("OcelotApplicationApiGatewayType", context => new OcelotServiceWebService (context)) + .RegisterServiceAsync("OcelotApplicationApiGatewayType", context => new OcelotServiceWebService(context)) .GetAwaiter() .GetResult(); @@ -44,8 +43,8 @@ public static void Main(string[] args) catch (Exception ex) { ServiceEventSource.Current.ServiceHostInitializationFailed(ex); - throw ex; + throw; } } } -} \ No newline at end of file +} diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventListener.cs b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventListener.cs index 555f9fde8..d6d64dac2 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventListener.cs +++ b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventListener.cs @@ -3,19 +3,15 @@ // Licensed under the MIT License (MIT). See License.txt in the repo root for license information. // ------------------------------------------------------------ +using System; +using System.Diagnostics.Tracing; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; + namespace OcelotApplicationApiGateway { - using System; - using System.IO; - using System.Linq; - using System.Diagnostics.Tracing; - using System.Fabric; - using System.Fabric.Common; - using System.Fabric.Common.Tracing; - using Microsoft.ServiceFabric.Services.Runtime; - using System.Globalization; - using System.Text; - /// /// ServiceEventListener is a class which listens to the eventsources registered and redirects the traces to a file /// Note that this class serves as a template to EventListener class and redirects the logs to /tmp/{appnameyyyyMMddHHmmssffff}. @@ -24,12 +20,12 @@ namespace OcelotApplicationApiGateway /// internal class ServiceEventListener : EventListener { - private string fileName; - private string filepath = Path.GetTempPath(); + private readonly string _fileName; + private readonly string _filepath = Path.GetTempPath(); public ServiceEventListener(string appName) { - this.fileName = appName + DateTime.Now.ToString("yyyyMMddHHmmssffff"); + _fileName = appName + DateTime.Now.ToString("yyyyMMddHHmmssffff"); } /// @@ -38,10 +34,10 @@ public ServiceEventListener(string appName) /// The event arguments that describe the event. protected override void OnEventWritten(EventWrittenEventArgs eventData) { - using (StreamWriter writer = new StreamWriter( new FileStream(filepath + fileName, FileMode.Append))) + using (var writer = new StreamWriter(new FileStream(_filepath + _fileName, FileMode.Append))) { // report all event information - writer.Write(" {0} ", Write(eventData.Task.ToString(), + writer.Write(" {0} ", Write(eventData.Task.ToString(), eventData.EventName, eventData.EventId.ToString(), eventData.Level)); @@ -55,9 +51,9 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) private static String Write(string taskName, string eventName, string id, EventLevel level) { - StringBuilder output = new StringBuilder(); + var output = new StringBuilder(); - DateTime now = DateTime.UtcNow; + var now = DateTime.UtcNow; output.Append(now.ToString("yyyy/MM/dd-HH:mm:ss.fff", CultureInfo.InvariantCulture)); output.Append(','); output.Append(ConvertLevelToString(level)); @@ -91,4 +87,4 @@ private static string ConvertLevelToString(EventLevel level) } } } -} \ No newline at end of file +} diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventSource.cs b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventSource.cs index c05799556..f880f6af0 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventSource.cs +++ b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ServiceEventSource.cs @@ -3,19 +3,17 @@ // Licensed under the MIT License (MIT). See License.txt in the repo root for license information. // ------------------------------------------------------------ +using System; +using System.Diagnostics.Tracing; + namespace OcelotApplicationApiGateway { - using System; - using System.Diagnostics.Tracing; - using System.Fabric; - using Microsoft.ServiceFabric.Services.Runtime; - /// /// Implements methods for logging service related events. /// public class ServiceEventSource : EventSource { - public static ServiceEventSource Current = new ServiceEventSource(); + public static ServiceEventSource Current = new(); // Define an instance method for each event you want to record and apply an [Event] attribute to it. // The method name is the name of the event. @@ -28,10 +26,10 @@ public class ServiceEventSource : EventSource [NonEvent] public void Message(string message, params object[] args) { - if (this.IsEnabled()) + if (IsEnabled()) { var finalMessage = string.Format(message, args); - this.Message(finalMessage); + Message(finalMessage); } } @@ -40,9 +38,9 @@ public void Message(string message, params object[] args) [Event(MessageEventId, Level = EventLevel.Informational, Message = "{0}")] public void Message(string message) { - if (this.IsEnabled()) + if (IsEnabled()) { - this.WriteEvent(MessageEventId, message); + WriteEvent(MessageEventId, message); } } @@ -51,13 +49,13 @@ public void Message(string message) [Event(ServiceTypeRegisteredEventId, Level = EventLevel.Informational, Message = "Service host process {0} registered service type {1}")] public void ServiceTypeRegistered(int hostProcessId, string serviceType) { - this.WriteEvent(ServiceTypeRegisteredEventId, hostProcessId, serviceType); + WriteEvent(ServiceTypeRegisteredEventId, hostProcessId, serviceType); } [NonEvent] public void ServiceHostInitializationFailed(Exception e) { - this.ServiceHostInitializationFailed(e.ToString()); + ServiceHostInitializationFailed(e.ToString()); } private const int ServiceHostInitializationFailedEventId = 4; @@ -65,13 +63,13 @@ public void ServiceHostInitializationFailed(Exception e) [Event(ServiceHostInitializationFailedEventId, Level = EventLevel.Error, Message = "Service host initialization failed: {0}")] private void ServiceHostInitializationFailed(string exception) { - this.WriteEvent(ServiceHostInitializationFailedEventId, exception); + WriteEvent(ServiceHostInitializationFailedEventId, exception); } [NonEvent] public void ServiceWebHostBuilderFailed(Exception e) { - this.ServiceWebHostBuilderFailed(e.ToString()); + ServiceWebHostBuilderFailed(e.ToString()); } private const int ServiceWebHostBuilderFailedEventId = 5; @@ -79,7 +77,7 @@ public void ServiceWebHostBuilderFailed(Exception e) [Event(ServiceWebHostBuilderFailedEventId, Level = EventLevel.Error, Message = "Service Owin Web Host Builder Failed: {0}")] private void ServiceWebHostBuilderFailed(string exception) { - this.WriteEvent(ServiceWebHostBuilderFailedEventId, exception); + WriteEvent(ServiceWebHostBuilderFailedEventId, exception); } } diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/WebCommunicationListener.cs b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/WebCommunicationListener.cs index c1f4a1730..18a80a34f 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/WebCommunicationListener.cs +++ b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/WebCommunicationListener.cs @@ -3,63 +3,62 @@ // Licensed under the MIT License (MIT). See License.txt in the repo root for license information. // ------------------------------------------------------------ +using System; +using System.Fabric; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.ServiceFabric.Services.Communication.Runtime; + +using Ocelot.DependencyInjection; +using Ocelot.Middleware; + namespace OcelotApplicationApiGateway { - using System; - using System.Fabric; - using System.Fabric.Description; - using System.Globalization; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Hosting; - using Microsoft.ServiceFabric.Services.Communication.Runtime; - using Microsoft.Extensions.Logging; - using Microsoft.Extensions.Configuration; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - using Ocelot.DependencyInjection; - using Ocelot.Middleware; - public class WebCommunicationListener : ICommunicationListener { - private readonly string appRoot; - private readonly ServiceContext serviceInitializationParameters; - private string listeningAddress; - private string publishAddress; + private readonly string _appRoot; + private readonly ServiceContext _serviceInitializationParameters; + private string _listeningAddress; + private string _publishAddress; // OWIN server handle. - private IWebHost webHost; + private IWebHost _webHost; public WebCommunicationListener(string appRoot, ServiceContext serviceInitializationParameters) { - this.appRoot = appRoot; - this.serviceInitializationParameters = serviceInitializationParameters; + _appRoot = appRoot; + _serviceInitializationParameters = serviceInitializationParameters; } public Task OpenAsync(CancellationToken cancellationToken) { ServiceEventSource.Current.Message("Initialize"); - EndpointResourceDescription serviceEndpoint = this.serviceInitializationParameters.CodePackageActivationContext.GetEndpoint("WebEndpoint"); - int port = serviceEndpoint.Port; + var serviceEndpoint = _serviceInitializationParameters.CodePackageActivationContext.GetEndpoint("WebEndpoint"); + var port = serviceEndpoint.Port; - this.listeningAddress = string.Format( + _listeningAddress = string.Format( CultureInfo.InvariantCulture, "http://+:{0}/{1}", port, - string.IsNullOrWhiteSpace(this.appRoot) + string.IsNullOrWhiteSpace(_appRoot) ? string.Empty - : this.appRoot.TrimEnd('/') + '/'); + : _appRoot.TrimEnd('/') + '/'); - this.publishAddress = this.listeningAddress.Replace("+", FabricRuntime.GetNodeContext().IPAddressOrFQDN); + _publishAddress = _listeningAddress.Replace("+", FabricRuntime.GetNodeContext().IPAddressOrFQDN); - ServiceEventSource.Current.Message("Starting web server on {0}", this.listeningAddress); + ServiceEventSource.Current.Message("Starting web server on {0}", _listeningAddress); try { - this.webHost = new WebHostBuilder() + _webHost = new WebHostBuilder() .UseKestrel() - .UseUrls(this.listeningAddress) + .UseUrls(_listeningAddress) .ConfigureAppConfiguration((hostingContext, config) => { config @@ -74,33 +73,35 @@ public Task OpenAsync(CancellationToken cancellationToken) logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); logging.AddConsole(); }) - .ConfigureServices(s => { + .ConfigureServices(s => + { s.AddOcelot(); }) - .Configure(a => { - a.UseOcelot().Wait(); + .Configure(a => + { + a.UseOcelot().Wait(cancellationToken); }) .Build(); - this.webHost.Start(); + _webHost.Start(); } catch (Exception ex) { ServiceEventSource.Current.ServiceWebHostBuilderFailed(ex); } - return Task.FromResult(this.publishAddress); + return Task.FromResult(_publishAddress); } public Task CloseAsync(CancellationToken cancellationToken) { - this.StopAll(); + StopAll(); return Task.FromResult(true); } public void Abort() { - this.StopAll(); + StopAll(); } /// @@ -110,10 +111,10 @@ private void StopAll() { try { - if (this.webHost != null) + if (_webHost != null) { ServiceEventSource.Current.Message("Stopping web server."); - this.webHost.Dispose(); + _webHost.Dispose(); } } catch (ObjectDisposedException) diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ocelot.json b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ocelot.json index 8a6792438..b541e95c4 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ocelot.json +++ b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ocelot.json @@ -1,21 +1,21 @@ -{ - "ReRoutes": [ - { - "DownstreamPathTemplate": "/api/values", - "UpstreamPathTemplate": "/EquipmentInterfaces", - "UpstreamHttpMethod": [ - "Get" - ], - "DownstreamScheme": "http", - "ServiceName": "OcelotServiceApplication/OcelotApplicationService" - } - ], - "GlobalConfiguration": { - "RequestIdKey": "OcRequestId", - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 19081, - "Type": "ServiceFabric" - } - } -} +{ + "Routes": [ + { + "DownstreamPathTemplate": "/api/values", + "UpstreamPathTemplate": "/EquipmentInterfaces", + "UpstreamHttpMethod": [ + "Get" + ], + "DownstreamScheme": "http", + "ServiceName": "OcelotServiceApplication/OcelotApplicationService" + } + ], + "GlobalConfiguration": { + "RequestIdKey": "OcRequestId", + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 19081, + "Type": "ServiceFabric" + } + } +} diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/ApiGateway.cs b/samples/OcelotServiceFabric/src/OcelotApplicationService/ApiGateway.cs index 8c0908858..e7b7b5f7f 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationService/ApiGateway.cs +++ b/samples/OcelotServiceFabric/src/OcelotApplicationService/ApiGateway.cs @@ -2,15 +2,13 @@ using System.Collections.Generic; using System.Fabric; using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; + using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.ServiceFabric.Services.Communication.AspNetCore; using Microsoft.ServiceFabric.Services.Communication.Runtime; using Microsoft.ServiceFabric.Services.Runtime; -using Microsoft.Extensions.Logging; namespace OcelotApplicationService { @@ -31,7 +29,7 @@ protected override IEnumerable CreateServiceInstanceLis { return new ServiceInstanceListener[] { - new ServiceInstanceListener(serviceContext => + new(serviceContext => new KestrelCommunicationListener(serviceContext, "ServiceEndpoint", (url, listener) => { Console.WriteLine($"Starting Kestrel on {url}"); @@ -41,7 +39,7 @@ protected override IEnumerable CreateServiceInstanceLis .UseKestrel() .ConfigureServices( services => services - .AddSingleton(serviceContext)) + .AddSingleton(serviceContext)) .UseContentRoot(Directory.GetCurrentDirectory()) .UseServiceFabricIntegration(listener, ServiceFabricIntegrationOptions.UseUniqueServiceUrl) .UseStartup() diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/Controllers/ValuesController.cs b/samples/OcelotServiceFabric/src/OcelotApplicationService/Controllers/ValuesController.cs index 13e569993..b9d4958f3 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationService/Controllers/ValuesController.cs +++ b/samples/OcelotServiceFabric/src/OcelotApplicationService/Controllers/ValuesController.cs @@ -1,7 +1,5 @@ -using System; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; + using Microsoft.AspNetCore.Mvc; namespace OcelotApplicationService.Controllers @@ -13,7 +11,7 @@ public class ValuesController : Controller [HttpGet] public IEnumerable Get() { - return new string[] { "value1", "value2" }; + return new[] { "value1", "value2" }; } // GET api/values/5 @@ -25,13 +23,13 @@ public string Get(int id) // POST api/values [HttpPost] - public void Post([FromBody]string value) + public void Post([FromBody] string value) { } // PUT api/values/5 [HttpPut("{id}")] - public void Put(int id, [FromBody]string value) + public void Put(int id, [FromBody] string value) { } diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/OcelotApplicationService.csproj b/samples/OcelotServiceFabric/src/OcelotApplicationService/OcelotApplicationService.csproj index ea45d7375..f84012617 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationService/OcelotApplicationService.csproj +++ b/samples/OcelotServiceFabric/src/OcelotApplicationService/OcelotApplicationService.csproj @@ -1,21 +1,18 @@ - - - Stateless Service Application - - Exe - netcoreapp3.1 - OcelotApplicationService - OcelotApplicationService - $(PackageTargetFallback) - - - - - - - - - - - - \ No newline at end of file + + + Stateless Service Application + net7.0 + OcelotApplicationService + OcelotApplicationService + $(PackageTargetFallback) + x64 + + + + + + + + + + diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/Program.cs b/samples/OcelotServiceFabric/src/OcelotApplicationService/Program.cs index b9d7d72e4..4862426a6 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationService/Program.cs +++ b/samples/OcelotServiceFabric/src/OcelotApplicationService/Program.cs @@ -1,8 +1,8 @@ -using Microsoft.ServiceFabric.Services.Runtime; using System; using System.Diagnostics; using System.Threading; -using System.Threading.Tasks; + +using Microsoft.ServiceFabric.Services.Runtime; namespace OcelotApplicationService { @@ -23,7 +23,7 @@ private static void Main() ServiceRuntime.RegisterServiceAsync("OcelotApplicationServiceType", context => new ApiGateway(context)).GetAwaiter().GetResult(); - ServiceEventSource.Current.ServiceTypeRegistered(Process.GetCurrentProcess().Id, typeof(ApiGateway).Name); + ServiceEventSource.Current.ServiceTypeRegistered(Environment.ProcessId, nameof(ApiGateway)); // Prevents this host process from terminating so services keeps running. Thread.Sleep(Timeout.Infinite); diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/Properties/launchSettings.json b/samples/OcelotServiceFabric/src/OcelotApplicationService/Properties/launchSettings.json new file mode 100644 index 000000000..fb64c02b5 --- /dev/null +++ b/samples/OcelotServiceFabric/src/OcelotApplicationService/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "OcelotApplicationService": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:57625;http://localhost:57626" + } + } +} diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/ServiceEventSource.cs b/samples/OcelotServiceFabric/src/OcelotApplicationService/ServiceEventSource.cs index 13e2df12a..f58ddcbcc 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationService/ServiceEventSource.cs +++ b/samples/OcelotServiceFabric/src/OcelotApplicationService/ServiceEventSource.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.Diagnostics.Tracing; using System.Fabric; -using System.Linq; -using System.Text; using System.Threading.Tasks; namespace OcelotApplicationService @@ -11,7 +8,7 @@ namespace OcelotApplicationService [EventSource(Name = "MyCompany-ServiceOcelotApplication-OcelotService")] internal sealed class ServiceEventSource : EventSource { - public static readonly ServiceEventSource Current = new ServiceEventSource(); + public static readonly ServiceEventSource Current = new(); static ServiceEventSource() { @@ -46,9 +43,9 @@ public static class Keywords [NonEvent] public void Message(string message, params object[] args) { - if (this.IsEnabled()) + if (IsEnabled()) { - string finalMessage = string.Format(message, args); + var finalMessage = string.Format(message, args); Message(finalMessage); } } @@ -57,7 +54,7 @@ public void Message(string message, params object[] args) [Event(MessageEventId, Level = EventLevel.Informational, Message = "{0}")] public void Message(string message) { - if (this.IsEnabled()) + if (IsEnabled()) { WriteEvent(MessageEventId, message); } @@ -66,10 +63,10 @@ public void Message(string message) [NonEvent] public void ServiceMessage(ServiceContext serviceContext, string message, params object[] args) { - if (this.IsEnabled()) + if (IsEnabled()) { - string finalMessage = string.Format(message, args); + var finalMessage = string.Format(message, args); ServiceMessage( serviceContext.ServiceName.ToString(), serviceContext.ServiceTypeName, @@ -157,14 +154,12 @@ public void ServiceRequestStop(string requestTypeName, string exception = "") #region Private methods private static long GetReplicaOrInstanceId(ServiceContext context) { - StatelessServiceContext stateless = context as StatelessServiceContext; - if (stateless != null) + if (context is StatelessServiceContext stateless) { return stateless.InstanceId; } - StatefulServiceContext stateful = context as StatefulServiceContext; - if (stateful != null) + if (context is StatefulServiceContext stateful) { return stateful.ReplicaId; } diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationService/Startup.cs b/samples/OcelotServiceFabric/src/OcelotApplicationService/Startup.cs index 022fcb0ee..53b408e90 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationService/Startup.cs +++ b/samples/OcelotServiceFabric/src/OcelotApplicationService/Startup.cs @@ -1,13 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Hosting; namespace OcelotApplicationService { @@ -27,14 +22,12 @@ public void ConfigureServices(IServiceCollection services) } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } - - app.UseMvc(); } } } diff --git a/samples/OcelotServiceFabric/src/global.json b/samples/OcelotServiceFabric/src/global.json deleted file mode 100644 index 8288598ab..000000000 --- a/samples/OcelotServiceFabric/src/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "projects": [ "../"], - "sdk": { - "version": "2.1.4" - } -} \ No newline at end of file diff --git a/src/Ocelot.Administration/IIdentityServerConfiguration.cs b/src/Ocelot.Administration/IIdentityServerConfiguration.cs index cbfa289e1..e2260e59a 100644 --- a/src/Ocelot.Administration/IIdentityServerConfiguration.cs +++ b/src/Ocelot.Administration/IIdentityServerConfiguration.cs @@ -1,7 +1,7 @@ +using System.Collections.Generic; + namespace Ocelot.Administration { - using System.Collections.Generic; - public interface IIdentityServerConfiguration { string ApiName { get; } diff --git a/src/Ocelot.Administration/IdentityServerConfiguration.cs b/src/Ocelot.Administration/IdentityServerConfiguration.cs index a560f032d..0e44044fd 100644 --- a/src/Ocelot.Administration/IdentityServerConfiguration.cs +++ b/src/Ocelot.Administration/IdentityServerConfiguration.cs @@ -1,7 +1,7 @@ +using System.Collections.Generic; + namespace Ocelot.Administration { - using System.Collections.Generic; - public class IdentityServerConfiguration : IIdentityServerConfiguration { public IdentityServerConfiguration( diff --git a/src/Ocelot.Administration/IdentityServerMiddlewareConfigurationProvider.cs b/src/Ocelot.Administration/IdentityServerMiddlewareConfigurationProvider.cs index b4b8994a9..1b57a0064 100644 --- a/src/Ocelot.Administration/IdentityServerMiddlewareConfigurationProvider.cs +++ b/src/Ocelot.Administration/IdentityServerMiddlewareConfigurationProvider.cs @@ -1,11 +1,14 @@ -namespace Ocelot.Administration -{ - using Configuration.Repository; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - using Ocelot.Middleware; - using System.Threading.Tasks; +using System.Threading.Tasks; + +using Ocelot.Configuration.Repository; + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Middleware; + +namespace Ocelot.Administration +{ public static class IdentityServerMiddlewareConfigurationProvider { public static OcelotMiddlewareConfigurationDelegate Get = builder => @@ -27,7 +30,13 @@ public static class IdentityServerMiddlewareConfigurationProvider } app.UseAuthentication(); - app.UseMvc(); + app.UseRouting(); + app.UseAuthorization(); + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + endpoints.MapControllers(); + }); }); } diff --git a/src/Ocelot.Administration/Ocelot.Administration.csproj b/src/Ocelot.Administration/Ocelot.Administration.csproj index c1be05990..05419353e 100644 --- a/src/Ocelot.Administration/Ocelot.Administration.csproj +++ b/src/Ocelot.Administration/Ocelot.Administration.csproj @@ -1,39 +1,40 @@ - - - netcoreapp3.1 - true - Provides Ocelot extensions to use the administration API and IdentityService dependencies that come with it - Ocelot.Administration - 0.0.0-dev - Ocelot.Administration - Ocelot.Administration - API Gateway;.NET core - https://github.com/ThreeMammals/Ocelot.Administration - https://github.com/ThreeMammals/Ocelot.Administration - http://threemammals.com/images/ocelot_logo.png - win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 - false - false - True - false - Tom Pallister - ..\..\codeanalysis.ruleset - - - full - True - - - - - - - all - - - - - - - - + + + net7.0 + true + Provides Ocelot extensions to use the administration API and IdentityService dependencies that come with it + Ocelot.Administration + 0.0.0-dev + Ocelot.Administration + Ocelot.Administration + API Gateway;.NET core + https://github.com/ThreeMammals/Ocelot.Administration + https://raw.githubusercontent.com/ThreeMammals/Ocelot/develop/images/ocelot_logo.png + win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 + false + false + True + false + Tom Pallister + ..\..\codeanalysis.ruleset + True + 1591 + + + full + True + + + + + + + all + + + + + + + + diff --git a/src/Ocelot.Administration/OcelotBuilderExtensions.cs b/src/Ocelot.Administration/OcelotBuilderExtensions.cs index da25beaa0..23a93460b 100644 --- a/src/Ocelot.Administration/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Administration/OcelotBuilderExtensions.cs @@ -1,15 +1,20 @@ -using Ocelot.DependencyInjection; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Cryptography.X509Certificates; + using IdentityServer4.AccessTokenValidation; using IdentityServer4.Models; -using Microsoft.AspNetCore.Builder; + +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.IdentityModel.Tokens; + +using Ocelot.DependencyInjection; using Ocelot.Middleware; -using System; -using System.Collections.Generic; -using System.IdentityModel.Tokens.Jwt; -using System.Security.Cryptography.X509Certificates; namespace Ocelot.Administration { @@ -18,7 +23,8 @@ public static class OcelotBuilderExtensions public static IOcelotAdministrationBuilder AddAdministration(this IOcelotBuilder builder, string path, string secret) { var administrationPath = new AdministrationPath(path); - builder.Services.AddSingleton(IdentityServerMiddlewareConfigurationProvider.Get); + + builder.Services.AddSingleton(IdentityServerMiddlewareConfigurationProvider.Get); //add identity server for admin area var identityServerConfiguration = IdentityServerConfigurationCreator.GetIdentityServerConfiguration(secret); @@ -32,10 +38,10 @@ public static IOcelotAdministrationBuilder AddAdministration(this IOcelotBuilder return new OcelotAdministrationBuilder(builder.Services, builder.Configuration); } - public static IOcelotAdministrationBuilder AddAdministration(this IOcelotBuilder builder, string path, Action configureOptions) + public static IOcelotAdministrationBuilder AddAdministration(this IOcelotBuilder builder, string path, Action configureOptions) { var administrationPath = new AdministrationPath(path); - builder.Services.AddSingleton(IdentityServerMiddlewareConfigurationProvider.Get); + builder.Services.AddSingleton(IdentityServerMiddlewareConfigurationProvider.Get); if (configureOptions != null) { @@ -46,21 +52,23 @@ public static IOcelotAdministrationBuilder AddAdministration(this IOcelotBuilder return new OcelotAdministrationBuilder(builder.Services, builder.Configuration); } - private static void AddIdentityServer(Action configOptions, IOcelotBuilder builder) + private static void AddIdentityServer(Action configOptions, IOcelotBuilder builder) { builder.Services .AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) - .AddIdentityServerAuthentication(configOptions); + .AddJwtBearer("Bearer", configOptions); } private static void AddIdentityServer(IIdentityServerConfiguration identityServerConfiguration, IAdministrationPath adminPath, IOcelotBuilder builder, IConfiguration configuration) { - builder.Services.TryAddSingleton(identityServerConfiguration); + builder.Services.TryAddSingleton(identityServerConfiguration); var identityServerBuilder = builder.Services .AddIdentityServer(o => { o.IssuerUri = "Ocelot"; + o.EmitStaticAudienceClaim = true; }) + .AddInMemoryApiScopes(ApiScopes(identityServerConfiguration)) .AddInMemoryApiResources(Resources(identityServerConfiguration)) .AddInMemoryClients(Client(identityServerConfiguration)); @@ -68,14 +76,17 @@ private static void AddIdentityServer(IIdentityServerConfiguration identityServe var baseSchemeUrlAndPort = urlFinder.Find(); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - builder.Services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) - .AddIdentityServerAuthentication(o => + builder.Services + .AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) + .AddJwtBearer("Bearer", options => { - o.Authority = baseSchemeUrlAndPort + adminPath.Path; - o.ApiName = identityServerConfiguration.ApiName; - o.RequireHttpsMetadata = identityServerConfiguration.RequireHttps; - o.SupportedTokens = SupportedTokens.Both; - o.ApiSecret = identityServerConfiguration.ApiSecret; + options.Authority = baseSchemeUrlAndPort + adminPath.Path; + options.RequireHttpsMetadata = identityServerConfiguration.RequireHttps; + + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false, + }; }); //todo - refactor naming.. @@ -91,19 +102,24 @@ private static void AddIdentityServer(IIdentityServerConfiguration identityServe } } + private static IEnumerable ApiScopes(IIdentityServerConfiguration identityServerConfiguration) + { + return identityServerConfiguration.AllowedScopes.Select(s => new ApiScope(s)); + } + private static List Resources(IIdentityServerConfiguration identityServerConfiguration) { return new List { - new ApiResource(identityServerConfiguration.ApiName, identityServerConfiguration.ApiName) + new(identityServerConfiguration.ApiName, identityServerConfiguration.ApiName) { ApiSecrets = new List { - new Secret + new() { - Value = identityServerConfiguration.ApiSecret.Sha256() - } - } + Value = identityServerConfiguration.ApiSecret.Sha256(), + }, + }, }, }; } @@ -112,13 +128,13 @@ private static List Client(IIdentityServerConfiguration identityServerCo { return new List { - new Client + new() { ClientId = identityServerConfiguration.ApiName, AllowedGrantTypes = GrantTypes.ClientCredentials, - ClientSecrets = new List {new Secret(identityServerConfiguration.ApiSecret.Sha256())}, - AllowedScopes = { identityServerConfiguration.ApiName } - } + ClientSecrets = new List {new(identityServerConfiguration.ApiSecret.Sha256())}, + AllowedScopes = identityServerConfiguration.AllowedScopes, + }, }; } } diff --git a/src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj b/src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj index 4c575fe54..898dd02c2 100644 --- a/src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj +++ b/src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj @@ -1,39 +1,41 @@ - - - netcoreapp3.1 - true - Provides Ocelot extensions to use CacheManager.Net - Ocelot.Cache.CacheManager - 0.0.0-dev - Ocelot.Cache.CacheManager - Ocelot.Cache.CacheManager - API Gateway;.NET core - https://github.com/ThreeMammals/Ocelot.Cache.CacheManager - https://github.com/ThreeMammals/Ocelot.Cache.CacheManager - win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 - false - false - True - false - Tom Pallister - ..\..\codeanalysis.ruleset - - - full - True - - - - - - - all - - - - - - - - - + + + net7.0 + true + Provides Ocelot extensions to use CacheManager.Net + Ocelot.Cache.CacheManager + 0.0.0-dev + Ocelot.Cache.CacheManager + Ocelot.Cache.CacheManager + API Gateway;.NET core + https://github.com/ThreeMammals/Ocelot.Cache.CacheManager + https://raw.githubusercontent.com/ThreeMammals/Ocelot/develop/images/ocelot_logo.png + win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 + false + false + True + false + Tom Pallister + ..\..\codeanalysis.ruleset + True + 1591 + + + full + True + + + + + + + all + + + + + + + + + diff --git a/src/Ocelot.Cache.CacheManager/OcelotBuilderExtensions.cs b/src/Ocelot.Cache.CacheManager/OcelotBuilderExtensions.cs index 7a2c2d868..f9799607c 100644 --- a/src/Ocelot.Cache.CacheManager/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Cache.CacheManager/OcelotBuilderExtensions.cs @@ -1,13 +1,17 @@ -namespace Ocelot.Cache.CacheManager -{ - using Configuration; - using Configuration.File; - using DependencyInjection; - using global::CacheManager.Core; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.DependencyInjection.Extensions; - using System; +using System; + +using Ocelot.Configuration; +using Ocelot.Configuration.File; + +using Ocelot.DependencyInjection; +using global::CacheManager.Core; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Ocelot.Cache.CacheManager +{ public static class OcelotBuilderExtensions { public static IOcelotBuilder AddCacheManager(this IOcelotBuilder builder, Action settings) @@ -17,21 +21,21 @@ public static IOcelotBuilder AddCacheManager(this IOcelotBuilder builder, Action builder.Services.RemoveAll(typeof(ICacheManager)); builder.Services.RemoveAll(typeof(IOcelotCache)); - builder.Services.AddSingleton>(cacheManagerOutputCache); + builder.Services.AddSingleton(cacheManagerOutputCache); builder.Services.AddSingleton>(ocelotOutputCacheManager); var ocelotConfigCacheManagerOutputCache = CacheFactory.Build("OcelotConfigurationCache", settings); var ocelotConfigCacheManager = new OcelotCacheManagerCache(ocelotConfigCacheManagerOutputCache); builder.Services.RemoveAll(typeof(ICacheManager)); builder.Services.RemoveAll(typeof(IOcelotCache)); - builder.Services.AddSingleton>(ocelotConfigCacheManagerOutputCache); + builder.Services.AddSingleton(ocelotConfigCacheManagerOutputCache); builder.Services.AddSingleton>(ocelotConfigCacheManager); var fileConfigCacheManagerOutputCache = CacheFactory.Build("FileConfigurationCache", settings); var fileConfigCacheManager = new OcelotCacheManagerCache(fileConfigCacheManagerOutputCache); builder.Services.RemoveAll(typeof(ICacheManager)); builder.Services.RemoveAll(typeof(IOcelotCache)); - builder.Services.AddSingleton>(fileConfigCacheManagerOutputCache); + builder.Services.AddSingleton(fileConfigCacheManagerOutputCache); builder.Services.AddSingleton>(fileConfigCacheManager); builder.Services.RemoveAll(typeof(ICacheKeyGenerator)); diff --git a/src/Ocelot.Cache.CacheManager/OcelotCacheManagerCache.cs b/src/Ocelot.Cache.CacheManager/OcelotCacheManagerCache.cs index a95df4c4f..b4b31aa65 100644 --- a/src/Ocelot.Cache.CacheManager/OcelotCacheManagerCache.cs +++ b/src/Ocelot.Cache.CacheManager/OcelotCacheManagerCache.cs @@ -1,8 +1,9 @@ -namespace Ocelot.Cache.CacheManager -{ - using global::CacheManager.Core; - using System; +using System; + +using global::CacheManager.Core; +namespace Ocelot.Cache.CacheManager +{ public class OcelotCacheManagerCache : IOcelotCache { private readonly ICacheManager _cacheManager; diff --git a/src/Ocelot.Provider.Consul/Consul.cs b/src/Ocelot.Provider.Consul/Consul.cs index 72f15d095..2e73f5271 100644 --- a/src/Ocelot.Provider.Consul/Consul.cs +++ b/src/Ocelot.Provider.Consul/Consul.cs @@ -1,15 +1,20 @@ -namespace Ocelot.Provider.Consul -{ - using global::Consul; - using Infrastructure.Extensions; - using Logging; - using ServiceDiscovery.Providers; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - using Values; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using global::Consul; + +using Ocelot.Infrastructure.Extensions; + +using Ocelot.Logging; +using Ocelot.ServiceDiscovery.Providers; + +using Ocelot.Values; + +namespace Ocelot.Provider.Consul +{ public class Consul : IServiceDiscoveryProvider { private readonly ConsulRegistryConfiguration _config; @@ -54,7 +59,7 @@ public async Task> Get() return services.ToList(); } - private Service BuildService(ServiceEntry serviceEntry, Node serviceNode) + private static Service BuildService(ServiceEntry serviceEntry, Node serviceNode) { return new Service( serviceEntry.Service.Service, @@ -64,7 +69,7 @@ private Service BuildService(ServiceEntry serviceEntry, Node serviceNode) serviceEntry.Service.Tags ?? Enumerable.Empty()); } - private bool IsValid(ServiceEntry serviceEntry) + private static bool IsValid(ServiceEntry serviceEntry) { if (string.IsNullOrEmpty(serviceEntry.Service.Address) || serviceEntry.Service.Address.Contains("http://") || serviceEntry.Service.Address.Contains("https://") || serviceEntry.Service.Port <= 0) { @@ -74,7 +79,7 @@ private bool IsValid(ServiceEntry serviceEntry) return true; } - private string GetVersionFromStrings(IEnumerable strings) + private static string GetVersionFromStrings(IEnumerable strings) { return strings ?.FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal)) diff --git a/src/Ocelot.Provider.Consul/ConsulClientFactory.cs b/src/Ocelot.Provider.Consul/ConsulClientFactory.cs index f3f0e54f6..c8142026f 100644 --- a/src/Ocelot.Provider.Consul/ConsulClientFactory.cs +++ b/src/Ocelot.Provider.Consul/ConsulClientFactory.cs @@ -1,15 +1,16 @@ -namespace Ocelot.Provider.Consul -{ - using global::Consul; - using System; +using System; + +using global::Consul; +namespace Ocelot.Provider.Consul +{ public class ConsulClientFactory : IConsulClientFactory { public IConsulClient Get(ConsulRegistryConfiguration config) { return new ConsulClient(c => { - c.Address = new Uri($"http://{config.Host}:{config.Port}"); + c.Address = new Uri($"{config.Scheme}://{config.Host}:{config.Port}"); if (!string.IsNullOrEmpty(config?.Token)) { diff --git a/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs b/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs index 1044b7b62..e67fd83eb 100644 --- a/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs +++ b/src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs @@ -1,16 +1,22 @@ -namespace Ocelot.Provider.Consul -{ - using Configuration.File; - using Configuration.Repository; - using global::Consul; - using Logging; - using Microsoft.Extensions.Options; - using Newtonsoft.Json; - using Responses; - using System; - using System.Text; - using System.Threading.Tasks; +using System; +using System.Text; +using System.Threading.Tasks; + +using Ocelot.Configuration.File; +using Ocelot.Configuration.Repository; + +using global::Consul; + +using Ocelot.Logging; +using Microsoft.Extensions.Options; + +using Newtonsoft.Json; + +using Ocelot.Responses; + +namespace Ocelot.Provider.Consul +{ public class ConsulFileConfigurationRepository : IFileConfigurationRepository { private readonly IConsulClient _consul; @@ -31,7 +37,7 @@ public ConsulFileConfigurationRepository( _configurationKey = string.IsNullOrWhiteSpace(serviceDiscoveryProvider.ConfigurationKey) ? "InternalConfiguration" : serviceDiscoveryProvider.ConfigurationKey; - var config = new ConsulRegistryConfiguration(serviceDiscoveryProvider.Host, + var config = new ConsulRegistryConfiguration(serviceDiscoveryProvider.Scheme, serviceDiscoveryProvider.Host, serviceDiscoveryProvider.Port, _configurationKey, serviceDiscoveryProvider.Token); _consul = factory.Get(config); @@ -70,7 +76,7 @@ public async Task Set(FileConfiguration ocelotConfiguration) var kvPair = new KVPair(_configurationKey) { - Value = bytes + Value = bytes, }; var result = await _consul.KV.Put(kvPair); diff --git a/src/Ocelot.Provider.Consul/ConsulMiddlewareConfigurationProvider.cs b/src/Ocelot.Provider.Consul/ConsulMiddlewareConfigurationProvider.cs index 1b89b4f33..4be20d55d 100644 --- a/src/Ocelot.Provider.Consul/ConsulMiddlewareConfigurationProvider.cs +++ b/src/Ocelot.Provider.Consul/ConsulMiddlewareConfigurationProvider.cs @@ -1,17 +1,21 @@ -namespace Ocelot.Provider.Consul -{ - using Configuration.Creator; - using Configuration.File; - using Configuration.Repository; - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Options; - using Middleware; - using Responses; - using System; - using System.Linq; - using System.Threading.Tasks; +using System; +using System.Linq; +using System.Threading.Tasks; + +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Repository; + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Ocelot.Middleware; + +using Ocelot.Responses; + +namespace Ocelot.Provider.Consul +{ public static class ConsulMiddlewareConfigurationProvider { public static OcelotMiddlewareConfigurationDelegate Get = async builder => @@ -77,7 +81,7 @@ private static async Task SetFileConfigInConsul(IApplicationBuilder builder, private static void ThrowToStopOcelotStarting(Response config) { - throw new Exception($"Unable to start Ocelot, errors are: {string.Join(",", config.Errors.Select(x => x.ToString()))}"); + throw new Exception($"Unable to start Ocelot, errors are: {string.Join(',', config.Errors.Select(x => x.ToString()))}"); } private static bool IsError(Response response) diff --git a/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs b/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs index 8cffa8825..9392f461b 100644 --- a/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs +++ b/src/Ocelot.Provider.Consul/ConsulProviderFactory.cs @@ -1,27 +1,29 @@ -namespace Ocelot.Provider.Consul -{ - using Logging; - using Microsoft.Extensions.DependencyInjection; - using ServiceDiscovery; +using Ocelot.Logging; - public static class ConsulProviderFactory - { - public static ServiceDiscoveryFinderDelegate Get = (provider, config, reRoute) => - { - var factory = provider.GetService(); +using Microsoft.Extensions.DependencyInjection; - var consulFactory = provider.GetService(); - - var consulRegistryConfiguration = new ConsulRegistryConfiguration(config.Host, config.Port, reRoute.ServiceName, config.Token); - - var consulServiceDiscoveryProvider = new Consul(consulRegistryConfiguration, factory, consulFactory); - - if (config.Type?.ToLower() == "pollconsul") - { - return new PollConsul(config.PollingInterval, factory, consulServiceDiscoveryProvider); - } - - return consulServiceDiscoveryProvider; - }; - } -} +using Ocelot.ServiceDiscovery; + +namespace Ocelot.Provider.Consul +{ + public static class ConsulProviderFactory + { + public static ServiceDiscoveryFinderDelegate Get = (provider, config, route) => + { + var factory = provider.GetService(); + + var consulFactory = provider.GetService(); + + var consulRegistryConfiguration = new ConsulRegistryConfiguration(config.Scheme, config.Host, config.Port, route.ServiceName, config.Token); + + var consulServiceDiscoveryProvider = new Consul(consulRegistryConfiguration, factory, consulFactory); + + if (config.Type?.ToLower() == "pollconsul") + { + return new PollConsul(config.PollingInterval, factory, consulServiceDiscoveryProvider); + } + + return consulServiceDiscoveryProvider; + }; + } +} diff --git a/src/Ocelot.Provider.Consul/ConsulRegistryConfiguration.cs b/src/Ocelot.Provider.Consul/ConsulRegistryConfiguration.cs index a84088b01..38bf515fa 100644 --- a/src/Ocelot.Provider.Consul/ConsulRegistryConfiguration.cs +++ b/src/Ocelot.Provider.Consul/ConsulRegistryConfiguration.cs @@ -2,15 +2,17 @@ { public class ConsulRegistryConfiguration { - public ConsulRegistryConfiguration(string host, int port, string keyOfServiceInConsul, string token) + public ConsulRegistryConfiguration(string scheme, string host, int port, string keyOfServiceInConsul, string token) { Host = string.IsNullOrEmpty(host) ? "localhost" : host; Port = port > 0 ? port : 8500; + Scheme = string.IsNullOrEmpty(scheme) ? "http" : scheme; KeyOfServiceInConsul = keyOfServiceInConsul; Token = token; } public string KeyOfServiceInConsul { get; } + public string Scheme { get; } public string Host { get; } public int Port { get; } public string Token { get; } diff --git a/src/Ocelot.Provider.Consul/IConsulClientFactory.cs b/src/Ocelot.Provider.Consul/IConsulClientFactory.cs index 3710a8181..f9430dec7 100644 --- a/src/Ocelot.Provider.Consul/IConsulClientFactory.cs +++ b/src/Ocelot.Provider.Consul/IConsulClientFactory.cs @@ -1,7 +1,7 @@ -namespace Ocelot.Provider.Consul -{ - using global::Consul; +using global::Consul; +namespace Ocelot.Provider.Consul +{ public interface IConsulClientFactory { IConsulClient Get(ConsulRegistryConfiguration config); diff --git a/src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj b/src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj index ece85c9b3..fc725e207 100644 --- a/src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj +++ b/src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj @@ -1,38 +1,39 @@ - - - netcoreapp3.1 - true - Provides Ocelot extensions to use Consul - Ocelot.Provider.Consul - 0.0.0-dev - Ocelot.Provider.Consul - Ocelot.Provider.Consul - API Gateway;.NET core - https://github.com/ThreeMammals/Ocelot.Provider.Consul - https://github.com/ThreeMammals/Ocelot.Provider.Consul - http://threemammals.com/images/ocelot_logo.png - win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 - false - false - True - false - Tom Pallister - ..\..\codeanalysis.ruleset - - - full - True - - - - - - - - all - - - - - - + + + net7.0 + true + Provides Ocelot extensions to use Consul + Ocelot.Provider.Consul + 0.0.0-dev + Ocelot.Provider.Consul + Ocelot.Provider.Consul + API Gateway;.NET core + https://github.com/ThreeMammals/Ocelot.Provider.Consul + https://raw.githubusercontent.com/ThreeMammals/Ocelot/develop/images/ocelot_logo.png + win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 + false + false + True + false + Tom Pallister + ..\..\codeanalysis.ruleset + True + 1591 + + + full + True + + + + + + + + all + + + + + + diff --git a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs index ce2ed95bb..40548178c 100644 --- a/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs @@ -1,17 +1,17 @@ -namespace Ocelot.Provider.Consul -{ - using Configuration.Repository; - using DependencyInjection; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.DependencyInjection.Extensions; - using Middleware; - using ServiceDiscovery; +using Ocelot.Configuration.Repository; + +using Ocelot.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Ocelot.Provider.Consul +{ public static class OcelotBuilderExtensions { public static IOcelotBuilder AddConsul(this IOcelotBuilder builder) { - builder.Services.AddSingleton(ConsulProviderFactory.Get); + builder.Services.AddSingleton(ConsulProviderFactory.Get); builder.Services.AddSingleton(); builder.Services.RemoveAll(typeof(IFileConfigurationPollerOptions)); builder.Services.AddSingleton(); @@ -20,7 +20,7 @@ public static IOcelotBuilder AddConsul(this IOcelotBuilder builder) public static IOcelotBuilder AddConfigStoredInConsul(this IOcelotBuilder builder) { - builder.Services.AddSingleton(ConsulMiddlewareConfigurationProvider.Get); + builder.Services.AddSingleton(ConsulMiddlewareConfigurationProvider.Get); builder.Services.AddHostedService(); builder.Services.AddSingleton(); return builder; diff --git a/src/Ocelot.Provider.Consul/PollConsul.cs b/src/Ocelot.Provider.Consul/PollConsul.cs new file mode 100644 index 000000000..b5ac65d86 --- /dev/null +++ b/src/Ocelot.Provider.Consul/PollConsul.cs @@ -0,0 +1,53 @@ +using Ocelot.Logging; +using Ocelot.ServiceDiscovery.Providers; +using Ocelot.Values; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Ocelot.Provider.Consul; + +public sealed class PollConsul : IServiceDiscoveryProvider, IDisposable +{ + private readonly IOcelotLogger _logger; + private readonly IServiceDiscoveryProvider _consulServiceDiscoveryProvider; + private Timer _timer; + private bool _polling; + private List _services; + + public PollConsul(int pollingInterval, IOcelotLoggerFactory factory, IServiceDiscoveryProvider consulServiceDiscoveryProvider) + { + _logger = factory.CreateLogger(); + _consulServiceDiscoveryProvider = consulServiceDiscoveryProvider; + _services = new List(); + + _timer = new Timer(async x => + { + if (_polling) + { + return; + } + + _polling = true; + await Poll(); + _polling = false; + }, null, pollingInterval, pollingInterval); + } + + public void Dispose() + { + _timer?.Dispose(); + _timer = null; + } + + public Task> Get() + { + return Task.FromResult(_services); + } + + private async Task Poll() + { + _services = await _consulServiceDiscoveryProvider.Get(); + } +} diff --git a/src/Ocelot.Provider.Consul/PollingConsulServiceDiscoveryProvider.cs b/src/Ocelot.Provider.Consul/PollingConsulServiceDiscoveryProvider.cs deleted file mode 100644 index 5bace8021..000000000 --- a/src/Ocelot.Provider.Consul/PollingConsulServiceDiscoveryProvider.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace Ocelot.Provider.Consul -{ - using Logging; - using ServiceDiscovery.Providers; - using System; - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; - using Values; - - public sealed class PollConsul : IServiceDiscoveryProvider, IDisposable - { - private readonly IOcelotLogger _logger; - private readonly IServiceDiscoveryProvider _consulServiceDiscoveryProvider; - private Timer _timer; - private bool _polling; - private List _services; - - public PollConsul(int pollingInterval, IOcelotLoggerFactory factory, IServiceDiscoveryProvider consulServiceDiscoveryProvider) - { - _logger = factory.CreateLogger(); - _consulServiceDiscoveryProvider = consulServiceDiscoveryProvider; - _services = new List(); - - _timer = new Timer(async x => - { - if (_polling) - { - return; - } - - _polling = true; - await Poll(); - _polling = false; - }, null, pollingInterval, pollingInterval); - } - - public void Dispose() - { - _timer?.Dispose(); - _timer = null; - } - - public Task> Get() - { - return Task.FromResult(_services); - } - - private async Task Poll() - { - _services = await _consulServiceDiscoveryProvider.Get(); - } - } -} diff --git a/src/Ocelot.Provider.Consul/UnableToSetConfigInConsulError.cs b/src/Ocelot.Provider.Consul/UnableToSetConfigInConsulError.cs index bc3103ace..abf763735 100644 --- a/src/Ocelot.Provider.Consul/UnableToSetConfigInConsulError.cs +++ b/src/Ocelot.Provider.Consul/UnableToSetConfigInConsulError.cs @@ -1,11 +1,11 @@ -namespace Ocelot.Provider.Consul -{ - using Errors; +using Ocelot.Errors; +namespace Ocelot.Provider.Consul +{ public class UnableToSetConfigInConsulError : Error { public UnableToSetConfigInConsulError(string s) - : base(s, OcelotErrorCode.UnknownError) + : base(s, OcelotErrorCode.UnknownError, 404) { } } diff --git a/src/Ocelot.Provider.Eureka/Eureka.cs b/src/Ocelot.Provider.Eureka/Eureka.cs index 8a064549e..b98fc9d04 100644 --- a/src/Ocelot.Provider.Eureka/Eureka.cs +++ b/src/Ocelot.Provider.Eureka/Eureka.cs @@ -1,35 +1,38 @@ -namespace Ocelot.Provider.Eureka -{ - using ServiceDiscovery.Providers; - using Steeltoe.Common.Discovery; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - using Values; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; - public class Eureka : IServiceDiscoveryProvider - { - private readonly IDiscoveryClient _client; - private readonly string _serviceName; +using Ocelot.ServiceDiscovery.Providers; - public Eureka(string serviceName, IDiscoveryClient client) - { - _client = client; - _serviceName = serviceName; - } +using Steeltoe.Discovery; - public Task> Get() - { - var services = new List(); - - var instances = _client.GetInstances(_serviceName); - - if (instances != null && instances.Any()) - { - services.AddRange(instances.Select(i => new Service(i.ServiceId, new ServiceHostAndPort(i.Host, i.Port, i.Uri.Scheme), "", "", new List()))); - } - - return Task.FromResult(services); - } - } -} +using Ocelot.Values; + +namespace Ocelot.Provider.Eureka +{ + public class Eureka : IServiceDiscoveryProvider + { + private readonly IDiscoveryClient _client; + private readonly string _serviceName; + + public Eureka(string serviceName, IDiscoveryClient client) + { + _client = client; + _serviceName = serviceName; + } + + public Task> Get() + { + var services = new List(); + + var instances = _client.GetInstances(_serviceName); + + if (instances != null && instances.Any()) + { + services.AddRange(instances.Select(i => new Service(i.ServiceId, new ServiceHostAndPort(i.Host, i.Port, i.Uri.Scheme), string.Empty, string.Empty, new List()))); + } + + return Task.FromResult(services); + } + } +} diff --git a/src/Ocelot.Provider.Eureka/EurekaMiddlewareConfigurationProvider.cs b/src/Ocelot.Provider.Eureka/EurekaMiddlewareConfigurationProvider.cs index f893ed722..5aec5708b 100644 --- a/src/Ocelot.Provider.Eureka/EurekaMiddlewareConfigurationProvider.cs +++ b/src/Ocelot.Provider.Eureka/EurekaMiddlewareConfigurationProvider.cs @@ -1,15 +1,19 @@ -namespace Ocelot.Provider.Eureka -{ - using Configuration; - using Configuration.Repository; - using Microsoft.Extensions.DependencyInjection; - using Middleware; - using Steeltoe.Discovery.Client; - using System.Threading.Tasks; +using System.Threading.Tasks; + +using Ocelot.Configuration; +using Ocelot.Configuration.Repository; + +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Middleware; + +using Steeltoe.Discovery.Client; + +namespace Ocelot.Provider.Eureka +{ public class EurekaMiddlewareConfigurationProvider { - public static OcelotMiddlewareConfigurationDelegate Get = builder => + public static OcelotMiddlewareConfigurationDelegate Get { get; } = builder => { var internalConfigRepo = builder.ApplicationServices.GetService(); @@ -17,7 +21,7 @@ public class EurekaMiddlewareConfigurationProvider if (UsingEurekaServiceDiscoveryProvider(config.Data)) { - builder.UseDiscoveryClient(); + //builder.UseDiscoveryClient(); } return Task.CompletedTask; diff --git a/src/Ocelot.Provider.Eureka/EurekaProviderFactory.cs b/src/Ocelot.Provider.Eureka/EurekaProviderFactory.cs index d9bba1791..a1d4f522a 100644 --- a/src/Ocelot.Provider.Eureka/EurekaProviderFactory.cs +++ b/src/Ocelot.Provider.Eureka/EurekaProviderFactory.cs @@ -1,18 +1,20 @@ -namespace Ocelot.Provider.Eureka -{ - using Microsoft.Extensions.DependencyInjection; - using ServiceDiscovery; - using Steeltoe.Common.Discovery; +using Microsoft.Extensions.DependencyInjection; + +using Ocelot.ServiceDiscovery; +using Steeltoe.Discovery; + +namespace Ocelot.Provider.Eureka +{ public static class EurekaProviderFactory { - public static ServiceDiscoveryFinderDelegate Get = (provider, config, reRoute) => + public static ServiceDiscoveryFinderDelegate Get = (provider, config, route) => { var client = provider.GetService(); if (config.Type?.ToLower() == "eureka" && client != null) { - return new Eureka(reRoute.ServiceName, client); + return new Eureka(route.ServiceName, client); } return null; diff --git a/src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj b/src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj index 587071e03..f9543b810 100644 --- a/src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj +++ b/src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1 + net7.0 true Provides Ocelot extensions to use Eureka Ocelot.Provider.Eureka @@ -10,7 +10,7 @@ API Gateway;.NET core https://github.com/ThreeMammals/Ocelot.Provider.Eureka https://github.com/ThreeMammals/Ocelot.Provider.Eureka - http://threemammals.com/images/ocelot_logo.png + https://raw.githubusercontent.com/ThreeMammals/Ocelot/develop/images/ocelot_logo.png win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 false false @@ -18,6 +18,8 @@ false Tom Pallister ..\..\codeanalysis.ruleset + True + 1591 full @@ -27,8 +29,9 @@ - - + + + all diff --git a/src/Ocelot.Provider.Eureka/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Eureka/OcelotBuilderExtensions.cs index e141d1947..0f3d42f61 100644 --- a/src/Ocelot.Provider.Eureka/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Eureka/OcelotBuilderExtensions.cs @@ -1,20 +1,18 @@ -namespace Ocelot.Provider.Eureka -{ - using DependencyInjection; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Middleware; - using ServiceDiscovery; - using Steeltoe.Discovery.Client; - using System.Linq; +using Ocelot.DependencyInjection; + +using Microsoft.Extensions.DependencyInjection; +using Steeltoe.Discovery.Client; + +namespace Ocelot.Provider.Eureka +{ public static class OcelotBuilderExtensions { public static IOcelotBuilder AddEureka(this IOcelotBuilder builder) { builder.Services.AddDiscoveryClient(builder.Configuration); - builder.Services.AddSingleton(EurekaProviderFactory.Get); - builder.Services.AddSingleton(EurekaMiddlewareConfigurationProvider.Get); + builder.Services.AddSingleton(EurekaProviderFactory.Get); + builder.Services.AddSingleton(EurekaMiddlewareConfigurationProvider.Get); return builder; } } diff --git a/src/Ocelot.Provider.Kubernetes/KubeApiClientExtensions/EndPointClientV1.cs b/src/Ocelot.Provider.Kubernetes/KubeApiClientExtensions/EndPointClientV1.cs new file mode 100644 index 000000000..cb6bd2aab --- /dev/null +++ b/src/Ocelot.Provider.Kubernetes/KubeApiClientExtensions/EndPointClientV1.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using HTTPlease; + +using KubeClient; +using KubeClient.Models; +using KubeClient.ResourceClients; + +namespace Ocelot.Provider.Kubernetes.KubeApiClientExtensions +{ + public class EndPointClientV1 : KubeResourceClient + { + private readonly HttpRequest _collection = KubeRequest.Create("api/v1/namespaces/{Namespace}/endpoints/{ServiceName}"); + + public EndPointClientV1(IKubeApiClient client) : base(client) + { + } + + public async Task Get(string serviceName, string kubeNamespace = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(serviceName)) + { + throw new ArgumentNullException(nameof(serviceName)); + } + + var response = await Http.GetAsync( + _collection.WithTemplateParameters(new + { + Namespace = kubeNamespace ?? KubeClient.DefaultNamespace, + ServiceName = serviceName, + }), + cancellationToken + ); + + if (response.IsSuccessStatusCode) + { + return await response.ReadContentAsAsync(); + } + + return null; + } + } +} diff --git a/src/Ocelot.Provider.Kubernetes/KubeProvider.cs b/src/Ocelot.Provider.Kubernetes/KubeProvider.cs deleted file mode 100644 index 22f5b1153..000000000 --- a/src/Ocelot.Provider.Kubernetes/KubeProvider.cs +++ /dev/null @@ -1,62 +0,0 @@ -using KubeClient; -using KubeClient.Models; -using Ocelot.Logging; -using Ocelot.ServiceDiscovery.Providers; -using Ocelot.Values; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Ocelot.Provider.Kubernetes -{ - public class Kube : IServiceDiscoveryProvider - { - private KubeRegistryConfiguration kubeRegistryConfiguration; - private IOcelotLogger logger; - private IKubeApiClient kubeApi; - - public Kube(KubeRegistryConfiguration kubeRegistryConfiguration, IOcelotLoggerFactory factory, IKubeApiClient kubeApi) - { - this.kubeRegistryConfiguration = kubeRegistryConfiguration; - this.logger = factory.CreateLogger(); - this.kubeApi = kubeApi; - } - - - public async Task> Get() - { - var service = await kubeApi.ServicesV1().Get(kubeRegistryConfiguration.KeyOfServiceInK8s, kubeRegistryConfiguration.KubeNamespace); - var services = new List(); - if (IsValid(service)) - { - services.Add(BuildService(service)); - } - 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"); - } - return services; - } - - private bool IsValid(ServiceV1 service) - { - if (string.IsNullOrEmpty(service.Spec.ClusterIP) || service.Spec.Ports.Count <= 0) - { - return false; - } - - return true; - } - - private Service BuildService(ServiceV1 serviceEntry) - { - var servicePort = serviceEntry.Spec.Ports.FirstOrDefault(); - return new Service( - serviceEntry.Metadata.Name, - new ServiceHostAndPort(serviceEntry.Spec.ClusterIP, servicePort.Port), - serviceEntry.Metadata.Uid, - string.Empty, - Enumerable.Empty()); - } - } -} diff --git a/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs b/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs index f12d0ebc0..0cfc2a23e 100644 --- a/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs +++ b/src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs @@ -1,35 +1,41 @@ -using KubeClient; -using Microsoft.Extensions.DependencyInjection; -using Ocelot.Logging; -using Ocelot.ServiceDiscovery; -using System; -using Ocelot.Configuration; +using System; -namespace Ocelot.Provider.Kubernetes -{ - public static class KubernetesProviderFactory - { - public static ServiceDiscoveryFinderDelegate Get = (provider, config, reRoute) => - { - var factory = provider.GetService(); - return GetkubeProvider(provider, config, reRoute, factory); - }; +using KubeClient; - private static ServiceDiscovery.Providers.IServiceDiscoveryProvider GetkubeProvider(IServiceProvider provider, Configuration.ServiceProviderConfiguration config, DownstreamReRoute reRoute, IOcelotLoggerFactory factory) - { - var kubeClient = provider.GetService(); - var k8sRegistryConfiguration = new KubeRegistryConfiguration() - { - KeyOfServiceInK8s = reRoute.ServiceName, - KubeNamespace = string.IsNullOrEmpty(reRoute.ServiceNamespace) ? config.Namespace : reRoute.ServiceNamespace - }; +using Microsoft.Extensions.DependencyInjection; - var k8sServiceDiscoveryProvider = new Kube(k8sRegistryConfiguration, factory, kubeClient); - if (config.Type?.ToLower() == "pollkube") - { - return new PollKube(config.PollingInterval, factory, k8sServiceDiscoveryProvider); - } - return k8sServiceDiscoveryProvider; - } - } -} +using Ocelot.Configuration; +using Ocelot.Logging; +using Ocelot.ServiceDiscovery; + +namespace Ocelot.Provider.Kubernetes +{ + public static class KubernetesProviderFactory + { + public static ServiceDiscoveryFinderDelegate Get = (provider, config, route) => + { + var factory = provider.GetService(); + return GetKubeProvider(provider, config, route, factory); + }; + + private static ServiceDiscovery.Providers.IServiceDiscoveryProvider GetKubeProvider(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route, IOcelotLoggerFactory factory) + { + var kubeClient = provider.GetService(); + + var k8sRegistryConfiguration = new KubeRegistryConfiguration + { + KeyOfServiceInK8s = route.ServiceName, + KubeNamespace = string.IsNullOrEmpty(route.ServiceNamespace) ? config.Namespace : route.ServiceNamespace, + }; + + var k8sServiceDiscoveryProvider = new KubernetesServiceDiscoveryProvider(k8sRegistryConfiguration, factory, kubeClient); + + if (config.Type?.ToLower() == "pollkube") + { + return new PollKubernetes(config.PollingInterval, factory, k8sServiceDiscoveryProvider); + } + + return k8sServiceDiscoveryProvider; + } + } +} diff --git a/src/Ocelot.Provider.Kubernetes/KubernetesServiceDiscoveryProvider.cs b/src/Ocelot.Provider.Kubernetes/KubernetesServiceDiscoveryProvider.cs new file mode 100644 index 000000000..dabf6dae4 --- /dev/null +++ b/src/Ocelot.Provider.Kubernetes/KubernetesServiceDiscoveryProvider.cs @@ -0,0 +1,58 @@ +using KubeClient; +using KubeClient.Models; +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes.KubeApiClientExtensions; +using Ocelot.ServiceDiscovery.Providers; +using Ocelot.Values; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.Provider.Kubernetes; + +public class KubernetesServiceDiscoveryProvider : IServiceDiscoveryProvider +{ + private readonly KubeRegistryConfiguration _kubeRegistryConfiguration; + private readonly IOcelotLogger _logger; + private readonly IKubeApiClient _kubeApi; + + public KubernetesServiceDiscoveryProvider(KubeRegistryConfiguration kubeRegistryConfiguration, IOcelotLoggerFactory factory, IKubeApiClient kubeApi) + { + _kubeRegistryConfiguration = kubeRegistryConfiguration; + _logger = factory.CreateLogger(); + _kubeApi = kubeApi; + } + + public async Task> Get() + { + var endpoint = await _kubeApi + .ResourceClient(client => new EndPointClientV1(client)) + .Get(_kubeRegistryConfiguration.KeyOfServiceInK8s, _kubeRegistryConfiguration.KubeNamespace); + + var services = new List(); + if (endpoint != null && endpoint.Subsets.Any()) + { + services.AddRange(BuildServices(endpoint)); + } + 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"); + } + + return services; + } + + private static List BuildServices(EndpointsV1 endpoint) + { + var services = new List(); + + foreach (var subset in endpoint.Subsets) + { + services.AddRange(subset.Addresses.Select(address => new Service(endpoint.Metadata.Name, + new ServiceHostAndPort(address.Ip, subset.Ports.First().Port), + endpoint.Metadata.Uid, string.Empty, Enumerable.Empty()))); + } + + return services; + } +} diff --git a/src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj b/src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj index 980cd909a..a9ac561a6 100644 --- a/src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj +++ b/src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj @@ -1,43 +1,48 @@ - - - - netcoreapp3.1 - true - Ocelot - Provides Ocelot extensions to use kubernetes - https://github.com/ThreeMammals/Ocelot - http://threemammals.com/images/ocelot_logo.png - - Ocelot.Provider.Kubernetes - Ocelot.Provider.Kubernetes - API Gateway;.NET core - win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 - false - false - true - false - 0.0.0-dev - geffzhang - - ..\..\codeanalysis.ruleset - - - - - - - - - - - - - - - - - - - - - + + + + net7.0 + true + Ocelot + Provides Ocelot extensions to use kubernetes + https://github.com/ThreeMammals/Ocelot + https://raw.githubusercontent.com/ThreeMammals/Ocelot/develop/images/ocelot_logo.png + + Ocelot.Provider.Kubernetes + Ocelot.Provider.Kubernetes + API Gateway;.NET core + win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 + false + false + true + false + 0.0.0-dev + geffzhang + + ..\..\codeanalysis.ruleset + True + 1591 + + + + + + + + + + + + all + + + + + + + + + + + + diff --git a/src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs index f979586a5..958b4929e 100644 --- a/src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs @@ -1,5 +1,7 @@ using KubeClient; + using Microsoft.Extensions.DependencyInjection; + using Ocelot.DependencyInjection; namespace Ocelot.Provider.Kubernetes diff --git a/src/Ocelot.Provider.Kubernetes/PollKube.cs b/src/Ocelot.Provider.Kubernetes/PollKubernetes.cs similarity index 77% rename from src/Ocelot.Provider.Kubernetes/PollKube.cs rename to src/Ocelot.Provider.Kubernetes/PollKubernetes.cs index 31d42a56b..cd0e854f4 100644 --- a/src/Ocelot.Provider.Kubernetes/PollKube.cs +++ b/src/Ocelot.Provider.Kubernetes/PollKubernetes.cs @@ -1,13 +1,14 @@ -using Ocelot.Logging; -using Ocelot.ServiceDiscovery.Providers; -using Ocelot.Values; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Ocelot.Logging; +using Ocelot.ServiceDiscovery.Providers; +using Ocelot.Values; + namespace Ocelot.Provider.Kubernetes { - public class PollKube : IServiceDiscoveryProvider + public class PollKubernetes : IServiceDiscoveryProvider { private readonly IOcelotLogger _logger; private readonly IServiceDiscoveryProvider _kubeServiceDiscoveryProvider; @@ -15,9 +16,9 @@ public class PollKube : IServiceDiscoveryProvider private bool _polling; private List _services; - public PollKube(int pollingInterval, IOcelotLoggerFactory factory, IServiceDiscoveryProvider kubeServiceDiscoveryProvider) + public PollKubernetes(int pollingInterval, IOcelotLoggerFactory factory, IServiceDiscoveryProvider kubeServiceDiscoveryProvider) { - _logger = factory.CreateLogger(); + _logger = factory.CreateLogger(); _kubeServiceDiscoveryProvider = kubeServiceDiscoveryProvider; _services = new List(); diff --git a/src/Ocelot.Provider.Polly/CircuitBreaker.cs b/src/Ocelot.Provider.Polly/CircuitBreaker.cs index 8d4c6d4c1..4c9a1ea67 100644 --- a/src/Ocelot.Provider.Polly/CircuitBreaker.cs +++ b/src/Ocelot.Provider.Polly/CircuitBreaker.cs @@ -1,21 +1,22 @@ -using Polly; using System.Collections.Generic; using System.Linq; +using Polly; + namespace Ocelot.Provider.Polly { public class CircuitBreaker { - private readonly List _policies = new List(); + private readonly List _policies = new(); public CircuitBreaker(params IAsyncPolicy[] policies) { foreach (var policy in policies.Where(p => p != null)) { - this._policies.Add(policy); + _policies.Add(policy); } } - public IAsyncPolicy[] Policies => this._policies.ToArray(); + public IAsyncPolicy[] Policies => _policies.ToArray(); } } diff --git a/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj b/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj index 447db1da4..c9ef62be3 100644 --- a/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj +++ b/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj @@ -1,38 +1,40 @@ - - - netcoreapp3.1 - true - Provides Ocelot extensions to use Polly.NET - Ocelot.Provider.Polly - 0.0.0-dev - Ocelot.Provider.Polly - Ocelot.Provider.Polly - API Gateway;.NET core - https://github.com/ThreeMammals/Ocelot.Provider.Polly - https://github.com/ThreeMammals/Ocelot.Provider.Polly - http://threemammals.com/images/ocelot_logo.png - win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 - false - false - True - false - Tom Pallister - ..\..\codeanalysis.ruleset - - - full - True - - - - - - - all - - - - - - - + + + net7.0 + true + Provides Ocelot extensions to use Polly.NET + Ocelot.Provider.Polly + 0.0.0-dev + Ocelot.Provider.Polly + Ocelot.Provider.Polly + API Gateway;.NET core + https://github.com/ThreeMammals/Ocelot.Provider.Polly + https://github.com/ThreeMammals/Ocelot.Provider.Polly + https://raw.githubusercontent.com/ThreeMammals/Ocelot/develop/images/ocelot_logo.png + win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 + false + false + True + false + Tom Pallister + ..\..\codeanalysis.ruleset + True + 1591 + + + full + True + + + + + + + all + + + + + + + diff --git a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs index f753d9d36..3bcf201f1 100644 --- a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs @@ -1,38 +1,45 @@ -namespace Ocelot.Provider.Polly -{ - using Configuration; - using DependencyInjection; - using Errors; - using global::Polly.CircuitBreaker; - using global::Polly.Timeout; - using Logging; - using Microsoft.Extensions.DependencyInjection; - using Requester; - using System; - using System.Collections.Generic; - using System.Net.Http; - using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; - 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)} - }; +using Ocelot.Configuration; - builder.Services.AddSingleton(errorMapping); +using Ocelot.DependencyInjection; - DelegatingHandler QosDelegatingHandlerDelegate(DownstreamReRoute reRoute, IOcelotLoggerFactory logger) - { - return new PollyCircuitBreakingDelegatingHandler(new PollyQoSProvider(reRoute, logger), logger); - } +using Ocelot.Errors; - builder.Services.AddSingleton((QosDelegatingHandlerDelegate)QosDelegatingHandlerDelegate); - return builder; - } - } -} +using global::Polly.CircuitBreaker; +using global::Polly.Timeout; + +using Ocelot.Logging; + +using Microsoft.Extensions.DependencyInjection; + +using Ocelot.Requester; + +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); + + DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute route, IOcelotLoggerFactory logger) + { + return new PollyCircuitBreakingDelegatingHandler(new PollyQoSProvider(route, logger), logger); + } + + builder.Services.AddSingleton((QosDelegatingHandlerDelegate)QosDelegatingHandlerDelegate); + return builder; + } + } +} diff --git a/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs b/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs index 8bdcaa4a7..43841c0a6 100644 --- a/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs +++ b/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs @@ -1,10 +1,12 @@ -using Ocelot.Logging; -using Polly; -using Polly.CircuitBreaker; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Ocelot.Logging; + +using Polly; +using Polly.CircuitBreaker; + namespace Ocelot.Provider.Polly { public class PollyCircuitBreakingDelegatingHandler : DelegatingHandler @@ -30,12 +32,12 @@ protected override async Task SendAsync(HttpRequestMessage } catch (BrokenCircuitException ex) { - _logger.LogError($"Reached to allowed number of exceptions. Circuit is open", ex); + _logger.LogError("Reached to allowed number of exceptions. Circuit is open", ex); throw; } catch (HttpRequestException ex) { - _logger.LogError($"Error in CircuitBreakingDelegatingHandler.SendAync", ex); + _logger.LogError("Error in CircuitBreakingDelegatingHandler.SendAync", ex); throw; } } diff --git a/src/Ocelot.Provider.Polly/PollyQoSProvider.cs b/src/Ocelot.Provider.Polly/PollyQoSProvider.cs index 3f2c6720e..3dcb697ed 100644 --- a/src/Ocelot.Provider.Polly/PollyQoSProvider.cs +++ b/src/Ocelot.Provider.Polly/PollyQoSProvider.cs @@ -1,59 +1,62 @@ -namespace Ocelot.Provider.Polly -{ - using global::Polly; - using global::Polly.CircuitBreaker; - using global::Polly.Timeout; - using Ocelot.Configuration; - using Ocelot.Logging; - using System; - using System.Net.Http; +using System; +using System.Net.Http; - public class PollyQoSProvider - { - private readonly AsyncCircuitBreakerPolicy _circuitBreakerPolicy; - private readonly AsyncTimeoutPolicy _timeoutPolicy; - private readonly IOcelotLogger _logger; +using Ocelot.Configuration; - public PollyQoSProvider(DownstreamReRoute reRoute, IOcelotLoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); +using global::Polly; +using global::Polly.CircuitBreaker; +using global::Polly.Timeout; - Enum.TryParse(reRoute.QosOptions.TimeoutStrategy, out TimeoutStrategy strategy); - - _timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromMilliseconds(reRoute.QosOptions.TimeoutValue), strategy); - - if (reRoute.QosOptions.ExceptionsAllowedBeforeBreaking > 0) - { - _circuitBreakerPolicy = Policy - .Handle() - .Or() - .Or() - .CircuitBreakerAsync( - exceptionsAllowedBeforeBreaking: reRoute.QosOptions.ExceptionsAllowedBeforeBreaking, - durationOfBreak: TimeSpan.FromMilliseconds(reRoute.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; } - } -} +using Ocelot.Logging; + +namespace Ocelot.Provider.Polly +{ + public class PollyQoSProvider + { + private readonly AsyncCircuitBreakerPolicy _circuitBreakerPolicy; + private readonly AsyncTimeoutPolicy _timeoutPolicy; + private readonly IOcelotLogger _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; } + } +} diff --git a/src/Ocelot.Provider.Polly/RequestTimedOutError.cs b/src/Ocelot.Provider.Polly/RequestTimedOutError.cs index 63ad74aea..d116ddaaf 100644 --- a/src/Ocelot.Provider.Polly/RequestTimedOutError.cs +++ b/src/Ocelot.Provider.Polly/RequestTimedOutError.cs @@ -1,12 +1,13 @@ -namespace Ocelot.Provider.Polly -{ - using Errors; - using System; +using System; + +using Ocelot.Errors; +namespace Ocelot.Provider.Polly +{ public class RequestTimedOutError : Error { public RequestTimedOutError(Exception exception) - : base($"Timeout making http request, exception: {exception}", OcelotErrorCode.RequestTimedOutError) + : base($"Timeout making http request, exception: {exception}", OcelotErrorCode.RequestTimedOutError, 503) { } } diff --git a/src/Ocelot.Provider.Rafty/BearerToken.cs b/src/Ocelot.Provider.Rafty/BearerToken.cs deleted file mode 100644 index c006aaf3a..000000000 --- a/src/Ocelot.Provider.Rafty/BearerToken.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Ocelot.Provider.Rafty -{ - using Newtonsoft.Json; - - internal class BearerToken - { - [JsonProperty("access_token")] - public string AccessToken { get; set; } - - [JsonProperty("expires_in")] - public int ExpiresIn { get; set; } - - [JsonProperty("token_type")] - public string TokenType { get; set; } - } -} diff --git a/src/Ocelot.Provider.Rafty/FakeCommand.cs b/src/Ocelot.Provider.Rafty/FakeCommand.cs deleted file mode 100644 index de611da74..000000000 --- a/src/Ocelot.Provider.Rafty/FakeCommand.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Ocelot.Provider.Rafty -{ - using global::Rafty.FiniteStateMachine; - - public class FakeCommand : ICommand - { - public FakeCommand(string value) - { - this.Value = value; - } - - public string Value { get; private set; } - } -} diff --git a/src/Ocelot.Provider.Rafty/FilePeer.cs b/src/Ocelot.Provider.Rafty/FilePeer.cs deleted file mode 100644 index 4bb57548c..000000000 --- a/src/Ocelot.Provider.Rafty/FilePeer.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Ocelot.Provider.Rafty -{ - public class FilePeer - { - public string HostAndPort { get; set; } - } -} diff --git a/src/Ocelot.Provider.Rafty/FilePeers.cs b/src/Ocelot.Provider.Rafty/FilePeers.cs deleted file mode 100644 index 4d5f9e39e..000000000 --- a/src/Ocelot.Provider.Rafty/FilePeers.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Ocelot.Provider.Rafty -{ - using System.Collections.Generic; - - public class FilePeers - { - public FilePeers() - { - Peers = new List(); - } - - public List Peers { get; set; } - } -} diff --git a/src/Ocelot.Provider.Rafty/FilePeersProvider.cs b/src/Ocelot.Provider.Rafty/FilePeersProvider.cs deleted file mode 100644 index ddca1ac2c..000000000 --- a/src/Ocelot.Provider.Rafty/FilePeersProvider.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace Ocelot.Provider.Rafty -{ - using Administration; - using Configuration.Repository; - using global::Rafty.Concensus.Peers; - using global::Rafty.Infrastructure; - using Microsoft.Extensions.Options; - using Middleware; - using System.Collections.Generic; - using System.Net.Http; - - public class FilePeersProvider : IPeersProvider - { - private readonly IOptions _options; - private readonly List _peers; - private IBaseUrlFinder _finder; - private IInternalConfigurationRepository _repo; - private IIdentityServerConfiguration _identityServerConfig; - - public FilePeersProvider(IOptions options, IBaseUrlFinder finder, IInternalConfigurationRepository repo, IIdentityServerConfiguration identityServerConfig) - { - _identityServerConfig = identityServerConfig; - _repo = repo; - _finder = finder; - _options = options; - _peers = new List(); - - var config = _repo.Get(); - foreach (var item in _options.Value.Peers) - { - var httpClient = new HttpClient(); - - //todo what if this errors? - var httpPeer = new HttpPeer(item.HostAndPort, httpClient, _finder, config.Data, _identityServerConfig); - _peers.Add(httpPeer); - } - } - - public List Get() - { - return _peers; - } - } -} diff --git a/src/Ocelot.Provider.Rafty/HttpPeer.cs b/src/Ocelot.Provider.Rafty/HttpPeer.cs deleted file mode 100644 index c3ed9e658..000000000 --- a/src/Ocelot.Provider.Rafty/HttpPeer.cs +++ /dev/null @@ -1,130 +0,0 @@ -namespace Ocelot.Provider.Rafty -{ - using Administration; - using Configuration; - using global::Rafty.Concensus.Messages; - using global::Rafty.Concensus.Peers; - using global::Rafty.FiniteStateMachine; - using global::Rafty.Infrastructure; - using Middleware; - using Newtonsoft.Json; - using System; - using System.Collections.Generic; - using System.Net.Http; - using System.Threading.Tasks; - - public class HttpPeer : IPeer - { - private readonly string _hostAndPort; - private readonly HttpClient _httpClient; - private readonly JsonSerializerSettings _jsonSerializerSettings; - private readonly string _baseSchemeUrlAndPort; - private BearerToken _token; - private readonly IInternalConfiguration _config; - private readonly IIdentityServerConfiguration _identityServerConfiguration; - - public HttpPeer(string hostAndPort, HttpClient httpClient, IBaseUrlFinder finder, IInternalConfiguration config, IIdentityServerConfiguration identityServerConfiguration) - { - _identityServerConfiguration = identityServerConfiguration; - _config = config; - Id = hostAndPort; - _hostAndPort = hostAndPort; - _httpClient = httpClient; - _jsonSerializerSettings = new JsonSerializerSettings() - { - TypeNameHandling = TypeNameHandling.All - }; - _baseSchemeUrlAndPort = finder.Find(); - } - - public string Id { get; } - - public async Task Request(RequestVote requestVote) - { - if (_token == null) - { - await SetToken(); - } - - var json = JsonConvert.SerializeObject(requestVote, _jsonSerializerSettings); - var content = new StringContent(json); - content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); - var response = await _httpClient.PostAsync($"{_hostAndPort}/administration/raft/requestvote", content); - if (response.IsSuccessStatusCode) - { - return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(), _jsonSerializerSettings); - } - - return new RequestVoteResponse(false, requestVote.Term); - } - - public async Task Request(AppendEntries appendEntries) - { - try - { - if (_token == null) - { - await SetToken(); - } - - var json = JsonConvert.SerializeObject(appendEntries, _jsonSerializerSettings); - var content = new StringContent(json); - content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); - var response = await _httpClient.PostAsync($"{_hostAndPort}/administration/raft/appendEntries", content); - if (response.IsSuccessStatusCode) - { - return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(), _jsonSerializerSettings); - } - - return new AppendEntriesResponse(appendEntries.Term, false); - } - catch (Exception ex) - { - Console.WriteLine(ex); - return new AppendEntriesResponse(appendEntries.Term, false); - } - } - - public async Task> Request(T command) - where T : ICommand - { - Console.WriteLine("SENDING REQUEST...."); - if (_token == null) - { - await SetToken(); - } - - var json = JsonConvert.SerializeObject(command, _jsonSerializerSettings); - var content = new StringContent(json); - content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); - var response = await _httpClient.PostAsync($"{_hostAndPort}/administration/raft/command", content); - if (response.IsSuccessStatusCode) - { - Console.WriteLine("REQUEST OK...."); - var okResponse = JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync(), _jsonSerializerSettings); - return new OkResponse((T)okResponse.Command); - } - - Console.WriteLine("REQUEST NOT OK...."); - return new ErrorResponse(await response.Content.ReadAsStringAsync(), command); - } - - private async Task SetToken() - { - var tokenUrl = $"{_baseSchemeUrlAndPort}{_config.AdministrationPath}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", _identityServerConfiguration.ApiName), - new KeyValuePair("client_secret", _identityServerConfiguration.ApiSecret), - new KeyValuePair("scope", _identityServerConfiguration.ApiName), - new KeyValuePair("grant_type", "client_credentials") - }; - var content = new FormUrlEncodedContent(formData); - var response = await _httpClient.PostAsync(tokenUrl, content); - var responseContent = await response.Content.ReadAsStringAsync(); - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(_token.TokenType, _token.AccessToken); - } - } -} diff --git a/src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj b/src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj deleted file mode 100644 index 8cb2ab420..000000000 --- a/src/Ocelot.Provider.Rafty/Ocelot.Provider.Rafty.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - netcoreapp3.1 - true - Provides Ocelot extensions to use Rafty - Ocelot.Provider.Rafty - 0.0.0-dev - Ocelot.Provider.Rafty - Ocelot.Provider.Rafty - API Gateway;.NET core - https://github.com/ThreeMammals/Ocelot.Provider.Rafty - https://github.com/ThreeMammals/Ocelot.Provider.Rafty - http://threemammals.com/images/ocelot_logo.png - win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 - false - false - True - false - Tom Pallister - ..\..\codeanalysis.ruleset - - - full - True - - - - - - - - - - all - - - - - - diff --git a/src/Ocelot.Provider.Rafty/OcelotAdministrationBuilderExtensions.cs b/src/Ocelot.Provider.Rafty/OcelotAdministrationBuilderExtensions.cs deleted file mode 100644 index a04876594..000000000 --- a/src/Ocelot.Provider.Rafty/OcelotAdministrationBuilderExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Ocelot.Provider.Rafty -{ - using Configuration.Setter; - using DependencyInjection; - using global::Rafty.Concensus.Node; - using global::Rafty.FiniteStateMachine; - using global::Rafty.Infrastructure; - using global::Rafty.Log; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.DependencyInjection.Extensions; - - public static class OcelotAdministrationBuilderExtensions - { - public static IOcelotAdministrationBuilder AddRafty(this IOcelotAdministrationBuilder builder) - { - var settings = new InMemorySettings(4000, 6000, 100, 10000); - builder.Services.RemoveAll(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(settings); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.Configure(builder.ConfigurationRoot); - return builder; - } - } -} diff --git a/src/Ocelot.Provider.Rafty/OcelotFiniteStateMachine.cs b/src/Ocelot.Provider.Rafty/OcelotFiniteStateMachine.cs deleted file mode 100644 index b054852ee..000000000 --- a/src/Ocelot.Provider.Rafty/OcelotFiniteStateMachine.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Ocelot.Provider.Rafty -{ - using Configuration.Setter; - using global::Rafty.FiniteStateMachine; - using global::Rafty.Log; - using System.Threading.Tasks; - - public class OcelotFiniteStateMachine : IFiniteStateMachine - { - private readonly IFileConfigurationSetter _setter; - - public OcelotFiniteStateMachine(IFileConfigurationSetter setter) - { - _setter = setter; - } - - public async Task Handle(LogEntry log) - { - //todo - handle an error - //hack it to just cast as at the moment we know this is the only command :P - var hack = (UpdateFileConfiguration)log.CommandData; - await _setter.Set(hack.Configuration); - } - } -} diff --git a/src/Ocelot.Provider.Rafty/Properties/AssemblyInfo.cs b/src/Ocelot.Provider.Rafty/Properties/AssemblyInfo.cs deleted file mode 100644 index dd8b7610c..000000000 --- a/src/Ocelot.Provider.Rafty/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Ocelot")] -[assembly: AssemblyTrademark("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("d6df4206-0dba-41d8-884d-c3e08290fdbb")] diff --git a/src/Ocelot.Provider.Rafty/RaftController.cs b/src/Ocelot.Provider.Rafty/RaftController.cs deleted file mode 100644 index a8006cdce..000000000 --- a/src/Ocelot.Provider.Rafty/RaftController.cs +++ /dev/null @@ -1,96 +0,0 @@ -namespace Ocelot.Provider.Rafty -{ - using global::Rafty.Concensus.Messages; - using global::Rafty.Concensus.Node; - using global::Rafty.FiniteStateMachine; - using Logging; - using Microsoft.AspNetCore.Authorization; - using Microsoft.AspNetCore.Mvc; - using Middleware; - using Newtonsoft.Json; - using System; - using System.IO; - using System.Threading.Tasks; - - [Authorize] - [Route("raft")] - public class RaftController : Controller - { - private readonly INode _node; - private readonly IOcelotLogger _logger; - private readonly string _baseSchemeUrlAndPort; - private readonly JsonSerializerSettings _jsonSerialiserSettings; - - public RaftController(INode node, IOcelotLoggerFactory loggerFactory, IBaseUrlFinder finder) - { - _jsonSerialiserSettings = new JsonSerializerSettings - { - TypeNameHandling = TypeNameHandling.All - }; - _baseSchemeUrlAndPort = finder.Find(); - _logger = loggerFactory.CreateLogger(); - _node = node; - } - - [Route("appendentries")] - public async Task AppendEntries() - { - using (var reader = new StreamReader(HttpContext.Request.Body)) - { - var json = await reader.ReadToEndAsync(); - - var appendEntries = JsonConvert.DeserializeObject(json, _jsonSerialiserSettings); - - _logger.LogDebug($"{_baseSchemeUrlAndPort}/appendentries called, my state is {_node.State.GetType().FullName}"); - - var appendEntriesResponse = await _node.Handle(appendEntries); - - return new OkObjectResult(appendEntriesResponse); - } - } - - [Route("requestvote")] - public async Task RequestVote() - { - using (var reader = new StreamReader(HttpContext.Request.Body)) - { - var json = await reader.ReadToEndAsync(); - - var requestVote = JsonConvert.DeserializeObject(json, _jsonSerialiserSettings); - - _logger.LogDebug($"{_baseSchemeUrlAndPort}/requestvote called, my state is {_node.State.GetType().FullName}"); - - var requestVoteResponse = await _node.Handle(requestVote); - - return new OkObjectResult(requestVoteResponse); - } - } - - [Route("command")] - public async Task Command() - { - try - { - using (var reader = new StreamReader(HttpContext.Request.Body)) - { - var json = await reader.ReadToEndAsync(); - - var command = JsonConvert.DeserializeObject(json, _jsonSerialiserSettings); - - _logger.LogDebug($"{_baseSchemeUrlAndPort}/command called, my state is {_node.State.GetType().FullName}"); - - var commandResponse = await _node.Accept(command); - - json = JsonConvert.SerializeObject(commandResponse, _jsonSerialiserSettings); - - return StatusCode(200, json); - } - } - catch (Exception e) - { - _logger.LogError($"THERE WAS A PROBLEM ON NODE {_node.State.CurrentState.Id}", e); - throw; - } - } - } -} diff --git a/src/Ocelot.Provider.Rafty/RaftyFileConfigurationSetter.cs b/src/Ocelot.Provider.Rafty/RaftyFileConfigurationSetter.cs deleted file mode 100644 index 21fc97a81..000000000 --- a/src/Ocelot.Provider.Rafty/RaftyFileConfigurationSetter.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Ocelot.Provider.Rafty -{ - using Configuration.File; - using Configuration.Setter; - using global::Rafty.Concensus.Node; - using global::Rafty.Infrastructure; - using System.Threading.Tasks; - - public class RaftyFileConfigurationSetter : IFileConfigurationSetter - { - private readonly INode _node; - - public RaftyFileConfigurationSetter(INode node) - { - _node = node; - } - - public async Task Set(FileConfiguration fileConfiguration) - { - var result = await _node.Accept(new UpdateFileConfiguration(fileConfiguration)); - - if (result.GetType() == typeof(ErrorResponse)) - { - return new Responses.ErrorResponse(new UnableToSaveAcceptCommand($"unable to save file configuration to state machine")); - } - - return new Responses.OkResponse(); - } - } -} diff --git a/src/Ocelot.Provider.Rafty/RaftyMiddlewareConfigurationProvider.cs b/src/Ocelot.Provider.Rafty/RaftyMiddlewareConfigurationProvider.cs deleted file mode 100644 index 62e9711da..000000000 --- a/src/Ocelot.Provider.Rafty/RaftyMiddlewareConfigurationProvider.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace Ocelot.Provider.Rafty -{ - using global::Rafty.Concensus.Node; - using global::Rafty.Infrastructure; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.DependencyInjection; - using Middleware; - using System.Threading.Tasks; - - public static class RaftyMiddlewareConfigurationProvider - { - public static OcelotMiddlewareConfigurationDelegate Get = builder => - { - if (UsingRafty(builder)) - { - SetUpRafty(builder); - } - - return Task.CompletedTask; - }; - - private static bool UsingRafty(IApplicationBuilder builder) - { - var node = builder.ApplicationServices.GetService(); - if (node != null) - { - return true; - } - - return false; - } - - private static void SetUpRafty(IApplicationBuilder builder) - { - var applicationLifetime = builder.ApplicationServices.GetService(); - applicationLifetime.ApplicationStopping.Register(() => OnShutdown(builder)); - var node = builder.ApplicationServices.GetService(); - var nodeId = builder.ApplicationServices.GetService(); - node.Start(nodeId); - } - - private static void OnShutdown(IApplicationBuilder app) - { - var node = app.ApplicationServices.GetService(); - node.Stop(); - } - } -} diff --git a/src/Ocelot.Provider.Rafty/SqlLiteLog.cs b/src/Ocelot.Provider.Rafty/SqlLiteLog.cs deleted file mode 100644 index 1be1645e9..000000000 --- a/src/Ocelot.Provider.Rafty/SqlLiteLog.cs +++ /dev/null @@ -1,334 +0,0 @@ -namespace Ocelot.Provider.Rafty -{ - using global::Rafty.Infrastructure; - using global::Rafty.Log; - using Microsoft.Data.Sqlite; - using Microsoft.Extensions.Logging; - using Newtonsoft.Json; - using System; - using System.Collections.Generic; - using System.IO; - using System.Threading; - using System.Threading.Tasks; - - public class SqlLiteLog : ILog - { - private readonly string _path; - private readonly SemaphoreSlim _sempaphore = new SemaphoreSlim(1, 1); - private readonly ILogger _logger; - private readonly NodeId _nodeId; - - public SqlLiteLog(NodeId nodeId, ILoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - _nodeId = nodeId; - _path = $"{nodeId.Id.Replace("/", "").Replace(":", "")}.db"; - _sempaphore.Wait(); - - if (!File.Exists(_path)) - { - var fs = File.Create(_path); - - fs.Dispose(); - - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - - const string sql = @"create table logs ( - id integer primary key, - data text not null - )"; - - using (var command = new SqliteCommand(sql, connection)) - { - var result = command.ExecuteNonQuery(); - - _logger.LogInformation(result == 0 - ? $"id: {_nodeId.Id} create database, result: {result}" - : $"id: {_nodeId.Id} did not create database., result: {result}"); - } - } - } - - _sempaphore.Release(); - } - - public async Task LastLogIndex() - { - _sempaphore.Wait(); - var result = 1; - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - var sql = @"select id from logs order by id desc limit 1"; - using (var command = new SqliteCommand(sql, connection)) - { - var index = Convert.ToInt32(await command.ExecuteScalarAsync()); - if (index > result) - { - result = index; - } - } - } - - _sempaphore.Release(); - return result; - } - - public async Task LastLogTerm() - { - _sempaphore.Wait(); - long result = 0; - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - var sql = @"select data from logs order by id desc limit 1"; - using (var command = new SqliteCommand(sql, connection)) - { - var data = Convert.ToString(await command.ExecuteScalarAsync()); - var jsonSerializerSettings = new JsonSerializerSettings() - { - TypeNameHandling = TypeNameHandling.All - }; - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - if (log != null && log.Term > result) - { - result = log.Term; - } - } - } - - _sempaphore.Release(); - return result; - } - - public async Task Count() - { - _sempaphore.Wait(); - var result = 0; - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - var sql = @"select count(id) from logs"; - using (var command = new SqliteCommand(sql, connection)) - { - var index = Convert.ToInt32(await command.ExecuteScalarAsync()); - if (index > result) - { - result = index; - } - } - } - - _sempaphore.Release(); - return result; - } - - public async Task Apply(LogEntry log) - { - _sempaphore.Wait(); - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - var jsonSerializerSettings = new JsonSerializerSettings() - { - TypeNameHandling = TypeNameHandling.All - }; - var data = JsonConvert.SerializeObject(log, jsonSerializerSettings); - - //todo - sql injection dont copy this.. - var sql = $"insert into logs (data) values ('{data}')"; - _logger.LogInformation($"id: {_nodeId.Id}, sql: {sql}"); - using (var command = new SqliteCommand(sql, connection)) - { - var result = await command.ExecuteNonQueryAsync(); - _logger.LogInformation($"id: {_nodeId.Id}, insert log result: {result}"); - } - - sql = "select last_insert_rowid()"; - using (var command = new SqliteCommand(sql, connection)) - { - var result = await command.ExecuteScalarAsync(); - _logger.LogInformation($"id: {_nodeId.Id}, about to release semaphore"); - _sempaphore.Release(); - _logger.LogInformation($"id: {_nodeId.Id}, saved log to sqlite"); - return Convert.ToInt32(result); - } - } - } - - public async Task DeleteConflictsFromThisLog(int index, LogEntry logEntry) - { - _sempaphore.Wait(); - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - - //todo - sql injection dont copy this.. - var sql = $"select data from logs where id = {index};"; - _logger.LogInformation($"id: {_nodeId.Id} sql: {sql}"); - using (var command = new SqliteCommand(sql, connection)) - { - var data = Convert.ToString(await command.ExecuteScalarAsync()); - var jsonSerializerSettings = new JsonSerializerSettings() - { - TypeNameHandling = TypeNameHandling.All - }; - - _logger.LogInformation($"id {_nodeId.Id} got log for index: {index}, data is {data} and new log term is {logEntry.Term}"); - - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - if (logEntry != null && log != null && logEntry.Term != log.Term) - { - //todo - sql injection dont copy this.. - var deleteSql = $"delete from logs where id >= {index};"; - _logger.LogInformation($"id: {_nodeId.Id} sql: {deleteSql}"); - using (var deleteCommand = new SqliteCommand(deleteSql, connection)) - { - var result = await deleteCommand.ExecuteNonQueryAsync(); - } - } - } - } - - _sempaphore.Release(); - } - - public async Task IsDuplicate(int index, LogEntry logEntry) - { - _sempaphore.Wait(); - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - - //todo - sql injection dont copy this.. - var sql = $"select data from logs where id = {index};"; - using (var command = new SqliteCommand(sql, connection)) - { - var data = Convert.ToString(await command.ExecuteScalarAsync()); - var jsonSerializerSettings = new JsonSerializerSettings() - { - TypeNameHandling = TypeNameHandling.All - }; - - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - - if (logEntry != null && log != null && logEntry.Term == log.Term) - { - _sempaphore.Release(); - return true; - } - } - } - - _sempaphore.Release(); - return false; - } - - public async Task Get(int index) - { - _sempaphore.Wait(); - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - - //todo - sql injection dont copy this.. - var sql = $"select data from logs where id = {index}"; - using (var command = new SqliteCommand(sql, connection)) - { - var data = Convert.ToString(await command.ExecuteScalarAsync()); - var jsonSerializerSettings = new JsonSerializerSettings() - { - TypeNameHandling = TypeNameHandling.All - }; - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - _sempaphore.Release(); - return log; - } - } - } - - public async Task> GetFrom(int index) - { - _sempaphore.Wait(); - var logsToReturn = new List<(int, LogEntry)>(); - - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - - //todo - sql injection dont copy this.. - var sql = $"select id, data from logs where id >= {index}"; - using (var command = new SqliteCommand(sql, connection)) - { - using (var reader = await command.ExecuteReaderAsync()) - { - while (reader.Read()) - { - var id = Convert.ToInt32(reader[0]); - var data = (string)reader[1]; - var jsonSerializerSettings = new JsonSerializerSettings() - { - TypeNameHandling = TypeNameHandling.All - }; - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - logsToReturn.Add((id, log)); - } - } - } - - _sempaphore.Release(); - return logsToReturn; - } - } - - public async Task GetTermAtIndex(int index) - { - _sempaphore.Wait(); - long result = 0; - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - - //todo - sql injection dont copy this.. - var sql = $"select data from logs where id = {index}"; - using (var command = new SqliteCommand(sql, connection)) - { - var data = Convert.ToString(await command.ExecuteScalarAsync()); - var jsonSerializerSettings = new JsonSerializerSettings() - { - TypeNameHandling = TypeNameHandling.All - }; - var log = JsonConvert.DeserializeObject(data, jsonSerializerSettings); - if (log != null && log.Term > result) - { - result = log.Term; - } - } - } - - _sempaphore.Release(); - return result; - } - - public async Task Remove(int indexOfCommand) - { - _sempaphore.Wait(); - using (var connection = new SqliteConnection($"Data Source={_path};")) - { - connection.Open(); - - //todo - sql injection dont copy this.. - var deleteSql = $"delete from logs where id >= {indexOfCommand};"; - _logger.LogInformation($"id: {_nodeId.Id} Remove {deleteSql}"); - using (var deleteCommand = new SqliteCommand(deleteSql, connection)) - { - var result = await deleteCommand.ExecuteNonQueryAsync(); - } - } - - _sempaphore.Release(); - } - } -} diff --git a/src/Ocelot.Provider.Rafty/UnableToSaveAcceptCommand.cs b/src/Ocelot.Provider.Rafty/UnableToSaveAcceptCommand.cs deleted file mode 100644 index 1a9f12e27..000000000 --- a/src/Ocelot.Provider.Rafty/UnableToSaveAcceptCommand.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Ocelot.Provider.Rafty -{ - using Errors; - - public class UnableToSaveAcceptCommand : Error - { - public UnableToSaveAcceptCommand(string message) - : base(message, OcelotErrorCode.UnknownError) - { - } - } -} diff --git a/src/Ocelot.Provider.Rafty/UpdateFileConfiguration.cs b/src/Ocelot.Provider.Rafty/UpdateFileConfiguration.cs deleted file mode 100644 index 894a758a6..000000000 --- a/src/Ocelot.Provider.Rafty/UpdateFileConfiguration.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Ocelot.Provider.Rafty -{ - using Configuration.File; - using global::Rafty.FiniteStateMachine; - - public class UpdateFileConfiguration : ICommand - { - public UpdateFileConfiguration(FileConfiguration configuration) - { - Configuration = configuration; - } - - public FileConfiguration Configuration { get; private set; } - } -} diff --git a/src/Ocelot.Tracing.Butterfly/ButterflyTracer.cs b/src/Ocelot.Tracing.Butterfly/ButterflyTracer.cs index 3cd63eb0a..1298e8e23 100644 --- a/src/Ocelot.Tracing.Butterfly/ButterflyTracer.cs +++ b/src/Ocelot.Tracing.Butterfly/ButterflyTracer.cs @@ -1,18 +1,21 @@ -namespace Ocelot.Tracing.Butterfly -{ - using global::Butterfly.Client.AspNetCore; - using global::Butterfly.Client.Tracing; - using global::Butterfly.OpenTracing; - using Infrastructure.Extensions; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.DependencyInjection; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net.Http; - using System.Threading; - using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using global::Butterfly.Client.AspNetCore; +using global::Butterfly.Client.Tracing; +using global::Butterfly.OpenTracing; + +using Ocelot.Infrastructure.Extensions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Ocelot.Tracing.Butterfly +{ public class ButterflyTracer : DelegatingHandler, Logging.ITracer { private readonly IServiceTracer _tracer; diff --git a/src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj b/src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj index f2401e024..0d6d4a9be 100644 --- a/src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj +++ b/src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net7.0 true This package provides methods to integrate Butterfly tracing with Ocelot. Ocelot.Tracing.Butterfly @@ -10,7 +10,7 @@ Ocelot.Tracing.Butterfly API Gateway;.NET core; Butterfly; ButterflyAPM https://github.com/ThreeMammals/Ocelot - https://github.com/ThreeMammals/Ocelot + https://raw.githubusercontent.com/ThreeMammals/Ocelot/develop/images/ocelot_logo.png win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 false false @@ -19,6 +19,8 @@ Tom Pallister ..\..\codeanalysis.ruleset Ocelot.Tracing.Butterfly + True + 1591 full @@ -30,6 +32,9 @@ + + all + diff --git a/src/Ocelot.Tracing.Butterfly/OcelotBuilderExtensions.cs b/src/Ocelot.Tracing.Butterfly/OcelotBuilderExtensions.cs index 8c67f0dfa..7cb920d18 100644 --- a/src/Ocelot.Tracing.Butterfly/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Tracing.Butterfly/OcelotBuilderExtensions.cs @@ -1,11 +1,15 @@ -namespace Ocelot.Tracing.Butterfly -{ - using DependencyInjection; - using global::Butterfly.Client.AspNetCore; - using Logging; - using Microsoft.Extensions.DependencyInjection; - using System; +using System; + +using Ocelot.DependencyInjection; + +using global::Butterfly.Client.AspNetCore; +using Ocelot.Logging; + +using Microsoft.Extensions.DependencyInjection; + +namespace Ocelot.Tracing.Butterfly +{ public static class OcelotBuilderExtensions { public static IOcelotBuilder AddButterfly(this IOcelotBuilder builder, Action settings) diff --git a/src/Ocelot.Tracing.OpenTracing/Ocelot.Tracing.OpenTracing.csproj b/src/Ocelot.Tracing.OpenTracing/Ocelot.Tracing.OpenTracing.csproj new file mode 100644 index 000000000..1cdcb6d06 --- /dev/null +++ b/src/Ocelot.Tracing.OpenTracing/Ocelot.Tracing.OpenTracing.csproj @@ -0,0 +1,32 @@ + + + + net7.0 + 0.0.0-dev + Kjell-Åke Gafvelin + This package provides OpenTracing support to Ocelot. + https://github.com/ThreeMammals/Ocelot + ocelot_logo.png + API Gateway;.NET core; OpenTracing + true + True + 1591 + + + + + + + + + + + all + + + + + + + + diff --git a/src/Ocelot.Tracing.OpenTracing/OcelotBuilderExtensions.cs b/src/Ocelot.Tracing.OpenTracing/OcelotBuilderExtensions.cs new file mode 100644 index 000000000..3c01810ae --- /dev/null +++ b/src/Ocelot.Tracing.OpenTracing/OcelotBuilderExtensions.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) ThreeMammals. All rights reserved. +// + +namespace Ocelot.Tracing.OpenTracing; + +using Microsoft.Extensions.DependencyInjection.Extensions; +using Ocelot.DependencyInjection; +using Ocelot.Logging; + +/// +/// Extension methods for the interface. +/// +public static class OcelotBuilderExtensions +{ + /// + /// Adds OpenTracing services using builder. + /// + /// The Ocelot builder with services. + /// An object. + public static IOcelotBuilder AddOpenTracing(this IOcelotBuilder builder) + { + builder.Services.TryAddSingleton(); + return builder; + } +} diff --git a/src/Ocelot.Tracing.OpenTracing/OpenTracingTracer.cs b/src/Ocelot.Tracing.OpenTracing/OpenTracingTracer.cs new file mode 100644 index 000000000..a268ae048 --- /dev/null +++ b/src/Ocelot.Tracing.OpenTracing/OpenTracingTracer.cs @@ -0,0 +1,88 @@ +// +// Copyright (c) ThreeMammals. All rights reserved. +// + +namespace Ocelot.Tracing.OpenTracing; + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using global::OpenTracing; +using global::OpenTracing.Propagation; +using global::OpenTracing.Tag; +using Microsoft.AspNetCore.Http; + +/// +/// Default tracer implementation for the interface. +/// +internal class OpenTracingTracer : Logging.ITracer +{ + private readonly ITracer tracer; + + /// + /// Initializes a new instance of the class. + /// + /// The tracer. + public OpenTracingTracer(ITracer tracer) + { + this.tracer = tracer ?? throw new ArgumentNullException(nameof(tracer)); + } + + /// + public void Event(HttpContext httpContext, string @event) + { + } + + /// + public async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken, + Action addTraceIdToRepo, + Func> baseSendAsync) + { + using (var scope = this.tracer.BuildSpan(request.RequestUri.AbsoluteUri).StartActive(finishSpanOnDispose: true)) + { + var span = scope.Span; + + span.SetTag(Tags.SpanKind, Tags.SpanKindClient) + .SetTag(Tags.HttpMethod, request.Method.Method) + .SetTag(Tags.HttpUrl, request.RequestUri.OriginalString); + + addTraceIdToRepo(span.Context.SpanId); + + var headers = new Dictionary(); + + this.tracer.Inject(span.Context, BuiltinFormats.HttpHeaders, new TextMapInjectAdapter(headers)); + + foreach (var item in headers) + { + request.Headers.Add(item.Key, item.Value); + } + + try + { + var response = await baseSendAsync(request, cancellationToken); + + span.SetTag(Tags.HttpStatus, (int)response.StatusCode); + + return response; + } + catch (HttpRequestException ex) + { + Tags.Error.Set(scope.Span, true); + + span.Log(new Dictionary(3) + { + { LogFields.Event, Tags.Error.Key }, + { LogFields.ErrorKind, ex.GetType().Name }, + { LogFields.ErrorObject, ex }, + }); + throw; + } + } + } +} diff --git a/src/Ocelot.Tracing.OpenTracing/stylecop.json b/src/Ocelot.Tracing.OpenTracing/stylecop.json new file mode 100644 index 000000000..ebdde954c --- /dev/null +++ b/src/Ocelot.Tracing.OpenTracing/stylecop.json @@ -0,0 +1,14 @@ +{ + // ACTION REQUIRED: This file was automatically added to your project, but it + // will not take effect until additional steps are taken to enable it. See the + // following page for additional information: + // + // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md + + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "ThreeMammals" + } + } +} diff --git a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs index 942e8cac4..9ea44364f 100644 --- a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs +++ b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs @@ -1,58 +1,65 @@ -using Microsoft.AspNetCore.Authentication; +using System.Threading.Tasks; + using Ocelot.Configuration; + using Ocelot.Logging; + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; + using Ocelot.Middleware; -using System.Threading.Tasks; namespace Ocelot.Authentication.Middleware { public class AuthenticationMiddleware : OcelotMiddleware { - private readonly OcelotRequestDelegate _next; + private readonly RequestDelegate _next; - public AuthenticationMiddleware(OcelotRequestDelegate next, + public AuthenticationMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory) : base(loggerFactory.CreateLogger()) { _next = next; } - public async Task Invoke(DownstreamContext context) + public async Task Invoke(HttpContext httpContext) { - if (context.HttpContext.Request.Method.ToUpper() != "OPTIONS" && IsAuthenticatedRoute(context.DownstreamReRoute)) + var downstreamRoute = httpContext.Items.DownstreamRoute(); + + if (httpContext.Request.Method.ToUpper() != "OPTIONS" && IsAuthenticatedRoute(downstreamRoute)) { - Logger.LogInformation($"{context.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 context.HttpContext.AuthenticateAsync(context.DownstreamReRoute.AuthenticationOptions.AuthenticationProviderKey); + var result = await httpContext.AuthenticateAsync(downstreamRoute.AuthenticationOptions.AuthenticationProviderKey); - context.HttpContext.User = result.Principal; + httpContext.User = result.Principal; - if (context.HttpContext.User.Identity.IsAuthenticated) + if (httpContext.User.Identity.IsAuthenticated) { - Logger.LogInformation($"Client has been authenticated for {context.HttpContext.Request.Path}"); - await _next.Invoke(context); + Logger.LogInformation($"Client has been authenticated for {httpContext.Request.Path}"); + await _next.Invoke(httpContext); } else { var error = new UnauthenticatedError( - $"Request for authenticated route {context.HttpContext.Request.Path} by {context.HttpContext.User.Identity.Name} was unauthenticated"); + $"Request for authenticated route {httpContext.Request.Path} by {httpContext.User.Identity.Name} was unauthenticated"); - Logger.LogWarning($"Client has NOT been authenticated for {context.HttpContext.Request.Path} and pipeline error set. {error}"); + Logger.LogWarning($"Client has NOT been authenticated for {httpContext.Request.Path} and pipeline error set. {error}"); - SetPipelineError(context, error); + httpContext.Items.SetError(error); } } else { - Logger.LogInformation($"No authentication needed for {context.HttpContext.Request.Path}"); + Logger.LogInformation($"No authentication needed for {httpContext.Request.Path}"); - await _next.Invoke(context); + await _next.Invoke(httpContext); } } - private static bool IsAuthenticatedRoute(DownstreamReRoute reRoute) + private static bool IsAuthenticatedRoute(DownstreamRoute route) { - return reRoute.IsAuthenticated; + return route.IsAuthenticated; } } } diff --git a/src/Ocelot/Authentication/Middleware/AuthenticationMiddlewareMiddlewareExtensions.cs b/src/Ocelot/Authentication/Middleware/AuthenticationMiddlewareMiddlewareExtensions.cs index edd49f32b..3adddff2d 100644 --- a/src/Ocelot/Authentication/Middleware/AuthenticationMiddlewareMiddlewareExtensions.cs +++ b/src/Ocelot/Authentication/Middleware/AuthenticationMiddlewareMiddlewareExtensions.cs @@ -1,12 +1,12 @@ -using Ocelot.Middleware.Pipeline; - -namespace Ocelot.Authentication.Middleware -{ - public static class AuthenticationMiddlewareMiddlewareExtensions - { - public static IOcelotPipelineBuilder UseAuthenticationMiddleware(this IOcelotPipelineBuilder builder) - { - return builder.UseMiddleware(); - } - } +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Authentication.Middleware +{ + public static class AuthenticationMiddlewareMiddlewareExtensions + { + public static IApplicationBuilder UseAuthenticationMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } } diff --git a/src/Ocelot/Authorisation/ClaimValueNotAuthorisedError.cs b/src/Ocelot/Authorisation/ClaimValueNotAuthorisedError.cs deleted file mode 100644 index d0535624f..000000000 --- a/src/Ocelot/Authorisation/ClaimValueNotAuthorisedError.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Ocelot.Errors; - -namespace Ocelot.Authorisation -{ - public class ClaimValueNotAuthorisedError : Error - { - public ClaimValueNotAuthorisedError(string message) - : base(message, OcelotErrorCode.ClaimValueNotAuthorisedError) - { - } - } -} diff --git a/src/Ocelot/Authorisation/IClaimsAuthoriser.cs b/src/Ocelot/Authorisation/IClaimsAuthoriser.cs deleted file mode 100644 index 5e4b9c59c..000000000 --- a/src/Ocelot/Authorisation/IClaimsAuthoriser.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.Responses; -using System.Security.Claims; - -namespace Ocelot.Authorisation -{ - using System.Collections.Generic; - - public interface IClaimsAuthoriser - { - Response Authorise( - ClaimsPrincipal claimsPrincipal, - Dictionary routeClaimsRequirement, - List urlPathPlaceholderNameAndValues - ); - } -} diff --git a/src/Ocelot/Authorisation/IScopesAuthoriser.cs b/src/Ocelot/Authorisation/IScopesAuthoriser.cs deleted file mode 100644 index 57047ce75..000000000 --- a/src/Ocelot/Authorisation/IScopesAuthoriser.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Ocelot.Responses; -using System.Security.Claims; - -namespace Ocelot.Authorisation -{ - using System.Collections.Generic; - - public interface IScopesAuthoriser - { - Response Authorise(ClaimsPrincipal claimsPrincipal, List routeAllowedScopes); - } -} diff --git a/src/Ocelot/Authorisation/Middleware/AuthorisationMiddleware.cs b/src/Ocelot/Authorisation/Middleware/AuthorisationMiddleware.cs deleted file mode 100644 index ca2f118f4..000000000 --- a/src/Ocelot/Authorisation/Middleware/AuthorisationMiddleware.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace Ocelot.Authorisation.Middleware -{ - using Configuration; - using Logging; - using Ocelot.Middleware; - using Responses; - using System.Threading.Tasks; - - public class AuthorisationMiddleware : OcelotMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly IClaimsAuthoriser _claimsAuthoriser; - private readonly IScopesAuthoriser _scopesAuthoriser; - - public AuthorisationMiddleware(OcelotRequestDelegate next, - IClaimsAuthoriser claimsAuthoriser, - IScopesAuthoriser scopesAuthoriser, - IOcelotLoggerFactory loggerFactory) - : base(loggerFactory.CreateLogger()) - { - _next = next; - _claimsAuthoriser = claimsAuthoriser; - _scopesAuthoriser = scopesAuthoriser; - } - - public async Task Invoke(DownstreamContext context) - { - if (!IsOptionsHttpMethod(context) && IsAuthenticatedRoute(context.DownstreamReRoute)) - { - Logger.LogInformation("route is authenticated scopes must be checked"); - - var authorised = _scopesAuthoriser.Authorise(context.HttpContext.User, context.DownstreamReRoute.AuthenticationOptions.AllowedScopes); - - if (authorised.IsError) - { - Logger.LogWarning("error authorising user scopes"); - - SetPipelineError(context, authorised.Errors); - return; - } - - if (IsAuthorised(authorised)) - { - Logger.LogInformation("user scopes is authorised calling next authorisation checks"); - } - else - { - Logger.LogWarning("user scopes is not authorised setting pipeline error"); - - SetPipelineError(context, new UnauthorisedError( - $"{context.HttpContext.User.Identity.Name} unable to access {context.DownstreamReRoute.UpstreamPathTemplate.OriginalValue}")); - } - } - - if (!IsOptionsHttpMethod(context) && IsAuthorisedRoute(context.DownstreamReRoute)) - { - Logger.LogInformation("route is authorised"); - - var authorised = _claimsAuthoriser.Authorise(context.HttpContext.User, context.DownstreamReRoute.RouteClaimsRequirement, context.TemplatePlaceholderNameAndValues); - - if (authorised.IsError) - { - Logger.LogWarning($"Error whilst authorising {context.HttpContext.User.Identity.Name}. Setting pipeline error"); - - SetPipelineError(context, authorised.Errors); - return; - } - - if (IsAuthorised(authorised)) - { - Logger.LogInformation($"{context.HttpContext.User.Identity.Name} has succesfully been authorised for {context.DownstreamReRoute.UpstreamPathTemplate.OriginalValue}."); - await _next.Invoke(context); - } - else - { - Logger.LogWarning($"{context.HttpContext.User.Identity.Name} is not authorised to access {context.DownstreamReRoute.UpstreamPathTemplate.OriginalValue}. Setting pipeline error"); - - SetPipelineError(context, new UnauthorisedError($"{context.HttpContext.User.Identity.Name} is not authorised to access {context.DownstreamReRoute.UpstreamPathTemplate.OriginalValue}")); - } - } - else - { - Logger.LogInformation($"{context.DownstreamReRoute.DownstreamPathTemplate.Value} route does not require user to be authorised"); - await _next.Invoke(context); - } - } - - private static bool IsAuthorised(Response authorised) - { - return authorised.Data; - } - - private static bool IsAuthenticatedRoute(DownstreamReRoute reRoute) - { - return reRoute.IsAuthenticated; - } - - private static bool IsAuthorisedRoute(DownstreamReRoute reRoute) - { - return reRoute.IsAuthorised; - } - - private static bool IsOptionsHttpMethod(DownstreamContext context) - { - return context.HttpContext.Request.Method.ToUpper() == "OPTIONS"; - } - } -} diff --git a/src/Ocelot/Authorisation/Middleware/AuthorisationMiddlewareMiddlewareExtensions.cs b/src/Ocelot/Authorisation/Middleware/AuthorisationMiddlewareMiddlewareExtensions.cs deleted file mode 100644 index a4999381c..000000000 --- a/src/Ocelot/Authorisation/Middleware/AuthorisationMiddlewareMiddlewareExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Ocelot.Middleware.Pipeline; - -namespace Ocelot.Authorisation.Middleware -{ - public static class AuthorisationMiddlewareMiddlewareExtensions - { - public static IOcelotPipelineBuilder UseAuthorisationMiddleware(this IOcelotPipelineBuilder builder) - { - return builder.UseMiddleware(); - } - } -} diff --git a/src/Ocelot/Authorisation/ScopeNotAuthorisedError.cs b/src/Ocelot/Authorisation/ScopeNotAuthorisedError.cs deleted file mode 100644 index 1abf57152..000000000 --- a/src/Ocelot/Authorisation/ScopeNotAuthorisedError.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Ocelot.Errors; - -namespace Ocelot.Authorisation -{ - public class ScopeNotAuthorisedError : Error - { - public ScopeNotAuthorisedError(string message) - : base(message, OcelotErrorCode.ScopeNotAuthorisedError) - { - } - } -} diff --git a/src/Ocelot/Authorisation/UnauthorisedError.cs b/src/Ocelot/Authorisation/UnauthorisedError.cs deleted file mode 100644 index 5e8f054ce..000000000 --- a/src/Ocelot/Authorisation/UnauthorisedError.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Ocelot.Errors; - -namespace Ocelot.Authorisation -{ - public class UnauthorisedError : Error - { - public UnauthorisedError(string message) - : base(message, OcelotErrorCode.UnauthorizedError) - { - } - } -} diff --git a/src/Ocelot/Authorization/ClaimValueNotAuthorizedError.cs b/src/Ocelot/Authorization/ClaimValueNotAuthorizedError.cs new file mode 100644 index 000000000..2f6b72c4b --- /dev/null +++ b/src/Ocelot/Authorization/ClaimValueNotAuthorizedError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Authorization +{ + public class ClaimValueNotAuthorizedError : Error + { + public ClaimValueNotAuthorizedError(string message) + : base(message, OcelotErrorCode.ClaimValueNotAuthorizedError, 403) + { + } + } +} diff --git a/src/Ocelot/Authorisation/ClaimsAuthoriser.cs b/src/Ocelot/Authorization/ClaimsAuthorizer.cs similarity index 82% rename from src/Ocelot/Authorisation/ClaimsAuthoriser.cs rename to src/Ocelot/Authorization/ClaimsAuthorizer.cs index 5ae95e910..850f197de 100644 --- a/src/Ocelot/Authorisation/ClaimsAuthoriser.cs +++ b/src/Ocelot/Authorization/ClaimsAuthorizer.cs @@ -1,91 +1,93 @@ -using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.Responses; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Text.RegularExpressions; - -namespace Ocelot.Authorisation -{ - using Infrastructure.Claims.Parser; - - public class ClaimsAuthoriser : IClaimsAuthoriser - { - private readonly IClaimsParser _claimsParser; - - public ClaimsAuthoriser(IClaimsParser claimsParser) - { - _claimsParser = claimsParser; - } - - public Response Authorise( - ClaimsPrincipal claimsPrincipal, - Dictionary routeClaimsRequirement, - List urlPathPlaceholderNameAndValues +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text.RegularExpressions; + +using Ocelot.DownstreamRouteFinder.UrlMatcher; + +using Ocelot.Infrastructure.Claims.Parser; + +using Ocelot.Responses; + +namespace Ocelot.Authorization +{ + public class ClaimsAuthorizer : IClaimsAuthorizer + { + private readonly IClaimsParser _claimsParser; + + public ClaimsAuthorizer(IClaimsParser claimsParser) + { + _claimsParser = claimsParser; + } + + public Response Authorize( + ClaimsPrincipal claimsPrincipal, + Dictionary routeClaimsRequirement, + List urlPathPlaceholderNameAndValues ) - { - foreach (var required in routeClaimsRequirement) - { - var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, required.Key); - - if (values.IsError) - { - return new ErrorResponse(values.Errors); - } - - if (values.Data != null) + { + foreach (var required in routeClaimsRequirement) + { + var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, required.Key); + + if (values.IsError) { - // dynamic claim - var match = Regex.Match(required.Value, @"^{(?.+)}$"); - if (match.Success) - { + return new ErrorResponse(values.Errors); + } + + if (values.Data != null) + { + // dynamic claim + var match = Regex.Match(required.Value, @"^{(?.+)}$"); + if (match.Success) + { var variableName = match.Captures[0].Value; - - var matchingPlaceholders = urlPathPlaceholderNameAndValues.Where(p => p.Name.Equals(variableName)).Take(2).ToArray(); - if (matchingPlaceholders.Length == 1) + + var matchingPlaceholders = urlPathPlaceholderNameAndValues.Where(p => p.Name.Equals(variableName)).Take(2).ToArray(); + if (matchingPlaceholders.Length == 1) { - // match - var actualValue = matchingPlaceholders[0].Value; - var authorised = values.Data.Contains(actualValue); - if (!authorised) - { - return new ErrorResponse(new ClaimValueNotAuthorisedError( - $"dynamic claim value for {variableName} of {string.Join(", ", values.Data)} is not the same as required value: {actualValue}")); - } - } - else + // match + var actualValue = matchingPlaceholders[0].Value; + var authorized = values.Data.Contains(actualValue); + if (!authorized) + { + return new ErrorResponse(new ClaimValueNotAuthorizedError( + $"dynamic claim value for {variableName} of {string.Join(", ", values.Data)} is not the same as required value: {actualValue}")); + } + } + else { - // config error - if (matchingPlaceholders.Length == 0) - { - return new ErrorResponse(new ClaimValueNotAuthorisedError( - $"config error: requires variable claim value: {variableName} placeholders does not contain that variable: {string.Join(", ", urlPathPlaceholderNameAndValues.Select(p => p.Name))}")); - } - else - { - return new ErrorResponse(new ClaimValueNotAuthorisedError( - $"config error: requires variable claim value: {required.Value} but placeholders are ambiguous: {string.Join(", ", urlPathPlaceholderNameAndValues.Where(p => p.Name.Equals(variableName)).Select(p => p.Value))}")); - } - } - } - else + // config error + if (matchingPlaceholders.Length == 0) + { + return new ErrorResponse(new ClaimValueNotAuthorizedError( + $"config error: requires variable claim value: {variableName} placeholders does not contain that variable: {string.Join(", ", urlPathPlaceholderNameAndValues.Select(p => p.Name))}")); + } + else + { + return new ErrorResponse(new ClaimValueNotAuthorizedError( + $"config error: requires variable claim value: {required.Value} but placeholders are ambiguous: {string.Join(", ", urlPathPlaceholderNameAndValues.Where(p => p.Name.Equals(variableName)).Select(p => p.Value))}")); + } + } + } + else { - // static claim - var authorised = values.Data.Contains(required.Value); - if (!authorised) - { - return new ErrorResponse(new ClaimValueNotAuthorisedError( - $"claim value: {string.Join(", ", values.Data)} is not the same as required value: {required.Value} for type: {required.Key}")); - } - } - } - else - { - return new ErrorResponse(new UserDoesNotHaveClaimError($"user does not have claim {required.Key}")); - } - } - - return new OkResponse(true); - } - } + // static claim + var authorized = values.Data.Contains(required.Value); + if (!authorized) + { + return new ErrorResponse(new ClaimValueNotAuthorizedError( + $"claim value: {string.Join(", ", values.Data)} is not the same as required value: {required.Value} for type: {required.Key}")); + } + } + } + else + { + return new ErrorResponse(new UserDoesNotHaveClaimError($"user does not have claim {required.Key}")); + } + } + + return new OkResponse(true); + } + } } diff --git a/src/Ocelot/Authorization/IClaimsAuthorizer.cs b/src/Ocelot/Authorization/IClaimsAuthorizer.cs new file mode 100644 index 000000000..b0853f982 --- /dev/null +++ b/src/Ocelot/Authorization/IClaimsAuthorizer.cs @@ -0,0 +1,17 @@ +using System.Security.Claims; + +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Responses; +using System.Collections.Generic; + +namespace Ocelot.Authorization +{ + public interface IClaimsAuthorizer + { + Response Authorize( + ClaimsPrincipal claimsPrincipal, + Dictionary routeClaimsRequirement, + List urlPathPlaceholderNameAndValues + ); + } +} diff --git a/src/Ocelot/Authorization/IScopesAuthorizer.cs b/src/Ocelot/Authorization/IScopesAuthorizer.cs new file mode 100644 index 000000000..e82f5e862 --- /dev/null +++ b/src/Ocelot/Authorization/IScopesAuthorizer.cs @@ -0,0 +1,12 @@ +using System.Security.Claims; + +using Ocelot.Responses; +using System.Collections.Generic; + +namespace Ocelot.Authorization +{ + public interface IScopesAuthorizer + { + Response Authorize(ClaimsPrincipal claimsPrincipal, List routeAllowedScopes); + } +} diff --git a/src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs b/src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs new file mode 100644 index 000000000..b1bbcb7c7 --- /dev/null +++ b/src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs @@ -0,0 +1,116 @@ +using System.Threading.Tasks; + +using Ocelot.Configuration; + +using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Middleware; + +using Ocelot.Responses; + +namespace Ocelot.Authorization.Middleware +{ + public class AuthorizationMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IClaimsAuthorizer _claimsAuthorizer; + private readonly IScopesAuthorizer _scopesAuthorizer; + + public AuthorizationMiddleware(RequestDelegate next, + IClaimsAuthorizer claimsAuthorizer, + IScopesAuthorizer scopesAuthorizer, + IOcelotLoggerFactory loggerFactory) + : base(loggerFactory.CreateLogger()) + { + _next = next; + _claimsAuthorizer = claimsAuthorizer; + _scopesAuthorizer = scopesAuthorizer; + } + + public async Task Invoke(HttpContext httpContext) + { + var downstreamRoute = httpContext.Items.DownstreamRoute(); + + if (!IsOptionsHttpMethod(httpContext) && IsAuthenticatedRoute(downstreamRoute)) + { + Logger.LogInformation("route is authenticated scopes must be checked"); + + var authorized = _scopesAuthorizer.Authorize(httpContext.User, downstreamRoute.AuthenticationOptions.AllowedScopes); + + if (authorized.IsError) + { + Logger.LogWarning("error authorizing user scopes"); + + httpContext.Items.UpsertErrors(authorized.Errors); + return; + } + + if (IsAuthorized(authorized)) + { + Logger.LogInformation("user scopes is authorized calling next authorization checks"); + } + else + { + Logger.LogWarning("user scopes is not authorized setting pipeline error"); + + httpContext.Items.SetError(new UnauthorizedError( + $"{httpContext.User.Identity.Name} unable to access {downstreamRoute.UpstreamPathTemplate.OriginalValue}")); + } + } + + if (!IsOptionsHttpMethod(httpContext) && IsAuthorizedRoute(downstreamRoute)) + { + Logger.LogInformation("route is authorized"); + + var authorized = _claimsAuthorizer.Authorize(httpContext.User, downstreamRoute.RouteClaimsRequirement, httpContext.Items.TemplatePlaceholderNameAndValues()); + + if (authorized.IsError) + { + Logger.LogWarning($"Error whilst authorizing {httpContext.User.Identity.Name}. Setting pipeline error"); + + httpContext.Items.UpsertErrors(authorized.Errors); + return; + } + + if (IsAuthorized(authorized)) + { + 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"); + + 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"); + await _next.Invoke(httpContext); + } + } + + private static bool IsAuthorized(Response authorized) + { + return authorized.Data; + } + + private static bool IsAuthenticatedRoute(DownstreamRoute route) + { + return route.IsAuthenticated; + } + + private static bool IsAuthorizedRoute(DownstreamRoute route) + { + return route.IsAuthorized; + } + + private static bool IsOptionsHttpMethod(HttpContext httpContext) + { + return httpContext.Request.Method.ToUpper() == "OPTIONS"; + } + } +} diff --git a/src/Ocelot/Authorization/Middleware/AuthorizationMiddlewareMiddlewareExtensions.cs b/src/Ocelot/Authorization/Middleware/AuthorizationMiddlewareMiddlewareExtensions.cs new file mode 100644 index 000000000..c53701098 --- /dev/null +++ b/src/Ocelot/Authorization/Middleware/AuthorizationMiddlewareMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Authorization.Middleware +{ + public static class AuthorizationMiddlewareMiddlewareExtensions + { + public static IApplicationBuilder UseAuthorizationMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/src/Ocelot/Authorization/ScopeNotAuthorizedError.cs b/src/Ocelot/Authorization/ScopeNotAuthorizedError.cs new file mode 100644 index 000000000..e1dcade7b --- /dev/null +++ b/src/Ocelot/Authorization/ScopeNotAuthorizedError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Authorization +{ + public class ScopeNotAuthorizedError : Error + { + public ScopeNotAuthorizedError(string message) + : base(message, OcelotErrorCode.ScopeNotAuthorizedError, 403) + { + } + } +} diff --git a/src/Ocelot/Authorisation/ScopesAuthoriser.cs b/src/Ocelot/Authorization/ScopesAuthorizer.cs similarity index 55% rename from src/Ocelot/Authorisation/ScopesAuthoriser.cs rename to src/Ocelot/Authorization/ScopesAuthorizer.cs index 8344d80bc..a451021c8 100644 --- a/src/Ocelot/Authorisation/ScopesAuthoriser.cs +++ b/src/Ocelot/Authorization/ScopesAuthorizer.cs @@ -1,47 +1,47 @@ -using Ocelot.Responses; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; - -namespace Ocelot.Authorisation -{ - using Infrastructure.Claims.Parser; - - public class ScopesAuthoriser : IScopesAuthoriser - { - private readonly IClaimsParser _claimsParser; - private readonly string _scope = "scope"; - - public ScopesAuthoriser(IClaimsParser claimsParser) - { - _claimsParser = claimsParser; - } - - public Response Authorise(ClaimsPrincipal claimsPrincipal, List routeAllowedScopes) - { - if (routeAllowedScopes == null || routeAllowedScopes.Count == 0) - { - return new OkResponse(true); - } - - var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, _scope); - - if (values.IsError) - { - return new ErrorResponse(values.Errors); - } - - var userScopes = values.Data; - - var matchesScopes = routeAllowedScopes.Intersect(userScopes).ToList(); - - if (matchesScopes.Count == 0) - { - return new ErrorResponse( - new ScopeNotAuthorisedError($"no one user scope: '{string.Join(",", userScopes)}' match with some allowed scope: '{string.Join(",", routeAllowedScopes)}'")); - } - - return new OkResponse(true); - } - } -} +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; + +using Ocelot.Responses; +using Ocelot.Infrastructure.Claims.Parser; + +namespace Ocelot.Authorization +{ + public class ScopesAuthorizer : IScopesAuthorizer + { + private readonly IClaimsParser _claimsParser; + private const string Scope = "scope"; + + public ScopesAuthorizer(IClaimsParser claimsParser) + { + _claimsParser = claimsParser; + } + + public Response Authorize(ClaimsPrincipal claimsPrincipal, List routeAllowedScopes) + { + if (routeAllowedScopes == null || routeAllowedScopes.Count == 0) + { + return new OkResponse(true); + } + + var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, Scope); + + if (values.IsError) + { + return new ErrorResponse(values.Errors); + } + + var userScopes = values.Data; + + var matchesScopes = routeAllowedScopes.Intersect(userScopes); + + if (!matchesScopes.Any()) + { + return new ErrorResponse( + new ScopeNotAuthorizedError($"no one user scope: '{string.Join(',', userScopes)}' match with some allowed scope: '{string.Join(',', routeAllowedScopes)}'")); + } + + return new OkResponse(true); + } + } +} diff --git a/src/Ocelot/Authorization/UnauthorizedError.cs b/src/Ocelot/Authorization/UnauthorizedError.cs new file mode 100644 index 000000000..efe1d9ff7 --- /dev/null +++ b/src/Ocelot/Authorization/UnauthorizedError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Authorization +{ + public class UnauthorizedError : Error + { + public UnauthorizedError(string message) + : base(message, OcelotErrorCode.UnauthorizedError, 403) + { + } + } +} diff --git a/src/Ocelot/Authorisation/UserDoesNotHaveClaimError.cs b/src/Ocelot/Authorization/UserDoesNotHaveClaimError.cs similarity index 80% rename from src/Ocelot/Authorisation/UserDoesNotHaveClaimError.cs rename to src/Ocelot/Authorization/UserDoesNotHaveClaimError.cs index 2cf8211d5..9994aaee3 100644 --- a/src/Ocelot/Authorisation/UserDoesNotHaveClaimError.cs +++ b/src/Ocelot/Authorization/UserDoesNotHaveClaimError.cs @@ -1,12 +1,12 @@ using Ocelot.Errors; -namespace Ocelot.Authorisation +namespace Ocelot.Authorization { public class UserDoesNotHaveClaimError : Error { - public UserDoesNotHaveClaimError(string message) - : base(message, OcelotErrorCode.UserDoesNotHaveClaimError) + public UserDoesNotHaveClaimError(string message) + : base(message, OcelotErrorCode.UserDoesNotHaveClaimError, 403) { } } -} +} diff --git a/src/Ocelot/Cache/InMemoryCache.cs b/src/Ocelot/Cache/AspMemoryCache.cs similarity index 62% rename from src/Ocelot/Cache/InMemoryCache.cs rename to src/Ocelot/Cache/AspMemoryCache.cs index c0c892ade..878b48a88 100644 --- a/src/Ocelot/Cache/InMemoryCache.cs +++ b/src/Ocelot/Cache/AspMemoryCache.cs @@ -1,16 +1,18 @@ -namespace Ocelot.Cache -{ - using System; - using System.Collections.Generic; +using System; +using System.Collections.Generic; + +using Microsoft.Extensions.Caching.Memory; - public class InMemoryCache : IOcelotCache +namespace Ocelot.Cache +{ + public class AspMemoryCache : IOcelotCache { - private readonly Dictionary> _cache; + private readonly IMemoryCache _memoryCache; private readonly Dictionary> _regions; - public InMemoryCache() + public AspMemoryCache(IMemoryCache memoryCache) { - _cache = new Dictionary>(); + _memoryCache = memoryCache; _regions = new Dictionary>(); } @@ -21,32 +23,19 @@ public void Add(string key, T value, TimeSpan ttl, string region) return; } - var expires = DateTime.UtcNow.Add(ttl); + _memoryCache.Set(key, value, ttl); - _cache.Add(key, new CacheObject(value, expires)); - - if (_regions.ContainsKey(region)) - { - var current = _regions[region]; - if (!current.Contains(key)) - { - current.Add(key); - } - } - else - { - _regions.Add(region, new List { key }); - } + SetRegion(region, key); } - public void AddAndDelete(string key, T value, TimeSpan ttl, string region) + public T Get(string key, string region) { - if (_cache.ContainsKey(key)) + if (_memoryCache.TryGetValue(key, out T value)) { - _cache.Remove(key); + return value; } - Add(key, value, ttl, region); + return default(T); } public void ClearRegion(string region) @@ -56,26 +45,35 @@ public void ClearRegion(string region) var keys = _regions[region]; foreach (var key in keys) { - _cache.Remove(key); + _memoryCache.Remove(key); } } } - public T Get(string key, string region) + public void AddAndDelete(string key, T value, TimeSpan ttl, string region) { - if (_cache.ContainsKey(key)) + if (_memoryCache.TryGetValue(key, out T _)) { - var cached = _cache[key]; + _memoryCache.Remove(key); + } + + Add(key, value, ttl, region); + } - if (cached.Expires > DateTime.UtcNow) + private void SetRegion(string region, string key) + { + if (_regions.ContainsKey(region)) + { + var current = _regions[region]; + if (!current.Contains(key)) { - return cached.Value; + current.Add(key); } - - _cache.Remove(key); } - - return default(T); + else + { + _regions.Add(region, new List { key }); + } } } } diff --git a/src/Ocelot/Cache/CacheKeyGenerator.cs b/src/Ocelot/Cache/CacheKeyGenerator.cs index 29b2174ba..46b431566 100644 --- a/src/Ocelot/Cache/CacheKeyGenerator.cs +++ b/src/Ocelot/Cache/CacheKeyGenerator.cs @@ -1,22 +1,22 @@ -using Ocelot.Middleware; -using System.Text; +using System.Text; using System.Threading.Tasks; +using Ocelot.Request.Middleware; + namespace Ocelot.Cache { public class CacheKeyGenerator : ICacheKeyGenerator { - public string GenerateRequestCacheKey(DownstreamContext context) + public string GenerateRequestCacheKey(DownstreamRequest downstreamRequest) { - string hashedContent = null; - StringBuilder downStreamUrlKeyBuilder = new StringBuilder($"{context.DownstreamRequest.Method}-{context.DownstreamRequest.OriginalString}"); - if (context.DownstreamRequest.Content != null) + var downStreamUrlKeyBuilder = new StringBuilder($"{downstreamRequest.Method}-{downstreamRequest.OriginalString}"); + if (downstreamRequest.Content != null) { - string requestContentString = Task.Run(async () => await context.DownstreamRequest.Content.ReadAsStringAsync()).Result; + var requestContentString = Task.Run(async () => await downstreamRequest.Content.ReadAsStringAsync()).Result; downStreamUrlKeyBuilder.Append(requestContentString); } - hashedContent = MD5Helper.GenerateMd5(downStreamUrlKeyBuilder.ToString()); + var hashedContent = MD5Helper.GenerateMd5(downStreamUrlKeyBuilder.ToString()); return hashedContent; } } diff --git a/src/Ocelot/Cache/CacheObject.cs b/src/Ocelot/Cache/CacheObject.cs index ef310a069..baa4e3f6b 100644 --- a/src/Ocelot/Cache/CacheObject.cs +++ b/src/Ocelot/Cache/CacheObject.cs @@ -1,7 +1,7 @@ -namespace Ocelot.Cache -{ - using System; +using System; +namespace Ocelot.Cache +{ internal class CacheObject { public CacheObject(T value, DateTime expires) diff --git a/src/Ocelot/Cache/CachedResponse.cs b/src/Ocelot/Cache/CachedResponse.cs index aa2fafcc6..b0806252c 100644 --- a/src/Ocelot/Cache/CachedResponse.cs +++ b/src/Ocelot/Cache/CachedResponse.cs @@ -16,18 +16,18 @@ string reasonPhrase StatusCode = statusCode; Headers = headers ?? new Dictionary>(); ContentHeaders = contentHeaders ?? new Dictionary>(); - Body = body ?? ""; + Body = body ?? string.Empty; ReasonPhrase = reasonPhrase; } - public HttpStatusCode StatusCode { get; private set; } + public HttpStatusCode StatusCode { get; } - public Dictionary> Headers { get; private set; } + public Dictionary> Headers { get; } - public Dictionary> ContentHeaders { get; private set; } + public Dictionary> ContentHeaders { get; } - public string Body { get; private set; } + public string Body { get; } - public string ReasonPhrase { get; private set; } + public string ReasonPhrase { get; } } } diff --git a/src/Ocelot/Cache/ICacheKeyGenerator.cs b/src/Ocelot/Cache/ICacheKeyGenerator.cs index fc6ad52bb..32a1f989e 100644 --- a/src/Ocelot/Cache/ICacheKeyGenerator.cs +++ b/src/Ocelot/Cache/ICacheKeyGenerator.cs @@ -1,9 +1,9 @@ -using Ocelot.Middleware; +using Ocelot.Request.Middleware; namespace Ocelot.Cache { public interface ICacheKeyGenerator { - string GenerateRequestCacheKey(DownstreamContext context); + string GenerateRequestCacheKey(DownstreamRequest downstreamRequest); } } diff --git a/src/Ocelot/Cache/IRegionCreator.cs b/src/Ocelot/Cache/IRegionCreator.cs index f35c9a250..da1b042da 100644 --- a/src/Ocelot/Cache/IRegionCreator.cs +++ b/src/Ocelot/Cache/IRegionCreator.cs @@ -4,6 +4,6 @@ namespace Ocelot.Cache { public interface IRegionCreator { - string Create(FileReRoute reRoute); + string Create(FileRoute route); } -} \ No newline at end of file +} diff --git a/src/Ocelot/Cache/MD5Helper.cs b/src/Ocelot/Cache/MD5Helper.cs index ff8eaa30b..c268fb021 100644 --- a/src/Ocelot/Cache/MD5Helper.cs +++ b/src/Ocelot/Cache/MD5Helper.cs @@ -7,10 +7,10 @@ public static class MD5Helper { public static string GenerateMd5(byte[] contentBytes) { - MD5 md5 = MD5.Create(); - byte[] hash = md5.ComputeHash(contentBytes); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < hash.Length; i++) + var md5 = MD5.Create(); + var hash = md5.ComputeHash(contentBytes); + var sb = new StringBuilder(); + for (var i = 0; i < hash.Length; i++) { sb.Append(hash[i].ToString("X2")); } @@ -20,7 +20,7 @@ public static string GenerateMd5(byte[] contentBytes) public static string GenerateMd5(string contentString) { - byte[] contentBytes = Encoding.Unicode.GetBytes(contentString); + var contentBytes = Encoding.Unicode.GetBytes(contentString); return GenerateMd5(contentBytes); } } diff --git a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs index 3d37febca..7679ac617 100644 --- a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs +++ b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs @@ -1,52 +1,59 @@ -namespace Ocelot.Cache.Middleware -{ - using Ocelot.Logging; - using Ocelot.Middleware; - using System; - using System.IO; - using System.Linq; - using System.Net.Http; - using System.Text; - using System.Threading.Tasks; +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Middleware; +namespace Ocelot.Cache.Middleware +{ public class OutputCacheMiddleware : OcelotMiddleware { - private readonly OcelotRequestDelegate _next; + private readonly RequestDelegate _next; private readonly IOcelotCache _outputCache; - private readonly ICacheKeyGenerator _cacheGeneratot; + private readonly ICacheKeyGenerator _cacheGenerator; - public OutputCacheMiddleware(OcelotRequestDelegate next, + public OutputCacheMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IOcelotCache outputCache, - ICacheKeyGenerator cacheGeneratot) + ICacheKeyGenerator cacheGenerator) : base(loggerFactory.CreateLogger()) { _next = next; _outputCache = outputCache; - _cacheGeneratot = cacheGeneratot; + _cacheGenerator = cacheGenerator; } - public async Task Invoke(DownstreamContext context) + public async Task Invoke(HttpContext httpContext) { - if (!context.DownstreamReRoute.IsCached) + var downstreamRoute = httpContext.Items.DownstreamRoute(); + + if (!downstreamRoute.IsCached) { - await _next.Invoke(context); + await _next.Invoke(httpContext); return; } - var downstreamUrlKey = $"{context.DownstreamRequest.Method}-{context.DownstreamRequest.OriginalString}"; - string downStreamRequestCacheKey = _cacheGeneratot.GenerateRequestCacheKey(context); + var downstreamRequest = httpContext.Items.DownstreamRequest(); + + var downstreamUrlKey = $"{downstreamRequest.Method}-{downstreamRequest.OriginalString}"; + var downStreamRequestCacheKey = _cacheGenerator.GenerateRequestCacheKey(downstreamRequest); Logger.LogDebug($"Started checking cache for {downstreamUrlKey}"); - var cached = _outputCache.Get(downStreamRequestCacheKey, context.DownstreamReRoute.CacheOptions.Region); + var cached = _outputCache.Get(downStreamRequestCacheKey, downstreamRoute.CacheOptions.Region); if (cached != null) { Logger.LogDebug($"cache entry exists for {downstreamUrlKey}"); var response = CreateHttpResponseMessage(cached); - SetHttpResponseMessageThisRequest(context, response); + SetHttpResponseMessageThisRequest(httpContext, response); Logger.LogDebug($"finished returned cached response for {downstreamUrlKey}"); @@ -55,40 +62,28 @@ public async Task Invoke(DownstreamContext context) Logger.LogDebug($"no resonse cached for {downstreamUrlKey}"); - await _next.Invoke(context); + await _next.Invoke(httpContext); - if (context.IsError) + if (httpContext.Items.Errors().Count > 0) { Logger.LogDebug($"there was a pipeline error for {downstreamUrlKey}"); return; } - cached = await CreateCachedResponse(context.DownstreamResponse); + var downstreamResponse = httpContext.Items.DownstreamResponse(); - _outputCache.Add(downStreamRequestCacheKey, cached, TimeSpan.FromSeconds(context.DownstreamReRoute.CacheOptions.TtlSeconds), context.DownstreamReRoute.CacheOptions.Region); + cached = await CreateCachedResponse(downstreamResponse); + + _outputCache.Add(downStreamRequestCacheKey, cached, TimeSpan.FromSeconds(downstreamRoute.CacheOptions.TtlSeconds), downstreamRoute.CacheOptions.Region); Logger.LogDebug($"finished response added to cache for {downstreamUrlKey}"); } - private void SetHttpResponseMessageThisRequest(DownstreamContext context, + private static void SetHttpResponseMessageThisRequest(HttpContext context, DownstreamResponse response) { - context.DownstreamResponse = response; - } - - private string GenerateRequestCacheKey(DownstreamContext context) - { - string hashedContent = null; - StringBuilder downStreamUrlKeyBuilder = new StringBuilder($"{context.DownstreamRequest.Method}-{context.DownstreamRequest.OriginalString}"); - if (context.DownstreamRequest.Content != null) - { - string requestContentString = Task.Run(async () => await context.DownstreamRequest.Content?.ReadAsStringAsync()).Result; - downStreamUrlKeyBuilder.Append(requestContentString); - } - - hashedContent = MD5Helper.GenerateMd5(downStreamUrlKeyBuilder.ToString()); - return hashedContent; + context.Items.UpsertDownstreamResponse(response); } internal DownstreamResponse CreateHttpResponseMessage(CachedResponse cached) diff --git a/src/Ocelot/Cache/Middleware/OutputCacheMiddlewareExtensions.cs b/src/Ocelot/Cache/Middleware/OutputCacheMiddlewareExtensions.cs index fa80a2370..76e406eeb 100644 --- a/src/Ocelot/Cache/Middleware/OutputCacheMiddlewareExtensions.cs +++ b/src/Ocelot/Cache/Middleware/OutputCacheMiddlewareExtensions.cs @@ -1,13 +1,12 @@ -using Microsoft.AspNetCore.Builder; -using Ocelot.Middleware.Pipeline; - -namespace Ocelot.Cache.Middleware -{ - public static class OutputCacheMiddlewareExtensions - { - public static IOcelotPipelineBuilder UseOutputCacheMiddleware(this IOcelotPipelineBuilder builder) - { - return builder.UseMiddleware(); - } - } +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Cache.Middleware +{ + public static class OutputCacheMiddlewareExtensions + { + public static IApplicationBuilder UseOutputCacheMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } } diff --git a/src/Ocelot/Cache/RegionCreator.cs b/src/Ocelot/Cache/RegionCreator.cs index cc1a93592..737d886e5 100644 --- a/src/Ocelot/Cache/RegionCreator.cs +++ b/src/Ocelot/Cache/RegionCreator.cs @@ -1,22 +1,23 @@ -using Ocelot.Configuration.File; using System.Linq; + +using Ocelot.Configuration.File; namespace Ocelot.Cache { public class RegionCreator : IRegionCreator { - public string Create(FileReRoute reRoute) + public string Create(FileRoute route) { - if (!string.IsNullOrEmpty(reRoute?.FileCacheOptions?.Region)) + if (!string.IsNullOrEmpty(route?.FileCacheOptions?.Region)) { - return reRoute?.FileCacheOptions?.Region; + return route?.FileCacheOptions?.Region; } - var methods = string.Join("", reRoute.UpstreamHttpMethod.Select(m => m)); + var methods = string.Join(string.Empty, route.UpstreamHttpMethod.Select(m => m)); + + var region = $"{methods}{route.UpstreamPathTemplate.Replace("/", string.Empty)}"; - var region = $"{methods}{reRoute.UpstreamPathTemplate.Replace("/", "")}"; - return region; } } -} +} diff --git a/src/Ocelot/Cache/Regions.cs b/src/Ocelot/Cache/Regions.cs index 05f254cb8..cfb1142a1 100644 --- a/src/Ocelot/Cache/Regions.cs +++ b/src/Ocelot/Cache/Regions.cs @@ -1,7 +1,7 @@ +using System.Collections.Generic; + namespace Ocelot.Cache { - using System.Collections.Generic; - public class Regions { public Regions(List value) diff --git a/src/Ocelot/Claims/AddClaimsToRequest.cs b/src/Ocelot/Claims/AddClaimsToRequest.cs index ab197c2b0..56e9f2824 100644 --- a/src/Ocelot/Claims/AddClaimsToRequest.cs +++ b/src/Ocelot/Claims/AddClaimsToRequest.cs @@ -1,10 +1,12 @@ -using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; + +using Microsoft.AspNetCore.Http; + using Ocelot.Configuration; using Ocelot.Infrastructure.Claims.Parser; using Ocelot.Responses; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; namespace Ocelot.Claims { @@ -37,7 +39,7 @@ public Response SetClaimsOnContext(List claimsToThings, HttpContex identity?.RemoveClaim(exists); } - identity?.AddClaim(new System.Security.Claims.Claim(config.ExistingKey, value.Data)); + identity?.AddClaim(new Claim(config.ExistingKey, value.Data)); } return new OkResponse(); diff --git a/src/Ocelot/Claims/IAddClaimsToRequest.cs b/src/Ocelot/Claims/IAddClaimsToRequest.cs index b9db39f5b..902df93af 100644 --- a/src/Ocelot/Claims/IAddClaimsToRequest.cs +++ b/src/Ocelot/Claims/IAddClaimsToRequest.cs @@ -1,7 +1,9 @@ -using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +using Microsoft.AspNetCore.Http; + using Ocelot.Configuration; -using Ocelot.Responses; -using System.Collections.Generic; +using Ocelot.Responses; namespace Ocelot.Claims { diff --git a/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddlewareExtensions.cs b/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddlewareExtensions.cs index 2751d23ac..eee0bb343 100644 --- a/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddlewareExtensions.cs +++ b/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddlewareExtensions.cs @@ -1,10 +1,10 @@ -using Ocelot.Middleware.Pipeline; +using Microsoft.AspNetCore.Builder; namespace Ocelot.Claims.Middleware { public static class ClaimsBuilderMiddlewareExtensions { - public static IOcelotPipelineBuilder UseClaimsToClaimsMiddleware(this IOcelotPipelineBuilder builder) + public static IApplicationBuilder UseClaimsToClaimsMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware(); } diff --git a/src/Ocelot/Claims/Middleware/ClaimsToClaimsMiddleware.cs b/src/Ocelot/Claims/Middleware/ClaimsToClaimsMiddleware.cs index 58838a971..e163a8849 100644 --- a/src/Ocelot/Claims/Middleware/ClaimsToClaimsMiddleware.cs +++ b/src/Ocelot/Claims/Middleware/ClaimsToClaimsMiddleware.cs @@ -1,42 +1,48 @@ -using Ocelot.Logging; -using Ocelot.Middleware; -using System.Linq; +using System.Linq; using System.Threading.Tasks; -namespace Ocelot.Claims.Middleware -{ - public class ClaimsToClaimsMiddleware : OcelotMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly IAddClaimsToRequest _addClaimsToRequest; - - public ClaimsToClaimsMiddleware(OcelotRequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IAddClaimsToRequest addClaimsToRequest) - : base(loggerFactory.CreateLogger()) - { - _next = next; - _addClaimsToRequest = addClaimsToRequest; - } - - public async Task Invoke(DownstreamContext context) - { - if (context.DownstreamReRoute.ClaimsToClaims.Any()) - { - Logger.LogDebug("this route has instructions to convert claims to other claims"); +using Ocelot.Logging; - var result = _addClaimsToRequest.SetClaimsOnContext(context.DownstreamReRoute.ClaimsToClaims, context.HttpContext); +using Microsoft.AspNetCore.Http; - if (result.IsError) - { - Logger.LogDebug("error converting claims to other claims, setting pipeline error"); - - SetPipelineError(context, result.Errors); - return; - } - } - - await _next.Invoke(context); - } - } -} +using Ocelot.Middleware; + +namespace Ocelot.Claims.Middleware +{ + public class ClaimsToClaimsMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IAddClaimsToRequest _addClaimsToRequest; + + public ClaimsToClaimsMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IAddClaimsToRequest addClaimsToRequest) + : base(loggerFactory.CreateLogger()) + { + _next = next; + _addClaimsToRequest = addClaimsToRequest; + } + + public async Task Invoke(HttpContext httpContext) + { + var downstreamRoute = httpContext.Items.DownstreamRoute(); + + if (downstreamRoute.ClaimsToClaims.Any()) + { + Logger.LogDebug("this route has instructions to convert claims to other claims"); + + var result = _addClaimsToRequest.SetClaimsOnContext(downstreamRoute.ClaimsToClaims, httpContext); + + if (result.IsError) + { + Logger.LogDebug("error converting claims to other claims, setting pipeline error"); + + httpContext.Items.UpsertErrors(result.Errors); + return; + } + } + + await _next.Invoke(httpContext); + } + } +} diff --git a/src/Ocelot/Configuration/AuthenticationOptions.cs b/src/Ocelot/Configuration/AuthenticationOptions.cs index 9cede7954..d9a912b53 100644 --- a/src/Ocelot/Configuration/AuthenticationOptions.cs +++ b/src/Ocelot/Configuration/AuthenticationOptions.cs @@ -10,7 +10,7 @@ public AuthenticationOptions(List allowedScopes, string authenticationPr AuthenticationProviderKey = authenticationProviderKey; } - public List AllowedScopes { get; private set; } - public string AuthenticationProviderKey { get; private set; } + public List AllowedScopes { get; } + public string AuthenticationProviderKey { get; } } } diff --git a/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs index 352bc8a6b..65408e3a4 100644 --- a/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs +++ b/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs @@ -4,7 +4,7 @@ namespace Ocelot.Configuration.Builder { public class AuthenticationOptionsBuilder { - private List _allowedScopes = new List(); + private List _allowedScopes = new(); private string _authenticationProviderKey; public AuthenticationOptionsBuilder WithAllowedScopes(List allowedScopes) @@ -24,4 +24,4 @@ public AuthenticationOptions Build() return new AuthenticationOptions(_allowedScopes, _authenticationProviderKey); } } -} \ No newline at end of file +} diff --git a/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs deleted file mode 100644 index 4b3e6ea30..000000000 --- a/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs +++ /dev/null @@ -1,288 +0,0 @@ -using Ocelot.Configuration.Creator; -using Ocelot.Values; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; - -namespace Ocelot.Configuration.Builder -{ - public class DownstreamReRouteBuilder - { - private AuthenticationOptions _authenticationOptions; - private string _loadBalancerKey; - private string _downstreamPathTemplate; - private UpstreamPathTemplate _upstreamTemplatePattern; - private List _upstreamHttpMethod; - private bool _isAuthenticated; - private List _claimsToHeaders; - private List _claimToClaims; - private Dictionary _routeClaimRequirement; - private bool _isAuthorised; - private List _claimToQueries; - private List _claimToDownstreamPath; - private string _requestIdHeaderKey; - private bool _isCached; - private CacheOptions _fileCacheOptions; - private string _downstreamScheme; - private LoadBalancerOptions _loadBalancerOptions; - private QoSOptions _qosOptions; - private HttpHandlerOptions _httpHandlerOptions; - private bool _enableRateLimiting; - private RateLimitOptions _rateLimitOptions; - private bool _useServiceDiscovery; - private string _serviceName; - private string _serviceNamespace; - private List _upstreamHeaderFindAndReplace; - private List _downstreamHeaderFindAndReplace; - private readonly List _downstreamAddresses; - private string _key; - private List _delegatingHandlers; - private List _addHeadersToDownstream; - private List _addHeadersToUpstream; - private bool _dangerousAcceptAnyServerCertificateValidator; - private SecurityOptions _securityOptions; - - public DownstreamReRouteBuilder() - { - _downstreamAddresses = new List(); - _delegatingHandlers = new List(); - _addHeadersToDownstream = new List(); - _addHeadersToUpstream = new List(); - } - - public DownstreamReRouteBuilder WithDownstreamAddresses(List downstreamAddresses) - { - _downstreamAddresses.AddRange(downstreamAddresses); - return this; - } - - public DownstreamReRouteBuilder WithLoadBalancerOptions(LoadBalancerOptions loadBalancerOptions) - { - _loadBalancerOptions = loadBalancerOptions; - return this; - } - - public DownstreamReRouteBuilder WithDownstreamScheme(string downstreamScheme) - { - _downstreamScheme = downstreamScheme; - return this; - } - - public DownstreamReRouteBuilder WithDownstreamPathTemplate(string input) - { - _downstreamPathTemplate = input; - return this; - } - - public DownstreamReRouteBuilder WithUpstreamPathTemplate(UpstreamPathTemplate input) - { - _upstreamTemplatePattern = input; - return this; - } - - public DownstreamReRouteBuilder WithUpstreamHttpMethod(List input) - { - _upstreamHttpMethod = (input.Count == 0) ? new List() : input.Select(x => new HttpMethod(x.Trim())).ToList(); - return this; - } - - public DownstreamReRouteBuilder WithIsAuthenticated(bool input) - { - _isAuthenticated = input; - return this; - } - - public DownstreamReRouteBuilder WithIsAuthorised(bool input) - { - _isAuthorised = input; - return this; - } - - public DownstreamReRouteBuilder WithRequestIdKey(string input) - { - _requestIdHeaderKey = input; - return this; - } - - public DownstreamReRouteBuilder WithClaimsToHeaders(List input) - { - _claimsToHeaders = input; - return this; - } - - public DownstreamReRouteBuilder WithClaimsToClaims(List input) - { - _claimToClaims = input; - return this; - } - - public DownstreamReRouteBuilder WithRouteClaimsRequirement(Dictionary input) - { - _routeClaimRequirement = input; - return this; - } - - public DownstreamReRouteBuilder WithClaimsToQueries(List input) - { - _claimToQueries = input; - return this; - } - - public DownstreamReRouteBuilder WithClaimsToDownstreamPath(List input) - { - _claimToDownstreamPath = input; - return this; - } - - public DownstreamReRouteBuilder WithIsCached(bool input) - { - _isCached = input; - return this; - } - - public DownstreamReRouteBuilder WithCacheOptions(CacheOptions input) - { - _fileCacheOptions = input; - return this; - } - - public DownstreamReRouteBuilder WithQosOptions(QoSOptions input) - { - _qosOptions = input; - return this; - } - - public DownstreamReRouteBuilder WithLoadBalancerKey(string loadBalancerKey) - { - _loadBalancerKey = loadBalancerKey; - return this; - } - - public DownstreamReRouteBuilder WithAuthenticationOptions(AuthenticationOptions authenticationOptions) - { - _authenticationOptions = authenticationOptions; - return this; - } - - public DownstreamReRouteBuilder WithEnableRateLimiting(bool input) - { - _enableRateLimiting = input; - return this; - } - - public DownstreamReRouteBuilder WithRateLimitOptions(RateLimitOptions input) - { - _rateLimitOptions = input; - return this; - } - - public DownstreamReRouteBuilder WithHttpHandlerOptions(HttpHandlerOptions input) - { - _httpHandlerOptions = input; - return this; - } - - public DownstreamReRouteBuilder WithUseServiceDiscovery(bool useServiceDiscovery) - { - _useServiceDiscovery = useServiceDiscovery; - return this; - } - - public DownstreamReRouteBuilder WithServiceName(string serviceName) - { - _serviceName = serviceName; - return this; - } - - public DownstreamReRouteBuilder WithServiceNamespace(string serviceNamespace) - { - _serviceNamespace = serviceNamespace; - return this; - } - - public DownstreamReRouteBuilder WithUpstreamHeaderFindAndReplace(List upstreamHeaderFindAndReplace) - { - _upstreamHeaderFindAndReplace = upstreamHeaderFindAndReplace; - return this; - } - - public DownstreamReRouteBuilder WithDownstreamHeaderFindAndReplace(List downstreamHeaderFindAndReplace) - { - _downstreamHeaderFindAndReplace = downstreamHeaderFindAndReplace; - return this; - } - - public DownstreamReRouteBuilder WithKey(string key) - { - _key = key; - return this; - } - - public DownstreamReRouteBuilder WithDelegatingHandlers(List delegatingHandlers) - { - _delegatingHandlers = delegatingHandlers; - return this; - } - - public DownstreamReRouteBuilder WithAddHeadersToDownstream(List addHeadersToDownstream) - { - _addHeadersToDownstream = addHeadersToDownstream; - return this; - } - - public DownstreamReRouteBuilder WithAddHeadersToUpstream(List addHeadersToUpstream) - { - _addHeadersToUpstream = addHeadersToUpstream; - return this; - } - - public DownstreamReRouteBuilder WithDangerousAcceptAnyServerCertificateValidator(bool dangerousAcceptAnyServerCertificateValidator) - { - _dangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; - return this; - } - - public DownstreamReRouteBuilder WithSecurityOptions(SecurityOptions securityOptions) - { - _securityOptions = securityOptions; - return this; - } - - public DownstreamReRoute Build() - { - return new DownstreamReRoute( - _key, - _upstreamTemplatePattern, - _upstreamHeaderFindAndReplace, - _downstreamHeaderFindAndReplace, - _downstreamAddresses, - _serviceName, - _serviceNamespace, - _httpHandlerOptions, - _useServiceDiscovery, - _enableRateLimiting, - _qosOptions, - _downstreamScheme, - _requestIdHeaderKey, - _isCached, - _fileCacheOptions, - _loadBalancerOptions, - _rateLimitOptions, - _routeClaimRequirement, - _claimToQueries, - _claimsToHeaders, - _claimToClaims, - _claimToDownstreamPath, - _isAuthenticated, - _isAuthorised, - _authenticationOptions, - new DownstreamPathTemplate(_downstreamPathTemplate), - _loadBalancerKey, - _delegatingHandlers, - _addHeadersToDownstream, - _addHeadersToUpstream, - _dangerousAcceptAnyServerCertificateValidator, - _securityOptions); - } - } -} diff --git a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs new file mode 100644 index 000000000..e185bf584 --- /dev/null +++ b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs @@ -0,0 +1,304 @@ +using Ocelot.Configuration.Creator; +using Ocelot.Values; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; + +namespace Ocelot.Configuration.Builder; + +public class DownstreamRouteBuilder +{ + private AuthenticationOptions _authenticationOptions; + private string _loadBalancerKey; + private string _downstreamPathTemplate; + private UpstreamPathTemplate _upstreamTemplatePattern; + private List _upstreamHttpMethod; + private bool _isAuthenticated; + private List _claimsToHeaders; + private List _claimToClaims; + private Dictionary _routeClaimRequirement; + private bool _isAuthorized; + private List _claimToQueries; + private List _claimToDownstreamPath; + private string _requestIdHeaderKey; + private bool _isCached; + private CacheOptions _fileCacheOptions; + private string _downstreamScheme; + private LoadBalancerOptions _loadBalancerOptions; + private QoSOptions _qosOptions; + private HttpHandlerOptions _httpHandlerOptions; + private bool _enableRateLimiting; + private RateLimitOptions _rateLimitOptions; + private bool _useServiceDiscovery; + private string _serviceName; + private string _serviceNamespace; + private List _upstreamHeaderFindAndReplace; + private List _downstreamHeaderFindAndReplace; + private readonly List _downstreamAddresses; + private string _key; + private List _delegatingHandlers; + private List _addHeadersToDownstream; + private List _addHeadersToUpstream; + private bool _dangerousAcceptAnyServerCertificateValidator; + private SecurityOptions _securityOptions; + private string _downstreamHttpMethod; + private Version _downstreamHttpVersion; + + public DownstreamRouteBuilder() + { + _downstreamAddresses = new List(); + _delegatingHandlers = new List(); + _addHeadersToDownstream = new List(); + _addHeadersToUpstream = new List(); + } + + public DownstreamRouteBuilder WithDownstreamAddresses(List downstreamAddresses) + { + _downstreamAddresses.AddRange(downstreamAddresses); + return this; + } + + public DownstreamRouteBuilder WithDownStreamHttpMethod(string method) + { + _downstreamHttpMethod = method; + return this; + } + + public DownstreamRouteBuilder WithLoadBalancerOptions(LoadBalancerOptions loadBalancerOptions) + { + _loadBalancerOptions = loadBalancerOptions; + return this; + } + + public DownstreamRouteBuilder WithDownstreamScheme(string downstreamScheme) + { + _downstreamScheme = downstreamScheme; + return this; + } + + public DownstreamRouteBuilder WithDownstreamPathTemplate(string input) + { + _downstreamPathTemplate = input; + return this; + } + + public DownstreamRouteBuilder WithUpstreamPathTemplate(UpstreamPathTemplate input) + { + _upstreamTemplatePattern = input; + return this; + } + + public DownstreamRouteBuilder WithUpstreamHttpMethod(List input) + { + _upstreamHttpMethod = (input.Count == 0) ? new List() : input.Select(x => new HttpMethod(x.Trim())).ToList(); + return this; + } + + public DownstreamRouteBuilder WithIsAuthenticated(bool input) + { + _isAuthenticated = input; + return this; + } + + public DownstreamRouteBuilder WithIsAuthorized(bool input) + { + _isAuthorized = input; + return this; + } + + public DownstreamRouteBuilder WithRequestIdKey(string input) + { + _requestIdHeaderKey = input; + return this; + } + + public DownstreamRouteBuilder WithClaimsToHeaders(List input) + { + _claimsToHeaders = input; + return this; + } + + public DownstreamRouteBuilder WithClaimsToClaims(List input) + { + _claimToClaims = input; + return this; + } + + public DownstreamRouteBuilder WithRouteClaimsRequirement(Dictionary input) + { + _routeClaimRequirement = input; + return this; + } + + public DownstreamRouteBuilder WithClaimsToQueries(List input) + { + _claimToQueries = input; + return this; + } + + public DownstreamRouteBuilder WithClaimsToDownstreamPath(List input) + { + _claimToDownstreamPath = input; + return this; + } + + public DownstreamRouteBuilder WithIsCached(bool input) + { + _isCached = input; + return this; + } + + public DownstreamRouteBuilder WithCacheOptions(CacheOptions input) + { + _fileCacheOptions = input; + return this; + } + + public DownstreamRouteBuilder WithQosOptions(QoSOptions input) + { + _qosOptions = input; + return this; + } + + public DownstreamRouteBuilder WithLoadBalancerKey(string loadBalancerKey) + { + _loadBalancerKey = loadBalancerKey; + return this; + } + + public DownstreamRouteBuilder WithAuthenticationOptions(AuthenticationOptions authenticationOptions) + { + _authenticationOptions = authenticationOptions; + return this; + } + + public DownstreamRouteBuilder WithEnableRateLimiting(bool input) + { + _enableRateLimiting = input; + return this; + } + + public DownstreamRouteBuilder WithRateLimitOptions(RateLimitOptions input) + { + _rateLimitOptions = input; + return this; + } + + public DownstreamRouteBuilder WithHttpHandlerOptions(HttpHandlerOptions input) + { + _httpHandlerOptions = input; + return this; + } + + public DownstreamRouteBuilder WithUseServiceDiscovery(bool useServiceDiscovery) + { + _useServiceDiscovery = useServiceDiscovery; + return this; + } + + public DownstreamRouteBuilder WithServiceName(string serviceName) + { + _serviceName = serviceName; + return this; + } + + public DownstreamRouteBuilder WithServiceNamespace(string serviceNamespace) + { + _serviceNamespace = serviceNamespace; + return this; + } + + public DownstreamRouteBuilder WithUpstreamHeaderFindAndReplace(List upstreamHeaderFindAndReplace) + { + _upstreamHeaderFindAndReplace = upstreamHeaderFindAndReplace; + return this; + } + + public DownstreamRouteBuilder WithDownstreamHeaderFindAndReplace(List downstreamHeaderFindAndReplace) + { + _downstreamHeaderFindAndReplace = downstreamHeaderFindAndReplace; + return this; + } + + public DownstreamRouteBuilder WithKey(string key) + { + _key = key; + return this; + } + + public DownstreamRouteBuilder WithDelegatingHandlers(List delegatingHandlers) + { + _delegatingHandlers = delegatingHandlers; + return this; + } + + public DownstreamRouteBuilder WithAddHeadersToDownstream(List addHeadersToDownstream) + { + _addHeadersToDownstream = addHeadersToDownstream; + return this; + } + + public DownstreamRouteBuilder WithAddHeadersToUpstream(List addHeadersToUpstream) + { + _addHeadersToUpstream = addHeadersToUpstream; + return this; + } + + public DownstreamRouteBuilder WithDangerousAcceptAnyServerCertificateValidator(bool dangerousAcceptAnyServerCertificateValidator) + { + _dangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; + return this; + } + + public DownstreamRouteBuilder WithSecurityOptions(SecurityOptions securityOptions) + { + _securityOptions = securityOptions; + return this; + } + + public DownstreamRouteBuilder WithDownstreamHttpVersion(Version downstreamHttpVersion) + { + _downstreamHttpVersion = downstreamHttpVersion; + return this; + } + + public DownstreamRoute Build() + { + return new DownstreamRoute( + _key, + _upstreamTemplatePattern, + _upstreamHeaderFindAndReplace, + _downstreamHeaderFindAndReplace, + _downstreamAddresses, + _serviceName, + _serviceNamespace, + _httpHandlerOptions, + _useServiceDiscovery, + _enableRateLimiting, + _qosOptions, + _downstreamScheme, + _requestIdHeaderKey, + _isCached, + _fileCacheOptions, + _loadBalancerOptions, + _rateLimitOptions, + _routeClaimRequirement, + _claimToQueries, + _claimsToHeaders, + _claimToClaims, + _claimToDownstreamPath, + _isAuthenticated, + _isAuthorized, + _authenticationOptions, + new DownstreamPathTemplate(_downstreamPathTemplate), + _loadBalancerKey, + _delegatingHandlers, + _addHeadersToDownstream, + _addHeadersToUpstream, + _dangerousAcceptAnyServerCertificateValidator, + _securityOptions, + _downstreamHttpMethod, + _downstreamHttpVersion); + } +} diff --git a/src/Ocelot/Configuration/Builder/QoSOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/QoSOptionsBuilder.cs index 413163dbc..717918485 100644 --- a/src/Ocelot/Configuration/Builder/QoSOptionsBuilder.cs +++ b/src/Ocelot/Configuration/Builder/QoSOptionsBuilder.cs @@ -1,42 +1,42 @@ -namespace Ocelot.Configuration.Builder -{ - public class QoSOptionsBuilder - { - private int _exceptionsAllowedBeforeBreaking; - - private int _durationOfBreak; - - private int _timeoutValue; - - private string _key; - - public QoSOptionsBuilder WithExceptionsAllowedBeforeBreaking(int exceptionsAllowedBeforeBreaking) - { - _exceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking; - return this; - } - - public QoSOptionsBuilder WithDurationOfBreak(int durationOfBreak) - { - _durationOfBreak = durationOfBreak; - return this; - } - - public QoSOptionsBuilder WithTimeoutValue(int timeoutValue) - { - _timeoutValue = timeoutValue; - return this; - } - - public QoSOptionsBuilder WithKey(string input) - { - _key = input; - return this; - } - - public QoSOptions Build() - { - return new QoSOptions(_exceptionsAllowedBeforeBreaking, _durationOfBreak, _timeoutValue, _key); - } - } +namespace Ocelot.Configuration.Builder +{ + public class QoSOptionsBuilder + { + private int _exceptionsAllowedBeforeBreaking; + + private int _durationOfBreak; + + private int _timeoutValue; + + private string _key; + + public QoSOptionsBuilder WithExceptionsAllowedBeforeBreaking(int exceptionsAllowedBeforeBreaking) + { + _exceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking; + return this; + } + + public QoSOptionsBuilder WithDurationOfBreak(int durationOfBreak) + { + _durationOfBreak = durationOfBreak; + return this; + } + + public QoSOptionsBuilder WithTimeoutValue(int timeoutValue) + { + _timeoutValue = timeoutValue; + return this; + } + + public QoSOptionsBuilder WithKey(string input) + { + _key = input; + return this; + } + + public QoSOptions Build() + { + return new QoSOptions(_exceptionsAllowedBeforeBreaking, _durationOfBreak, _timeoutValue, _key); + } + } } diff --git a/src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs index eaaa4dab6..93ee9cb1e 100644 --- a/src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs +++ b/src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs @@ -7,7 +7,6 @@ public class RateLimitOptionsBuilder { private bool _enableRateLimiting; private string _clientIdHeader; - private List _clientWhitelist; private Func> _getClientWhitelist; private bool _disableRateLimitHeaders; private string _quotaExceededMessage; diff --git a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs deleted file mode 100644 index 4716c03af..000000000 --- a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs +++ /dev/null @@ -1,78 +0,0 @@ -namespace Ocelot.Configuration.Builder -{ - using Ocelot.Configuration.File; - using Ocelot.Values; - using System.Collections.Generic; - using System.Linq; - using System.Net.Http; - - public class ReRouteBuilder - { - private UpstreamPathTemplate _upstreamTemplatePattern; - private List _upstreamHttpMethod; - private string _upstreamHost; - private List _downstreamReRoutes; - private List _downstreamReRoutesConfig; - private string _aggregator; - - public ReRouteBuilder() - { - _downstreamReRoutes = new List(); - _downstreamReRoutesConfig = new List(); - } - - public ReRouteBuilder WithDownstreamReRoute(DownstreamReRoute value) - { - _downstreamReRoutes.Add(value); - return this; - } - - public ReRouteBuilder WithDownstreamReRoutes(List value) - { - _downstreamReRoutes = value; - return this; - } - - public ReRouteBuilder WithUpstreamHost(string upstreamAddresses) - { - _upstreamHost = upstreamAddresses; - return this; - } - - public ReRouteBuilder WithUpstreamPathTemplate(UpstreamPathTemplate input) - { - _upstreamTemplatePattern = input; - return this; - } - - public ReRouteBuilder WithUpstreamHttpMethod(List input) - { - _upstreamHttpMethod = (input.Count == 0) ? new List() : input.Select(x => new HttpMethod(x.Trim())).ToList(); - return this; - } - - public ReRouteBuilder WithAggregateReRouteConfig(List aggregateReRouteConfigs) - { - _downstreamReRoutesConfig = aggregateReRouteConfigs; - return this; - } - - public ReRouteBuilder WithAggregator(string aggregator) - { - _aggregator = aggregator; - return this; - } - - public ReRoute Build() - { - return new ReRoute( - _downstreamReRoutes, - _downstreamReRoutesConfig, - _upstreamHttpMethod, - _upstreamTemplatePattern, - _upstreamHost, - _aggregator - ); - } - } -} diff --git a/src/Ocelot/Configuration/Builder/ReRouteOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/ReRouteOptionsBuilder.cs deleted file mode 100644 index 53fc54c79..000000000 --- a/src/Ocelot/Configuration/Builder/ReRouteOptionsBuilder.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace Ocelot.Configuration.Builder -{ - public class ReRouteOptionsBuilder - { - private bool _isAuthenticated; - private bool _isAuthorised; - private bool _isCached; - private bool _enableRateLimiting; - private bool _useServiceDiscovery; - - public ReRouteOptionsBuilder WithIsCached(bool isCached) - { - _isCached = isCached; - return this; - } - - public ReRouteOptionsBuilder WithIsAuthenticated(bool isAuthenticated) - { - _isAuthenticated = isAuthenticated; - return this; - } - - public ReRouteOptionsBuilder WithIsAuthorised(bool isAuthorised) - { - _isAuthorised = isAuthorised; - return this; - } - - public ReRouteOptionsBuilder WithRateLimiting(bool enableRateLimiting) - { - _enableRateLimiting = enableRateLimiting; - return this; - } - - public ReRouteOptionsBuilder WithUseServiceDiscovery(bool useServiceDiscovery) - { - _useServiceDiscovery = useServiceDiscovery; - return this; - } - - public ReRouteOptions Build() - { - return new ReRouteOptions(_isAuthenticated, _isAuthorised, _isCached, _enableRateLimiting, _useServiceDiscovery); - } - } -} diff --git a/src/Ocelot/Configuration/Builder/RouteBuilder.cs b/src/Ocelot/Configuration/Builder/RouteBuilder.cs new file mode 100644 index 000000000..8129606cf --- /dev/null +++ b/src/Ocelot/Configuration/Builder/RouteBuilder.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; + +using Ocelot.Configuration.File; + +using Ocelot.Values; + +namespace Ocelot.Configuration.Builder +{ + public class RouteBuilder + { + private UpstreamPathTemplate _upstreamTemplatePattern; + private List _upstreamHttpMethod; + private string _upstreamHost; + private List _downstreamRoutes; + private List _downstreamRoutesConfig; + private string _aggregator; + + public RouteBuilder() + { + _downstreamRoutes = new List(); + _downstreamRoutesConfig = new List(); + } + + public RouteBuilder WithDownstreamRoute(DownstreamRoute value) + { + _downstreamRoutes.Add(value); + return this; + } + + public RouteBuilder WithDownstreamRoutes(List value) + { + _downstreamRoutes = value; + return this; + } + + public RouteBuilder WithUpstreamHost(string upstreamAddresses) + { + _upstreamHost = upstreamAddresses; + return this; + } + + public RouteBuilder WithUpstreamPathTemplate(UpstreamPathTemplate input) + { + _upstreamTemplatePattern = input; + return this; + } + + public RouteBuilder WithUpstreamHttpMethod(List input) + { + _upstreamHttpMethod = (input.Count == 0) ? new List() : input.Select(x => new HttpMethod(x.Trim())).ToList(); + return this; + } + + public RouteBuilder WithAggregateRouteConfig(List aggregateRouteConfigs) + { + _downstreamRoutesConfig = aggregateRouteConfigs; + return this; + } + + public RouteBuilder WithAggregator(string aggregator) + { + _aggregator = aggregator; + return this; + } + + public Route Build() + { + return new Route( + _downstreamRoutes, + _downstreamRoutesConfig, + _upstreamHttpMethod, + _upstreamTemplatePattern, + _upstreamHost, + _aggregator + ); + } + } +} diff --git a/src/Ocelot/Configuration/Builder/RouteOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/RouteOptionsBuilder.cs new file mode 100644 index 000000000..7906dad39 --- /dev/null +++ b/src/Ocelot/Configuration/Builder/RouteOptionsBuilder.cs @@ -0,0 +1,46 @@ +namespace Ocelot.Configuration.Builder +{ + public class RouteOptionsBuilder + { + private bool _isAuthenticated; + private bool _isAuthorized; + private bool _isCached; + private bool _enableRateLimiting; + private bool _useServiceDiscovery; + + public RouteOptionsBuilder WithIsCached(bool isCached) + { + _isCached = isCached; + return this; + } + + public RouteOptionsBuilder WithIsAuthenticated(bool isAuthenticated) + { + _isAuthenticated = isAuthenticated; + return this; + } + + public RouteOptionsBuilder WithIsAuthorized(bool isAuthorized) + { + _isAuthorized = isAuthorized; + return this; + } + + public RouteOptionsBuilder WithRateLimiting(bool enableRateLimiting) + { + _enableRateLimiting = enableRateLimiting; + return this; + } + + public RouteOptionsBuilder WithUseServiceDiscovery(bool useServiceDiscovery) + { + _useServiceDiscovery = useServiceDiscovery; + return this; + } + + public RouteOptions Build() + { + return new RouteOptions(_isAuthenticated, _isAuthorized, _isCached, _enableRateLimiting, _useServiceDiscovery); + } + } +} diff --git a/src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs b/src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs index 16ec15d07..2d304700a 100644 --- a/src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs +++ b/src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs @@ -2,6 +2,7 @@ namespace Ocelot.Configuration.Builder { public class ServiceProviderConfigurationBuilder { + private string _serviceDiscoveryProviderScheme; private string _serviceDiscoveryProviderHost; private int _serviceDiscoveryProviderPort; private string _type; @@ -10,6 +11,12 @@ public class ServiceProviderConfigurationBuilder private int _pollingInterval; private string _namespace; + public ServiceProviderConfigurationBuilder WithScheme(string serviceDiscoveryProviderScheme) + { + _serviceDiscoveryProviderScheme = serviceDiscoveryProviderScheme; + return this; + } + public ServiceProviderConfigurationBuilder WithHost(string serviceDiscoveryProviderHost) { _serviceDiscoveryProviderHost = serviceDiscoveryProviderHost; @@ -54,7 +61,7 @@ public ServiceProviderConfigurationBuilder WithNamespace(string @namespace) public ServiceProviderConfiguration Build() { - return new ServiceProviderConfiguration(_type, _serviceDiscoveryProviderHost, _serviceDiscoveryProviderPort, _token, _configurationKey, _pollingInterval, _namespace); + return new ServiceProviderConfiguration(_type, _serviceDiscoveryProviderScheme, _serviceDiscoveryProviderHost, _serviceDiscoveryProviderPort, _token, _configurationKey, _pollingInterval, _namespace); } } } diff --git a/src/Ocelot/Configuration/Builder/UpstreamPathTemplateBuilder.cs b/src/Ocelot/Configuration/Builder/UpstreamPathTemplateBuilder.cs index 21b213025..cf3a1f012 100644 --- a/src/Ocelot/Configuration/Builder/UpstreamPathTemplateBuilder.cs +++ b/src/Ocelot/Configuration/Builder/UpstreamPathTemplateBuilder.cs @@ -1,7 +1,7 @@ -namespace Ocelot.Configuration.Builder -{ - using Values; +using Ocelot.Values; +namespace Ocelot.Configuration.Builder +{ public class UpstreamPathTemplateBuilder { private string _template; diff --git a/src/Ocelot/Configuration/CacheOptions.cs b/src/Ocelot/Configuration/CacheOptions.cs index e5738a46e..d509b38e9 100644 --- a/src/Ocelot/Configuration/CacheOptions.cs +++ b/src/Ocelot/Configuration/CacheOptions.cs @@ -8,8 +8,8 @@ public CacheOptions(int ttlSeconds, string region) Region = region; } - public int TtlSeconds { get; private set; } + public int TtlSeconds { get; } - public string Region { get; private set; } + public string Region { get; } } } diff --git a/src/Ocelot/Configuration/ChangeTracking/IOcelotConfigurationChangeTokenSource.cs b/src/Ocelot/Configuration/ChangeTracking/IOcelotConfigurationChangeTokenSource.cs new file mode 100644 index 000000000..a79c2e06b --- /dev/null +++ b/src/Ocelot/Configuration/ChangeTracking/IOcelotConfigurationChangeTokenSource.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Primitives; + +namespace Ocelot.Configuration.ChangeTracking +{ + /// + /// source which is activated when Ocelot's configuration is changed. + /// + public interface IOcelotConfigurationChangeTokenSource + { + IChangeToken ChangeToken { get; } + + void Activate(); + } +} diff --git a/src/Ocelot/Configuration/ChangeTracking/OcelotConfigurationChangeToken.cs b/src/Ocelot/Configuration/ChangeTracking/OcelotConfigurationChangeToken.cs new file mode 100644 index 000000000..7b584a8ee --- /dev/null +++ b/src/Ocelot/Configuration/ChangeTracking/OcelotConfigurationChangeToken.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; + +using Microsoft.Extensions.Primitives; + +namespace Ocelot.Configuration.ChangeTracking +{ + public class OcelotConfigurationChangeToken : IChangeToken + { + public const double PollingIntervalSeconds = 1; + + private readonly ICollection _callbacks = new List(); + private readonly object _lock = new(); + private DateTime? _timeChanged; + + public IDisposable RegisterChangeCallback(Action callback, object state) + { + lock (_lock) + { + var wrapper = new CallbackWrapper(callback, state, _callbacks, _lock); + _callbacks.Add(wrapper); + return wrapper; + } + } + + public void Activate() + { + lock (_lock) + { + _timeChanged = DateTime.UtcNow; + foreach (var wrapper in _callbacks) + { + wrapper.Invoke(); + } + } + } + + // Token stays active for PollingIntervalSeconds after a change (could be parameterised) - otherwise HasChanged would be true forever. + // Taking suggestions for better ways to reset HasChanged back to false. + public bool HasChanged => _timeChanged.HasValue && (DateTime.UtcNow - _timeChanged.Value).TotalSeconds < PollingIntervalSeconds; + + public bool ActiveChangeCallbacks => true; + + private class CallbackWrapper : IDisposable + { + private readonly ICollection _callbacks; + private readonly object _lock; + + public CallbackWrapper(Action callback, object state, ICollection callbacks, object @lock) + { + _callbacks = callbacks; + _lock = @lock; + Callback = callback; + State = state; + } + + public void Invoke() + { + Callback.Invoke(State); + } + + public void Dispose() + { + lock (_lock) + { + _callbacks.Remove(this); + } + } + + public Action Callback { get; } + + public object State { get; } + } + } +} diff --git a/src/Ocelot/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSource.cs b/src/Ocelot/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSource.cs new file mode 100644 index 000000000..049cac821 --- /dev/null +++ b/src/Ocelot/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSource.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Primitives; + +namespace Ocelot.Configuration.ChangeTracking +{ + public class OcelotConfigurationChangeTokenSource : IOcelotConfigurationChangeTokenSource + { + private readonly OcelotConfigurationChangeToken _changeToken = new(); + + public IChangeToken ChangeToken => _changeToken; + + public void Activate() + { + _changeToken.Activate(); + } + } +} diff --git a/src/Ocelot/Configuration/ChangeTracking/OcelotConfigurationMonitor.cs b/src/Ocelot/Configuration/ChangeTracking/OcelotConfigurationMonitor.cs new file mode 100644 index 000000000..f085d024d --- /dev/null +++ b/src/Ocelot/Configuration/ChangeTracking/OcelotConfigurationMonitor.cs @@ -0,0 +1,32 @@ +using System; + +using Microsoft.Extensions.Options; + +using Ocelot.Configuration.Repository; + +namespace Ocelot.Configuration.ChangeTracking +{ + public class OcelotConfigurationMonitor : IOptionsMonitor + { + private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource; + private readonly IInternalConfigurationRepository _repo; + + public OcelotConfigurationMonitor(IInternalConfigurationRepository repo, IOcelotConfigurationChangeTokenSource changeTokenSource) + { + _changeTokenSource = changeTokenSource; + _repo = repo; + } + + public IInternalConfiguration Get(string name) + { + return _repo.Get().Data; + } + + public IDisposable OnChange(Action listener) + { + return _changeTokenSource.ChangeToken.RegisterChangeCallback(_ => listener(CurrentValue, string.Empty), null); + } + + public IInternalConfiguration CurrentValue => _repo.Get().Data; + } +} diff --git a/src/Ocelot/Configuration/ClaimToThing.cs b/src/Ocelot/Configuration/ClaimToThing.cs index 9264de5f0..784dbff7f 100644 --- a/src/Ocelot/Configuration/ClaimToThing.cs +++ b/src/Ocelot/Configuration/ClaimToThing.cs @@ -10,9 +10,9 @@ public ClaimToThing(string existingKey, string newKey, string delimiter, int ind ExistingKey = existingKey; } - public string ExistingKey { get; private set; } - public string NewKey { get; private set; } - public string Delimiter { get; private set; } - public int Index { get; private set; } + public string ExistingKey { get; } + public string NewKey { get; } + public string Delimiter { get; } + public int Index { get; } } } diff --git a/src/Ocelot/Configuration/Creator/AggregatesCreator.cs b/src/Ocelot/Configuration/Creator/AggregatesCreator.cs index 1dd8fea51..0edbc1591 100644 --- a/src/Ocelot/Configuration/Creator/AggregatesCreator.cs +++ b/src/Ocelot/Configuration/Creator/AggregatesCreator.cs @@ -1,55 +1,57 @@ -namespace Ocelot.Configuration.Creator -{ - using Builder; - using File; - using System.Collections.Generic; - using System.Linq; - - public class AggregatesCreator : IAggregatesCreator - { - private readonly IUpstreamTemplatePatternCreator _creator; - - public AggregatesCreator(IUpstreamTemplatePatternCreator creator) - { - _creator = creator; - } - - public List Create(FileConfiguration fileConfiguration, List reRoutes) - { - return fileConfiguration.Aggregates - .Select(aggregate => SetUpAggregateReRoute(reRoutes, aggregate, fileConfiguration.GlobalConfiguration)) - .Where(aggregate => aggregate != null) - .ToList(); - } - - private ReRoute SetUpAggregateReRoute(IEnumerable reRoutes, FileAggregateReRoute aggregateReRoute, FileGlobalConfiguration globalConfiguration) - { - var applicableReRoutes = new List(); - var allReRoutes = reRoutes.SelectMany(x => x.DownstreamReRoute); - - foreach (var reRouteKey in aggregateReRoute.ReRouteKeys) - { - var selec = allReRoutes.FirstOrDefault(q => q.Key == reRouteKey); - if (selec == null) - { - return null; - } - - applicableReRoutes.Add(selec); - } - - var upstreamTemplatePattern = _creator.Create(aggregateReRoute); - - var reRoute = new ReRouteBuilder() - .WithUpstreamHttpMethod(aggregateReRoute.UpstreamHttpMethod) - .WithUpstreamPathTemplate(upstreamTemplatePattern) - .WithDownstreamReRoutes(applicableReRoutes) - .WithAggregateReRouteConfig(aggregateReRoute.ReRouteKeysConfig) - .WithUpstreamHost(aggregateReRoute.UpstreamHost) - .WithAggregator(aggregateReRoute.Aggregator) - .Build(); - - return reRoute; - } - } -} +using System.Collections.Generic; +using System.Linq; + +using Ocelot.Configuration.Builder; + +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public class AggregatesCreator : IAggregatesCreator + { + private readonly IUpstreamTemplatePatternCreator _creator; + + public AggregatesCreator(IUpstreamTemplatePatternCreator creator) + { + _creator = creator; + } + + public List Create(FileConfiguration fileConfiguration, List routes) + { + return fileConfiguration.Aggregates + .Select(aggregate => SetUpAggregateRoute(routes, aggregate, fileConfiguration.GlobalConfiguration)) + .Where(aggregate => aggregate != null) + .ToList(); + } + + private Route SetUpAggregateRoute(IEnumerable routes, FileAggregateRoute aggregateRoute, FileGlobalConfiguration globalConfiguration) + { + var applicableRoutes = new List(); + var allRoutes = routes.SelectMany(x => x.DownstreamRoute); + + foreach (var routeKey in aggregateRoute.RouteKeys) + { + var selec = allRoutes.FirstOrDefault(q => q.Key == routeKey); + if (selec == null) + { + return null; + } + + applicableRoutes.Add(selec); + } + + var upstreamTemplatePattern = _creator.Create(aggregateRoute); + + var route = new RouteBuilder() + .WithUpstreamHttpMethod(aggregateRoute.UpstreamHttpMethod) + .WithUpstreamPathTemplate(upstreamTemplatePattern) + .WithDownstreamRoutes(applicableRoutes) + .WithAggregateRouteConfig(aggregateRoute.RouteKeysConfig) + .WithUpstreamHost(aggregateRoute.UpstreamHost) + .WithAggregator(aggregateRoute.Aggregator) + .Build(); + + return route; + } + } +} diff --git a/src/Ocelot/Configuration/Creator/AuthenticationOptionsCreator.cs b/src/Ocelot/Configuration/Creator/AuthenticationOptionsCreator.cs index 69f4f49d2..275d6d90d 100644 --- a/src/Ocelot/Configuration/Creator/AuthenticationOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/AuthenticationOptionsCreator.cs @@ -4,9 +4,9 @@ namespace Ocelot.Configuration.Creator { public class AuthenticationOptionsCreator : IAuthenticationOptionsCreator { - public AuthenticationOptions Create(FileReRoute reRoute) + public AuthenticationOptions Create(FileRoute route) { - return new AuthenticationOptions(reRoute.AuthenticationOptions.AllowedScopes, reRoute.AuthenticationOptions.AuthenticationProviderKey); - } + return new AuthenticationOptions(route.AuthenticationOptions.AllowedScopes, route.AuthenticationOptions.AuthenticationProviderKey); + } } -} +} diff --git a/src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs b/src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs index 1500f5d3a..e14e13483 100644 --- a/src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs +++ b/src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs @@ -1,6 +1,7 @@ -using Ocelot.Configuration.Parser; -using Ocelot.Logging; using System.Collections.Generic; + +using Ocelot.Configuration.Parser; +using Ocelot.Logging; namespace Ocelot.Configuration.Creator { diff --git a/src/Ocelot/Configuration/Creator/ConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/ConfigurationCreator.cs index 1d5abc325..f9816a30b 100644 --- a/src/Ocelot/Configuration/Creator/ConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/ConfigurationCreator.cs @@ -1,55 +1,64 @@ -namespace Ocelot.Configuration.Creator -{ - using DependencyInjection; - using File; - using Microsoft.Extensions.DependencyInjection; - using System; - using System.Collections.Generic; +using System; +using System.Collections.Generic; - public class ConfigurationCreator : IConfigurationCreator - { - private readonly IServiceProviderConfigurationCreator _serviceProviderConfigCreator; - private readonly IQoSOptionsCreator _qosOptionsCreator; - private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; - private readonly IAdministrationPath _adminPath; - private readonly ILoadBalancerOptionsCreator _loadBalancerOptionsCreator; +using Ocelot.DependencyInjection; - public ConfigurationCreator( - IServiceProviderConfigurationCreator serviceProviderConfigCreator, - IQoSOptionsCreator qosOptionsCreator, - IHttpHandlerOptionsCreator httpHandlerOptionsCreator, - IServiceProvider serviceProvider, - ILoadBalancerOptionsCreator loadBalancerOptionsCreator - ) - { - _adminPath = serviceProvider.GetService(); - _loadBalancerOptionsCreator = loadBalancerOptionsCreator; - _serviceProviderConfigCreator = serviceProviderConfigCreator; - _qosOptionsCreator = qosOptionsCreator; - _httpHandlerOptionsCreator = httpHandlerOptionsCreator; - } +using Ocelot.Configuration.File; - public InternalConfiguration Create(FileConfiguration fileConfiguration, List reRoutes) - { - var serviceProviderConfiguration = _serviceProviderConfigCreator.Create(fileConfiguration.GlobalConfiguration); - - var lbOptions = _loadBalancerOptionsCreator.Create(fileConfiguration.GlobalConfiguration.LoadBalancerOptions); - - var qosOptions = _qosOptionsCreator.Create(fileConfiguration.GlobalConfiguration.QoSOptions); - - var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileConfiguration.GlobalConfiguration.HttpHandlerOptions); - - var adminPath = _adminPath != null ? _adminPath.Path : null; - - return new InternalConfiguration(reRoutes, - adminPath, - serviceProviderConfiguration, - fileConfiguration.GlobalConfiguration.RequestIdKey, - lbOptions, - fileConfiguration.GlobalConfiguration.DownstreamScheme, - qosOptions, - httpHandlerOptions - ); - } - } -} +using Microsoft.Extensions.DependencyInjection; + +namespace Ocelot.Configuration.Creator +{ + public class ConfigurationCreator : IConfigurationCreator + { + private readonly IServiceProviderConfigurationCreator _serviceProviderConfigCreator; + private readonly IQoSOptionsCreator _qosOptionsCreator; + private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; + private readonly IAdministrationPath _adminPath; + private readonly ILoadBalancerOptionsCreator _loadBalancerOptionsCreator; + private readonly IVersionCreator _versionCreator; + + public ConfigurationCreator( + IServiceProviderConfigurationCreator serviceProviderConfigCreator, + IQoSOptionsCreator qosOptionsCreator, + IHttpHandlerOptionsCreator httpHandlerOptionsCreator, + IServiceProvider serviceProvider, + ILoadBalancerOptionsCreator loadBalancerOptionsCreator, + IVersionCreator versionCreator + ) + { + _adminPath = serviceProvider.GetService(); + _loadBalancerOptionsCreator = loadBalancerOptionsCreator; + _serviceProviderConfigCreator = serviceProviderConfigCreator; + _qosOptionsCreator = qosOptionsCreator; + _httpHandlerOptionsCreator = httpHandlerOptionsCreator; + _versionCreator = versionCreator; + } + + public InternalConfiguration Create(FileConfiguration fileConfiguration, List routes) + { + var serviceProviderConfiguration = _serviceProviderConfigCreator.Create(fileConfiguration.GlobalConfiguration); + + var lbOptions = _loadBalancerOptionsCreator.Create(fileConfiguration.GlobalConfiguration.LoadBalancerOptions); + + var qosOptions = _qosOptionsCreator.Create(fileConfiguration.GlobalConfiguration.QoSOptions); + + var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileConfiguration.GlobalConfiguration.HttpHandlerOptions); + + var adminPath = _adminPath?.Path; + + var version = _versionCreator.Create(fileConfiguration.GlobalConfiguration.DownstreamHttpVersion); + + return new InternalConfiguration(routes, + adminPath, + serviceProviderConfiguration, + fileConfiguration.GlobalConfiguration.RequestIdKey, + lbOptions, + fileConfiguration.GlobalConfiguration.DownstreamScheme, + qosOptions, + httpHandlerOptions, + version + ); + } + } +} diff --git a/src/Ocelot/Configuration/Creator/DownstreamAddressesCreator.cs b/src/Ocelot/Configuration/Creator/DownstreamAddressesCreator.cs index 78718c84e..e72a2f947 100644 --- a/src/Ocelot/Configuration/Creator/DownstreamAddressesCreator.cs +++ b/src/Ocelot/Configuration/Creator/DownstreamAddressesCreator.cs @@ -1,14 +1,15 @@ -using Ocelot.Configuration.File; using System.Collections.Generic; using System.Linq; + +using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator { public class DownstreamAddressesCreator : IDownstreamAddressesCreator { - public List Create(FileReRoute reRoute) + public List Create(FileRoute route) { - return reRoute.DownstreamHostAndPorts.Select(hostAndPort => new DownstreamHostAndPort(hostAndPort.Host, hostAndPort.Port)).ToList(); + return route.DownstreamHostAndPorts.Select(hostAndPort => new DownstreamHostAndPort(hostAndPort.Host, hostAndPort.Port)).ToList(); } } -} +} diff --git a/src/Ocelot/Configuration/Creator/DynamicsCreator.cs b/src/Ocelot/Configuration/Creator/DynamicsCreator.cs index 2b5193c0b..5f0a6c659 100644 --- a/src/Ocelot/Configuration/Creator/DynamicsCreator.cs +++ b/src/Ocelot/Configuration/Creator/DynamicsCreator.cs @@ -1,42 +1,49 @@ -namespace Ocelot.Configuration.Creator -{ - using Builder; - using File; - using System.Collections.Generic; - using System.Linq; +using System.Collections.Generic; +using System.Linq; - public class DynamicsCreator : IDynamicsCreator - { - private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; +using Ocelot.Configuration.Builder; - public DynamicsCreator(IRateLimitOptionsCreator rateLimitOptionsCreator) - { - _rateLimitOptionsCreator = rateLimitOptionsCreator; - } - - public List Create(FileConfiguration fileConfiguration) - { - return fileConfiguration.DynamicReRoutes - .Select(dynamic => SetUpDynamicReRoute(dynamic, fileConfiguration.GlobalConfiguration)) - .ToList(); - } - - private ReRoute SetUpDynamicReRoute(FileDynamicReRoute fileDynamicReRoute, FileGlobalConfiguration globalConfiguration) - { - var rateLimitOption = _rateLimitOptionsCreator - .Create(fileDynamicReRoute.RateLimitRule, globalConfiguration); - - var downstreamReRoute = new DownstreamReRouteBuilder() - .WithEnableRateLimiting(rateLimitOption.EnableRateLimiting) - .WithRateLimitOptions(rateLimitOption) - .WithServiceName(fileDynamicReRoute.ServiceName) - .Build(); - - var reRoute = new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) - .Build(); - - return reRoute; - } - } -} +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public class DynamicsCreator : IDynamicsCreator + { + private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; + private readonly IVersionCreator _versionCreator; + + public DynamicsCreator(IRateLimitOptionsCreator rateLimitOptionsCreator, IVersionCreator versionCreator) + { + _rateLimitOptionsCreator = rateLimitOptionsCreator; + _versionCreator = versionCreator; + } + + public List Create(FileConfiguration fileConfiguration) + { + return fileConfiguration.DynamicRoutes + .Select(dynamic => SetUpDynamicRoute(dynamic, fileConfiguration.GlobalConfiguration)) + .ToList(); + } + + private Route SetUpDynamicRoute(FileDynamicRoute fileDynamicRoute, FileGlobalConfiguration globalConfiguration) + { + var rateLimitOption = _rateLimitOptionsCreator + .Create(fileDynamicRoute.RateLimitRule, globalConfiguration); + + var version = _versionCreator.Create(fileDynamicRoute.DownstreamHttpVersion); + + var downstreamRoute = new DownstreamRouteBuilder() + .WithEnableRateLimiting(rateLimitOption.EnableRateLimiting) + .WithRateLimitOptions(rateLimitOption) + .WithServiceName(fileDynamicRoute.ServiceName) + .WithDownstreamHttpVersion(version) + .Build(); + + var route = new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) + .Build(); + + return route; + } + } +} diff --git a/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs index 1a12cab53..ebcb0316d 100644 --- a/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs @@ -1,57 +1,60 @@ -namespace Ocelot.Configuration.Creator -{ - using File; - using Responses; - using System.Linq; - using System.Threading.Tasks; - using Validator; - - public class FileInternalConfigurationCreator : IInternalConfigurationCreator - { - private readonly IConfigurationValidator _configurationValidator; - private readonly IConfigurationCreator _configCreator; - private readonly IDynamicsCreator _dynamicsCreator; - private readonly IReRoutesCreator _reRoutesCreator; - private readonly IAggregatesCreator _aggregatesCreator; - - public FileInternalConfigurationCreator( - IConfigurationValidator configurationValidator, - IReRoutesCreator reRoutesCreator, - IAggregatesCreator aggregatesCreator, - IDynamicsCreator dynamicsCreator, - IConfigurationCreator configCreator - ) - { - _configCreator = configCreator; - _dynamicsCreator = dynamicsCreator; - _aggregatesCreator = aggregatesCreator; - _reRoutesCreator = reRoutesCreator; - _configurationValidator = configurationValidator; - } - - public async Task> Create(FileConfiguration fileConfiguration) - { - var response = await _configurationValidator.IsValid(fileConfiguration); - - if (response.Data.IsError) - { - return new ErrorResponse(response.Data.Errors); - } - - var reRoutes = _reRoutesCreator.Create(fileConfiguration); - - var aggregates = _aggregatesCreator.Create(fileConfiguration, reRoutes); - - var dynamicReRoute = _dynamicsCreator.Create(fileConfiguration); - - var mergedReRoutes = reRoutes - .Union(aggregates) - .Union(dynamicReRoute) - .ToList(); - - var config = _configCreator.Create(fileConfiguration, mergedReRoutes); - - return new OkResponse(config); - } - } -} +using System.Linq; +using System.Threading.Tasks; + +using Ocelot.Configuration.File; + +using Ocelot.Responses; + +using Ocelot.Configuration.Validator; + +namespace Ocelot.Configuration.Creator +{ + public class FileInternalConfigurationCreator : IInternalConfigurationCreator + { + private readonly IConfigurationValidator _configurationValidator; + private readonly IConfigurationCreator _configCreator; + private readonly IDynamicsCreator _dynamicsCreator; + private readonly IRoutesCreator _routesCreator; + private readonly IAggregatesCreator _aggregatesCreator; + + public FileInternalConfigurationCreator( + IConfigurationValidator configurationValidator, + IRoutesCreator routesCreator, + IAggregatesCreator aggregatesCreator, + IDynamicsCreator dynamicsCreator, + IConfigurationCreator configCreator + ) + { + _configCreator = configCreator; + _dynamicsCreator = dynamicsCreator; + _aggregatesCreator = aggregatesCreator; + _routesCreator = routesCreator; + _configurationValidator = configurationValidator; + } + + public async Task> Create(FileConfiguration fileConfiguration) + { + var response = await _configurationValidator.IsValid(fileConfiguration); + + if (response.Data.IsError) + { + return new ErrorResponse(response.Data.Errors); + } + + var routes = _routesCreator.Create(fileConfiguration); + + var aggregates = _aggregatesCreator.Create(fileConfiguration, routes); + + var dynamicRoute = _dynamicsCreator.Create(fileConfiguration); + + var mergedRoutes = routes + .Union(aggregates) + .Union(dynamicRoute) + .ToList(); + + var config = _configCreator.Create(fileConfiguration, mergedRoutes); + + return new OkResponse(config); + } + } +} diff --git a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs index fdf05450b..d9ae0e301 100644 --- a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs +++ b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs @@ -1,9 +1,10 @@ +using System; +using System.Collections.Generic; + using Ocelot.Configuration.File; using Ocelot.Infrastructure; -using Ocelot.Infrastructure.Extensions; using Ocelot.Logging; -using Ocelot.Responses; -using System.Collections.Generic; +using Ocelot.Responses; namespace Ocelot.Configuration.Creator { @@ -18,12 +19,12 @@ public HeaderFindAndReplaceCreator(IPlaceholders placeholders, IOcelotLoggerFact _placeholders = placeholders; } - public HeaderTransformations Create(FileReRoute fileReRoute) + public HeaderTransformations Create(FileRoute fileRoute) { var upstream = new List(); var addHeadersToUpstream = new List(); - foreach (var input in fileReRoute.UpstreamHeaderTransform) + foreach (var input in fileRoute.UpstreamHeaderTransform) { if (input.Value.Contains(",")) { @@ -44,9 +45,9 @@ public HeaderTransformations Create(FileReRoute fileReRoute) } var downstream = new List(); - var addHeadersToDownstream = new List(); - - foreach (var input in fileReRoute.DownstreamHeaderTransform) + var addHeadersToDownstream = new List(); + + foreach (var input in fileRoute.DownstreamHeaderTransform) { if (input.Value.Contains(",")) { @@ -64,8 +65,8 @@ public HeaderTransformations Create(FileReRoute fileReRoute) { addHeadersToDownstream.Add(new AddHeader(input.Key, input.Value)); } - } - + } + return new HeaderTransformations(upstream, downstream, addHeadersToDownstream, addHeadersToUpstream); } @@ -75,11 +76,11 @@ private Response Map(KeyValuePair input) var replace = findAndReplace[1].TrimStart(); - var startOfPlaceholder = replace.IndexOf("{"); + var startOfPlaceholder = replace.IndexOf('{', StringComparison.Ordinal); if (startOfPlaceholder > -1) { - var endOfPlaceholder = replace.IndexOf("}", startOfPlaceholder); - + var endOfPlaceholder = replace.IndexOf("}", startOfPlaceholder, StringComparison.Ordinal); + var placeholder = replace.Substring(startOfPlaceholder, startOfPlaceholder + (endOfPlaceholder + 1)); var value = _placeholders.Get(placeholder); @@ -92,9 +93,9 @@ private Response Map(KeyValuePair input) replace = replace.Replace(placeholder, value.Data); } - var hAndr = new HeaderFindAndReplace(input.Key, findAndReplace[0], replace, 0); - + var hAndr = new HeaderFindAndReplace(input.Key, findAndReplace[0], replace, 0); + return new OkResponse(hAndr); } } -} +} diff --git a/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs b/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs index 4e38100be..ce9ad8799 100644 --- a/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs @@ -1,10 +1,13 @@ -namespace Ocelot.Configuration.Creator -{ - using Logging; - using Microsoft.Extensions.DependencyInjection; - using Ocelot.Configuration.File; - using System; +using System; + +using Ocelot.Configuration.File; + +using Ocelot.Logging; +using Microsoft.Extensions.DependencyInjection; + +namespace Ocelot.Configuration.Creator +{ public class HttpHandlerOptionsCreator : IHttpHandlerOptionsCreator { private readonly ITracer _tracer; @@ -19,7 +22,7 @@ public HttpHandlerOptions Create(FileHttpHandlerOptions options) var useTracing = _tracer != null && options.UseTracing; //be sure that maxConnectionPerServer is in correct range of values - int maxConnectionPerServer = (options.MaxConnectionsPerServer > 0) ? maxConnectionPerServer = options.MaxConnectionsPerServer : maxConnectionPerServer = int.MaxValue; + var maxConnectionPerServer = (options.MaxConnectionsPerServer > 0) ? options.MaxConnectionsPerServer : int.MaxValue; return new HttpHandlerOptions(options.AllowAutoRedirect, options.UseCookieContainer, useTracing, options.UseProxy, maxConnectionPerServer); diff --git a/src/Ocelot/Configuration/Creator/HttpVersionCreator.cs b/src/Ocelot/Configuration/Creator/HttpVersionCreator.cs new file mode 100644 index 000000000..705fdbcab --- /dev/null +++ b/src/Ocelot/Configuration/Creator/HttpVersionCreator.cs @@ -0,0 +1,17 @@ +using System; + +namespace Ocelot.Configuration.Creator +{ + public class HttpVersionCreator : IVersionCreator + { + public Version Create(string downstreamHttpVersion) + { + if (!Version.TryParse(downstreamHttpVersion, out var version)) + { + version = new Version(1, 1); + } + + return version; + } + } +} diff --git a/src/Ocelot/Configuration/Creator/IAggregatesCreator.cs b/src/Ocelot/Configuration/Creator/IAggregatesCreator.cs index 05fe2177b..ff52af849 100644 --- a/src/Ocelot/Configuration/Creator/IAggregatesCreator.cs +++ b/src/Ocelot/Configuration/Creator/IAggregatesCreator.cs @@ -1,10 +1,11 @@ -using Ocelot.Configuration.File; -using System.Collections.Generic; +using System.Collections.Generic; -namespace Ocelot.Configuration.Creator -{ - public interface IAggregatesCreator - { - List Create(FileConfiguration fileConfiguration, List reRoutes); - } -} +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public interface IAggregatesCreator + { + List Create(FileConfiguration fileConfiguration, List routes); + } +} diff --git a/src/Ocelot/Configuration/Creator/IAuthenticationOptionsCreator.cs b/src/Ocelot/Configuration/Creator/IAuthenticationOptionsCreator.cs index 95636af5a..6edf1c63a 100644 --- a/src/Ocelot/Configuration/Creator/IAuthenticationOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/IAuthenticationOptionsCreator.cs @@ -4,6 +4,6 @@ namespace Ocelot.Configuration.Creator { public interface IAuthenticationOptionsCreator { - AuthenticationOptions Create(FileReRoute reRoute); + AuthenticationOptions Create(FileRoute route); } -} +} diff --git a/src/Ocelot/Configuration/Creator/IConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/IConfigurationCreator.cs index 8aa42d0e0..62710603a 100644 --- a/src/Ocelot/Configuration/Creator/IConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/IConfigurationCreator.cs @@ -1,10 +1,11 @@ -using Ocelot.Configuration.File; -using System.Collections.Generic; +using System.Collections.Generic; -namespace Ocelot.Configuration.Creator -{ - public interface IConfigurationCreator - { - InternalConfiguration Create(FileConfiguration fileConfiguration, List reRoutes); - } -} +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public interface IConfigurationCreator + { + InternalConfiguration Create(FileConfiguration fileConfiguration, List routes); + } +} diff --git a/src/Ocelot/Configuration/Creator/IDownstreamAddressesCreator.cs b/src/Ocelot/Configuration/Creator/IDownstreamAddressesCreator.cs index e60812900..c7f23159b 100644 --- a/src/Ocelot/Configuration/Creator/IDownstreamAddressesCreator.cs +++ b/src/Ocelot/Configuration/Creator/IDownstreamAddressesCreator.cs @@ -1,10 +1,11 @@ -using Ocelot.Configuration.File; using System.Collections.Generic; + +using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator { public interface IDownstreamAddressesCreator { - List Create(FileReRoute reRoute); + List Create(FileRoute route); } -} +} diff --git a/src/Ocelot/Configuration/Creator/IDynamicsCreator.cs b/src/Ocelot/Configuration/Creator/IDynamicsCreator.cs index 715db0984..a10b1d6da 100644 --- a/src/Ocelot/Configuration/Creator/IDynamicsCreator.cs +++ b/src/Ocelot/Configuration/Creator/IDynamicsCreator.cs @@ -1,10 +1,11 @@ -using Ocelot.Configuration.File; -using System.Collections.Generic; +using System.Collections.Generic; -namespace Ocelot.Configuration.Creator -{ - public interface IDynamicsCreator - { - List Create(FileConfiguration fileConfiguration); - } -} +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public interface IDynamicsCreator + { + List Create(FileConfiguration fileConfiguration); + } +} diff --git a/src/Ocelot/Configuration/Creator/IHeaderFindAndReplaceCreator.cs b/src/Ocelot/Configuration/Creator/IHeaderFindAndReplaceCreator.cs index 496e17bf5..a37b1cca6 100644 --- a/src/Ocelot/Configuration/Creator/IHeaderFindAndReplaceCreator.cs +++ b/src/Ocelot/Configuration/Creator/IHeaderFindAndReplaceCreator.cs @@ -4,6 +4,6 @@ namespace Ocelot.Configuration.Creator { public interface IHeaderFindAndReplaceCreator { - HeaderTransformations Create(FileReRoute fileReRoute); + HeaderTransformations Create(FileRoute fileRoute); } -} +} diff --git a/src/Ocelot/Configuration/Creator/IHttpHandlerOptionsCreator.cs b/src/Ocelot/Configuration/Creator/IHttpHandlerOptionsCreator.cs index 76a9bc94a..a37dc2425 100644 --- a/src/Ocelot/Configuration/Creator/IHttpHandlerOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/IHttpHandlerOptionsCreator.cs @@ -3,10 +3,10 @@ namespace Ocelot.Configuration.Creator { /// - /// Describes creation of HttpHandlerOptions + /// Describes creation of HttpHandlerOptions. /// public interface IHttpHandlerOptionsCreator { - HttpHandlerOptions Create(FileHttpHandlerOptions fileReRoute); + HttpHandlerOptions Create(FileHttpHandlerOptions fileRoute); } -} +} diff --git a/src/Ocelot/Configuration/Creator/IInternalConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/IInternalConfigurationCreator.cs index f8e39d7bc..d44debf6f 100644 --- a/src/Ocelot/Configuration/Creator/IInternalConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/IInternalConfigurationCreator.cs @@ -1,6 +1,7 @@ +using System.Threading.Tasks; + using Ocelot.Configuration.File; using Ocelot.Responses; -using System.Threading.Tasks; namespace Ocelot.Configuration.Creator { diff --git a/src/Ocelot/Configuration/Creator/IQoSOptionsCreator.cs b/src/Ocelot/Configuration/Creator/IQoSOptionsCreator.cs index 128058a9f..b8a147042 100644 --- a/src/Ocelot/Configuration/Creator/IQoSOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/IQoSOptionsCreator.cs @@ -1,6 +1,7 @@ -using Ocelot.Configuration.File; using System.Collections.Generic; +using Ocelot.Configuration.File; + namespace Ocelot.Configuration.Creator { public interface IQoSOptionsCreator diff --git a/src/Ocelot/Configuration/Creator/IReRouteKeyCreator.cs b/src/Ocelot/Configuration/Creator/IReRouteKeyCreator.cs deleted file mode 100644 index 05f78354f..000000000 --- a/src/Ocelot/Configuration/Creator/IReRouteKeyCreator.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Ocelot.Configuration.File; - -namespace Ocelot.Configuration.Creator -{ - public interface IReRouteKeyCreator - { - string Create(FileReRoute fileReRoute); - } -} diff --git a/src/Ocelot/Configuration/Creator/IReRouteOptionsCreator.cs b/src/Ocelot/Configuration/Creator/IReRouteOptionsCreator.cs deleted file mode 100644 index 1d5761d5e..000000000 --- a/src/Ocelot/Configuration/Creator/IReRouteOptionsCreator.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Ocelot.Configuration.File; - -namespace Ocelot.Configuration.Creator -{ - public interface IReRouteOptionsCreator - { - ReRouteOptions Create(FileReRoute fileReRoute); - } -} diff --git a/src/Ocelot/Configuration/Creator/IReRoutesCreator.cs b/src/Ocelot/Configuration/Creator/IReRoutesCreator.cs deleted file mode 100644 index 18b26528a..000000000 --- a/src/Ocelot/Configuration/Creator/IReRoutesCreator.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Ocelot.Configuration.File; -using System.Collections.Generic; - -namespace Ocelot.Configuration.Creator -{ - public interface IReRoutesCreator - { - List Create(FileConfiguration fileConfiguration); - } -} diff --git a/src/Ocelot/Configuration/Creator/IRequestIdKeyCreator.cs b/src/Ocelot/Configuration/Creator/IRequestIdKeyCreator.cs index 898a653bb..695a11808 100644 --- a/src/Ocelot/Configuration/Creator/IRequestIdKeyCreator.cs +++ b/src/Ocelot/Configuration/Creator/IRequestIdKeyCreator.cs @@ -4,6 +4,6 @@ namespace Ocelot.Configuration.Creator { public interface IRequestIdKeyCreator { - string Create(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration); + string Create(FileRoute fileRoute, FileGlobalConfiguration globalConfiguration); } -} \ No newline at end of file +} diff --git a/src/Ocelot/Configuration/Creator/IRouteKeyCreator.cs b/src/Ocelot/Configuration/Creator/IRouteKeyCreator.cs new file mode 100644 index 000000000..f94413181 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IRouteKeyCreator.cs @@ -0,0 +1,9 @@ +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public interface IRouteKeyCreator + { + string Create(FileRoute fileRoute); + } +} diff --git a/src/Ocelot/Configuration/Creator/IRouteOptionsCreator.cs b/src/Ocelot/Configuration/Creator/IRouteOptionsCreator.cs new file mode 100644 index 000000000..5cb38cc13 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IRouteOptionsCreator.cs @@ -0,0 +1,9 @@ +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public interface IRouteOptionsCreator + { + RouteOptions Create(FileRoute fileRoute); + } +} diff --git a/src/Ocelot/Configuration/Creator/IRoutesCreator.cs b/src/Ocelot/Configuration/Creator/IRoutesCreator.cs new file mode 100644 index 000000000..5123ff122 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IRoutesCreator.cs @@ -0,0 +1,9 @@ +using Ocelot.Configuration.File; +using System.Collections.Generic; + +namespace Ocelot.Configuration.Creator; + +public interface IRoutesCreator +{ + List Create(FileConfiguration fileConfiguration); +} diff --git a/src/Ocelot/Configuration/Creator/IServiceProviderConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/IServiceProviderConfigurationCreator.cs index f2b750b4e..9241c1c64 100644 --- a/src/Ocelot/Configuration/Creator/IServiceProviderConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/IServiceProviderConfigurationCreator.cs @@ -6,4 +6,4 @@ public interface IServiceProviderConfigurationCreator { ServiceProviderConfiguration Create(FileGlobalConfiguration globalConfiguration); } -} \ No newline at end of file +} diff --git a/src/Ocelot/Configuration/Creator/IUpstreamTemplatePatternCreator.cs b/src/Ocelot/Configuration/Creator/IUpstreamTemplatePatternCreator.cs index aaec76345..9c27a823f 100644 --- a/src/Ocelot/Configuration/Creator/IUpstreamTemplatePatternCreator.cs +++ b/src/Ocelot/Configuration/Creator/IUpstreamTemplatePatternCreator.cs @@ -5,6 +5,6 @@ namespace Ocelot.Configuration.Creator { public interface IUpstreamTemplatePatternCreator { - UpstreamPathTemplate Create(IReRoute reRoute); + UpstreamPathTemplate Create(IRoute route); } -} +} diff --git a/src/Ocelot/Configuration/Creator/IVersionCreator.cs b/src/Ocelot/Configuration/Creator/IVersionCreator.cs new file mode 100644 index 000000000..23d45a1f0 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IVersionCreator.cs @@ -0,0 +1,9 @@ +using System; + +namespace Ocelot.Configuration.Creator +{ + public interface IVersionCreator + { + Version Create(string downstreamHttpVersion); + } +} diff --git a/src/Ocelot/Configuration/Creator/QoSOptionsCreator.cs b/src/Ocelot/Configuration/Creator/QoSOptionsCreator.cs index 27ae2544e..b3461210d 100644 --- a/src/Ocelot/Configuration/Creator/QoSOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/QoSOptionsCreator.cs @@ -1,10 +1,12 @@ +using System.Collections.Generic; +using System.Linq; + +using Ocelot.Configuration.Builder; + +using Ocelot.Configuration.File; + namespace Ocelot.Configuration.Creator { - using Ocelot.Configuration.Builder; - using Ocelot.Configuration.File; - using System.Collections.Generic; - using System.Linq; - public class QoSOptionsCreator : IQoSOptionsCreator { public QoSOptions Create(FileQoSOptions options) @@ -30,7 +32,7 @@ public QoSOptions Create(QoSOptions options, string pathTemplate, List h return Map(key, options.TimeoutValue, options.DurationOfBreak, options.ExceptionsAllowedBeforeBreaking); } - private QoSOptions Map(string key, int timeoutValue, int durationOfBreak, int exceptionsAllowedBeforeBreaking) + private static QoSOptions Map(string key, int timeoutValue, int durationOfBreak, int exceptionsAllowedBeforeBreaking) { return new QoSOptionsBuilder() .WithExceptionsAllowedBeforeBreaking(exceptionsAllowedBeforeBreaking) @@ -40,9 +42,9 @@ private QoSOptions Map(string key, int timeoutValue, int durationOfBreak, int ex .Build(); } - private string CreateKey(string pathTemplate, List httpMethods) + private static string CreateKey(string pathTemplate, IEnumerable httpMethods) { - return $"{pathTemplate.FirstOrDefault()}|{string.Join(",", httpMethods)}"; + return $"{pathTemplate.FirstOrDefault()}|{string.Join(',', httpMethods)}"; } } } diff --git a/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs b/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs index ba167bfe8..7bbd7263a 100644 --- a/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs @@ -7,7 +7,7 @@ public class RateLimitOptionsCreator : IRateLimitOptionsCreator { public RateLimitOptions Create(FileRateLimitRule fileRateLimitRule, FileGlobalConfiguration globalConfiguration) { - if (fileRateLimitRule != null && fileRateLimitRule.EnableRateLimiting) + if (fileRateLimitRule?.EnableRateLimiting == true) { return new RateLimitOptionsBuilder() .WithClientIdHeader(globalConfiguration.RateLimitOptions.ClientIdHeader) diff --git a/src/Ocelot/Configuration/Creator/ReRouteKeyCreator.cs b/src/Ocelot/Configuration/Creator/ReRouteKeyCreator.cs deleted file mode 100644 index 44be1afa4..000000000 --- a/src/Ocelot/Configuration/Creator/ReRouteKeyCreator.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Ocelot.Configuration.File; -using Ocelot.LoadBalancer.LoadBalancers; -using System.Linq; - -namespace Ocelot.Configuration.Creator -{ - public class ReRouteKeyCreator : IReRouteKeyCreator - { - public string Create(FileReRoute fileReRoute) - { - if (IsStickySession(fileReRoute)) - { - return $"{nameof(CookieStickySessions)}:{fileReRoute.LoadBalancerOptions.Key}"; - } - - return $"{fileReRoute.UpstreamPathTemplate}|{string.Join(",", fileReRoute.UpstreamHttpMethod)}|{string.Join(",", fileReRoute.DownstreamHostAndPorts.Select(x => $"{x.Host}:{x.Port}"))}"; - } - - private bool IsStickySession(FileReRoute fileReRoute) - { - if (!string.IsNullOrEmpty(fileReRoute.LoadBalancerOptions.Type) - && !string.IsNullOrEmpty(fileReRoute.LoadBalancerOptions.Key) - && fileReRoute.LoadBalancerOptions.Type == nameof(CookieStickySessions)) - { - return true; - } - - return false; - } - } -} diff --git a/src/Ocelot/Configuration/Creator/ReRouteOptionsCreator.cs b/src/Ocelot/Configuration/Creator/ReRouteOptionsCreator.cs deleted file mode 100644 index ce710c8e0..000000000 --- a/src/Ocelot/Configuration/Creator/ReRouteOptionsCreator.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Ocelot.Configuration.Builder; -using Ocelot.Configuration.File; - -namespace Ocelot.Configuration.Creator -{ - public class ReRouteOptionsCreator : IReRouteOptionsCreator - { - public ReRouteOptions Create(FileReRoute fileReRoute) - { - var isAuthenticated = IsAuthenticated(fileReRoute); - var isAuthorised = IsAuthorised(fileReRoute); - var isCached = IsCached(fileReRoute); - var enableRateLimiting = IsEnableRateLimiting(fileReRoute); - var useServiceDiscovery = !string.IsNullOrEmpty(fileReRoute.ServiceName); - - var options = new ReRouteOptionsBuilder() - .WithIsAuthenticated(isAuthenticated) - .WithIsAuthorised(isAuthorised) - .WithIsCached(isCached) - .WithRateLimiting(enableRateLimiting) - .WithUseServiceDiscovery(useServiceDiscovery) - .Build(); - - return options; - } - - private static bool IsEnableRateLimiting(FileReRoute fileReRoute) - { - return (fileReRoute.RateLimitOptions != null && fileReRoute.RateLimitOptions.EnableRateLimiting) ? true : false; - } - - private bool IsAuthenticated(FileReRoute fileReRoute) - { - return !string.IsNullOrEmpty(fileReRoute.AuthenticationOptions?.AuthenticationProviderKey); - } - - private bool IsAuthorised(FileReRoute fileReRoute) - { - return fileReRoute.RouteClaimsRequirement?.Count > 0; - } - - private bool IsCached(FileReRoute fileReRoute) - { - return fileReRoute.FileCacheOptions.TtlSeconds > 0; - } - } -} diff --git a/src/Ocelot/Configuration/Creator/RequestIdKeyCreator.cs b/src/Ocelot/Configuration/Creator/RequestIdKeyCreator.cs index 8d4a81fea..d03712313 100644 --- a/src/Ocelot/Configuration/Creator/RequestIdKeyCreator.cs +++ b/src/Ocelot/Configuration/Creator/RequestIdKeyCreator.cs @@ -4,15 +4,15 @@ namespace Ocelot.Configuration.Creator { public class RequestIdKeyCreator : IRequestIdKeyCreator { - public string Create(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration) + public string Create(FileRoute fileRoute, FileGlobalConfiguration globalConfiguration) { - var reRouteId = !string.IsNullOrEmpty(fileReRoute.RequestIdKey); - - var requestIdKey = reRouteId - ? fileReRoute.RequestIdKey - : globalConfiguration.RequestIdKey; - + var routeId = !string.IsNullOrEmpty(fileRoute.RequestIdKey); + + var requestIdKey = routeId + ? fileRoute.RequestIdKey + : globalConfiguration.RequestIdKey; + return requestIdKey; } } -} +} diff --git a/src/Ocelot/Configuration/Creator/RouteKeyCreator.cs b/src/Ocelot/Configuration/Creator/RouteKeyCreator.cs new file mode 100644 index 000000000..3c183a264 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/RouteKeyCreator.cs @@ -0,0 +1,19 @@ +using System.Linq; + +using Ocelot.Configuration.File; +using Ocelot.LoadBalancer.LoadBalancers; + +namespace Ocelot.Configuration.Creator +{ + public class RouteKeyCreator : IRouteKeyCreator + { + public string Create(FileRoute fileRoute) => IsStickySession(fileRoute) + ? $"{nameof(CookieStickySessions)}:{fileRoute.LoadBalancerOptions.Key}" + : $"{fileRoute.UpstreamPathTemplate}|{string.Join(',', fileRoute.UpstreamHttpMethod)}|{string.Join(',', fileRoute.DownstreamHostAndPorts.Select(x => $"{x.Host}:{x.Port}"))}"; + + private static bool IsStickySession(FileRoute fileRoute) => + !string.IsNullOrEmpty(fileRoute.LoadBalancerOptions.Type) + && !string.IsNullOrEmpty(fileRoute.LoadBalancerOptions.Key) + && fileRoute.LoadBalancerOptions.Type == nameof(CookieStickySessions); + } +} diff --git a/src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs b/src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs new file mode 100644 index 000000000..e96d889b3 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/RouteOptionsCreator.cs @@ -0,0 +1,36 @@ +using Ocelot.Configuration.Builder; + +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public class RouteOptionsCreator : IRouteOptionsCreator + { + public RouteOptions Create(FileRoute fileRoute) + { + var isAuthenticated = IsAuthenticated(fileRoute); + var isAuthorized = IsAuthorized(fileRoute); + var isCached = IsCached(fileRoute); + var enableRateLimiting = IsEnableRateLimiting(fileRoute); + var useServiceDiscovery = !string.IsNullOrEmpty(fileRoute.ServiceName); + + var options = new RouteOptionsBuilder() + .WithIsAuthenticated(isAuthenticated) + .WithIsAuthorized(isAuthorized) + .WithIsCached(isCached) + .WithRateLimiting(enableRateLimiting) + .WithUseServiceDiscovery(useServiceDiscovery) + .Build(); + + return options; + } + + private static bool IsEnableRateLimiting(FileRoute fileRoute) => fileRoute.RateLimitOptions?.EnableRateLimiting == true; + + private static bool IsAuthenticated(FileRoute fileRoute) => !string.IsNullOrEmpty(fileRoute.AuthenticationOptions?.AuthenticationProviderKey); + + private static bool IsAuthorized(FileRoute fileRoute) => fileRoute.RouteClaimsRequirement?.Count > 0; + + private static bool IsCached(FileRoute fileRoute) => fileRoute.FileCacheOptions.TtlSeconds > 0; + } +} diff --git a/src/Ocelot/Configuration/Creator/ReRoutesCreator.cs b/src/Ocelot/Configuration/Creator/RoutesCreator.cs similarity index 54% rename from src/Ocelot/Configuration/Creator/ReRoutesCreator.cs rename to src/Ocelot/Configuration/Creator/RoutesCreator.cs index 4443e201a..7161aad78 100644 --- a/src/Ocelot/Configuration/Creator/ReRoutesCreator.cs +++ b/src/Ocelot/Configuration/Creator/RoutesCreator.cs @@ -1,160 +1,170 @@ -namespace Ocelot.Configuration.Creator -{ - using Builder; - using Cache; - using File; - using System.Collections.Generic; - using System.Linq; - - public class ReRoutesCreator : IReRoutesCreator - { - private readonly ILoadBalancerOptionsCreator _loadBalancerOptionsCreator; - private readonly IClaimsToThingCreator _claimsToThingCreator; - private readonly IAuthenticationOptionsCreator _authOptionsCreator; - private readonly IUpstreamTemplatePatternCreator _upstreamTemplatePatternCreator; - private readonly IRequestIdKeyCreator _requestIdKeyCreator; - private readonly IQoSOptionsCreator _qosOptionsCreator; - private readonly IReRouteOptionsCreator _fileReRouteOptionsCreator; - private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; - private readonly IRegionCreator _regionCreator; - private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; - private readonly IHeaderFindAndReplaceCreator _headerFAndRCreator; - private readonly IDownstreamAddressesCreator _downstreamAddressesCreator; - private readonly IReRouteKeyCreator _reRouteKeyCreator; - private readonly ISecurityOptionsCreator _securityOptionsCreator; - - public ReRoutesCreator( - IClaimsToThingCreator claimsToThingCreator, - IAuthenticationOptionsCreator authOptionsCreator, - IUpstreamTemplatePatternCreator upstreamTemplatePatternCreator, - IRequestIdKeyCreator requestIdKeyCreator, - IQoSOptionsCreator qosOptionsCreator, - IReRouteOptionsCreator fileReRouteOptionsCreator, - IRateLimitOptionsCreator rateLimitOptionsCreator, - IRegionCreator regionCreator, - IHttpHandlerOptionsCreator httpHandlerOptionsCreator, - IHeaderFindAndReplaceCreator headerFAndRCreator, - IDownstreamAddressesCreator downstreamAddressesCreator, - ILoadBalancerOptionsCreator loadBalancerOptionsCreator, - IReRouteKeyCreator reRouteKeyCreator, - ISecurityOptionsCreator securityOptionsCreator - ) - { - _reRouteKeyCreator = reRouteKeyCreator; - _loadBalancerOptionsCreator = loadBalancerOptionsCreator; - _downstreamAddressesCreator = downstreamAddressesCreator; - _headerFAndRCreator = headerFAndRCreator; - _regionCreator = regionCreator; - _rateLimitOptionsCreator = rateLimitOptionsCreator; - _requestIdKeyCreator = requestIdKeyCreator; - _upstreamTemplatePatternCreator = upstreamTemplatePatternCreator; - _authOptionsCreator = authOptionsCreator; - _claimsToThingCreator = claimsToThingCreator; - _qosOptionsCreator = qosOptionsCreator; - _fileReRouteOptionsCreator = fileReRouteOptionsCreator; - _httpHandlerOptionsCreator = httpHandlerOptionsCreator; - _loadBalancerOptionsCreator = loadBalancerOptionsCreator; - _securityOptionsCreator = securityOptionsCreator; - } - - public List Create(FileConfiguration fileConfiguration) - { - return fileConfiguration.ReRoutes - .Select(reRoute => - { - var downstreamReRoute = SetUpDownstreamReRoute(reRoute, fileConfiguration.GlobalConfiguration); - return SetUpReRoute(reRoute, downstreamReRoute); - }) - .ToList(); - } - - private DownstreamReRoute SetUpDownstreamReRoute(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration) - { - var fileReRouteOptions = _fileReRouteOptionsCreator.Create(fileReRoute); - - var requestIdKey = _requestIdKeyCreator.Create(fileReRoute, globalConfiguration); - - var reRouteKey = _reRouteKeyCreator.Create(fileReRoute); - - var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileReRoute); - - var authOptionsForRoute = _authOptionsCreator.Create(fileReRoute); - - var claimsToHeaders = _claimsToThingCreator.Create(fileReRoute.AddHeadersToRequest); - - var claimsToClaims = _claimsToThingCreator.Create(fileReRoute.AddClaimsToRequest); - - var claimsToQueries = _claimsToThingCreator.Create(fileReRoute.AddQueriesToRequest); - - var claimsToDownstreamPath = _claimsToThingCreator.Create(fileReRoute.ChangeDownstreamPathTemplate); - - var qosOptions = _qosOptionsCreator.Create(fileReRoute.QoSOptions, fileReRoute.UpstreamPathTemplate, fileReRoute.UpstreamHttpMethod); - - var rateLimitOption = _rateLimitOptionsCreator.Create(fileReRoute.RateLimitOptions, globalConfiguration); - - var region = _regionCreator.Create(fileReRoute); - - var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileReRoute.HttpHandlerOptions); - - var hAndRs = _headerFAndRCreator.Create(fileReRoute); - - var downstreamAddresses = _downstreamAddressesCreator.Create(fileReRoute); - - var lbOptions = _loadBalancerOptionsCreator.Create(fileReRoute.LoadBalancerOptions); - - var securityOptions = _securityOptionsCreator.Create(fileReRoute.SecurityOptions); - - var reRoute = new DownstreamReRouteBuilder() - .WithKey(fileReRoute.Key) - .WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate) - .WithUpstreamHttpMethod(fileReRoute.UpstreamHttpMethod) - .WithUpstreamPathTemplate(upstreamTemplatePattern) - .WithIsAuthenticated(fileReRouteOptions.IsAuthenticated) - .WithAuthenticationOptions(authOptionsForRoute) - .WithClaimsToHeaders(claimsToHeaders) - .WithClaimsToClaims(claimsToClaims) - .WithRouteClaimsRequirement(fileReRoute.RouteClaimsRequirement) - .WithIsAuthorised(fileReRouteOptions.IsAuthorised) - .WithClaimsToQueries(claimsToQueries) - .WithClaimsToDownstreamPath(claimsToDownstreamPath) - .WithRequestIdKey(requestIdKey) - .WithIsCached(fileReRouteOptions.IsCached) - .WithCacheOptions(new CacheOptions(fileReRoute.FileCacheOptions.TtlSeconds, region)) - .WithDownstreamScheme(fileReRoute.DownstreamScheme) - .WithLoadBalancerOptions(lbOptions) - .WithDownstreamAddresses(downstreamAddresses) - .WithLoadBalancerKey(reRouteKey) - .WithQosOptions(qosOptions) - .WithEnableRateLimiting(fileReRouteOptions.EnableRateLimiting) - .WithRateLimitOptions(rateLimitOption) - .WithHttpHandlerOptions(httpHandlerOptions) - .WithServiceName(fileReRoute.ServiceName) - .WithServiceNamespace(fileReRoute.ServiceNamespace) - .WithUseServiceDiscovery(fileReRouteOptions.UseServiceDiscovery) - .WithUpstreamHeaderFindAndReplace(hAndRs.Upstream) - .WithDownstreamHeaderFindAndReplace(hAndRs.Downstream) - .WithDelegatingHandlers(fileReRoute.DelegatingHandlers) - .WithAddHeadersToDownstream(hAndRs.AddHeadersToDownstream) - .WithAddHeadersToUpstream(hAndRs.AddHeadersToUpstream) - .WithDangerousAcceptAnyServerCertificateValidator(fileReRoute.DangerousAcceptAnyServerCertificateValidator) - .WithSecurityOptions(securityOptions) - .Build(); - - return reRoute; - } - - private ReRoute SetUpReRoute(FileReRoute fileReRoute, DownstreamReRoute downstreamReRoutes) - { - var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileReRoute); - - var reRoute = new ReRouteBuilder() - .WithUpstreamHttpMethod(fileReRoute.UpstreamHttpMethod) - .WithUpstreamPathTemplate(upstreamTemplatePattern) - .WithDownstreamReRoute(downstreamReRoutes) - .WithUpstreamHost(fileReRoute.UpstreamHost) - .Build(); - - return reRoute; - } - } -} +using System.Collections.Generic; +using System.Linq; + +using Ocelot.Configuration.Builder; + +using Ocelot.Cache; + +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator +{ + public class RoutesCreator : IRoutesCreator + { + private readonly ILoadBalancerOptionsCreator _loadBalancerOptionsCreator; + private readonly IClaimsToThingCreator _claimsToThingCreator; + private readonly IAuthenticationOptionsCreator _authOptionsCreator; + private readonly IUpstreamTemplatePatternCreator _upstreamTemplatePatternCreator; + private readonly IRequestIdKeyCreator _requestIdKeyCreator; + private readonly IQoSOptionsCreator _qosOptionsCreator; + private readonly IRouteOptionsCreator _fileRouteOptionsCreator; + private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; + private readonly IRegionCreator _regionCreator; + private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; + private readonly IHeaderFindAndReplaceCreator _headerFAndRCreator; + private readonly IDownstreamAddressesCreator _downstreamAddressesCreator; + private readonly IRouteKeyCreator _routeKeyCreator; + private readonly ISecurityOptionsCreator _securityOptionsCreator; + private readonly IVersionCreator _versionCreator; + + public RoutesCreator( + IClaimsToThingCreator claimsToThingCreator, + IAuthenticationOptionsCreator authOptionsCreator, + IUpstreamTemplatePatternCreator upstreamTemplatePatternCreator, + IRequestIdKeyCreator requestIdKeyCreator, + IQoSOptionsCreator qosOptionsCreator, + IRouteOptionsCreator fileRouteOptionsCreator, + IRateLimitOptionsCreator rateLimitOptionsCreator, + IRegionCreator regionCreator, + IHttpHandlerOptionsCreator httpHandlerOptionsCreator, + IHeaderFindAndReplaceCreator headerFAndRCreator, + IDownstreamAddressesCreator downstreamAddressesCreator, + ILoadBalancerOptionsCreator loadBalancerOptionsCreator, + IRouteKeyCreator routeKeyCreator, + ISecurityOptionsCreator securityOptionsCreator, + IVersionCreator versionCreator + ) + { + _routeKeyCreator = routeKeyCreator; + _loadBalancerOptionsCreator = loadBalancerOptionsCreator; + _downstreamAddressesCreator = downstreamAddressesCreator; + _headerFAndRCreator = headerFAndRCreator; + _regionCreator = regionCreator; + _rateLimitOptionsCreator = rateLimitOptionsCreator; + _requestIdKeyCreator = requestIdKeyCreator; + _upstreamTemplatePatternCreator = upstreamTemplatePatternCreator; + _authOptionsCreator = authOptionsCreator; + _claimsToThingCreator = claimsToThingCreator; + _qosOptionsCreator = qosOptionsCreator; + _fileRouteOptionsCreator = fileRouteOptionsCreator; + _httpHandlerOptionsCreator = httpHandlerOptionsCreator; + _loadBalancerOptionsCreator = loadBalancerOptionsCreator; + _securityOptionsCreator = securityOptionsCreator; + _versionCreator = versionCreator; + } + + public List Create(FileConfiguration fileConfiguration) + { + return fileConfiguration.Routes + .Select(route => + { + var downstreamRoute = SetUpDownstreamRoute(route, fileConfiguration.GlobalConfiguration); + return SetUpRoute(route, downstreamRoute); + }) + .ToList(); + } + + private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConfiguration globalConfiguration) + { + var fileRouteOptions = _fileRouteOptionsCreator.Create(fileRoute); + + var requestIdKey = _requestIdKeyCreator.Create(fileRoute, globalConfiguration); + + var routeKey = _routeKeyCreator.Create(fileRoute); + + var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute); + + var authOptionsForRoute = _authOptionsCreator.Create(fileRoute); + + var claimsToHeaders = _claimsToThingCreator.Create(fileRoute.AddHeadersToRequest); + + var claimsToClaims = _claimsToThingCreator.Create(fileRoute.AddClaimsToRequest); + + var claimsToQueries = _claimsToThingCreator.Create(fileRoute.AddQueriesToRequest); + + var claimsToDownstreamPath = _claimsToThingCreator.Create(fileRoute.ChangeDownstreamPathTemplate); + + var qosOptions = _qosOptionsCreator.Create(fileRoute.QoSOptions, fileRoute.UpstreamPathTemplate, fileRoute.UpstreamHttpMethod); + + var rateLimitOption = _rateLimitOptionsCreator.Create(fileRoute.RateLimitOptions, globalConfiguration); + + var region = _regionCreator.Create(fileRoute); + + var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileRoute.HttpHandlerOptions); + + var hAndRs = _headerFAndRCreator.Create(fileRoute); + + var downstreamAddresses = _downstreamAddressesCreator.Create(fileRoute); + + var lbOptions = _loadBalancerOptionsCreator.Create(fileRoute.LoadBalancerOptions); + + var securityOptions = _securityOptionsCreator.Create(fileRoute.SecurityOptions); + + var downstreamHttpVersion = _versionCreator.Create(fileRoute.DownstreamHttpVersion); + + var route = new DownstreamRouteBuilder() + .WithKey(fileRoute.Key) + .WithDownstreamPathTemplate(fileRoute.DownstreamPathTemplate) + .WithUpstreamHttpMethod(fileRoute.UpstreamHttpMethod) + .WithUpstreamPathTemplate(upstreamTemplatePattern) + .WithIsAuthenticated(fileRouteOptions.IsAuthenticated) + .WithAuthenticationOptions(authOptionsForRoute) + .WithClaimsToHeaders(claimsToHeaders) + .WithClaimsToClaims(claimsToClaims) + .WithRouteClaimsRequirement(fileRoute.RouteClaimsRequirement) + .WithIsAuthorized(fileRouteOptions.IsAuthorized) + .WithClaimsToQueries(claimsToQueries) + .WithClaimsToDownstreamPath(claimsToDownstreamPath) + .WithRequestIdKey(requestIdKey) + .WithIsCached(fileRouteOptions.IsCached) + .WithCacheOptions(new CacheOptions(fileRoute.FileCacheOptions.TtlSeconds, region)) + .WithDownstreamScheme(fileRoute.DownstreamScheme) + .WithLoadBalancerOptions(lbOptions) + .WithDownstreamAddresses(downstreamAddresses) + .WithLoadBalancerKey(routeKey) + .WithQosOptions(qosOptions) + .WithEnableRateLimiting(fileRouteOptions.EnableRateLimiting) + .WithRateLimitOptions(rateLimitOption) + .WithHttpHandlerOptions(httpHandlerOptions) + .WithServiceName(fileRoute.ServiceName) + .WithServiceNamespace(fileRoute.ServiceNamespace) + .WithUseServiceDiscovery(fileRouteOptions.UseServiceDiscovery) + .WithUpstreamHeaderFindAndReplace(hAndRs.Upstream) + .WithDownstreamHeaderFindAndReplace(hAndRs.Downstream) + .WithDelegatingHandlers(fileRoute.DelegatingHandlers) + .WithAddHeadersToDownstream(hAndRs.AddHeadersToDownstream) + .WithAddHeadersToUpstream(hAndRs.AddHeadersToUpstream) + .WithDangerousAcceptAnyServerCertificateValidator(fileRoute.DangerousAcceptAnyServerCertificateValidator) + .WithSecurityOptions(securityOptions) + .WithDownstreamHttpVersion(downstreamHttpVersion) + .WithDownStreamHttpMethod(fileRoute.DownstreamHttpMethod) + .Build(); + + return route; + } + + private Route SetUpRoute(FileRoute fileRoute, DownstreamRoute downstreamRoutes) + { + var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute); + + var route = new RouteBuilder() + .WithUpstreamHttpMethod(fileRoute.UpstreamHttpMethod) + .WithUpstreamPathTemplate(upstreamTemplatePattern) + .WithDownstreamRoute(downstreamRoutes) + .WithUpstreamHost(fileRoute.UpstreamHost) + .Build(); + + return route; + } + } +} diff --git a/src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs index aa631f87c..ee6c9a7ae 100644 --- a/src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs @@ -8,6 +8,7 @@ public class ServiceProviderConfigurationCreator : IServiceProviderConfiguration public ServiceProviderConfiguration Create(FileGlobalConfiguration globalConfiguration) { var port = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0; + var scheme = globalConfiguration?.ServiceDiscoveryProvider?.Scheme ?? "http"; var host = globalConfiguration?.ServiceDiscoveryProvider?.Host ?? "localhost"; var type = !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Type) ? globalConfiguration?.ServiceDiscoveryProvider?.Type @@ -16,6 +17,7 @@ public ServiceProviderConfiguration Create(FileGlobalConfiguration globalConfigu var k8snamespace = globalConfiguration?.ServiceDiscoveryProvider?.Namespace ?? string.Empty; return new ServiceProviderConfigurationBuilder() + .WithScheme(scheme) .WithHost(host) .WithPort(port) .WithType(type) diff --git a/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs b/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs index 7de660b7d..054db635f 100644 --- a/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs +++ b/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs @@ -1,93 +1,95 @@ -using Ocelot.Configuration.File; -using Ocelot.Values; -using System.Collections.Generic; - -namespace Ocelot.Configuration.Creator -{ - public class UpstreamTemplatePatternCreator : IUpstreamTemplatePatternCreator - { - private const string RegExMatchOneOrMoreOfEverything = ".+"; - private const string RegExMatchOneOrMoreOfEverythingUntilNextForwardSlash = "[^/]+"; - private const string RegExMatchEndString = "$"; - private const string RegExIgnoreCase = "(?i)"; - private const string RegExForwardSlashOnly = "^/$"; - private const string RegExForwardSlashAndOnePlaceHolder = "^/.*"; - - public UpstreamPathTemplate Create(IReRoute reRoute) - { - var upstreamTemplate = reRoute.UpstreamPathTemplate; - - var placeholders = new List(); - - for (var i = 0; i < upstreamTemplate.Length; i++) - { - if (IsPlaceHolder(upstreamTemplate, i)) - { - var postitionOfPlaceHolderClosingBracket = upstreamTemplate.IndexOf('}', i); - var difference = postitionOfPlaceHolderClosingBracket - i + 1; - var placeHolderName = upstreamTemplate.Substring(i, difference); - placeholders.Add(placeHolderName); - - //hack to handle /{url} case - if (ForwardSlashAndOnePlaceHolder(upstreamTemplate, placeholders, postitionOfPlaceHolderClosingBracket)) - { - return new UpstreamPathTemplate(RegExForwardSlashAndOnePlaceHolder, 0, false, reRoute.UpstreamPathTemplate); - } - } - } - - var containsQueryString = false; - - if (upstreamTemplate.Contains("?")) - { - containsQueryString = true; - upstreamTemplate = upstreamTemplate.Replace("?", "\\?"); - } +using System; +using System.Collections.Generic; - for (int i = 0; i < placeholders.Count; i++) - { - var indexOfPlaceholder = upstreamTemplate.IndexOf(placeholders[i]); - var indexOfNextForwardSlash = upstreamTemplate.IndexOf("/", indexOfPlaceholder); - if (indexOfNextForwardSlash < indexOfPlaceholder || (containsQueryString && upstreamTemplate.IndexOf("?") < upstreamTemplate.IndexOf(placeholders[i]))) - { - upstreamTemplate = upstreamTemplate.Replace(placeholders[i], RegExMatchOneOrMoreOfEverything); - } - else - { - upstreamTemplate = upstreamTemplate.Replace(placeholders[i], RegExMatchOneOrMoreOfEverythingUntilNextForwardSlash); - } - } - - if (upstreamTemplate == "/") - { - return new UpstreamPathTemplate(RegExForwardSlashOnly, reRoute.Priority, containsQueryString, reRoute.UpstreamPathTemplate); - } - - if (upstreamTemplate.EndsWith("/")) - { - upstreamTemplate = upstreamTemplate.Remove(upstreamTemplate.Length - 1, 1) + "(/|)"; - } - - var route = reRoute.ReRouteIsCaseSensitive - ? $"^{upstreamTemplate}{RegExMatchEndString}" - : $"^{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}"; - - return new UpstreamPathTemplate(route, reRoute.Priority, containsQueryString, reRoute.UpstreamPathTemplate); - } - - private bool ForwardSlashAndOnePlaceHolder(string upstreamTemplate, List placeholders, int postitionOfPlaceHolderClosingBracket) - { - if (upstreamTemplate.Substring(0, 2) == "/{" && placeholders.Count == 1 && upstreamTemplate.Length == postitionOfPlaceHolderClosingBracket + 1) - { - return true; - } - - return false; - } - - private bool IsPlaceHolder(string upstreamTemplate, int i) - { - return upstreamTemplate[i] == '{'; - } - } -} +using Ocelot.Configuration.File; +using Ocelot.Values; + +namespace Ocelot.Configuration.Creator +{ + public class UpstreamTemplatePatternCreator : IUpstreamTemplatePatternCreator + { + private const string RegExMatchOneOrMoreOfEverything = ".+"; + private const string RegExMatchOneOrMoreOfEverythingUntilNextForwardSlash = "[^/]+"; + private const string RegExMatchEndString = "$"; + private const string RegExIgnoreCase = "(?i)"; + private const string RegExForwardSlashOnly = "^/$"; + private const string RegExForwardSlashAndOnePlaceHolder = "^/.*"; + + public UpstreamPathTemplate Create(IRoute route) + { + var upstreamTemplate = route.UpstreamPathTemplate; + + var placeholders = new List(); + + for (var i = 0; i < upstreamTemplate.Length; i++) + { + if (IsPlaceHolder(upstreamTemplate, i)) + { + var postitionOfPlaceHolderClosingBracket = upstreamTemplate.IndexOf('}', i); + var difference = postitionOfPlaceHolderClosingBracket - i + 1; + var placeHolderName = upstreamTemplate.Substring(i, difference); + placeholders.Add(placeHolderName); + + //hack to handle /{url} case + if (ForwardSlashAndOnePlaceHolder(upstreamTemplate, placeholders, postitionOfPlaceHolderClosingBracket)) + { + return new UpstreamPathTemplate(RegExForwardSlashAndOnePlaceHolder, 0, false, route.UpstreamPathTemplate); + } + } + } + + var containsQueryString = false; + + if (upstreamTemplate.Contains('?')) + { + containsQueryString = true; + upstreamTemplate = upstreamTemplate.Replace("?", "\\?"); + } + + for (var i = 0; i < placeholders.Count; i++) + { + var indexOfPlaceholder = upstreamTemplate.IndexOf(placeholders[i], StringComparison.Ordinal); + var indexOfNextForwardSlash = upstreamTemplate.IndexOf("/", indexOfPlaceholder, StringComparison.Ordinal); + if (indexOfNextForwardSlash < indexOfPlaceholder || (containsQueryString && upstreamTemplate.IndexOf('?', StringComparison.Ordinal) < upstreamTemplate.IndexOf(placeholders[i], StringComparison.Ordinal))) + { + upstreamTemplate = upstreamTemplate.Replace(placeholders[i], RegExMatchOneOrMoreOfEverything); + } + else + { + upstreamTemplate = upstreamTemplate.Replace(placeholders[i], RegExMatchOneOrMoreOfEverythingUntilNextForwardSlash); + } + } + + if (upstreamTemplate == "/") + { + return new UpstreamPathTemplate(RegExForwardSlashOnly, route.Priority, containsQueryString, route.UpstreamPathTemplate); + } + + if (upstreamTemplate.EndsWith("/")) + { + upstreamTemplate = upstreamTemplate.Remove(upstreamTemplate.Length - 1, 1) + "(/|)"; + } + + var template = route.RouteIsCaseSensitive + ? $"^{upstreamTemplate}{RegExMatchEndString}" + : $"^{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}"; + + return new UpstreamPathTemplate(template, route.Priority, containsQueryString, route.UpstreamPathTemplate); + } + + private static bool ForwardSlashAndOnePlaceHolder(string upstreamTemplate, List placeholders, int postitionOfPlaceHolderClosingBracket) + { + if (upstreamTemplate.Substring(0, 2) == "/{" && placeholders.Count == 1 && upstreamTemplate.Length == postitionOfPlaceHolderClosingBracket + 1) + { + return true; + } + + return false; + } + + private static bool IsPlaceHolder(string upstreamTemplate, int i) + { + return upstreamTemplate[i] == '{'; + } + } +} diff --git a/src/Ocelot/Configuration/DownstreamHostAndPort.cs b/src/Ocelot/Configuration/DownstreamHostAndPort.cs index 672a8b9c5..de72278bf 100644 --- a/src/Ocelot/Configuration/DownstreamHostAndPort.cs +++ b/src/Ocelot/Configuration/DownstreamHostAndPort.cs @@ -8,7 +8,7 @@ public DownstreamHostAndPort(string host, int port) Port = port; } - public string Host { get; private set; } - public int Port { get; private set; } + public string Host { get; } + public int Port { get; } } } diff --git a/src/Ocelot/Configuration/DownstreamReRoute.cs b/src/Ocelot/Configuration/DownstreamRoute.cs similarity index 87% rename from src/Ocelot/Configuration/DownstreamReRoute.cs rename to src/Ocelot/Configuration/DownstreamRoute.cs index e8dfade51..40f96b42a 100644 --- a/src/Ocelot/Configuration/DownstreamReRoute.cs +++ b/src/Ocelot/Configuration/DownstreamRoute.cs @@ -1,110 +1,119 @@ -namespace Ocelot.Configuration -{ - using Creator; - using System.Collections.Generic; - using Values; +using System; +using System.Collections.Generic; - public class DownstreamReRoute - { - public DownstreamReRoute( - string key, - UpstreamPathTemplate upstreamPathTemplate, - List upstreamHeadersFindAndReplace, - List downstreamHeadersFindAndReplace, - List downstreamAddresses, - string serviceName, - string serviceNamespace, - HttpHandlerOptions httpHandlerOptions, - bool useServiceDiscovery, - bool enableEndpointEndpointRateLimiting, - QoSOptions qosOptions, - string downstreamScheme, - string requestIdKey, - bool isCached, - CacheOptions cacheOptions, - LoadBalancerOptions loadBalancerOptions, - RateLimitOptions rateLimitOptions, - Dictionary routeClaimsRequirement, - List claimsToQueries, - List claimsToHeaders, - List claimsToClaims, - List claimsToPath, - bool isAuthenticated, - bool isAuthorised, - AuthenticationOptions authenticationOptions, - DownstreamPathTemplate downstreamPathTemplate, - string loadBalancerKey, - List delegatingHandlers, - List addHeadersToDownstream, - List addHeadersToUpstream, - bool dangerousAcceptAnyServerCertificateValidator, - SecurityOptions securityOptions) - { - DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; - AddHeadersToDownstream = addHeadersToDownstream; - DelegatingHandlers = delegatingHandlers; - Key = key; - UpstreamPathTemplate = upstreamPathTemplate; - UpstreamHeadersFindAndReplace = upstreamHeadersFindAndReplace ?? new List(); - DownstreamHeadersFindAndReplace = downstreamHeadersFindAndReplace ?? new List(); - DownstreamAddresses = downstreamAddresses ?? new List(); - ServiceName = serviceName; - ServiceNamespace = serviceNamespace; - HttpHandlerOptions = httpHandlerOptions; - UseServiceDiscovery = useServiceDiscovery; - EnableEndpointEndpointRateLimiting = enableEndpointEndpointRateLimiting; - QosOptions = qosOptions; - DownstreamScheme = downstreamScheme; - RequestIdKey = requestIdKey; - IsCached = isCached; - CacheOptions = cacheOptions; - LoadBalancerOptions = loadBalancerOptions; - RateLimitOptions = rateLimitOptions; - RouteClaimsRequirement = routeClaimsRequirement; - ClaimsToQueries = claimsToQueries ?? new List(); - ClaimsToHeaders = claimsToHeaders ?? new List(); - ClaimsToClaims = claimsToClaims ?? new List(); - ClaimsToPath = claimsToPath ?? new List(); - IsAuthenticated = isAuthenticated; - IsAuthorised = isAuthorised; - AuthenticationOptions = authenticationOptions; - DownstreamPathTemplate = downstreamPathTemplate; - LoadBalancerKey = loadBalancerKey; - AddHeadersToUpstream = addHeadersToUpstream; - SecurityOptions = securityOptions; - } +using Ocelot.Configuration.Creator; - public string Key { get; } - public UpstreamPathTemplate UpstreamPathTemplate { get; } - public List UpstreamHeadersFindAndReplace { get; } - public List DownstreamHeadersFindAndReplace { get; } - public List DownstreamAddresses { get; } - public string ServiceName { get; } - public string ServiceNamespace { get; } - public HttpHandlerOptions HttpHandlerOptions { get; } - public bool UseServiceDiscovery { get; } - public bool EnableEndpointEndpointRateLimiting { get; } - public QoSOptions QosOptions { get; } - public string DownstreamScheme { get; } - public string RequestIdKey { get; } - public bool IsCached { get; } - public CacheOptions CacheOptions { get; } - public LoadBalancerOptions LoadBalancerOptions { get; } - public RateLimitOptions RateLimitOptions { get; } - public Dictionary RouteClaimsRequirement { get; } - public List ClaimsToQueries { get; } - public List ClaimsToHeaders { get; } - public List ClaimsToClaims { get; } - public List ClaimsToPath { get; } - public bool IsAuthenticated { get; } - public bool IsAuthorised { get; } - public AuthenticationOptions AuthenticationOptions { get; } - public DownstreamPathTemplate DownstreamPathTemplate { get; } - public string LoadBalancerKey { get; } - public List DelegatingHandlers { get; } - public List AddHeadersToDownstream { get; } - public List AddHeadersToUpstream { get; } - public bool DangerousAcceptAnyServerCertificateValidator { get; } - public SecurityOptions SecurityOptions { get; } - } -} +using Ocelot.Values; + +namespace Ocelot.Configuration +{ + public class DownstreamRoute + { + public DownstreamRoute( + string key, + UpstreamPathTemplate upstreamPathTemplate, + List upstreamHeadersFindAndReplace, + List downstreamHeadersFindAndReplace, + List downstreamAddresses, + string serviceName, + string serviceNamespace, + HttpHandlerOptions httpHandlerOptions, + bool useServiceDiscovery, + bool enableEndpointEndpointRateLimiting, + QoSOptions qosOptions, + string downstreamScheme, + string requestIdKey, + bool isCached, + CacheOptions cacheOptions, + LoadBalancerOptions loadBalancerOptions, + RateLimitOptions rateLimitOptions, + Dictionary routeClaimsRequirement, + List claimsToQueries, + List claimsToHeaders, + List claimsToClaims, + List claimsToPath, + bool isAuthenticated, + bool isAuthorized, + AuthenticationOptions authenticationOptions, + DownstreamPathTemplate downstreamPathTemplate, + string loadBalancerKey, + List delegatingHandlers, + List addHeadersToDownstream, + List addHeadersToUpstream, + bool dangerousAcceptAnyServerCertificateValidator, + SecurityOptions securityOptions, + string downstreamHttpMethod, + Version downstreamHttpVersion) + { + DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; + AddHeadersToDownstream = addHeadersToDownstream; + DelegatingHandlers = delegatingHandlers; + Key = key; + UpstreamPathTemplate = upstreamPathTemplate; + UpstreamHeadersFindAndReplace = upstreamHeadersFindAndReplace ?? new List(); + DownstreamHeadersFindAndReplace = downstreamHeadersFindAndReplace ?? new List(); + DownstreamAddresses = downstreamAddresses ?? new List(); + ServiceName = serviceName; + ServiceNamespace = serviceNamespace; + HttpHandlerOptions = httpHandlerOptions; + UseServiceDiscovery = useServiceDiscovery; + EnableEndpointEndpointRateLimiting = enableEndpointEndpointRateLimiting; + QosOptions = qosOptions; + DownstreamScheme = downstreamScheme; + RequestIdKey = requestIdKey; + IsCached = isCached; + CacheOptions = cacheOptions; + LoadBalancerOptions = loadBalancerOptions; + RateLimitOptions = rateLimitOptions; + RouteClaimsRequirement = routeClaimsRequirement; + ClaimsToQueries = claimsToQueries ?? new List(); + ClaimsToHeaders = claimsToHeaders ?? new List(); + ClaimsToClaims = claimsToClaims ?? new List(); + ClaimsToPath = claimsToPath ?? new List(); + IsAuthenticated = isAuthenticated; + IsAuthorized = isAuthorized; + AuthenticationOptions = authenticationOptions; + DownstreamPathTemplate = downstreamPathTemplate; + LoadBalancerKey = loadBalancerKey; + AddHeadersToUpstream = addHeadersToUpstream; + SecurityOptions = securityOptions; + DownstreamHttpMethod = downstreamHttpMethod; + DownstreamHttpVersion = downstreamHttpVersion; + } + + public string Key { get; } + public UpstreamPathTemplate UpstreamPathTemplate { get; } + public List UpstreamHeadersFindAndReplace { get; } + public List DownstreamHeadersFindAndReplace { get; } + public List DownstreamAddresses { get; } + public string ServiceName { get; } + public string ServiceNamespace { get; } + public HttpHandlerOptions HttpHandlerOptions { get; } + public bool UseServiceDiscovery { get; } + public bool EnableEndpointEndpointRateLimiting { get; } + public QoSOptions QosOptions { get; } + public string DownstreamScheme { get; } + public string RequestIdKey { get; } + public bool IsCached { get; } + public CacheOptions CacheOptions { get; } + public LoadBalancerOptions LoadBalancerOptions { get; } + public RateLimitOptions RateLimitOptions { get; } + public Dictionary RouteClaimsRequirement { get; } + public List ClaimsToQueries { get; } + public List ClaimsToHeaders { get; } + public List ClaimsToClaims { get; } + public List ClaimsToPath { get; } + public bool IsAuthenticated { get; } + public bool IsAuthorized { get; } + public AuthenticationOptions AuthenticationOptions { get; } + public DownstreamPathTemplate DownstreamPathTemplate { get; } + public string LoadBalancerKey { get; } + public List DelegatingHandlers { get; } + public List AddHeadersToDownstream { get; } + public List AddHeadersToUpstream { get; } + public bool DangerousAcceptAnyServerCertificateValidator { get; } + public SecurityOptions SecurityOptions { get; } + public string DownstreamHttpMethod { get; } + public Version DownstreamHttpVersion { get; } + } +} diff --git a/src/Ocelot/Configuration/File/AggregateReRouteConfig.cs b/src/Ocelot/Configuration/File/AggregateRouteConfig.cs similarity index 61% rename from src/Ocelot/Configuration/File/AggregateReRouteConfig.cs rename to src/Ocelot/Configuration/File/AggregateRouteConfig.cs index 6519d6b5a..c63ed6b4a 100644 --- a/src/Ocelot/Configuration/File/AggregateReRouteConfig.cs +++ b/src/Ocelot/Configuration/File/AggregateRouteConfig.cs @@ -1,9 +1,9 @@ -namespace Ocelot.Configuration.File -{ - public class AggregateReRouteConfig - { - public string ReRouteKey { get; set; } - public string Parameter { get; set; } - public string JsonPath { get; set; } - } -} +namespace Ocelot.Configuration.File +{ + public class AggregateRouteConfig + { + public string RouteKey { get; set; } + public string Parameter { get; set; } + public string JsonPath { get; set; } + } +} diff --git a/src/Ocelot/Configuration/File/FileAggregateReRoute.cs b/src/Ocelot/Configuration/File/FileAggregateRoute.cs similarity index 50% rename from src/Ocelot/Configuration/File/FileAggregateReRoute.cs rename to src/Ocelot/Configuration/File/FileAggregateRoute.cs index 2f1b4676e..461846433 100644 --- a/src/Ocelot/Configuration/File/FileAggregateReRoute.cs +++ b/src/Ocelot/Configuration/File/FileAggregateRoute.cs @@ -1,22 +1,19 @@ -using System.Collections.Generic; - -namespace Ocelot.Configuration.File +using System.Collections.Generic; + +namespace Ocelot.Configuration.File { - public class FileAggregateReRoute : IReRoute - { - public List ReRouteKeys { get; set; } - public List ReRouteKeysConfig { get; set; } - public string UpstreamPathTemplate { get; set; } - public string UpstreamHost { get; set; } - public bool ReRouteIsCaseSensitive { get; set; } - public string Aggregator { get; set; } - - // Only supports GET..are you crazy!! POST, PUT WOULD BE CRAZY!! :) - public List UpstreamHttpMethod - { - get { return new List { "Get" }; } - } - - public int Priority { get; set; } = 1; - } -} + public class FileAggregateRoute : IRoute + { + public List RouteKeys { get; set; } + public List RouteKeysConfig { get; set; } + public string UpstreamPathTemplate { get; set; } + public string UpstreamHost { get; set; } + public bool RouteIsCaseSensitive { get; set; } + public string Aggregator { get; set; } + + // Only supports GET..are you crazy!! POST, PUT WOULD BE CRAZY!! :) + public List UpstreamHttpMethod => new() { "Get" }; + + public int Priority { get; set; } = 1; + } +} diff --git a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs index 81805df12..e29fc6a3c 100644 --- a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs +++ b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs @@ -1,5 +1,4 @@ -using Ocelot.Infrastructure.Extensions; -using System.Collections.Generic; +using System.Collections.Generic; using System.Text; namespace Ocelot.Configuration.File @@ -19,7 +18,7 @@ public override string ToString() var sb = new StringBuilder(); sb.Append($"{nameof(AuthenticationProviderKey)}:{AuthenticationProviderKey},{nameof(AllowedScopes)}:["); sb.AppendJoin(',', AllowedScopes); - sb.Append("]"); + sb.Append(']'); return sb.ToString(); } } diff --git a/src/Ocelot/Configuration/File/FileConfiguration.cs b/src/Ocelot/Configuration/File/FileConfiguration.cs index ecd41a352..b517f298b 100644 --- a/src/Ocelot/Configuration/File/FileConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileConfiguration.cs @@ -1,23 +1,23 @@ -using System.Collections.Generic; - -namespace Ocelot.Configuration.File -{ - public class FileConfiguration - { - public FileConfiguration() - { - ReRoutes = new List(); - GlobalConfiguration = new FileGlobalConfiguration(); - Aggregates = new List(); - DynamicReRoutes = new List(); - } - - public List ReRoutes { get; set; } - public List DynamicReRoutes { get; set; } - - // Seperate field for aggregates because this let's you re-use ReRoutes in multiple Aggregates - public List Aggregates { get; set; } - - public FileGlobalConfiguration GlobalConfiguration { get; set; } - } -} +using System.Collections.Generic; + +namespace Ocelot.Configuration.File +{ + public class FileConfiguration + { + public FileConfiguration() + { + Routes = new List(); + GlobalConfiguration = new FileGlobalConfiguration(); + Aggregates = new List(); + DynamicRoutes = new List(); + } + + public List Routes { get; set; } + public List DynamicRoutes { get; set; } + + // Seperate field for aggregates because this let's you re-use Routes in multiple Aggregates + public List Aggregates { get; set; } + + public FileGlobalConfiguration GlobalConfiguration { get; set; } + } +} diff --git a/src/Ocelot/Configuration/File/FileDynamicReRoute.cs b/src/Ocelot/Configuration/File/FileDynamicRoute.cs similarity index 61% rename from src/Ocelot/Configuration/File/FileDynamicReRoute.cs rename to src/Ocelot/Configuration/File/FileDynamicRoute.cs index 26d8b4d4f..fb93c2f91 100644 --- a/src/Ocelot/Configuration/File/FileDynamicReRoute.cs +++ b/src/Ocelot/Configuration/File/FileDynamicRoute.cs @@ -1,8 +1,9 @@ -namespace Ocelot.Configuration.File -{ - public class FileDynamicReRoute - { - public string ServiceName { get; set; } - public FileRateLimitRule RateLimitRule { get; set; } - } -} +namespace Ocelot.Configuration.File +{ + public class FileDynamicRoute + { + public string ServiceName { get; set; } + public FileRateLimitRule RateLimitRule { get; set; } + public string DownstreamHttpVersion { get; set; } + } +} diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs index 9844c15d4..6692ba046 100644 --- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -26,5 +26,7 @@ public FileGlobalConfiguration() public string DownstreamScheme { get; set; } public FileHttpHandlerOptions HttpHandlerOptions { get; set; } + + public string DownstreamHttpVersion { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileJwtConfig.cs b/src/Ocelot/Configuration/File/FileJwtConfig.cs index a028c6018..e87b1dfa4 100644 --- a/src/Ocelot/Configuration/File/FileJwtConfig.cs +++ b/src/Ocelot/Configuration/File/FileJwtConfig.cs @@ -6,4 +6,4 @@ public class FileJwtConfig public string Audience { get; set; } } -} \ No newline at end of file +} diff --git a/src/Ocelot/Configuration/File/FileRateLimitOptions.cs b/src/Ocelot/Configuration/File/FileRateLimitOptions.cs index 51be9ed09..557f8225e 100644 --- a/src/Ocelot/Configuration/File/FileRateLimitOptions.cs +++ b/src/Ocelot/Configuration/File/FileRateLimitOptions.cs @@ -3,30 +3,51 @@ public class FileRateLimitOptions { /// - /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId + /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId. /// + /// + /// A string with the HTTP header that holds the client identifier, by default is X-ClientId. + /// public string ClientIdHeader { get; set; } = "ClientId"; /// /// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message. /// If none specified the default will be: - /// API calls quota exceeded! maximum admitted {0} per {1} + /// API calls quota exceeded! maximum admitted {0} per {1}. /// + /// + /// A string value that will be used as a formatter. + /// + /// If none specified the default will be: "API calls quota exceeded! maximum admitted {0} per {1}". + /// + /// public string QuotaExceededMessage { get; set; } /// - /// Gets or sets the counter prefix, used to compose the rate limit counter cache key + /// Gets or sets the counter prefix, used to compose the rate limit counter cache key. /// + /// + /// A string with counter prefix, used to compose the rate limit counter cache key. + /// public string RateLimitCounterPrefix { get; set; } = "ocelot"; /// - /// Disables X-Rate-Limit and Rety-After headers + /// Disables X-Rate-Limit and Rety-After headers. /// + /// + /// A boolean value for disabling X-Rate-Limit and Rety-After headers. + /// public bool DisableRateLimitHeaders { get; set; } /// - /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests) + /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests). /// + /// + /// An integer value with the HTTP Status code returned when rate limiting occurs. + /// + /// Default value: 429 (Too Many Requests). + /// + /// public int HttpStatusCode { get; set; } = 429; } } diff --git a/src/Ocelot/Configuration/File/FileRateLimitRule.cs b/src/Ocelot/Configuration/File/FileRateLimitRule.cs index 575ad1906..2d2a3204e 100644 --- a/src/Ocelot/Configuration/File/FileRateLimitRule.cs +++ b/src/Ocelot/Configuration/File/FileRateLimitRule.cs @@ -1,5 +1,4 @@ -using Ocelot.Infrastructure.Extensions; -using System.Collections.Generic; +using System.Collections.Generic; using System.Text; namespace Ocelot.Configuration.File @@ -11,25 +10,47 @@ public FileRateLimitRule() ClientWhitelist = new List(); } + /// + /// The list of allowed clients. + /// + /// + /// A collection of allowed clients. + /// public List ClientWhitelist { get; set; } /// - /// Enables endpoint rate limiting based URL path and HTTP verb + /// Enables endpoint rate limiting based URL path and HTTP verb. /// + /// + /// A boolean value for enabling endpoint rate limiting based URL path and HTTP verb. + /// public bool EnableRateLimiting { get; set; } /// - /// Rate limit period as in 1s, 1m, 1h + /// Rate limit period as in 1s, 1m, 1h. /// + /// + /// A string of rate limit period. + /// public string Period { get; set; } + /// + /// Rate limit period to wait before new request (in seconds). + /// + /// + /// A double floating integer with rate limit period. + /// public double PeriodTimespan { get; set; } /// - /// Maximum number of requests that a client can make in a defined period + /// Maximum number of requests that a client can make in a defined period. /// + /// + /// A long integer with maximum number of requests. + /// public long Limit { get; set; } + /// public override string ToString() { if (!EnableRateLimiting) diff --git a/src/Ocelot/Configuration/File/FileReRoute.cs b/src/Ocelot/Configuration/File/FileRoute.cs similarity index 91% rename from src/Ocelot/Configuration/File/FileReRoute.cs rename to src/Ocelot/Configuration/File/FileRoute.cs index b15653ea9..6ff9856b2 100644 --- a/src/Ocelot/Configuration/File/FileReRoute.cs +++ b/src/Ocelot/Configuration/File/FileRoute.cs @@ -1,59 +1,61 @@ -using System.Collections.Generic; - -namespace Ocelot.Configuration.File +using System.Collections.Generic; + +namespace Ocelot.Configuration.File { - public class FileReRoute : IReRoute - { - public FileReRoute() - { - UpstreamHttpMethod = new List(); - AddHeadersToRequest = new Dictionary(); - AddClaimsToRequest = new Dictionary(); - RouteClaimsRequirement = new Dictionary(); - AddQueriesToRequest = new Dictionary(); - ChangeDownstreamPathTemplate = new Dictionary(); - DownstreamHeaderTransform = new Dictionary(); - 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 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 ReRouteIsCaseSensitive { 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 class FileRoute : IRoute + { + public FileRoute() + { + UpstreamHttpMethod = new List(); + AddHeadersToRequest = new Dictionary(); + AddClaimsToRequest = new Dictionary(); + RouteClaimsRequirement = new Dictionary(); + AddQueriesToRequest = new Dictionary(); + ChangeDownstreamPathTemplate = new Dictionary(); + DownstreamHeaderTransform = new Dictionary(); + 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; } + } +} diff --git a/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs b/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs index c41f1e24a..71a34e42e 100644 --- a/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs +++ b/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs @@ -2,6 +2,7 @@ namespace Ocelot.Configuration.File { public class FileServiceDiscoveryProvider { + public string Scheme { get; set; } public string Host { get; set; } public int Port { get; set; } public string Type { get; set; } diff --git a/src/Ocelot/Configuration/File/IReRoute.cs b/src/Ocelot/Configuration/File/IReRoute.cs deleted file mode 100644 index b51914283..000000000 --- a/src/Ocelot/Configuration/File/IReRoute.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Ocelot.Configuration.File -{ - public interface IReRoute - { - string UpstreamPathTemplate { get; set; } - bool ReRouteIsCaseSensitive { get; set; } - int Priority { get; set; } - } -} diff --git a/src/Ocelot/Configuration/File/IRoute.cs b/src/Ocelot/Configuration/File/IRoute.cs new file mode 100644 index 000000000..74df79b23 --- /dev/null +++ b/src/Ocelot/Configuration/File/IRoute.cs @@ -0,0 +1,8 @@ +namespace Ocelot.Configuration.File; + +public interface IRoute +{ + string UpstreamPathTemplate { get; set; } + bool RouteIsCaseSensitive { get; set; } + int Priority { get; set; } +} diff --git a/src/Ocelot/Configuration/FileConfigurationController.cs b/src/Ocelot/Configuration/FileConfigurationController.cs index 2f6a51af0..c909cc5ff 100644 --- a/src/Ocelot/Configuration/FileConfigurationController.cs +++ b/src/Ocelot/Configuration/FileConfigurationController.cs @@ -1,14 +1,15 @@ +using System; +using System.Threading.Tasks; + using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; + using Ocelot.Configuration.File; using Ocelot.Configuration.Setter; -using System; -using System.Threading.Tasks; +using Ocelot.Configuration.Repository; namespace Ocelot.Configuration { - using Repository; - [Authorize] [Route("configuration")] public class FileConfigurationController : Controller @@ -38,7 +39,7 @@ public async Task Get() } [HttpPost] - public async Task Post([FromBody]FileConfiguration fileConfiguration) + public async Task Post([FromBody] FileConfiguration fileConfiguration) { try { diff --git a/src/Ocelot/Configuration/HttpHandlerOptions.cs b/src/Ocelot/Configuration/HttpHandlerOptions.cs index e76cc117d..6976c3f46 100644 --- a/src/Ocelot/Configuration/HttpHandlerOptions.cs +++ b/src/Ocelot/Configuration/HttpHandlerOptions.cs @@ -2,7 +2,7 @@ { /// /// Describes configuration parameters for http handler, - /// that is created to handle a request to service + /// that is created to handle a request to service. /// public class HttpHandlerOptions { @@ -15,35 +15,34 @@ public HttpHandlerOptions(bool allowAutoRedirect, bool useCookieContainer, bool MaxConnectionsPerServer = maxConnectionsPerServer; } - /// - /// Specify if auto redirect is enabled - /// - /// AllowAutoRedirect - public bool AllowAutoRedirect { get; private set; } + /// Specify if auto redirect is enabled. + /// + /// AllowAutoRedirect. + public bool AllowAutoRedirect { get; } /// - /// Specify is handler has to use a cookie container - /// - /// UseCookieContainer - public bool UseCookieContainer { get; private set; } + /// Specify is handler has to use a cookie container. + /// + /// UseCookieContainer. + public bool UseCookieContainer { get; } /// - /// Specify is handler has to use a opentracing - /// - /// UseTracing - public bool UseTracing { get; private set; } + /// Specify is handler has to use a opentracing. + /// + /// UseTracing. + public bool UseTracing { get; } /// - /// Specify if handler has to use a proxy - /// - /// UseProxy - public bool UseProxy { get; private set; } + /// Specify if handler has to use a proxy. + /// + /// UseProxy. + public bool UseProxy { get; } /// - /// Specify the maximum of concurrent connection to a network endpoint - /// - /// MaxConnectionsPerServer - public int MaxConnectionsPerServer { get; private set; } + /// Specify the maximum of concurrent connection to a network endpoint. + /// + /// MaxConnectionsPerServer. + public int MaxConnectionsPerServer { get; } } } diff --git a/src/Ocelot/Configuration/HttpHandlerOptionsBuilder.cs b/src/Ocelot/Configuration/HttpHandlerOptionsBuilder.cs index 8e66094b4..bc96296d6 100644 --- a/src/Ocelot/Configuration/HttpHandlerOptionsBuilder.cs +++ b/src/Ocelot/Configuration/HttpHandlerOptionsBuilder.cs @@ -31,13 +31,13 @@ public HttpHandlerOptionsBuilder WithUseProxy(bool useProxy) _useProxy = useProxy; return this; } + public HttpHandlerOptionsBuilder WithUseMaxConnectionPerServer(int maxConnectionPerServer) { _maxConnectionPerServer = maxConnectionPerServer; return this; } - public HttpHandlerOptions Build() { return new HttpHandlerOptions(_allowAutoRedirect, _useCookieContainer, _useTracing, _useProxy, _maxConnectionPerServer); diff --git a/src/Ocelot/Configuration/IInternalConfiguration.cs b/src/Ocelot/Configuration/IInternalConfiguration.cs index 1780da330..980ad1259 100644 --- a/src/Ocelot/Configuration/IInternalConfiguration.cs +++ b/src/Ocelot/Configuration/IInternalConfiguration.cs @@ -1,23 +1,26 @@ using System.Collections.Generic; - -namespace Ocelot.Configuration -{ - public interface IInternalConfiguration - { - List ReRoutes { get; } - - string AdministrationPath { get; } - - ServiceProviderConfiguration ServiceProviderConfiguration { get; } - - string RequestId { get; } - - LoadBalancerOptions LoadBalancerOptions { get; } - - string DownstreamScheme { get; } - - QoSOptions QoSOptions { get; } - - HttpHandlerOptions HttpHandlerOptions { get; } - } -} +using System; + +namespace Ocelot.Configuration +{ + public interface IInternalConfiguration + { + List Routes { get; } + + string AdministrationPath { get; } + + ServiceProviderConfiguration ServiceProviderConfiguration { get; } + + string RequestId { get; } + + LoadBalancerOptions LoadBalancerOptions { get; } + + string DownstreamScheme { get; } + + QoSOptions QoSOptions { get; } + + HttpHandlerOptions HttpHandlerOptions { get; } + + Version DownstreamHttpVersion { get; } + } +} diff --git a/src/Ocelot/Configuration/InternalConfiguration.cs b/src/Ocelot/Configuration/InternalConfiguration.cs index 7b7649e4a..c2a814f31 100644 --- a/src/Ocelot/Configuration/InternalConfiguration.cs +++ b/src/Ocelot/Configuration/InternalConfiguration.cs @@ -1,36 +1,41 @@ using System.Collections.Generic; - -namespace Ocelot.Configuration -{ - public class InternalConfiguration : IInternalConfiguration - { - public InternalConfiguration( - List reRoutes, - string administrationPath, - ServiceProviderConfiguration serviceProviderConfiguration, - string requestId, - LoadBalancerOptions loadBalancerOptions, - string downstreamScheme, - QoSOptions qoSOptions, - HttpHandlerOptions httpHandlerOptions) - { - ReRoutes = reRoutes; - AdministrationPath = administrationPath; - ServiceProviderConfiguration = serviceProviderConfiguration; - RequestId = requestId; - LoadBalancerOptions = loadBalancerOptions; - DownstreamScheme = downstreamScheme; - QoSOptions = qoSOptions; - HttpHandlerOptions = httpHandlerOptions; - } - - public List ReRoutes { get; } - public string AdministrationPath { get; } - public ServiceProviderConfiguration ServiceProviderConfiguration { get; } - public string RequestId { get; } - public LoadBalancerOptions LoadBalancerOptions { get; } - public string DownstreamScheme { get; } - public QoSOptions QoSOptions { get; } - public HttpHandlerOptions HttpHandlerOptions { get; } - } -} +using System; + +namespace Ocelot.Configuration +{ + public class InternalConfiguration : IInternalConfiguration + { + public InternalConfiguration( + List routes, + string administrationPath, + ServiceProviderConfiguration serviceProviderConfiguration, + string requestId, + LoadBalancerOptions loadBalancerOptions, + string downstreamScheme, + QoSOptions qoSOptions, + HttpHandlerOptions httpHandlerOptions, + Version downstreamHttpVersion) + { + Routes = routes; + AdministrationPath = administrationPath; + ServiceProviderConfiguration = serviceProviderConfiguration; + RequestId = requestId; + LoadBalancerOptions = loadBalancerOptions; + DownstreamScheme = downstreamScheme; + QoSOptions = qoSOptions; + HttpHandlerOptions = httpHandlerOptions; + DownstreamHttpVersion = downstreamHttpVersion; + } + + public List Routes { get; } + public string AdministrationPath { get; } + public ServiceProviderConfiguration ServiceProviderConfiguration { get; } + public string RequestId { get; } + public LoadBalancerOptions LoadBalancerOptions { get; } + public string DownstreamScheme { get; } + public QoSOptions QoSOptions { get; } + public HttpHandlerOptions HttpHandlerOptions { get; } + + public Version DownstreamHttpVersion { get; } + } +} diff --git a/src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs b/src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs index 76614f960..2b5dd571a 100644 --- a/src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs +++ b/src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs @@ -1,24 +1,25 @@ -using Ocelot.Responses; -using System; +using System; using System.Text.RegularExpressions; + +using Ocelot.Responses; namespace Ocelot.Configuration.Parser { public class ClaimToThingConfigurationParser : IClaimToThingConfigurationParser { - private readonly Regex _claimRegex = new Regex("Claims\\[.*\\]"); - private readonly Regex _indexRegex = new Regex("value\\[.*\\]"); - private const string SplitToken = ">"; + private readonly Regex _claimRegex = new("Claims\\[.*\\]"); + private readonly Regex _indexRegex = new("value\\[.*\\]"); + private const char SplitToken = '>'; public Response Extract(string existingKey, string value) { try { - var instructions = value.Split(SplitToken.ToCharArray()); + var instructions = value.Split(SplitToken); if (instructions.Length <= 1) { - return new ErrorResponse(new NoInstructionsError(SplitToken)); + return new ErrorResponse(new NoInstructionsError(SplitToken.ToString())); } var claimMatch = _claimRegex.IsMatch(instructions[0]); @@ -47,10 +48,10 @@ public Response Extract(string existingKey, string value) } } - private string GetIndexValue(string instruction) + private static string GetIndexValue(string instruction) { - var firstIndexer = instruction.IndexOf("[", StringComparison.Ordinal); - var lastIndexer = instruction.IndexOf("]", StringComparison.Ordinal); + var firstIndexer = instruction.IndexOf('[', StringComparison.Ordinal); + var lastIndexer = instruction.IndexOf(']', StringComparison.Ordinal); var length = lastIndexer - firstIndexer; var claimKey = instruction.Substring(firstIndexer + 1, length - 1); return claimKey; diff --git a/src/Ocelot/Configuration/Parser/IClaimToThingConfigurationParser.cs b/src/Ocelot/Configuration/Parser/IClaimToThingConfigurationParser.cs index 3af0a32e9..d62500a97 100644 --- a/src/Ocelot/Configuration/Parser/IClaimToThingConfigurationParser.cs +++ b/src/Ocelot/Configuration/Parser/IClaimToThingConfigurationParser.cs @@ -6,4 +6,4 @@ public interface IClaimToThingConfigurationParser { Response Extract(string existingKey, string value); } -} \ No newline at end of file +} diff --git a/src/Ocelot/Configuration/Parser/InstructionNotForClaimsError.cs b/src/Ocelot/Configuration/Parser/InstructionNotForClaimsError.cs index 24535653a..b42f4621a 100644 --- a/src/Ocelot/Configuration/Parser/InstructionNotForClaimsError.cs +++ b/src/Ocelot/Configuration/Parser/InstructionNotForClaimsError.cs @@ -1,12 +1,12 @@ -using Ocelot.Errors; - -namespace Ocelot.Configuration.Parser -{ - public class InstructionNotForClaimsError : Error - { +using Ocelot.Errors; + +namespace Ocelot.Configuration.Parser +{ + public class InstructionNotForClaimsError : Error + { public InstructionNotForClaimsError() - : base("instructions did not contain claims, at the moment we only support claims extraction", OcelotErrorCode.InstructionNotForClaimsError) - { - } - } + : base("instructions did not contain claims, at the moment we only support claims extraction", OcelotErrorCode.InstructionNotForClaimsError, 404) + { + } + } } diff --git a/src/Ocelot/Configuration/Parser/NoInstructionsError.cs b/src/Ocelot/Configuration/Parser/NoInstructionsError.cs index 67bcd3e18..ff3a08444 100644 --- a/src/Ocelot/Configuration/Parser/NoInstructionsError.cs +++ b/src/Ocelot/Configuration/Parser/NoInstructionsError.cs @@ -1,12 +1,12 @@ -using Ocelot.Errors; - -namespace Ocelot.Configuration.Parser -{ - public class NoInstructionsError : Error - { +using Ocelot.Errors; + +namespace Ocelot.Configuration.Parser +{ + public class NoInstructionsError : Error + { public NoInstructionsError(string splitToken) - : base($"There we no instructions splitting on {splitToken}", OcelotErrorCode.NoInstructionsError) - { - } - } + : base($"There we no instructions splitting on {splitToken}", OcelotErrorCode.NoInstructionsError, 404) + { + } + } } diff --git a/src/Ocelot/Configuration/Parser/ParsingConfigurationHeaderError.cs b/src/Ocelot/Configuration/Parser/ParsingConfigurationHeaderError.cs index fba6a6db0..bcff18693 100644 --- a/src/Ocelot/Configuration/Parser/ParsingConfigurationHeaderError.cs +++ b/src/Ocelot/Configuration/Parser/ParsingConfigurationHeaderError.cs @@ -1,13 +1,14 @@ -using Ocelot.Errors; -using System; - -namespace Ocelot.Configuration.Parser -{ - public class ParsingConfigurationHeaderError : Error - { +using System; + +using Ocelot.Errors; + +namespace Ocelot.Configuration.Parser +{ + public class ParsingConfigurationHeaderError : Error + { public ParsingConfigurationHeaderError(Exception exception) - : base($"error parsing configuration eception is {exception.Message}", OcelotErrorCode.ParsingConfigurationHeaderError) - { - } - } + : base($"error parsing configuration eception is {exception.Message}", OcelotErrorCode.ParsingConfigurationHeaderError, 404) + { + } + } } diff --git a/src/Ocelot/Configuration/RateLimitOptions.cs b/src/Ocelot/Configuration/RateLimitOptions.cs index 28472825b..15827a066 100644 --- a/src/Ocelot/Configuration/RateLimitOptions.cs +++ b/src/Ocelot/Configuration/RateLimitOptions.cs @@ -4,7 +4,7 @@ namespace Ocelot.Configuration { /// - /// RateLimit Options + /// RateLimit Options. /// public class RateLimitOptions { @@ -21,45 +21,73 @@ public RateLimitOptions(bool enableRateLimiting, string clientIdHeader, Func + /// Gets a Rate Limit rule. + /// + /// + /// A object that represents the rule. + /// + public RateLimitRule RateLimitRule { get; } + /// - /// Gets the list of white listed clients + /// Gets the list of white listed clients. /// - public List ClientWhitelist { get => _getClientWhitelist(); } - + /// + /// A collection with white listed clients. + /// + public List ClientWhitelist => _getClientWhitelist(); + /// - /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId + /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId. /// - public string ClientIdHeader { get; private set; } - + /// + /// A string value with the HTTP header. + /// + public string ClientIdHeader { get; } + /// - /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests) + /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests). /// - public int HttpStatusCode { get; private set; } - + /// + /// An integer value with the HTTP Status code. + /// Default value: 429 (Too Many Requests). + /// + public int HttpStatusCode { get; } + /// - /// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message. - /// If none specified the default will be: - /// API calls quota exceeded! maximum admitted {0} per {1} + /// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message. + /// If none specified the default will be: "API calls quota exceeded! maximum admitted {0} per {1}". /// - public string QuotaExceededMessage { get; private set; } - + /// + /// A string value with a formatter for the QuotaExceeded response message. + /// Default will be: "API calls quota exceeded! maximum admitted {0} per {1}". + /// + public string QuotaExceededMessage { get; } + /// - /// Gets or sets the counter prefix, used to compose the rate limit counter cache key + /// Gets or sets the counter prefix, used to compose the rate limit counter cache key. /// - public string RateLimitCounterPrefix { get; private set; } - + /// + /// A string value with the counter prefix. + /// + public string RateLimitCounterPrefix { get; } + /// - /// Enables endpoint rate limiting based URL path and HTTP verb + /// Enables endpoint rate limiting based URL path and HTTP verb. /// - public bool EnableRateLimiting { get; private set; } - + /// + /// A boolean value for enabling endpoint rate limiting based URL path and HTTP verb. + /// + public bool EnableRateLimiting { get; } + /// - /// Disables X-Rate-Limit and Rety-After headers + /// Disables X-Rate-Limit and Rety-After headers. /// - public bool DisableRateLimitHeaders { get; private set; } + /// + /// A boolean value for disabling X-Rate-Limit and Rety-After headers. + /// + public bool DisableRateLimitHeaders { get; } } } diff --git a/src/Ocelot/Configuration/RateLimitRule.cs b/src/Ocelot/Configuration/RateLimitRule.cs index b3393ae59..5b22ae618 100644 --- a/src/Ocelot/Configuration/RateLimitRule.cs +++ b/src/Ocelot/Configuration/RateLimitRule.cs @@ -7,18 +7,30 @@ public RateLimitRule(string period, double periodTimespan, long limit) Period = period; PeriodTimespan = periodTimespan; Limit = limit; - } - + } + /// - /// Rate limit period as in 1s, 1m, 1h,1d + /// Rate limit period as in 1s, 1m, 1h, 1d. + /// + /// + /// A string value with rate limit period. + /// + public string Period { get; } + + /// + /// Timespan to wait after reaching the rate limit, in seconds. /// - public string Period { get; private set; } - - public double PeriodTimespan { get; private set; } + /// + /// A double floating-point integer with timespan, in seconds. + /// + public double PeriodTimespan { get; } /// - /// Maximum number of requests that a client can make in a defined period + /// Maximum number of requests that a client can make in a defined period. /// - public long Limit { get; private set; } + /// + /// A long integer with maximum number of requests. + /// + public long Limit { get; } } } diff --git a/src/Ocelot/Configuration/ReRoute.cs b/src/Ocelot/Configuration/ReRoute.cs deleted file mode 100644 index e7efe12bc..000000000 --- a/src/Ocelot/Configuration/ReRoute.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Ocelot.Configuration -{ - using Ocelot.Configuration.File; - using Ocelot.Values; - using System.Collections.Generic; - using System.Net.Http; - - public class ReRoute - { - public ReRoute(List downstreamReRoute, - List downstreamReRouteConfig, - List upstreamHttpMethod, - UpstreamPathTemplate upstreamTemplatePattern, - string upstreamHost, - string aggregator) - { - UpstreamHost = upstreamHost; - DownstreamReRoute = downstreamReRoute; - DownstreamReRouteConfig = downstreamReRouteConfig; - UpstreamHttpMethod = upstreamHttpMethod; - UpstreamTemplatePattern = upstreamTemplatePattern; - Aggregator = aggregator; - } - - public UpstreamPathTemplate UpstreamTemplatePattern { get; private set; } - public List UpstreamHttpMethod { get; private set; } - public string UpstreamHost { get; private set; } - public List DownstreamReRoute { get; private set; } - public List DownstreamReRouteConfig { get; private set; } - public string Aggregator { get; private set; } - } -} diff --git a/src/Ocelot/Configuration/ReRouteOptions.cs b/src/Ocelot/Configuration/ReRouteOptions.cs deleted file mode 100644 index e2382d5c6..000000000 --- a/src/Ocelot/Configuration/ReRouteOptions.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Ocelot.Configuration -{ - public class ReRouteOptions - { - public ReRouteOptions(bool isAuthenticated, bool isAuthorised, bool isCached, bool isEnableRateLimiting, bool useServiceDiscovery) - { - IsAuthenticated = isAuthenticated; - IsAuthorised = isAuthorised; - IsCached = isCached; - EnableRateLimiting = isEnableRateLimiting; - UseServiceDiscovery = useServiceDiscovery; - } - - public bool IsAuthenticated { get; private set; } - public bool IsAuthorised { get; private set; } - public bool IsCached { get; private set; } - public bool EnableRateLimiting { get; private set; } - public bool UseServiceDiscovery { get; private set; } - } -} diff --git a/src/Ocelot/Configuration/Repository/ConsulFileConfigurationPollerOption.cs b/src/Ocelot/Configuration/Repository/ConsulFileConfigurationPollerOption.cs index a324a3fb8..2c6f26357 100644 --- a/src/Ocelot/Configuration/Repository/ConsulFileConfigurationPollerOption.cs +++ b/src/Ocelot/Configuration/Repository/ConsulFileConfigurationPollerOption.cs @@ -1,5 +1,4 @@ -using Ocelot.Responses; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Ocelot.Configuration.Repository { @@ -19,9 +18,9 @@ public ConsulFileConfigurationPollerOption(IInternalConfigurationRepository inte private int GetDelay() { - int delay = 1000; + var delay = 1000; - Response fileConfig = Task.Run(async () => await _fileConfigurationRepository.Get()).Result; + var fileConfig = Task.Run(async () => await _fileConfigurationRepository.Get()).Result; if (fileConfig?.Data?.GlobalConfiguration?.ServiceDiscoveryProvider != null && !fileConfig.IsError && fileConfig.Data.GlobalConfiguration.ServiceDiscoveryProvider.PollingInterval > 0) @@ -30,7 +29,7 @@ private int GetDelay() } else { - Response internalConfig = _internalConfigRepo.Get(); + var internalConfig = _internalConfigRepo.Get(); if (internalConfig?.Data?.ServiceProviderConfiguration != null && !internalConfig.IsError && internalConfig.Data.ServiceProviderConfiguration.PollingInterval > 0) diff --git a/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs index 770d7b7a6..def56f087 100644 --- a/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs +++ b/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs @@ -1,21 +1,27 @@ +using System; +using System.Threading.Tasks; + using Microsoft.AspNetCore.Hosting; + using Newtonsoft.Json; + +using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.File; using Ocelot.Responses; -using System; -using System.Threading.Tasks; namespace Ocelot.Configuration.Repository { public class DiskFileConfigurationRepository : IFileConfigurationRepository { + private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource; private readonly string _environmentFilePath; private readonly string _ocelotFilePath; - private static readonly object _lock = new object(); + private static readonly object _lock = new(); private const string ConfigurationFileName = "ocelot"; - public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment) + public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment, IOcelotConfigurationChangeTokenSource changeTokenSource) { + _changeTokenSource = changeTokenSource; _environmentFilePath = $"{AppContext.BaseDirectory}{ConfigurationFileName}{(string.IsNullOrEmpty(hostingEnvironment.EnvironmentName) ? string.Empty : ".")}{hostingEnvironment.EnvironmentName}.json"; _ocelotFilePath = $"{AppContext.BaseDirectory}{ConfigurationFileName}.json"; @@ -37,7 +43,7 @@ public Task> Get() public Task Set(FileConfiguration fileConfiguration) { - string jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); lock (_lock) { @@ -56,6 +62,7 @@ public Task Set(FileConfiguration fileConfiguration) System.IO.File.WriteAllText(_ocelotFilePath, jsonConfiguration); } + _changeTokenSource.Activate(); return Task.FromResult(new OkResponse()); } } diff --git a/src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs b/src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs index 9a2b67e62..c77c00716 100644 --- a/src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs +++ b/src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs @@ -1,12 +1,15 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using Newtonsoft.Json; + using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Logging; -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; namespace Ocelot.Configuration.Repository { @@ -33,7 +36,7 @@ public FileConfigurationPoller( _options = options; _logger = factory.CreateLogger(); _repo = repo; - _previousAsJson = ""; + _previousAsJson = string.Empty; } public Task StartAsync(CancellationToken cancellationToken) @@ -72,7 +75,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; } @@ -94,10 +97,10 @@ private async Task Poll() } /// - /// We could do object comparison here but performance isnt really a problem. This might be an issue one day! + /// We could do object comparison here but performance isnt really a problem. This might be an issue one day!. /// - /// hash of the config - private string ToJson(FileConfiguration config) + /// hash of the config. + private static string ToJson(FileConfiguration config) { var currentHash = JsonConvert.SerializeObject(config); return currentHash; diff --git a/src/Ocelot/Configuration/Repository/IFileConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/IFileConfigurationRepository.cs index 55ad6ce79..15841e636 100644 --- a/src/Ocelot/Configuration/Repository/IFileConfigurationRepository.cs +++ b/src/Ocelot/Configuration/Repository/IFileConfigurationRepository.cs @@ -1,6 +1,7 @@ -using Ocelot.Configuration.File; -using Ocelot.Responses; using System.Threading.Tasks; + +using Ocelot.Configuration.File; +using Ocelot.Responses; namespace Ocelot.Configuration.Repository { diff --git a/src/Ocelot/Configuration/Repository/InMemoryInternalConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/InMemoryInternalConfigurationRepository.cs index 2ac954e38..5cadebbb8 100644 --- a/src/Ocelot/Configuration/Repository/InMemoryInternalConfigurationRepository.cs +++ b/src/Ocelot/Configuration/Repository/InMemoryInternalConfigurationRepository.cs @@ -1,15 +1,22 @@ -using Ocelot.Responses; +using Ocelot.Configuration.ChangeTracking; +using Ocelot.Responses; namespace Ocelot.Configuration.Repository { /// - /// Register as singleton + /// Register as singleton. /// public class InMemoryInternalConfigurationRepository : IInternalConfigurationRepository { - private static readonly object LockObject = new object(); + private static readonly object LockObject = new(); private IInternalConfiguration _internalConfiguration; + private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource; + + public InMemoryInternalConfigurationRepository(IOcelotConfigurationChangeTokenSource changeTokenSource) + { + _changeTokenSource = changeTokenSource; + } public Response Get() { @@ -23,6 +30,7 @@ public Response AddOrReplace(IInternalConfiguration internalConfiguration) _internalConfiguration = internalConfiguration; } + _changeTokenSource.Activate(); return new OkResponse(); } } diff --git a/src/Ocelot/Configuration/Route.cs b/src/Ocelot/Configuration/Route.cs new file mode 100644 index 000000000..157c9b1be --- /dev/null +++ b/src/Ocelot/Configuration/Route.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Net.Http; + +using Ocelot.Configuration.File; + +using Ocelot.Values; + +namespace Ocelot.Configuration +{ + public class Route + { + public Route(List downstreamRoute, + List downstreamRouteConfig, + List upstreamHttpMethod, + UpstreamPathTemplate upstreamTemplatePattern, + string upstreamHost, + string aggregator) + { + UpstreamHost = upstreamHost; + DownstreamRoute = downstreamRoute; + DownstreamRouteConfig = downstreamRouteConfig; + UpstreamHttpMethod = upstreamHttpMethod; + UpstreamTemplatePattern = upstreamTemplatePattern; + Aggregator = aggregator; + } + + public UpstreamPathTemplate UpstreamTemplatePattern { get; } + public List UpstreamHttpMethod { get; } + public string UpstreamHost { get; } + public List DownstreamRoute { get; } + public List DownstreamRouteConfig { get; } + public string Aggregator { get; } + } +} diff --git a/src/Ocelot/Configuration/RouteOptions.cs b/src/Ocelot/Configuration/RouteOptions.cs new file mode 100644 index 000000000..8a005abe4 --- /dev/null +++ b/src/Ocelot/Configuration/RouteOptions.cs @@ -0,0 +1,20 @@ +namespace Ocelot.Configuration +{ + public class RouteOptions + { + public RouteOptions(bool isAuthenticated, bool isAuthorized, bool isCached, bool isEnableRateLimiting, bool useServiceDiscovery) + { + IsAuthenticated = isAuthenticated; + IsAuthorized = isAuthorized; + IsCached = isCached; + EnableRateLimiting = isEnableRateLimiting; + UseServiceDiscovery = useServiceDiscovery; + } + + public bool IsAuthenticated { get; } + public bool IsAuthorized { get; } + public bool IsCached { get; } + public bool EnableRateLimiting { get; } + public bool UseServiceDiscovery { get; } + } +} diff --git a/src/Ocelot/Configuration/SecurityOptions.cs b/src/Ocelot/Configuration/SecurityOptions.cs index e4cf42052..33af8a945 100644 --- a/src/Ocelot/Configuration/SecurityOptions.cs +++ b/src/Ocelot/Configuration/SecurityOptions.cs @@ -6,12 +6,12 @@ public class SecurityOptions { public SecurityOptions(List allowedList, List blockedList) { - this.IPAllowedList = allowedList; - this.IPBlockedList = blockedList; + IPAllowedList = allowedList; + IPBlockedList = blockedList; } - public List IPAllowedList { get; private set; } + public List IPAllowedList { get; } - public List IPBlockedList { get; private set; } + public List IPBlockedList { get; } } } diff --git a/src/Ocelot/Configuration/ServiceProviderConfiguration.cs b/src/Ocelot/Configuration/ServiceProviderConfiguration.cs index 86178c6e2..545ff4903 100644 --- a/src/Ocelot/Configuration/ServiceProviderConfiguration.cs +++ b/src/Ocelot/Configuration/ServiceProviderConfiguration.cs @@ -2,9 +2,10 @@ { public class ServiceProviderConfiguration { - public ServiceProviderConfiguration(string type, string host, int port, string token, string configurationKey, int pollingInterval, string @namespace = "") + public ServiceProviderConfiguration(string type, string scheme, string host, int port, string token, string configurationKey, int pollingInterval, string @namespace = "") { ConfigurationKey = configurationKey; + Scheme = scheme; Host = host; Port = port; Token = token; @@ -13,6 +14,8 @@ public ServiceProviderConfiguration(string type, string host, int port, string t Namespace = @namespace; } + public string Scheme { get; } + public string Host { get; } public int Port { get; } diff --git a/src/Ocelot/Configuration/Setter/FileAndInternalConfigurationSetter.cs b/src/Ocelot/Configuration/Setter/FileAndInternalConfigurationSetter.cs index 127e20be5..01fc3cc3e 100644 --- a/src/Ocelot/Configuration/Setter/FileAndInternalConfigurationSetter.cs +++ b/src/Ocelot/Configuration/Setter/FileAndInternalConfigurationSetter.cs @@ -1,14 +1,15 @@ +using System.Threading.Tasks; + using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.Responses; -using System.Threading.Tasks; namespace Ocelot.Configuration.Setter { public class FileAndInternalConfigurationSetter : IFileConfigurationSetter { - private readonly IInternalConfigurationRepository internalConfigRepo; + private readonly IInternalConfigurationRepository _internalConfigRepo; private readonly IInternalConfigurationCreator _configCreator; private readonly IFileConfigurationRepository _repo; @@ -17,7 +18,7 @@ public FileAndInternalConfigurationSetter( IInternalConfigurationCreator configCreator, IFileConfigurationRepository repo) { - internalConfigRepo = configRepo; + _internalConfigRepo = configRepo; _configCreator = configCreator; _repo = repo; } @@ -35,7 +36,7 @@ public async Task Set(FileConfiguration fileConfig) if (!config.IsError) { - internalConfigRepo.AddOrReplace(config.Data); + _internalConfigRepo.AddOrReplace(config.Data); } return new ErrorResponse(config.Errors); diff --git a/src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs b/src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs index 9e8b4cc77..fef8adfe1 100644 --- a/src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs +++ b/src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs @@ -1,6 +1,7 @@ +using System.Threading.Tasks; + using Ocelot.Configuration.File; using Ocelot.Responses; -using System.Threading.Tasks; namespace Ocelot.Configuration.Setter { diff --git a/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs b/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs index d1c875306..2b232f802 100644 --- a/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs +++ b/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs @@ -1,8 +1,9 @@ -namespace Ocelot.Configuration.Validator -{ - using Ocelot.Errors; - using System.Collections.Generic; +using System.Collections.Generic; + +using Ocelot.Errors; +namespace Ocelot.Configuration.Validator +{ public class ConfigurationValidationResult { public ConfigurationValidationResult(bool isError) diff --git a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs index e2f7f24d1..1d900c59a 100644 --- a/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs +++ b/src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs @@ -1,184 +1,174 @@ -namespace Ocelot.Configuration.Validator -{ - using Errors; - using File; - using FluentValidation; - using Microsoft.Extensions.DependencyInjection; - using Responses; - using ServiceDiscovery; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text.RegularExpressions; - using System.Threading.Tasks; - - public class FileConfigurationFluentValidator : AbstractValidator, IConfigurationValidator - { - private readonly List _serviceDiscoveryFinderDelegates; - - public FileConfigurationFluentValidator(IServiceProvider provider, ReRouteFluentValidator reRouteFluentValidator, FileGlobalConfigurationFluentValidator fileGlobalConfigurationFluentValidator) - { - _serviceDiscoveryFinderDelegates = provider - .GetServices() - .ToList(); - - RuleForEach(configuration => configuration.ReRoutes) - .SetValidator(reRouteFluentValidator); - - RuleFor(configuration => configuration.GlobalConfiguration) - .SetValidator(fileGlobalConfigurationFluentValidator); - - RuleForEach(configuration => configuration.ReRoutes) - .Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.ReRoutes)) - .WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicate"); - - RuleForEach(configuration => configuration.ReRoutes) - .Must((config, reRoute) => HaveServiceDiscoveryProviderRegistered(reRoute, config.GlobalConfiguration.ServiceDiscoveryProvider)) - .WithMessage((config, reRoute) => $"Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?"); - - RuleForEach(configuration => configuration.ReRoutes) - .Must((config, reRoute) => IsPlaceholderNotDuplicatedIn(reRoute.UpstreamPathTemplate)) - .WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicated placeholder"); - - RuleFor(configuration => configuration.GlobalConfiguration.ServiceDiscoveryProvider) - .Must(HaveServiceDiscoveryProviderRegistered) - .WithMessage((config, reRoute) => $"Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?"); - - RuleForEach(configuration => configuration.ReRoutes) - .Must((config, reRoute) => IsNotDuplicateIn(reRoute, config.Aggregates)) - .WithMessage((config, reRoute) => $"{nameof(reRoute)} {reRoute.UpstreamPathTemplate} has duplicate aggregate"); - - RuleForEach(configuration => configuration.Aggregates) - .Must((config, aggregateReRoute) => IsNotDuplicateIn(aggregateReRoute, config.Aggregates)) - .WithMessage((config, aggregate) => $"{nameof(aggregate)} {aggregate.UpstreamPathTemplate} has duplicate aggregate"); - - RuleForEach(configuration => configuration.Aggregates) - .Must((config, aggregateReRoute) => AllReRoutesForAggregateExist(aggregateReRoute, config.ReRoutes)) - .WithMessage((config, aggregateReRoute) => $"ReRoutes for {nameof(aggregateReRoute)} {aggregateReRoute.UpstreamPathTemplate} either do not exist or do not have correct ServiceName property"); - - RuleForEach(configuration => configuration.Aggregates) - .Must((config, aggregateReRoute) => DoesNotContainReRoutesWithSpecificRequestIdKeys(aggregateReRoute, config.ReRoutes)) - .WithMessage((config, aggregateReRoute) => $"{nameof(aggregateReRoute)} {aggregateReRoute.UpstreamPathTemplate} contains ReRoute with specific RequestIdKey, this is not possible with Aggregates"); - } - - private bool HaveServiceDiscoveryProviderRegistered(FileReRoute reRoute, FileServiceDiscoveryProvider serviceDiscoveryProvider) - { - if (string.IsNullOrEmpty(reRoute.ServiceName)) - { - return true; - } - - if (serviceDiscoveryProvider?.Type?.ToLower() == "servicefabric") - { - return true; - } - - return _serviceDiscoveryFinderDelegates.Any(); - } - - private bool HaveServiceDiscoveryProviderRegistered(FileServiceDiscoveryProvider serviceDiscoveryProvider) - { - if (serviceDiscoveryProvider == null) - { - return true; - } - - if (serviceDiscoveryProvider?.Type?.ToLower() == "servicefabric") - { - return true; - } - - return string.IsNullOrEmpty(serviceDiscoveryProvider.Type) || _serviceDiscoveryFinderDelegates.Any(); - } - - public async Task> IsValid(FileConfiguration configuration) - { - var validateResult = await ValidateAsync(configuration); - - if (validateResult.IsValid) - { - return new OkResponse(new ConfigurationValidationResult(false)); - } - - var errors = validateResult.Errors.Select(failure => new FileValidationFailedError(failure.ErrorMessage)); - - var result = new ConfigurationValidationResult(true, errors.Cast().ToList()); - - return new OkResponse(result); - } - - private bool AllReRoutesForAggregateExist(FileAggregateReRoute fileAggregateReRoute, List reRoutes) - { - var reRoutesForAggregate = reRoutes.Where(r => fileAggregateReRoute.ReRouteKeys.Contains(r.Key)); - - return reRoutesForAggregate.Count() == fileAggregateReRoute.ReRouteKeys.Count; - } - - private bool IsPlaceholderNotDuplicatedIn(string upstreamPathTemplate) - { - Regex regExPlaceholder = new Regex("{[^}]+}"); - var matches = regExPlaceholder.Matches(upstreamPathTemplate); - var upstreamPathPlaceholders = matches.Select(m => m.Value); - return upstreamPathPlaceholders.Count() == upstreamPathPlaceholders.Distinct().Count(); - } - - private static bool DoesNotContainReRoutesWithSpecificRequestIdKeys(FileAggregateReRoute fileAggregateReRoute, - List reRoutes) - { - var reRoutesForAggregate = reRoutes.Where(r => fileAggregateReRoute.ReRouteKeys.Contains(r.Key)); - - return reRoutesForAggregate.All(r => string.IsNullOrEmpty(r.RequestIdKey)); - } - - private static bool IsNotDuplicateIn(FileReRoute reRoute, - List reRoutes) - { - var matchingReRoutes = reRoutes - .Where(r => r.UpstreamPathTemplate == reRoute.UpstreamPathTemplate - && (r.UpstreamHost == reRoute.UpstreamHost || reRoute.UpstreamHost == null)) - .ToList(); - - if (matchingReRoutes.Count == 1) - { - return true; - } - - var allowAllVerbs = matchingReRoutes.Any(x => x.UpstreamHttpMethod.Count == 0); - - var duplicateAllowAllVerbs = matchingReRoutes.Count(x => x.UpstreamHttpMethod.Count == 0) > 1; - - var specificVerbs = matchingReRoutes.Any(x => x.UpstreamHttpMethod.Count != 0); - - var duplicateSpecificVerbs = matchingReRoutes.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any(); - - if (duplicateAllowAllVerbs || duplicateSpecificVerbs || (allowAllVerbs && specificVerbs)) - { - return false; - } - - return true; - } - - private static bool IsNotDuplicateIn(FileReRoute reRoute, - List aggregateReRoutes) - { - var duplicate = aggregateReRoutes - .Any(a => a.UpstreamPathTemplate == reRoute.UpstreamPathTemplate - && a.UpstreamHost == reRoute.UpstreamHost - && reRoute.UpstreamHttpMethod.Select(x => x.ToLower()).Contains("get")); - - return !duplicate; - } - - private static bool IsNotDuplicateIn(FileAggregateReRoute reRoute, - List aggregateReRoutes) - { - var matchingReRoutes = aggregateReRoutes - .Where(r => r.UpstreamPathTemplate == reRoute.UpstreamPathTemplate - && r.UpstreamHost == reRoute.UpstreamHost) - .ToList(); - - return matchingReRoutes.Count <= 1; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +using Ocelot.Errors; + +using Ocelot.Configuration.File; + +using FluentValidation; + +using Microsoft.Extensions.DependencyInjection; + +using Ocelot.Responses; + +using Ocelot.ServiceDiscovery; + +namespace Ocelot.Configuration.Validator +{ + public class FileConfigurationFluentValidator : AbstractValidator, IConfigurationValidator + { + private const string Servicefabric = "servicefabric"; + private readonly List _serviceDiscoveryFinderDelegates; + + public FileConfigurationFluentValidator(IServiceProvider provider, RouteFluentValidator routeFluentValidator, FileGlobalConfigurationFluentValidator fileGlobalConfigurationFluentValidator) + { + _serviceDiscoveryFinderDelegates = provider + .GetServices() + .ToList(); + + RuleForEach(configuration => configuration.Routes) + .SetValidator(routeFluentValidator); + + RuleFor(configuration => configuration.GlobalConfiguration) + .SetValidator(fileGlobalConfigurationFluentValidator); + + RuleForEach(configuration => configuration.Routes) + .Must((config, route) => IsNotDuplicateIn(route, config.Routes)) + .WithMessage((_, route) => $"{nameof(route)} {route.UpstreamPathTemplate} has duplicate"); + + RuleForEach(configuration => configuration.Routes) + .Must((config, route) => HaveServiceDiscoveryProviderRegistered(route, config.GlobalConfiguration.ServiceDiscoveryProvider)) + .WithMessage((_, _) => "Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?"); + + RuleForEach(configuration => configuration.Routes) + .Must((_, route) => IsPlaceholderNotDuplicatedIn(route.UpstreamPathTemplate)) + .WithMessage((_, route) => $"{nameof(route)} {route.UpstreamPathTemplate} has duplicated placeholder"); + + RuleFor(configuration => configuration.GlobalConfiguration.ServiceDiscoveryProvider) + .Must(HaveServiceDiscoveryProviderRegistered) + .WithMessage((_, _) => "Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?"); + + RuleForEach(configuration => configuration.Routes) + .Must((config, route) => IsNotDuplicateIn(route, config.Aggregates)) + .WithMessage((_, route) => $"{nameof(route)} {route.UpstreamPathTemplate} has duplicate aggregate"); + + RuleForEach(configuration => configuration.Aggregates) + .Must((config, aggregateRoute) => IsNotDuplicateIn(aggregateRoute, config.Aggregates)) + .WithMessage((_, aggregate) => $"{nameof(aggregate)} {aggregate.UpstreamPathTemplate} has duplicate aggregate"); + + RuleForEach(configuration => configuration.Aggregates) + .Must((config, aggregateRoute) => AllRoutesForAggregateExist(aggregateRoute, config.Routes)) + .WithMessage((_, aggregateRoute) => $"Routes for {nameof(aggregateRoute)} {aggregateRoute.UpstreamPathTemplate} either do not exist or do not have correct ServiceName property"); + + RuleForEach(configuration => configuration.Aggregates) + .Must((config, aggregateRoute) => DoesNotContainRoutesWithSpecificRequestIdKeys(aggregateRoute, config.Routes)) + .WithMessage((_, aggregateRoute) => $"{nameof(aggregateRoute)} {aggregateRoute.UpstreamPathTemplate} contains Route with specific RequestIdKey, this is not possible with Aggregates"); + } + + private bool HaveServiceDiscoveryProviderRegistered(FileRoute route, FileServiceDiscoveryProvider serviceDiscoveryProvider) + { + return string.IsNullOrEmpty(route.ServiceName) || + serviceDiscoveryProvider?.Type?.ToLower() == Servicefabric || + _serviceDiscoveryFinderDelegates.Any(); + } + + private bool HaveServiceDiscoveryProviderRegistered(FileServiceDiscoveryProvider serviceDiscoveryProvider) + { + return serviceDiscoveryProvider == null || + serviceDiscoveryProvider?.Type?.ToLower() == Servicefabric || + string.IsNullOrEmpty(serviceDiscoveryProvider.Type) || _serviceDiscoveryFinderDelegates.Any(); + } + + public async Task> IsValid(FileConfiguration configuration) + { + var validateResult = await ValidateAsync(configuration); + + if (validateResult.IsValid) + { + return new OkResponse(new ConfigurationValidationResult(false)); + } + + var errors = validateResult.Errors.Select(failure => new FileValidationFailedError(failure.ErrorMessage)); + + var result = new ConfigurationValidationResult(true, errors.Cast().ToList()); + + return new OkResponse(result); + } + + private static bool AllRoutesForAggregateExist(FileAggregateRoute fileAggregateRoute, List routes) + { + var routesForAggregate = routes.Where(r => fileAggregateRoute.RouteKeys.Contains(r.Key)); + + return routesForAggregate.Count() == fileAggregateRoute.RouteKeys.Count; + } + + private static bool IsPlaceholderNotDuplicatedIn(string upstreamPathTemplate) + { + var regExPlaceholder = new Regex("{[^}]+}"); + var matches = regExPlaceholder.Matches(upstreamPathTemplate); + var upstreamPathPlaceholders = matches.Select(m => m.Value); + return upstreamPathPlaceholders.Count() == upstreamPathPlaceholders.Distinct().Count(); + } + + private static bool DoesNotContainRoutesWithSpecificRequestIdKeys(FileAggregateRoute fileAggregateRoute, + IEnumerable routes) + { + var routesForAggregate = routes.Where(r => fileAggregateRoute.RouteKeys.Contains(r.Key)); + + return routesForAggregate.All(r => string.IsNullOrEmpty(r.RequestIdKey)); + } + + private static bool IsNotDuplicateIn(FileRoute route, + IEnumerable routes) + { + var matchingRoutes = routes + .Where(r => r.UpstreamPathTemplate == route.UpstreamPathTemplate + && r.UpstreamHost == route.UpstreamHost) + .ToList(); + + if (matchingRoutes.Count == 1) + { + return true; + } + + var allowAllVerbs = matchingRoutes.Any(x => x.UpstreamHttpMethod.Count == 0); + + var duplicateAllowAllVerbs = matchingRoutes.Count(x => x.UpstreamHttpMethod.Count == 0) > 1; + + var specificVerbs = matchingRoutes.Any(x => x.UpstreamHttpMethod.Count != 0); + + var duplicateSpecificVerbs = matchingRoutes.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any(); + + if (duplicateAllowAllVerbs || duplicateSpecificVerbs || (allowAllVerbs && specificVerbs)) + { + return false; + } + + return true; + } + + private static bool IsNotDuplicateIn(FileRoute route, + IEnumerable aggregateRoutes) + { + var duplicate = aggregateRoutes + .Any(a => a.UpstreamPathTemplate == route.UpstreamPathTemplate + && a.UpstreamHost == route.UpstreamHost + && route.UpstreamHttpMethod.Select(x => x.ToLower()).Contains("get")); + + return !duplicate; + } + + private static bool IsNotDuplicateIn(FileAggregateRoute route, + IEnumerable aggregateRoutes) + { + var matchingRoutes = aggregateRoutes + .Where(r => r.UpstreamPathTemplate == route.UpstreamPathTemplate + && r.UpstreamHost == route.UpstreamHost); + + return matchingRoutes.Count() <= 1; + } + } +} diff --git a/src/Ocelot/Configuration/Validator/FileGlobalConfigurationFluentValidator.cs b/src/Ocelot/Configuration/Validator/FileGlobalConfigurationFluentValidator.cs index 22e1690f9..ff42dfff8 100644 --- a/src/Ocelot/Configuration/Validator/FileGlobalConfigurationFluentValidator.cs +++ b/src/Ocelot/Configuration/Validator/FileGlobalConfigurationFluentValidator.cs @@ -1,8 +1,9 @@ +using Ocelot.Configuration.File; + +using FluentValidation; + namespace Ocelot.Configuration.Validator { - using File; - using FluentValidation; - public class FileGlobalConfigurationFluentValidator : AbstractValidator { public FileGlobalConfigurationFluentValidator(FileQoSOptionsFluentValidator fileQoSOptionsFluentValidator) diff --git a/src/Ocelot/Configuration/Validator/FileQoSOptionsFluentValidator.cs b/src/Ocelot/Configuration/Validator/FileQoSOptionsFluentValidator.cs index 4a6c8496f..667cdb0e1 100644 --- a/src/Ocelot/Configuration/Validator/FileQoSOptionsFluentValidator.cs +++ b/src/Ocelot/Configuration/Validator/FileQoSOptionsFluentValidator.cs @@ -1,30 +1,34 @@ -namespace Ocelot.Configuration.Validator -{ - using File; - using FluentValidation; - using Microsoft.Extensions.DependencyInjection; - using Requester; - using System; +using System; - public class FileQoSOptionsFluentValidator : AbstractValidator - { - private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate; +using Ocelot.Configuration.File; - public FileQoSOptionsFluentValidator(IServiceProvider provider) - { - _qosDelegatingHandlerDelegate = provider.GetService(); +using FluentValidation; - When(qosOptions => qosOptions.TimeoutValue > 0 && qosOptions.ExceptionsAllowedBeforeBreaking > 0, () => - { - RuleFor(qosOptions => qosOptions) - .Must(HaveQosHandlerRegistered) - .WithMessage("Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?"); - }); - } +using Microsoft.Extensions.DependencyInjection; - private bool HaveQosHandlerRegistered(FileQoSOptions arg) - { - return _qosDelegatingHandlerDelegate != null; - } - } -} +using Ocelot.Requester; + +namespace Ocelot.Configuration.Validator +{ + public class FileQoSOptionsFluentValidator : AbstractValidator + { + private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate; + + public FileQoSOptionsFluentValidator(IServiceProvider provider) + { + _qosDelegatingHandlerDelegate = provider.GetService(); + + When(qosOptions => qosOptions.TimeoutValue > 0 && qosOptions.ExceptionsAllowedBeforeBreaking > 0, () => + { + RuleFor(qosOptions => qosOptions) + .Must(HaveQosHandlerRegistered) + .WithMessage("Unable to start Ocelot because either a Route or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?"); + }); + } + + private bool HaveQosHandlerRegistered(FileQoSOptions arg) + { + return _qosDelegatingHandlerDelegate != null; + } + } +} diff --git a/src/Ocelot/Configuration/Validator/FileValidationFailedError.cs b/src/Ocelot/Configuration/Validator/FileValidationFailedError.cs index a1918341e..fbb1521ed 100644 --- a/src/Ocelot/Configuration/Validator/FileValidationFailedError.cs +++ b/src/Ocelot/Configuration/Validator/FileValidationFailedError.cs @@ -1,12 +1,12 @@ -namespace Ocelot.Configuration.Validator -{ - using Errors; - - public class FileValidationFailedError : Error - { +using Ocelot.Errors; + +namespace Ocelot.Configuration.Validator +{ + public class FileValidationFailedError : Error + { public FileValidationFailedError(string message) - : base(message, OcelotErrorCode.FileValidationFailedError) - { - } - } + : base(message, OcelotErrorCode.FileValidationFailedError, 404) + { + } + } } diff --git a/src/Ocelot/Configuration/Validator/HostAndPortValidator.cs b/src/Ocelot/Configuration/Validator/HostAndPortValidator.cs index f93a2ccdf..b4eb70e36 100644 --- a/src/Ocelot/Configuration/Validator/HostAndPortValidator.cs +++ b/src/Ocelot/Configuration/Validator/HostAndPortValidator.cs @@ -1,15 +1,16 @@ -namespace Ocelot.Configuration.Validator -{ - using FluentValidation; - using Ocelot.Configuration.File; +using Ocelot.Configuration.File; - public class HostAndPortValidator : AbstractValidator - { - public HostAndPortValidator() - { - RuleFor(r => r.Host) - .NotEmpty() - .WithMessage("When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using ReRoute.Host or Ocelot cannot find your service!"); - } - } -} +using FluentValidation; + +namespace Ocelot.Configuration.Validator +{ + public class HostAndPortValidator : AbstractValidator + { + public HostAndPortValidator() + { + RuleFor(r => r.Host) + .NotEmpty() + .WithMessage("When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!"); + } + } +} diff --git a/src/Ocelot/Configuration/Validator/IConfigurationValidator.cs b/src/Ocelot/Configuration/Validator/IConfigurationValidator.cs index f9b7eef94..a0ed48f02 100644 --- a/src/Ocelot/Configuration/Validator/IConfigurationValidator.cs +++ b/src/Ocelot/Configuration/Validator/IConfigurationValidator.cs @@ -1,9 +1,11 @@ -namespace Ocelot.Configuration.Validator -{ - using Ocelot.Configuration.File; - using Ocelot.Responses; - using System.Threading.Tasks; +using System.Threading.Tasks; + +using Ocelot.Configuration.File; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Validator +{ public interface IConfigurationValidator { Task> IsValid(FileConfiguration configuration); diff --git a/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs b/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs similarity index 65% rename from src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs rename to src/Ocelot/Configuration/Validator/RouteFluentValidator.cs index 746c430c0..7992d40f9 100644 --- a/src/Ocelot/Configuration/Validator/ReRouteFluentValidator.cs +++ b/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs @@ -1,122 +1,130 @@ -namespace Ocelot.Configuration.Validator -{ - using File; - using FluentValidation; - using Microsoft.AspNetCore.Authentication; - using System.Linq; - using System.Text.RegularExpressions; - using System.Threading; - using System.Threading.Tasks; - - public class ReRouteFluentValidator : AbstractValidator - { - private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; - - public ReRouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, HostAndPortValidator hostAndPortValidator, FileQoSOptionsFluentValidator fileQoSOptionsFluentValidator) - { - _authenticationSchemeProvider = authenticationSchemeProvider; - - RuleFor(reRoute => reRoute.QoSOptions) - .SetValidator(fileQoSOptionsFluentValidator); - - RuleFor(reRoute => reRoute.DownstreamPathTemplate) - .NotEmpty() - .WithMessage("{PropertyName} cannot be empty"); - - RuleFor(reRoute => reRoute.UpstreamPathTemplate) - .NotEmpty() - .WithMessage("{PropertyName} cannot be empty"); - - When(reRoute => !string.IsNullOrEmpty(reRoute.DownstreamPathTemplate), () => - { - RuleFor(reRoute => reRoute.DownstreamPathTemplate) - .Must(path => path.StartsWith("/")) - .WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash"); - - RuleFor(reRoute => reRoute.DownstreamPathTemplate) - .Must(path => !path.Contains("//")) - .WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."); - - RuleFor(reRoute => reRoute.DownstreamPathTemplate) - .Must(path => !path.Contains("https://") && !path.Contains("http://")) - .WithMessage("{PropertyName} {PropertyValue} contains scheme"); - }); - - When(reRoute => !string.IsNullOrEmpty(reRoute.UpstreamPathTemplate), () => - { - RuleFor(reRoute => reRoute.UpstreamPathTemplate) - .Must(path => !path.Contains("//")) - .WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."); - - RuleFor(reRoute => reRoute.UpstreamPathTemplate) - .Must(path => path.StartsWith("/")) - .WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash"); - - RuleFor(reRoute => reRoute.UpstreamPathTemplate) - .Must(path => !path.Contains("https://") && !path.Contains("http://")) - .WithMessage("{PropertyName} {PropertyValue} contains scheme"); - }); - - When(reRoute => reRoute.RateLimitOptions.EnableRateLimiting, () => - { - RuleFor(reRoute => reRoute.RateLimitOptions.Period) - .NotEmpty() - .WithMessage("RateLimitOptions.Period is empty"); - - RuleFor(reRoute => reRoute.RateLimitOptions) - .Must(IsValidPeriod) - .WithMessage("RateLimitOptions.Period does not contain integer then s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period"); - }); - - RuleFor(reRoute => reRoute.AuthenticationOptions) - .MustAsync(IsSupportedAuthenticationProviders) - .WithMessage("{PropertyName} {PropertyValue} is unsupported authentication provider"); - - When(reRoute => string.IsNullOrEmpty(reRoute.ServiceName), () => - { - RuleFor(r => r.DownstreamHostAndPorts).NotEmpty() - .WithMessage("When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!"); - }); - - When(reRoute => string.IsNullOrEmpty(reRoute.ServiceName), () => - { - RuleForEach(reRoute => reRoute.DownstreamHostAndPorts) - .SetValidator(hostAndPortValidator); - }); - } - - private async Task IsSupportedAuthenticationProviders(FileAuthenticationOptions authenticationOptions, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(authenticationOptions.AuthenticationProviderKey)) - { - return true; - } - - var schemes = await _authenticationSchemeProvider.GetAllSchemesAsync(); - - var supportedSchemes = schemes.Select(scheme => scheme.Name).ToList(); - - return supportedSchemes.Contains(authenticationOptions.AuthenticationProviderKey); - } - - private static bool IsValidPeriod(FileRateLimitRule rateLimitOptions) - { - if (string.IsNullOrEmpty(rateLimitOptions.Period)) - { - return false; - } - - var period = rateLimitOptions.Period; - - var secondsRegEx = new Regex("^[0-9]+s"); - var minutesRegEx = new Regex("^[0-9]+m"); - var hoursRegEx = new Regex("^[0-9]+h"); - var daysRegEx = new Regex("^[0-9]+d"); - - return secondsRegEx.Match(period).Success - || minutesRegEx.Match(period).Success - || hoursRegEx.Match(period).Success - || daysRegEx.Match(period).Success; - } - } -} +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +using Ocelot.Configuration.File; + +using FluentValidation; + +using Microsoft.AspNetCore.Authentication; + +namespace Ocelot.Configuration.Validator +{ + public class RouteFluentValidator : AbstractValidator + { + private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; + + public RouteFluentValidator(IAuthenticationSchemeProvider authenticationSchemeProvider, HostAndPortValidator hostAndPortValidator, FileQoSOptionsFluentValidator fileQoSOptionsFluentValidator) + { + _authenticationSchemeProvider = authenticationSchemeProvider; + + RuleFor(route => route.QoSOptions) + .SetValidator(fileQoSOptionsFluentValidator); + + RuleFor(route => route.DownstreamPathTemplate) + .NotEmpty() + .WithMessage("{PropertyName} cannot be empty"); + + RuleFor(route => route.UpstreamPathTemplate) + .NotEmpty() + .WithMessage("{PropertyName} cannot be empty"); + + When(route => !string.IsNullOrEmpty(route.DownstreamPathTemplate), () => + { + RuleFor(route => route.DownstreamPathTemplate) + .Must(path => path.StartsWith("/")) + .WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash"); + + RuleFor(route => route.DownstreamPathTemplate) + .Must(path => !path.Contains("//")) + .WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."); + + RuleFor(route => route.DownstreamPathTemplate) + .Must(path => !path.Contains("https://") && !path.Contains("http://")) + .WithMessage("{PropertyName} {PropertyValue} contains scheme"); + }); + + When(route => !string.IsNullOrEmpty(route.UpstreamPathTemplate), () => + { + RuleFor(route => route.UpstreamPathTemplate) + .Must(path => !path.Contains("//")) + .WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."); + + RuleFor(route => route.UpstreamPathTemplate) + .Must(path => path.StartsWith("/")) + .WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash"); + + RuleFor(route => route.UpstreamPathTemplate) + .Must(path => !path.Contains("https://") && !path.Contains("http://")) + .WithMessage("{PropertyName} {PropertyValue} contains scheme"); + }); + + When(route => route.RateLimitOptions.EnableRateLimiting, () => + { + RuleFor(route => route.RateLimitOptions.Period) + .NotEmpty() + .WithMessage("RateLimitOptions.Period is empty"); + + RuleFor(route => route.RateLimitOptions) + .Must(IsValidPeriod) + .WithMessage("RateLimitOptions.Period does not contain integer then s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period"); + }); + + RuleFor(route => route.AuthenticationOptions) + .MustAsync(IsSupportedAuthenticationProviders) + .WithMessage("{PropertyName} {PropertyValue} is unsupported authentication provider"); + + When(route => string.IsNullOrEmpty(route.ServiceName), () => + { + RuleFor(r => r.DownstreamHostAndPorts).NotEmpty() + .WithMessage("When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!"); + }); + + When(route => string.IsNullOrEmpty(route.ServiceName), () => + { + RuleForEach(route => route.DownstreamHostAndPorts) + .SetValidator(hostAndPortValidator); + }); + + When(route => !string.IsNullOrEmpty(route.DownstreamHttpVersion), () => + { + RuleFor(r => r.DownstreamHttpVersion).Matches("^[0-9]([.,][0-9]{1,1})?$"); + }); + } + + private async Task IsSupportedAuthenticationProviders(FileAuthenticationOptions authenticationOptions, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(authenticationOptions.AuthenticationProviderKey)) + { + return true; + } + + var schemes = await _authenticationSchemeProvider.GetAllSchemesAsync(); + + var supportedSchemes = schemes.Select(scheme => scheme.Name); + + return supportedSchemes.Contains(authenticationOptions.AuthenticationProviderKey); + } + + private static bool IsValidPeriod(FileRateLimitRule rateLimitOptions) + { + if (string.IsNullOrEmpty(rateLimitOptions.Period)) + { + return false; + } + + var period = rateLimitOptions.Period; + + var secondsRegEx = new Regex("^[0-9]+s"); + var minutesRegEx = new Regex("^[0-9]+m"); + var hoursRegEx = new Regex("^[0-9]+h"); + var daysRegEx = new Regex("^[0-9]+d"); + + return secondsRegEx.Match(period).Success + || minutesRegEx.Match(period).Success + || hoursRegEx.Match(period).Success + || daysRegEx.Match(period).Success; + } + } +} diff --git a/src/Ocelot/DependencyInjection/AdministrationPath.cs b/src/Ocelot/DependencyInjection/AdministrationPath.cs index 13f4c0d1e..4e19c10c8 100644 --- a/src/Ocelot/DependencyInjection/AdministrationPath.cs +++ b/src/Ocelot/DependencyInjection/AdministrationPath.cs @@ -7,6 +7,6 @@ public AdministrationPath(string path) Path = path; } - public string Path { get; private set; } + public string Path { get; } } } diff --git a/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs b/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs index 3d29ef514..db6f4b1e7 100644 --- a/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs +++ b/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs @@ -1,85 +1,88 @@ -namespace Ocelot.DependencyInjection -{ - using Configuration.File; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.Configuration.Memory; - using Newtonsoft.Json; - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Text.RegularExpressions; - - public static class ConfigurationBuilderExtensions - { - [Obsolete("Please set BaseUrl in ocelot.json GlobalConfiguration.BaseUrl")] - public static IConfigurationBuilder AddOcelotBaseUrl(this IConfigurationBuilder builder, string baseUrl) - { - var memorySource = new MemoryConfigurationSource - { - InitialData = new List> - { - new KeyValuePair("BaseUrl", baseUrl) - } - }; - - builder.Add(memorySource); - - return builder; - } - - public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env) - { - return builder.AddOcelot(".", env); - } - - public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env) - { - const string primaryConfigFile = "ocelot.json"; - - const string globalConfigFile = "ocelot.global.json"; - - const string subConfigPattern = @"^ocelot\.(.*?)\.json$"; - - string excludeConfigName = env?.EnvironmentName != null ? $"ocelot.{env.EnvironmentName}.json" : string.Empty; - - var reg = new Regex(subConfigPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline); - - var files = new DirectoryInfo(folder) - .EnumerateFiles() - .Where(fi => reg.IsMatch(fi.Name) && (fi.Name != excludeConfigName)) - .ToList(); - - var fileConfiguration = new FileConfiguration(); - - foreach (var file in files) - { - if (files.Count > 1 && file.Name.Equals(primaryConfigFile, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var lines = File.ReadAllText(file.FullName); - - var config = JsonConvert.DeserializeObject(lines); - - if (file.Name.Equals(globalConfigFile, StringComparison.OrdinalIgnoreCase)) - { - fileConfiguration.GlobalConfiguration = config.GlobalConfiguration; - } - - fileConfiguration.Aggregates.AddRange(config.Aggregates); - fileConfiguration.ReRoutes.AddRange(config.ReRoutes); - } - - var json = JsonConvert.SerializeObject(fileConfiguration); - - File.WriteAllText(primaryConfigFile, json); - - builder.AddJsonFile(primaryConfigFile, false, false); - - return builder; - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +using Ocelot.Configuration.File; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; + +using Newtonsoft.Json; + +namespace Ocelot.DependencyInjection +{ + public static class ConfigurationBuilderExtensions + { + [Obsolete("Please set BaseUrl in ocelot.json GlobalConfiguration.BaseUrl")] + public static IConfigurationBuilder AddOcelotBaseUrl(this IConfigurationBuilder builder, string baseUrl) + { + var memorySource = new MemoryConfigurationSource + { + InitialData = new List> + { + new("BaseUrl", baseUrl), + }, + }; + + builder.Add(memorySource); + + return builder; + } + + public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env) + { + return builder.AddOcelot(".", env); + } + + public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env) + { + const string primaryConfigFile = "ocelot.json"; + + const string globalConfigFile = "ocelot.global.json"; + + const string subConfigPattern = @"^ocelot\.(.*?)\.json$"; + + var excludeConfigName = env?.EnvironmentName != null ? $"ocelot.{env.EnvironmentName}.json" : string.Empty; + + var reg = new Regex(subConfigPattern, RegexOptions.IgnoreCase | RegexOptions.Singleline); + + var files = new DirectoryInfo(folder) + .EnumerateFiles() + .Where(fi => reg.IsMatch(fi.Name) && (fi.Name != excludeConfigName)) + .ToArray(); + + var fileConfiguration = new FileConfiguration(); + + foreach (var file in files) + { + if (files.Length > 1 && file.Name.Equals(primaryConfigFile, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var lines = File.ReadAllText(file.FullName); + + var config = JsonConvert.DeserializeObject(lines); + + if (file.Name.Equals(globalConfigFile, StringComparison.OrdinalIgnoreCase)) + { + fileConfiguration.GlobalConfiguration = config.GlobalConfiguration; + } + + fileConfiguration.Aggregates.AddRange(config.Aggregates); + fileConfiguration.Routes.AddRange(config.Routes); + } + + var json = JsonConvert.SerializeObject(fileConfiguration); + + File.WriteAllText(primaryConfigFile, json); + + builder.AddJsonFile(primaryConfigFile, false, false); + + return builder; + } + } +} diff --git a/src/Ocelot/DependencyInjection/IOcelotAdministrationBuilder.cs b/src/Ocelot/DependencyInjection/IOcelotAdministrationBuilder.cs index bcb01189d..f145916e5 100644 --- a/src/Ocelot/DependencyInjection/IOcelotAdministrationBuilder.cs +++ b/src/Ocelot/DependencyInjection/IOcelotAdministrationBuilder.cs @@ -1,8 +1,8 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + namespace Ocelot.DependencyInjection { - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - public interface IOcelotAdministrationBuilder { IServiceCollection Services { get; } diff --git a/src/Ocelot/DependencyInjection/IOcelotBuilder.cs b/src/Ocelot/DependencyInjection/IOcelotBuilder.cs index 13ec5a6af..507d8c782 100644 --- a/src/Ocelot/DependencyInjection/IOcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/IOcelotBuilder.cs @@ -1,9 +1,14 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Ocelot.Middleware.Multiplexer; using System; using System.Net.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +using Ocelot.Configuration; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Multiplexer; +using Ocelot.ServiceDiscovery.Providers; + namespace Ocelot.DependencyInjection { public interface IOcelotBuilder @@ -25,6 +30,23 @@ IOcelotBuilder AddSingletonDefinedAggregator() IOcelotBuilder AddTransientDefinedAggregator() where T : class, IDefinedAggregator; + IOcelotBuilder AddCustomLoadBalancer() + where T : ILoadBalancer, new(); + + IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) + where T : ILoadBalancer; + + IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) + where T : ILoadBalancer; + + IOcelotBuilder AddCustomLoadBalancer( + Func loadBalancerFactoryFunc) + where T : ILoadBalancer; + + IOcelotBuilder AddCustomLoadBalancer( + Func loadBalancerFactoryFunc) + where T : ILoadBalancer; + IOcelotBuilder AddConfigPlaceholders(); } } diff --git a/src/Ocelot/DependencyInjection/OcelotAdministrationBuilder.cs b/src/Ocelot/DependencyInjection/OcelotAdministrationBuilder.cs index 1ef75d069..f05bbcc45 100644 --- a/src/Ocelot/DependencyInjection/OcelotAdministrationBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotAdministrationBuilder.cs @@ -1,8 +1,8 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + namespace Ocelot.DependencyInjection { - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - public class OcelotAdministrationBuilder : IOcelotAdministrationBuilder { public IServiceCollection Services { get; } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 8f120997c..0e1ae6c67 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -1,46 +1,71 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Reflection; + +using Ocelot.Authorization; + +using Ocelot.Cache; + +using Ocelot.Claims; + +using Ocelot.Configuration; +using Ocelot.Configuration.ChangeTracking; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Parser; +using Ocelot.Configuration.Repository; +using Ocelot.Configuration.Setter; +using Ocelot.Configuration.Validator; + +using Ocelot.DownstreamRouteFinder.Finder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; + +using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; + +using Ocelot.Headers; + +using Ocelot.Infrastructure; +using Ocelot.Infrastructure.RequestData; + +using Ocelot.LoadBalancer.LoadBalancers; + +using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +using Ocelot.Middleware; + +using Ocelot.Multiplexer; + +using Ocelot.Infrastructure.Claims.Parser; + +using Ocelot.PathManipulation; + +using Ocelot.QueryStrings; + +using Ocelot.RateLimit; + +using Ocelot.Request.Creator; +using Ocelot.Request.Mapper; + +using Ocelot.Requester; +using Ocelot.Requester.QoS; + +using Ocelot.Responder; + +using Ocelot.Security; +using Ocelot.Security.IPSecurity; + +using Ocelot.ServiceDiscovery; +using Ocelot.ServiceDiscovery.Providers; + namespace Ocelot.DependencyInjection { - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.DependencyInjection.Extensions; - using Ocelot.Authorisation; - using Ocelot.Cache; - using Ocelot.Claims; - using Ocelot.Configuration; - using Ocelot.Configuration.Creator; - using Ocelot.Configuration.File; - using Ocelot.Configuration.Parser; - using Ocelot.Configuration.Repository; - using Ocelot.Configuration.Setter; - using Ocelot.Configuration.Validator; - using Ocelot.DownstreamRouteFinder.Finder; - using Ocelot.DownstreamRouteFinder.UrlMatcher; - using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; - using Ocelot.Headers; - using Ocelot.Infrastructure; - using Ocelot.Infrastructure.Claims.Parser; - using Ocelot.Infrastructure.RequestData; - using Ocelot.LoadBalancer.LoadBalancers; - using Ocelot.Logging; - using Ocelot.Middleware; - using Ocelot.Middleware.Multiplexer; - using Ocelot.PathManipulation; - using Ocelot.QueryStrings; - using Ocelot.RateLimit; - using Ocelot.Request.Creator; - using Ocelot.Request.Mapper; - using Ocelot.Requester; - using Ocelot.Requester.QoS; - using Ocelot.Responder; - using Ocelot.Security; - using Ocelot.Security.IPSecurity; - using Ocelot.ServiceDiscovery; - using System; - using System.Linq; - using System.Net.Http; - using System.Reflection; - public class OcelotBuilder : IOcelotBuilder { public IServiceCollection Services { get; } @@ -53,8 +78,8 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services = services; Services.Configure(configurationRoot); - Services.TryAddSingleton, InMemoryCache>(); - Services.TryAddSingleton, InMemoryCache>(); + Services.TryAddSingleton, AspMemoryCache>(); + Services.TryAddSingleton, AspMemoryCache>(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); @@ -62,13 +87,13 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); @@ -77,20 +102,24 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); + Services.AddSingleton(); + Services.AddSingleton(); + Services.AddSingleton(); + Services.AddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); - Services.TryAddSingleton(); + Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); @@ -99,7 +128,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.AddSingleton(); + Services.AddSingleton(); Services.AddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); @@ -112,6 +141,8 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); + Services.TryAddSingleton(); + Services.TryAddSingleton, OcelotConfigurationMonitor>(); // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc // could maybe use a scoped data repository @@ -119,7 +150,6 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.AddMemoryCache(); Services.TryAddSingleton(); - Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); @@ -131,18 +161,19 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); + Services.TryAddSingleton(); //add security - this.AddSecurity(); + AddSecurity(); //add asp.net services.. var assembly = typeof(FileConfigurationController).GetTypeInfo().Assembly; - this.MvcCoreBuilder = Services.AddMvcCore() + MvcCoreBuilder = Services.AddMvcCore() .AddApplicationPart(assembly) .AddControllersAsServices() .AddAuthorization() - .AddNewtonsoftJson(); + .AddNewtonsoftJson(); Services.AddLogging(); Services.AddMiddlewareAnalysis(); @@ -163,6 +194,47 @@ public IOcelotBuilder AddTransientDefinedAggregator() return this; } + public IOcelotBuilder AddCustomLoadBalancer() + where T : ILoadBalancer, new() + { + AddCustomLoadBalancer((provider, route, serviceDiscoveryProvider) => new T()); + return this; + } + + public IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) + where T : ILoadBalancer + { + AddCustomLoadBalancer((provider, route, serviceDiscoveryProvider) => + loadBalancerFactoryFunc()); + return this; + } + + public IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) + where T : ILoadBalancer + { + AddCustomLoadBalancer((provider, route, serviceDiscoveryProvider) => + loadBalancerFactoryFunc(provider)); + return this; + } + + public IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) + where T : ILoadBalancer + { + AddCustomLoadBalancer((provider, route, serviceDiscoveryProvider) => + loadBalancerFactoryFunc(route, serviceDiscoveryProvider)); + return this; + } + + public IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) + where T : ILoadBalancer + { + Services.AddSingleton(provider => + new DelegateInvokingLoadBalancerCreator( + (route, serviceDiscoveryProvider) => + loadBalancerFactoryFunc(provider, route, serviceDiscoveryProvider))); + return this; + } + private void AddSecurity() { Services.TryAddSingleton(); @@ -171,14 +243,16 @@ private void AddSecurity() public IOcelotBuilder AddDelegatingHandler(Type delegateType, bool global = false) { - if (!typeof(DelegatingHandler).IsAssignableFrom(delegateType)) throw new ArgumentOutOfRangeException(nameof(delegateType), delegateType.Name, "It is not a delegatin handler"); + if (!typeof(DelegatingHandler).IsAssignableFrom(delegateType)) + { + throw new ArgumentOutOfRangeException(nameof(delegateType), delegateType.Name, "It is not a delegatin handler"); + } if (global) { Services.AddTransient(delegateType); - Services.AddTransient(s => + Services.AddTransient(s => { - var service = s.GetService(delegateType) as DelegatingHandler; return new GlobalDelegatingHandler(service); }); @@ -197,7 +271,7 @@ public IOcelotBuilder AddDelegatingHandler(bool global = false) if (global) { Services.AddTransient(); - Services.AddTransient(s => + Services.AddTransient(s => { var service = s.GetService(); return new GlobalDelegatingHandler(service); @@ -215,21 +289,21 @@ public IOcelotBuilder AddConfigPlaceholders() { // see: https://greatrexpectations.com/2018/10/25/decorators-in-net-core-with-dependency-injection var wrappedDescriptor = Services.First(x => x.ServiceType == typeof(IPlaceholders)); - + var objectFactory = ActivatorUtilities.CreateFactory( typeof(ConfigAwarePlaceholders), new[] { typeof(IPlaceholders) }); Services.Replace(ServiceDescriptor.Describe( typeof(IPlaceholders), - s => (IPlaceholders) objectFactory(s, - new[] {CreateInstance(s, wrappedDescriptor)}), + s => (IPlaceholders)objectFactory(s, + new[] { CreateInstance(s, wrappedDescriptor) }), wrappedDescriptor.Lifetime )); return this; } - + private static object CreateInstance(IServiceProvider services, ServiceDescriptor descriptor) { if (descriptor.ImplementationInstance != null) diff --git a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs index 0d7a3bb73..a8991280b 100644 --- a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using System.Linq; namespace Ocelot.DependencyInjection { diff --git a/src/Ocelot/DownstreamPathManipulation/ChangeDownstreamPathTemplate.cs b/src/Ocelot/DownstreamPathManipulation/ChangeDownstreamPathTemplate.cs index a458992f4..c9cd53503 100644 --- a/src/Ocelot/DownstreamPathManipulation/ChangeDownstreamPathTemplate.cs +++ b/src/Ocelot/DownstreamPathManipulation/ChangeDownstreamPathTemplate.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; + using Ocelot.Configuration; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Infrastructure; diff --git a/src/Ocelot/DownstreamPathManipulation/IChangeDownstreamPathTemplate.cs b/src/Ocelot/DownstreamPathManipulation/IChangeDownstreamPathTemplate.cs index 36ed7f6d5..3c17ee415 100644 --- a/src/Ocelot/DownstreamPathManipulation/IChangeDownstreamPathTemplate.cs +++ b/src/Ocelot/DownstreamPathManipulation/IChangeDownstreamPathTemplate.cs @@ -1,12 +1,10 @@ -using Ocelot.Configuration; +using System.Collections.Generic; +using System.Security.Claims; + +using Ocelot.Configuration; using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.Request.Middleware; using Ocelot.Responses; using Ocelot.Values; -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Text; namespace Ocelot.PathManipulation { diff --git a/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs b/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs index 47107c67e..f58a53962 100644 --- a/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs +++ b/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs @@ -1,42 +1,53 @@ -using Ocelot.Logging; -using Ocelot.Middleware; -using System.Linq; +using System.Linq; using System.Threading.Tasks; -namespace Ocelot.PathManipulation.Middleware -{ - public class ClaimsToDownstreamPathMiddleware : OcelotMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly IChangeDownstreamPathTemplate _changeDownstreamPathTemplate; - - public ClaimsToDownstreamPathMiddleware(OcelotRequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IChangeDownstreamPathTemplate changeDownstreamPathTemplate) - : base(loggerFactory.CreateLogger()) - { - _next = next; - _changeDownstreamPathTemplate = changeDownstreamPathTemplate; - } +using Ocelot.Logging; - public async Task Invoke(DownstreamContext context) - { - if (context.DownstreamReRoute.ClaimsToPath.Any()) - { - Logger.LogInformation($"{context.DownstreamReRoute.DownstreamPathTemplate.Value} has instructions to convert claims to path"); - var response = _changeDownstreamPathTemplate.ChangeDownstreamPath(context.DownstreamReRoute.ClaimsToPath, context.HttpContext.User.Claims, - context.DownstreamReRoute.DownstreamPathTemplate, context.TemplatePlaceholderNameAndValues); +using Microsoft.AspNetCore.Http; - if (response.IsError) - { - Logger.LogWarning("there was an error setting queries on context, setting pipeline error"); - - SetPipelineError(context, response.Errors); - return; - } - } +using Ocelot.Middleware; - await _next.Invoke(context); - } - } -} +using Ocelot.PathManipulation; + +namespace Ocelot.DownstreamPathManipulation.Middleware +{ + public class ClaimsToDownstreamPathMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IChangeDownstreamPathTemplate _changeDownstreamPathTemplate; + + public ClaimsToDownstreamPathMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IChangeDownstreamPathTemplate changeDownstreamPathTemplate) + : base(loggerFactory.CreateLogger()) + { + _next = next; + _changeDownstreamPathTemplate = changeDownstreamPathTemplate; + } + + public async Task Invoke(HttpContext httpContext) + { + var downstreamRoute = httpContext.Items.DownstreamRoute(); + + if (downstreamRoute.ClaimsToPath.Any()) + { + Logger.LogInformation($"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to path"); + + var templatePlaceholderNameAndValues = httpContext.Items.TemplatePlaceholderNameAndValues(); + + var response = _changeDownstreamPathTemplate.ChangeDownstreamPath(downstreamRoute.ClaimsToPath, httpContext.User.Claims, + downstreamRoute.DownstreamPathTemplate, templatePlaceholderNameAndValues); + + if (response.IsError) + { + Logger.LogWarning("there was an error setting queries on context, setting pipeline error"); + + httpContext.Items.UpsertErrors(response.Errors); + return; + } + } + + await _next.Invoke(httpContext); + } + } +} diff --git a/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddlewareExtensions.cs b/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddlewareExtensions.cs index 04fbc78d1..70a4131d7 100644 --- a/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddlewareExtensions.cs +++ b/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddlewareExtensions.cs @@ -1,10 +1,10 @@ -using Ocelot.Middleware.Pipeline; +using Microsoft.AspNetCore.Builder; -namespace Ocelot.PathManipulation.Middleware +namespace Ocelot.DownstreamPathManipulation.Middleware { public static class ClaimsToDownstreamPathMiddlewareExtensions { - public static IOcelotPipelineBuilder UseClaimsToDownstreamPathMiddleware(this IOcelotPipelineBuilder builder) + public static IApplicationBuilder UseClaimsToDownstreamPathMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware(); } diff --git a/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs b/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs deleted file mode 100644 index a691e11e2..000000000 --- a/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Ocelot.Configuration; -using Ocelot.DownstreamRouteFinder.UrlMatcher; -using System.Collections.Generic; - -namespace Ocelot.DownstreamRouteFinder -{ - public class DownstreamRoute - { - public DownstreamRoute(List templatePlaceholderNameAndValues, ReRoute reRoute) - { - TemplatePlaceholderNameAndValues = templatePlaceholderNameAndValues; - ReRoute = reRoute; - } - - public List TemplatePlaceholderNameAndValues { get; private set; } - public ReRoute ReRoute { get; private set; } - } -} diff --git a/src/Ocelot/DownstreamRouteFinder/DownstreamRouteHolder.cs b/src/Ocelot/DownstreamRouteFinder/DownstreamRouteHolder.cs new file mode 100644 index 000000000..e735781b4 --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/DownstreamRouteHolder.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +using Ocelot.Configuration; + +using Ocelot.DownstreamRouteFinder.UrlMatcher; + +namespace Ocelot.DownstreamRouteFinder +{ + public class DownstreamRouteHolder + { + public DownstreamRouteHolder() + { + } + + public DownstreamRouteHolder(List templatePlaceholderNameAndValues, Route route) + { + TemplatePlaceholderNameAndValues = templatePlaceholderNameAndValues; + Route = route; + } + + public List TemplatePlaceholderNameAndValues { get; } + public Route Route { get; } + } +} diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs index ec6821adf..b960f7cea 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteCreator.cs @@ -1,138 +1,140 @@ -namespace Ocelot.DownstreamRouteFinder.Finder -{ - using Configuration; - using Configuration.Builder; - using Configuration.Creator; - using LoadBalancer.LoadBalancers; - using Responses; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Linq; - using UrlMatcher; - - public class DownstreamRouteCreator : IDownstreamRouteProvider - { - private readonly IQoSOptionsCreator _qoSOptionsCreator; - private readonly ConcurrentDictionary> _cache; - - public DownstreamRouteCreator(IQoSOptionsCreator qoSOptionsCreator) - { - _qoSOptionsCreator = qoSOptionsCreator; - _cache = new ConcurrentDictionary>(); - } - - public Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost) - { - var serviceName = GetServiceName(upstreamUrlPath); - - var downstreamPath = GetDownstreamPath(upstreamUrlPath); - - if (HasQueryString(downstreamPath)) - { - downstreamPath = RemoveQueryString(downstreamPath); - } - - var downstreamPathForKeys = $"/{serviceName}{downstreamPath}"; - - var loadBalancerKey = CreateLoadBalancerKey(downstreamPathForKeys, upstreamHttpMethod, configuration.LoadBalancerOptions); - - if (_cache.TryGetValue(loadBalancerKey, out var downstreamRoute)) - { - return downstreamRoute; - } - - var qosOptions = _qoSOptionsCreator.Create(configuration.QoSOptions, downstreamPathForKeys, new List { upstreamHttpMethod }); - - var upstreamPathTemplate = new UpstreamPathTemplateBuilder().WithOriginalValue(upstreamUrlPath).Build(); - - var downstreamReRouteBuilder = new DownstreamReRouteBuilder() - .WithServiceName(serviceName) - .WithLoadBalancerKey(loadBalancerKey) - .WithDownstreamPathTemplate(downstreamPath) - .WithUseServiceDiscovery(true) - .WithHttpHandlerOptions(configuration.HttpHandlerOptions) - .WithQosOptions(qosOptions) - .WithDownstreamScheme(configuration.DownstreamScheme) - .WithLoadBalancerOptions(configuration.LoadBalancerOptions) - .WithUpstreamPathTemplate(upstreamPathTemplate); - - var rateLimitOptions = configuration.ReRoutes != null - ? configuration.ReRoutes - .SelectMany(x => x.DownstreamReRoute) - .FirstOrDefault(x => x.ServiceName == serviceName) - : null; - - if (rateLimitOptions != null) - { - downstreamReRouteBuilder - .WithRateLimitOptions(rateLimitOptions.RateLimitOptions) - .WithEnableRateLimiting(true); - } - - var downstreamReRoute = downstreamReRouteBuilder.Build(); - - var reRoute = new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) - .WithUpstreamHttpMethod(new List() { upstreamHttpMethod }) - .WithUpstreamPathTemplate(upstreamPathTemplate) - .Build(); - - downstreamRoute = new OkResponse(new DownstreamRoute(new List(), reRoute)); - - _cache.AddOrUpdate(loadBalancerKey, downstreamRoute, (x, y) => downstreamRoute); - - return downstreamRoute; - } - - private static string RemoveQueryString(string downstreamPath) - { - return downstreamPath - .Substring(0, downstreamPath.IndexOf('?')); - } - - private static bool HasQueryString(string downstreamPath) - { - return downstreamPath.Contains("?"); - } - - private static string GetDownstreamPath(string upstreamUrlPath) - { - if (upstreamUrlPath.IndexOf('/', 1) == -1) - { - return "/"; - } - - return upstreamUrlPath - .Substring(upstreamUrlPath.IndexOf('/', 1)); - } - - private static string GetServiceName(string upstreamUrlPath) - { - if (upstreamUrlPath.IndexOf('/', 1) == -1) - { - return upstreamUrlPath - .Substring(1); - } - - return upstreamUrlPath - .Substring(1, upstreamUrlPath.IndexOf('/', 1)) - .TrimEnd('/'); - } - - private string CreateLoadBalancerKey(string downstreamTemplatePath, string httpMethod, LoadBalancerOptions loadBalancerOptions) - { - if (!string.IsNullOrEmpty(loadBalancerOptions.Type) && !string.IsNullOrEmpty(loadBalancerOptions.Key) && loadBalancerOptions.Type == nameof(CookieStickySessions)) - { - return $"{nameof(CookieStickySessions)}:{loadBalancerOptions.Key}"; - } - - return CreateQoSKey(downstreamTemplatePath, httpMethod); - } - - private string CreateQoSKey(string downstreamTemplatePath, string httpMethod) - { - var loadBalancerKey = $"{downstreamTemplatePath}|{httpMethod}"; - return loadBalancerKey; - } - } -} +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; + +using Ocelot.LoadBalancer.LoadBalancers; + +using Ocelot.Responses; + +using Ocelot.DownstreamRouteFinder.UrlMatcher; + +namespace Ocelot.DownstreamRouteFinder.Finder +{ + public class DownstreamRouteCreator : IDownstreamRouteProvider + { + private readonly IQoSOptionsCreator _qoSOptionsCreator; + private readonly ConcurrentDictionary> _cache; + + public DownstreamRouteCreator(IQoSOptionsCreator qoSOptionsCreator) + { + _qoSOptionsCreator = qoSOptionsCreator; + _cache = new ConcurrentDictionary>(); + } + + public Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost) + { + var serviceName = GetServiceName(upstreamUrlPath); + + var downstreamPath = GetDownstreamPath(upstreamUrlPath); + + if (HasQueryString(downstreamPath)) + { + downstreamPath = RemoveQueryString(downstreamPath); + } + + var downstreamPathForKeys = $"/{serviceName}{downstreamPath}"; + + var loadBalancerKey = CreateLoadBalancerKey(downstreamPathForKeys, upstreamHttpMethod, configuration.LoadBalancerOptions); + + if (_cache.TryGetValue(loadBalancerKey, out var downstreamRouteHolder)) + { + return downstreamRouteHolder; + } + + var qosOptions = _qoSOptionsCreator.Create(configuration.QoSOptions, downstreamPathForKeys, new List { upstreamHttpMethod }); + + var upstreamPathTemplate = new UpstreamPathTemplateBuilder().WithOriginalValue(upstreamUrlPath).Build(); + + var downstreamRouteBuilder = new DownstreamRouteBuilder() + .WithServiceName(serviceName) + .WithLoadBalancerKey(loadBalancerKey) + .WithDownstreamPathTemplate(downstreamPath) + .WithUseServiceDiscovery(true) + .WithHttpHandlerOptions(configuration.HttpHandlerOptions) + .WithQosOptions(qosOptions) + .WithDownstreamScheme(configuration.DownstreamScheme) + .WithLoadBalancerOptions(configuration.LoadBalancerOptions) + .WithDownstreamHttpVersion(configuration.DownstreamHttpVersion) + .WithUpstreamPathTemplate(upstreamPathTemplate); + + var rateLimitOptions = configuration.Routes?.SelectMany(x => x.DownstreamRoute) + .FirstOrDefault(x => x.ServiceName == serviceName); + + if (rateLimitOptions != null) + { + downstreamRouteBuilder + .WithRateLimitOptions(rateLimitOptions.RateLimitOptions) + .WithEnableRateLimiting(true); + } + + var downstreamRoute = downstreamRouteBuilder.Build(); + + var route = new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(new List { upstreamHttpMethod }) + .WithUpstreamPathTemplate(upstreamPathTemplate) + .Build(); + + downstreamRouteHolder = new OkResponse(new DownstreamRouteHolder(new List(), route)); + + _cache.AddOrUpdate(loadBalancerKey, downstreamRouteHolder, (x, y) => downstreamRouteHolder); + + return downstreamRouteHolder; + } + + private static string RemoveQueryString(string downstreamPath) + { + return downstreamPath + .Substring(0, downstreamPath.IndexOf('?')); + } + + private static bool HasQueryString(string downstreamPath) + { + return downstreamPath.Contains('?'); + } + + private static string GetDownstreamPath(string upstreamUrlPath) + { + if (upstreamUrlPath.IndexOf('/', 1) == -1) + { + return "/"; + } + + return upstreamUrlPath + .Substring(upstreamUrlPath.IndexOf('/', 1)); + } + + private static string GetServiceName(string upstreamUrlPath) + { + if (upstreamUrlPath.IndexOf('/', 1) == -1) + { + return upstreamUrlPath + .Substring(1); + } + + return upstreamUrlPath + .Substring(1, upstreamUrlPath.IndexOf('/', 1)) + .TrimEnd('/'); + } + + private static string CreateLoadBalancerKey(string downstreamTemplatePath, string httpMethod, LoadBalancerOptions loadBalancerOptions) + { + if (!string.IsNullOrEmpty(loadBalancerOptions.Type) && !string.IsNullOrEmpty(loadBalancerOptions.Key) && loadBalancerOptions.Type == nameof(CookieStickySessions)) + { + return $"{nameof(CookieStickySessions)}:{loadBalancerOptions.Key}"; + } + + return CreateQoSKey(downstreamTemplatePath, httpMethod); + } + + private static string CreateQoSKey(string downstreamTemplatePath, string httpMethod) + { + var loadBalancerKey = $"{downstreamTemplatePath}|{httpMethod}"; + return loadBalancerKey; + } + } +} diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs index 115fbd424..30a9eef5c 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs @@ -1,62 +1,63 @@ -using Ocelot.Configuration; -using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.Responses; -using System.Collections.Generic; -using System.Linq; - -namespace Ocelot.DownstreamRouteFinder.Finder -{ - public class DownstreamRouteFinder : IDownstreamRouteProvider - { - private readonly IUrlPathToUrlTemplateMatcher _urlMatcher; - private readonly IPlaceholderNameAndValueFinder _placeholderNameAndValueFinder; - - public DownstreamRouteFinder(IUrlPathToUrlTemplateMatcher urlMatcher, IPlaceholderNameAndValueFinder urlPathPlaceholderNameAndValueFinder) - { - _urlMatcher = urlMatcher; - _placeholderNameAndValueFinder = urlPathPlaceholderNameAndValueFinder; - } - - public Response Get(string upstreamUrlPath, string upstreamQueryString, string httpMethod, IInternalConfiguration configuration, string upstreamHost) - { - var downstreamRoutes = new List(); - - var applicableReRoutes = configuration.ReRoutes - .Where(r => RouteIsApplicableToThisRequest(r, httpMethod, upstreamHost)) - .OrderByDescending(x => x.UpstreamTemplatePattern.Priority); - - foreach (var reRoute in applicableReRoutes) - { - var urlMatch = _urlMatcher.Match(upstreamUrlPath, upstreamQueryString, reRoute.UpstreamTemplatePattern); - - if (urlMatch.Data.Match) - { - downstreamRoutes.Add(GetPlaceholderNamesAndValues(upstreamUrlPath, upstreamQueryString, reRoute)); - } - } +using System.Collections.Generic; +using System.Linq; - if (downstreamRoutes.Any()) - { - var notNullOption = downstreamRoutes.FirstOrDefault(x => !string.IsNullOrEmpty(x.ReRoute.UpstreamHost)); - var nullOption = downstreamRoutes.FirstOrDefault(x => string.IsNullOrEmpty(x.ReRoute.UpstreamHost)); - - return notNullOption != null ? new OkResponse(notNullOption) : new OkResponse(nullOption); - } - - return new ErrorResponse(new UnableToFindDownstreamRouteError(upstreamUrlPath, httpMethod)); - } - - private bool RouteIsApplicableToThisRequest(ReRoute reRoute, string httpMethod, string upstreamHost) - { - return (reRoute.UpstreamHttpMethod.Count == 0 || reRoute.UpstreamHttpMethod.Select(x => x.Method.ToLower()).Contains(httpMethod.ToLower())) && - (string.IsNullOrEmpty(reRoute.UpstreamHost) || reRoute.UpstreamHost == upstreamHost); - } - - private DownstreamRoute GetPlaceholderNamesAndValues(string path, string query, ReRoute reRoute) - { - var templatePlaceholderNameAndValues = _placeholderNameAndValueFinder.Find(path, query, reRoute.UpstreamTemplatePattern.OriginalValue); - - return new DownstreamRoute(templatePlaceholderNameAndValues.Data, reRoute); - } - } -} +using Ocelot.Configuration; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Responses; + +namespace Ocelot.DownstreamRouteFinder.Finder +{ + public class DownstreamRouteFinder : IDownstreamRouteProvider + { + private readonly IUrlPathToUrlTemplateMatcher _urlMatcher; + private readonly IPlaceholderNameAndValueFinder _placeholderNameAndValueFinder; + + public DownstreamRouteFinder(IUrlPathToUrlTemplateMatcher urlMatcher, IPlaceholderNameAndValueFinder urlPathPlaceholderNameAndValueFinder) + { + _urlMatcher = urlMatcher; + _placeholderNameAndValueFinder = urlPathPlaceholderNameAndValueFinder; + } + + public Response Get(string upstreamUrlPath, string upstreamQueryString, string httpMethod, IInternalConfiguration configuration, string upstreamHost) + { + var downstreamRoutes = new List(); + + var applicableRoutes = configuration.Routes + .Where(r => RouteIsApplicableToThisRequest(r, httpMethod, upstreamHost)) + .OrderByDescending(x => x.UpstreamTemplatePattern.Priority); + + foreach (var route in applicableRoutes) + { + var urlMatch = _urlMatcher.Match(upstreamUrlPath, upstreamQueryString, route.UpstreamTemplatePattern); + + if (urlMatch.Data.Match) + { + downstreamRoutes.Add(GetPlaceholderNamesAndValues(upstreamUrlPath, upstreamQueryString, route)); + } + } + + if (downstreamRoutes.Any()) + { + var notNullOption = downstreamRoutes.FirstOrDefault(x => !string.IsNullOrEmpty(x.Route.UpstreamHost)); + var nullOption = downstreamRoutes.FirstOrDefault(x => string.IsNullOrEmpty(x.Route.UpstreamHost)); + + return notNullOption != null ? new OkResponse(notNullOption) : new OkResponse(nullOption); + } + + return new ErrorResponse(new UnableToFindDownstreamRouteError(upstreamUrlPath, httpMethod)); + } + + private static bool RouteIsApplicableToThisRequest(Route route, string httpMethod, string upstreamHost) + { + return (route.UpstreamHttpMethod.Count == 0 || route.UpstreamHttpMethod.Select(x => x.Method.ToLower()).Contains(httpMethod.ToLower())) && + (string.IsNullOrEmpty(route.UpstreamHost) || route.UpstreamHost == upstreamHost); + } + + private DownstreamRouteHolder GetPlaceholderNamesAndValues(string path, string query, Route route) + { + var templatePlaceholderNameAndValues = _placeholderNameAndValueFinder.Find(path, query, route.UpstreamTemplatePattern.OriginalValue); + + return new DownstreamRouteHolder(templatePlaceholderNameAndValues.Data, route); + } + } +} diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs index 32f689367..6d97ff23b 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs @@ -1,44 +1,42 @@ -namespace Ocelot.DownstreamRouteFinder.Finder -{ - using Configuration; - using Microsoft.Extensions.DependencyInjection; - using Ocelot.Logging; - using System; - using System.Collections.Generic; - using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; - public class DownstreamRouteProviderFactory : IDownstreamRouteProviderFactory - { - private readonly Dictionary _providers; - private readonly IOcelotLogger _logger; +using Ocelot.Configuration; - public DownstreamRouteProviderFactory(IServiceProvider provider, IOcelotLoggerFactory factory) - { - _logger = factory.CreateLogger(); - _providers = provider.GetServices().ToDictionary(x => x.GetType().Name); - } +using Ocelot.Logging; - public IDownstreamRouteProvider Get(IInternalConfiguration config) - { - //todo - this is a bit hacky we are saying there are no reRoutes or there are reRoutes but none of them have - //an upstream path template which means they are dyanmic and service discovery is on... - if ((!config.ReRoutes.Any() || config.ReRoutes.All(x => string.IsNullOrEmpty(x.UpstreamTemplatePattern?.OriginalValue))) && IsServiceDiscovery(config.ServiceProviderConfiguration)) - { - _logger.LogInformation($"Selected {nameof(DownstreamRouteCreator)} as DownstreamRouteProvider for this request"); - return _providers[nameof(DownstreamRouteCreator)]; - } - - return _providers[nameof(DownstreamRouteFinder)]; - } - - private bool IsServiceDiscovery(ServiceProviderConfiguration config) - { - if (!string.IsNullOrEmpty(config?.Host) && config?.Port > 0 && !string.IsNullOrEmpty(config.Type)) - { - return true; - } - - return false; - } - } -} +using Microsoft.Extensions.DependencyInjection; + +namespace Ocelot.DownstreamRouteFinder.Finder +{ + public class DownstreamRouteProviderFactory : IDownstreamRouteProviderFactory + { + private readonly Dictionary _providers; + private readonly IOcelotLogger _logger; + + public DownstreamRouteProviderFactory(IServiceProvider provider, IOcelotLoggerFactory factory) + { + _logger = factory.CreateLogger(); + _providers = provider.GetServices().ToDictionary(x => x.GetType().Name); + } + + public IDownstreamRouteProvider Get(IInternalConfiguration config) + { + //todo - this is a bit hacky we are saying there are no routes or there are routes but none of them have + //an upstream path template which means they are dyanmic and service discovery is on... + 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)]; + } + + return _providers[nameof(DownstreamRouteFinder)]; + } + + private static bool IsServiceDiscovery(ServiceProviderConfiguration config) + { + return !string.IsNullOrEmpty(config?.Host) && config?.Port > 0 && !string.IsNullOrEmpty(config.Type); + } + } +} diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs b/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs index b2809ac9a..ed2a657ef 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs @@ -1,10 +1,10 @@ -using Ocelot.Configuration; -using Ocelot.Responses; - -namespace Ocelot.DownstreamRouteFinder.Finder -{ - public interface IDownstreamRouteProvider - { - Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost); - } -} +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.DownstreamRouteFinder.Finder +{ + public interface IDownstreamRouteProvider + { + Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost); + } +} diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProviderFactory.cs b/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProviderFactory.cs index 35fdd297d..e76a00aa0 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProviderFactory.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProviderFactory.cs @@ -1,7 +1,7 @@ -namespace Ocelot.DownstreamRouteFinder.Finder -{ - using Configuration; +using Ocelot.Configuration; +namespace Ocelot.DownstreamRouteFinder.Finder +{ public interface IDownstreamRouteProviderFactory { IDownstreamRouteProvider Get(IInternalConfiguration config); diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/UnableToFindDownstreamRouteError.cs b/src/Ocelot/DownstreamRouteFinder/Finder/UnableToFindDownstreamRouteError.cs index 5a0a358c6..b0396f208 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/UnableToFindDownstreamRouteError.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/UnableToFindDownstreamRouteError.cs @@ -1,12 +1,12 @@ -using Ocelot.Errors; - -namespace Ocelot.DownstreamRouteFinder.Finder -{ - public class UnableToFindDownstreamRouteError : Error - { +using Ocelot.Errors; + +namespace Ocelot.DownstreamRouteFinder.Finder +{ + public class UnableToFindDownstreamRouteError : Error + { public UnableToFindDownstreamRouteError(string path, string httpVerb) - : base($"Failed to match ReRoute configuration for upstream path: {path}, verb: {httpVerb}.", OcelotErrorCode.UnableToFindDownstreamRouteError) - { - } - } + : base($"Failed to match Route configuration for upstream path: {path}, verb: {httpVerb}.", OcelotErrorCode.UnableToFindDownstreamRouteError, 404) + { + } + } } diff --git a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs index 8ac0705cd..4efa4867f 100644 --- a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs +++ b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs @@ -1,59 +1,66 @@ +using System.Linq; +using System.Threading.Tasks; + using Ocelot.DownstreamRouteFinder.Finder; + using Ocelot.Infrastructure.Extensions; + using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; + using Ocelot.Middleware; -using Ocelot.Middleware.Multiplexer; -using System.Linq; -using System.Threading.Tasks; namespace Ocelot.DownstreamRouteFinder.Middleware { public class DownstreamRouteFinderMiddleware : OcelotMiddleware { - private readonly OcelotRequestDelegate _next; + private readonly RequestDelegate _next; private readonly IDownstreamRouteProviderFactory _factory; - private readonly IMultiplexer _multiplexer; - public DownstreamRouteFinderMiddleware(OcelotRequestDelegate next, + public DownstreamRouteFinderMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, - IDownstreamRouteProviderFactory downstreamRouteFinder, - IMultiplexer multiplexer) + IDownstreamRouteProviderFactory downstreamRouteFinder + ) : base(loggerFactory.CreateLogger()) { - _multiplexer = multiplexer; _next = next; _factory = downstreamRouteFinder; } - public async Task Invoke(DownstreamContext context) + public async Task Invoke(HttpContext httpContext) { - var upstreamUrlPath = context.HttpContext.Request.Path.ToString(); + var upstreamUrlPath = httpContext.Request.Path.ToString(); - var upstreamQueryString = context.HttpContext.Request.QueryString.ToString(); + var upstreamQueryString = httpContext.Request.QueryString.ToString(); - var upstreamHost = context.HttpContext.Request.Headers["Host"]; + var upstreamHost = httpContext.Request.Headers["Host"]; Logger.LogDebug($"Upstream url path is {upstreamUrlPath}"); - var provider = _factory.Get(context.Configuration); + var internalConfiguration = httpContext.Items.IInternalConfiguration(); - var downstreamRoute = provider.Get(upstreamUrlPath, upstreamQueryString, context.HttpContext.Request.Method, context.Configuration, upstreamHost); + var provider = _factory.Get(internalConfiguration); - if (downstreamRoute.IsError) + var response = provider.Get(upstreamUrlPath, upstreamQueryString, httpContext.Request.Method, internalConfiguration, upstreamHost); + + if (response.IsError) { - Logger.LogWarning($"{MiddlewareName} setting pipeline errors. IDownstreamRouteFinder returned {downstreamRoute.Errors.ToErrorString()}"); + Logger.LogWarning($"{MiddlewareName} setting pipeline errors. IDownstreamRouteFinder returned {response.Errors.ToErrorString()}"); - SetPipelineError(context, downstreamRoute.Errors); + httpContext.Items.UpsertErrors(response.Errors); return; } - var downstreamPathTemplates = string.Join(", ", downstreamRoute.Data.ReRoute.DownstreamReRoute.Select(r => r.DownstreamPathTemplate.Value)); - + var downstreamPathTemplates = string.Join(", ", response.Data.Route.DownstreamRoute.Select(r => r.DownstreamPathTemplate.Value)); Logger.LogDebug($"downstream templates are {downstreamPathTemplates}"); - context.TemplatePlaceholderNameAndValues = downstreamRoute.Data.TemplatePlaceholderNameAndValues; + // why set both of these on HttpContext + httpContext.Items.UpsertTemplatePlaceholderNameAndValues(response.Data.TemplatePlaceholderNameAndValues); + + httpContext.Items.UpsertDownstreamRoute(response.Data); - await _multiplexer.Multiplex(context, downstreamRoute.Data.ReRoute, _next); + await _next.Invoke(httpContext); } } } diff --git a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddlewareExtensions.cs b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddlewareExtensions.cs index 08a856b6b..f04887b21 100644 --- a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddlewareExtensions.cs +++ b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddlewareExtensions.cs @@ -1,11 +1,10 @@ using Microsoft.AspNetCore.Builder; -using Ocelot.Middleware.Pipeline; namespace Ocelot.DownstreamRouteFinder.Middleware { public static class DownstreamRouteFinderMiddlewareExtensions { - public static IOcelotPipelineBuilder UseDownstreamRouteFinderMiddleware(this IOcelotPipelineBuilder builder) + public static IApplicationBuilder UseDownstreamRouteFinderMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware(); } diff --git a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/IPlaceholderNameAndValueFinder.cs b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/IPlaceholderNameAndValueFinder.cs index c9b162600..502f3255c 100644 --- a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/IPlaceholderNameAndValueFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/IPlaceholderNameAndValueFinder.cs @@ -1,5 +1,6 @@ -using Ocelot.Responses; -using System.Collections.Generic; +using System.Collections.Generic; + +using Ocelot.Responses; namespace Ocelot.DownstreamRouteFinder.UrlMatcher { diff --git a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/PlaceholderNameAndValue.cs b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/PlaceholderNameAndValue.cs index 4bd29414f..ccaf099cc 100644 --- a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/PlaceholderNameAndValue.cs +++ b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/PlaceholderNameAndValue.cs @@ -8,7 +8,7 @@ public PlaceholderNameAndValue(string name, string value) Value = value; } - public string Name { get; private set; } - public string Value { get; private set; } + public string Name { get; } + public string Value { get; } } } diff --git a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlMatch.cs b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlMatch.cs index 9ef6396c7..b328e0728 100644 --- a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlMatch.cs +++ b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlMatch.cs @@ -7,6 +7,6 @@ public UrlMatch(bool match) Match = match; } - public bool Match { get; private set; } + public bool Match { get; } } } diff --git a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinder.cs b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinder.cs index 50f1ef7ad..4b5676660 100644 --- a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinder.cs @@ -1,6 +1,7 @@ -using Ocelot.Responses; using System.Collections.Generic; +using Ocelot.Responses; + namespace Ocelot.DownstreamRouteFinder.UrlMatcher { public class UrlPathPlaceholderNameAndValueFinder : IPlaceholderNameAndValueFinder @@ -11,12 +12,12 @@ public Response> Find(string path, string query, s path = $"{path}{query}"; - int counterForPath = 0; + var counterForPath = 0; var delimiter = '/'; var nextDelimiter = '/'; - for (int counterForTemplate = 0; counterForTemplate < pathTemplate.Length; counterForTemplate++) + for (var counterForTemplate = 0; counterForTemplate < pathTemplate.Length; counterForTemplate++) { if ((path.Length > counterForPath) && CharactersDontMatch(pathTemplate[counterForTemplate], path[counterForPath]) && ContinueScanningUrl(counterForPath, path.Length)) { @@ -59,7 +60,7 @@ public Response> Find(string path, string query, s if (NothingAfterFirstForwardSlash(path)) { - placeHolderNameAndValues.Add(new PlaceholderNameAndValue(placeholderName, "")); + placeHolderNameAndValues.Add(new PlaceholderNameAndValue(placeholderName, string.Empty)); } else { @@ -83,27 +84,27 @@ private static bool NoMoreForwardSlash(string pathTemplate, int counterForTempla private static bool NotPassedQueryString(string pathTemplate, int counterForTemplate) { - return !pathTemplate.Substring(0, counterForTemplate).Contains("?"); + return !pathTemplate.Substring(0, counterForTemplate).Contains('?'); } private static bool PassedQueryString(string pathTemplate, int counterForTemplate) { - return pathTemplate.Substring(0, counterForTemplate).Contains("?"); + return pathTemplate.Substring(0, counterForTemplate).Contains('?'); } - private bool IsCatchAll(string path, int counterForPath, string pathTemplate) + private static bool IsCatchAll(string path, int counterForPath, string pathTemplate) { return string.IsNullOrEmpty(path) || (path.Length > counterForPath && path[counterForPath] == '/') && pathTemplate.Length > 1 && pathTemplate.Substring(0, 2) == "/{" && pathTemplate.IndexOf('}') == pathTemplate.Length - 1; } - private bool NothingAfterFirstForwardSlash(string path) + private static bool NothingAfterFirstForwardSlash(string path) { return path.Length == 1 || path.Length == 0; } - private string GetPlaceholderValue(string urlPathTemplate, string query, string variableName, string urlPath, int counterForUrl, char delimiter) + private static string GetPlaceholderValue(string urlPathTemplate, string query, string variableName, string urlPath, int counterForUrl, char delimiter) { var positionOfNextSlash = urlPath.IndexOf(delimiter, counterForUrl); @@ -117,7 +118,7 @@ private string GetPlaceholderValue(string urlPathTemplate, string query, string return variableValue; } - private string GetPlaceholderName(string urlPathTemplate, int counterForTemplate) + private static string GetPlaceholderName(string urlPathTemplate, int counterForTemplate) { var postitionOfPlaceHolderClosingBracket = urlPathTemplate.IndexOf('}', counterForTemplate) + 1; @@ -126,25 +127,22 @@ private string GetPlaceholderName(string urlPathTemplate, int counterForTemplate return variableName; } - private int GetNextCounterPosition(string urlTemplate, int counterForTemplate, char delimiter) + private static int GetNextCounterPosition(string urlTemplate, int counterForTemplate, char delimiter) { var closingPlaceHolderPositionOnTemplate = urlTemplate.IndexOf(delimiter, counterForTemplate); return closingPlaceHolderPositionOnTemplate + 1; } - private bool CharactersDontMatch(char characterOne, char characterTwo) + private static bool CharactersDontMatch(char characterOne, char characterTwo) { return char.ToLower(characterOne) != char.ToLower(characterTwo); } - private bool ContinueScanningUrl(int counterForUrl, int urlLength) + private static bool ContinueScanningUrl(int counterForUrl, int urlLength) { return counterForUrl < urlLength; } - private bool IsPlaceholder(char character) - { - return character == '{'; - } + private static bool IsPlaceholder(char character) => character == '{'; } } diff --git a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs index 7d19d523b..4db8f5d96 100644 --- a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs +++ b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs @@ -1,132 +1,156 @@ -using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; -using Ocelot.Logging; -using Ocelot.Middleware; -using Ocelot.Responses; -using Ocelot.Values; using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace Ocelot.DownstreamUrlCreator.Middleware -{ - using System.Text.RegularExpressions; - - public class DownstreamUrlCreatorMiddleware : OcelotMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly IDownstreamPathPlaceholderReplacer _replacer; - - public DownstreamUrlCreatorMiddleware(OcelotRequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IDownstreamPathPlaceholderReplacer replacer) - : base(loggerFactory.CreateLogger()) - { - _next = next; - _replacer = replacer; - } - - public async Task Invoke(DownstreamContext context) - { - var response = _replacer - .Replace(context.DownstreamReRoute.DownstreamPathTemplate.Value, context.TemplatePlaceholderNameAndValues); - - if (response.IsError) - { - Logger.LogDebug("IDownstreamPathPlaceholderReplacer returned an error, setting pipeline error"); - - SetPipelineError(context, response.Errors); - return; - } - - if (!string.IsNullOrEmpty(context.DownstreamReRoute.DownstreamScheme)) - { - context.DownstreamRequest.Scheme = context.DownstreamReRoute.DownstreamScheme; - } - - if (ServiceFabricRequest(context)) - { - var pathAndQuery = CreateServiceFabricUri(context, response); - context.DownstreamRequest.AbsolutePath = pathAndQuery.path; - context.DownstreamRequest.Query = pathAndQuery.query; - } - else - { - var dsPath = response.Data; +using Ocelot.Configuration; - if (ContainsQueryString(dsPath)) - { - context.DownstreamRequest.AbsolutePath = GetPath(dsPath); +using Ocelot.DownstreamRouteFinder.UrlMatcher; - if (string.IsNullOrEmpty(context.DownstreamRequest.Query)) - { - context.DownstreamRequest.Query = GetQueryString(dsPath); - } - else - { - context.DownstreamRequest.Query += GetQueryString(dsPath).Replace('?', '&'); - } - } - else - { - RemoveQueryStringParametersThatHaveBeenUsedInTemplate(context); - - context.DownstreamRequest.AbsolutePath = dsPath.Value; - } - } - - Logger.LogDebug($"Downstream url is {context.DownstreamRequest}"); - - await _next.Invoke(context); - } - - private static void RemoveQueryStringParametersThatHaveBeenUsedInTemplate(DownstreamContext context) - { - foreach (var nAndV in context.TemplatePlaceholderNameAndValues) - { - var name = nAndV.Name.Replace("{", "").Replace("}", ""); - - if (context.DownstreamRequest.Query.Contains(name) && - context.DownstreamRequest.Query.Contains(nAndV.Value)) - { - var questionMarkOrAmpersand = context.DownstreamRequest.Query.IndexOf(name, StringComparison.Ordinal); - context.DownstreamRequest.Query = context.DownstreamRequest.Query.Remove(questionMarkOrAmpersand - 1, 1); - - var rgx = new Regex($@"\b{name}={nAndV.Value}\b"); - context.DownstreamRequest.Query = rgx.Replace(context.DownstreamRequest.Query, ""); - - if (!string.IsNullOrEmpty(context.DownstreamRequest.Query)) - { - context.DownstreamRequest.Query = '?' + context.DownstreamRequest.Query.Substring(1); - } - } - } - } +using Ocelot.Logging; - private string GetPath(DownstreamPath dsPath) - { - return dsPath.Value.Substring(0, dsPath.Value.IndexOf("?", StringComparison.Ordinal)); - } +using Microsoft.AspNetCore.Http; - private string GetQueryString(DownstreamPath dsPath) - { - return dsPath.Value.Substring(dsPath.Value.IndexOf("?", StringComparison.Ordinal)); - } +using Ocelot.Middleware; +using Ocelot.Request.Middleware; - private bool ContainsQueryString(DownstreamPath dsPath) - { - return dsPath.Value.Contains("?"); - } +using Ocelot.Responses; - private (string path, string query) CreateServiceFabricUri(DownstreamContext context, Response dsPath) - { - var query = context.DownstreamRequest.Query; - var serviceName = _replacer.Replace(context.DownstreamReRoute.ServiceName, context.TemplatePlaceholderNameAndValues); - var pathTemplate = $"/{serviceName.Data.Value}{dsPath.Data.Value}"; - return (pathTemplate, query); - } +using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; - private static bool ServiceFabricRequest(DownstreamContext context) - { - return context.Configuration.ServiceProviderConfiguration.Type?.ToLower() == "servicefabric" && context.DownstreamReRoute.UseServiceDiscovery; - } - } -} +using Ocelot.Values; + +namespace Ocelot.DownstreamUrlCreator.Middleware +{ + public class DownstreamUrlCreatorMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IDownstreamPathPlaceholderReplacer _replacer; + + public DownstreamUrlCreatorMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IDownstreamPathPlaceholderReplacer replacer + ) + : base(loggerFactory.CreateLogger()) + { + _next = next; + _replacer = 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 downstreamRequest = httpContext.Items.DownstreamRequest(); + + if (response.IsError) + { + Logger.LogDebug("IDownstreamPathPlaceholderReplacer returned an error, setting pipeline error"); + + httpContext.Items.UpsertErrors(response.Errors); + return; + } + + if (!string.IsNullOrEmpty(downstreamRoute.DownstreamScheme)) + { + //todo make sure this works, hopefully there is a test ;E + httpContext.Items.DownstreamRequest().Scheme = downstreamRoute.DownstreamScheme; + } + + var internalConfiguration = httpContext.Items.IInternalConfiguration(); + + if (ServiceFabricRequest(internalConfiguration, downstreamRoute)) + { + var pathAndQuery = CreateServiceFabricUri(downstreamRequest, downstreamRoute, templatePlaceholderNameAndValues, response); + + //todo check this works again hope there is a test.. + downstreamRequest.AbsolutePath = pathAndQuery.Path; + downstreamRequest.Query = pathAndQuery.Query; + } + else + { + var dsPath = response.Data; + + if (ContainsQueryString(dsPath)) + { + downstreamRequest.AbsolutePath = GetPath(dsPath); + + if (string.IsNullOrEmpty(downstreamRequest.Query)) + { + downstreamRequest.Query = GetQueryString(dsPath); + } + else + { + downstreamRequest.Query += GetQueryString(dsPath).Replace('?', '&'); + } + } + else + { + RemoveQueryStringParametersThatHaveBeenUsedInTemplate(downstreamRequest, templatePlaceholderNameAndValues); + + downstreamRequest.AbsolutePath = dsPath.Value; + } + } + + Logger.LogDebug($"Downstream url is {downstreamRequest}"); + + await _next.Invoke(httpContext); + } + + private static void RemoveQueryStringParametersThatHaveBeenUsedInTemplate(DownstreamRequest downstreamRequest, List templatePlaceholderNameAndValues) + { + foreach (var nAndV in templatePlaceholderNameAndValues) + { + var name = nAndV.Name.Replace("{", string.Empty).Replace("}", string.Empty); + + if (downstreamRequest.Query.Contains(name) && + downstreamRequest.Query.Contains(nAndV.Value)) + { + var questionMarkOrAmpersand = downstreamRequest.Query.IndexOf(name, StringComparison.Ordinal); + downstreamRequest.Query = downstreamRequest.Query.Remove(questionMarkOrAmpersand - 1, 1); + + var rgx = new Regex($@"\b{name}={nAndV.Value}\b"); + downstreamRequest.Query = rgx.Replace(downstreamRequest.Query, string.Empty); + + if (!string.IsNullOrEmpty(downstreamRequest.Query)) + { + downstreamRequest.Query = '?' + downstreamRequest.Query.Substring(1); + } + } + } + } + + private static string GetPath(DownstreamPath dsPath) + { + return dsPath.Value.Substring(0, dsPath.Value.IndexOf('?', StringComparison.Ordinal)); + } + + private static string GetQueryString(DownstreamPath dsPath) + { + return dsPath.Value.Substring(dsPath.Value.IndexOf('?', StringComparison.Ordinal)); + } + + 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; + var serviceName = _replacer.Replace(downstreamRoute.ServiceName, templatePlaceholderNameAndValues); + var pathTemplate = $"/{serviceName.Data.Value}{dsPath.Data.Value}"; + return (pathTemplate, query); + } + + private static bool ServiceFabricRequest(IInternalConfiguration config, DownstreamRoute downstreamRoute) + { + return config.ServiceProviderConfiguration.Type?.ToLower() == "servicefabric" && downstreamRoute.UseServiceDiscovery; + } + } +} diff --git a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddlewareExtensions.cs b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddlewareExtensions.cs index 8904d3af1..38412f504 100644 --- a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddlewareExtensions.cs +++ b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddlewareExtensions.cs @@ -1,13 +1,12 @@ -using Microsoft.AspNetCore.Builder; -using Ocelot.Middleware.Pipeline; - -namespace Ocelot.DownstreamUrlCreator.Middleware -{ - public static class DownstreamUrlCreatorMiddlewareExtensions - { - public static IOcelotPipelineBuilder UseDownstreamUrlCreatorMiddleware(this IOcelotPipelineBuilder builder) - { - return builder.UseMiddleware(); - } - } +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.DownstreamUrlCreator.Middleware +{ + public static class DownstreamUrlCreatorMiddlewareExtensions + { + public static IApplicationBuilder UseDownstreamUrlCreatorMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } } diff --git a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamTemplatePathPlaceholderReplacer.cs b/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamTemplatePathPlaceholderReplacer.cs index a58c66071..50bd9ec10 100644 --- a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamTemplatePathPlaceholderReplacer.cs +++ b/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamTemplatePathPlaceholderReplacer.cs @@ -1,8 +1,9 @@ +using System.Collections.Generic; +using System.Text; + using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Responses; using Ocelot.Values; -using System.Collections.Generic; -using System.Text; namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer { diff --git a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamPathPlaceholderReplacer.cs b/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamPathPlaceholderReplacer.cs index e6c0864ad..827e77bb1 100644 --- a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamPathPlaceholderReplacer.cs +++ b/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamPathPlaceholderReplacer.cs @@ -1,7 +1,8 @@ +using System.Collections.Generic; + using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Responses; using Ocelot.Values; -using System.Collections.Generic; namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer { diff --git a/src/Ocelot/Errors/Error.cs b/src/Ocelot/Errors/Error.cs index 18c3c49f6..dc5c0ce55 100644 --- a/src/Ocelot/Errors/Error.cs +++ b/src/Ocelot/Errors/Error.cs @@ -2,14 +2,16 @@ namespace Ocelot.Errors { public abstract class Error { - protected Error(string message, OcelotErrorCode code) - { + protected Error(string message, OcelotErrorCode code, int httpStatusCode) + { + HttpStatusCode = httpStatusCode; Message = message; Code = code; } - public string Message { get; private set; } - public OcelotErrorCode Code { get; private set; } + public string Message { get; } + public OcelotErrorCode Code { get; } + public int HttpStatusCode { get; } public override string ToString() { diff --git a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs index 6d583e66b..26532804b 100644 --- a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs @@ -1,102 +1,93 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +using Ocelot.Configuration; + +using Ocelot.Infrastructure.RequestData; + +using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Middleware; + namespace Ocelot.Errors.Middleware { - using Configuration; - using Ocelot.Configuration.Repository; - using Ocelot.Infrastructure.Extensions; - using Ocelot.Infrastructure.RequestData; - using Ocelot.Logging; - using Ocelot.Middleware; - using System; - using System.Linq; - using System.Threading.Tasks; - /// - /// Catches all unhandled exceptions thrown by middleware, logs and returns a 500 + /// Catches all unhandled exceptions thrown by middleware, logs and returns a 500. /// public class ExceptionHandlerMiddleware : OcelotMiddleware { - private readonly OcelotRequestDelegate _next; - private readonly IInternalConfigurationRepository _configRepo; + private readonly RequestDelegate _next; private readonly IRequestScopedDataRepository _repo; - public ExceptionHandlerMiddleware(OcelotRequestDelegate next, + public ExceptionHandlerMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, - IInternalConfigurationRepository configRepo, IRequestScopedDataRepository repo) : base(loggerFactory.CreateLogger()) { - _configRepo = configRepo; - _repo = repo; _next = next; + _repo = repo; } - public async Task Invoke(DownstreamContext context) + public async Task Invoke(HttpContext httpContext) { try { - context.HttpContext.RequestAborted.ThrowIfCancellationRequested(); - - //try and get the global request id and set it for logs... - //should this basically be immutable per request...i guess it should! - //first thing is get config - var configuration = _configRepo.Get(); - - if (configuration.IsError) - { - throw new Exception($"{MiddlewareName} setting pipeline errors. IOcelotConfigurationProvider returned {configuration.Errors.ToErrorString()}"); - } + httpContext.RequestAborted.ThrowIfCancellationRequested(); - TrySetGlobalRequestId(context, configuration.Data); + var internalConfiguration = httpContext.Items.IInternalConfiguration(); - context.Configuration = configuration.Data; + TrySetGlobalRequestId(httpContext, internalConfiguration); Logger.LogDebug("ocelot pipeline started"); - await _next.Invoke(context); + await _next.Invoke(httpContext); } - catch (OperationCanceledException e) when (context.HttpContext.RequestAborted.IsCancellationRequested) + catch (OperationCanceledException) when (httpContext.RequestAborted.IsCancellationRequested) { Logger.LogDebug("operation canceled"); - if (!context.HttpContext.Response.HasStarted) + if (!httpContext.Response.HasStarted) { - context.HttpContext.Response.StatusCode = 499; + httpContext.Response.StatusCode = 499; } } catch (Exception e) { Logger.LogDebug("error calling middleware"); - var message = CreateMessage(context, e); + var message = CreateMessage(httpContext, e); Logger.LogError(message, e); - SetInternalServerErrorOnResponse(context); + SetInternalServerErrorOnResponse(httpContext); } Logger.LogDebug("ocelot pipeline finished"); } - private void TrySetGlobalRequestId(DownstreamContext context, IInternalConfiguration configuration) + private void TrySetGlobalRequestId(HttpContext httpContext, IInternalConfiguration configuration) { var key = configuration.RequestId; - if (!string.IsNullOrEmpty(key) && context.HttpContext.Request.Headers.TryGetValue(key, out var upstreamRequestIds)) + if (!string.IsNullOrEmpty(key) && httpContext.Request.Headers.TryGetValue(key, out var upstreamRequestIds)) { - context.HttpContext.TraceIdentifier = upstreamRequestIds.First(); + httpContext.TraceIdentifier = upstreamRequestIds.First(); } - _repo.Add("RequestId", context.HttpContext.TraceIdentifier); + _repo.Add("RequestId", httpContext.TraceIdentifier); } - private void SetInternalServerErrorOnResponse(DownstreamContext context) + private static void SetInternalServerErrorOnResponse(HttpContext httpContext) { - if (!context.HttpContext.Response.HasStarted) + if (!httpContext.Response.HasStarted) { - context.HttpContext.Response.StatusCode = 500; + httpContext.Response.StatusCode = 500; } } - private string CreateMessage(DownstreamContext context, Exception e) + private static string CreateMessage(HttpContext httpContext, Exception e) { var message = $"Exception caught in global error handler, exception message: {e.Message}, exception stack: {e.StackTrace}"; @@ -107,7 +98,7 @@ private string CreateMessage(DownstreamContext context, Exception e) $"{message}, inner exception message {e.InnerException.Message}, inner exception stack {e.InnerException.StackTrace}"; } - return $"{message} RequestId: {context.HttpContext.TraceIdentifier}"; + return $"{message} RequestId: {httpContext.TraceIdentifier}"; } } } diff --git a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddlewareExtensions.cs b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddlewareExtensions.cs index 82e1050e5..e355c7f80 100644 --- a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddlewareExtensions.cs +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddlewareExtensions.cs @@ -1,13 +1,12 @@ -using Microsoft.AspNetCore.Builder; -using Ocelot.Middleware.Pipeline; - -namespace Ocelot.Errors.Middleware -{ - public static class ExceptionHandlerMiddlewareExtensions - { - public static IOcelotPipelineBuilder UseExceptionHandlerMiddleware(this IOcelotPipelineBuilder builder) - { - return builder.UseMiddleware(); - } - } +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Errors.Middleware +{ + public static class ExceptionHandlerMiddlewareExtensions + { + public static IApplicationBuilder UseExceptionHandlerMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } } diff --git a/src/Ocelot/Errors/OcelotErrorCode.cs b/src/Ocelot/Errors/OcelotErrorCode.cs index d8b6f3336..9063e7142 100644 --- a/src/Ocelot/Errors/OcelotErrorCode.cs +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -1,44 +1,47 @@ -namespace Ocelot.Errors -{ - public enum OcelotErrorCode - { +namespace Ocelot.Errors +{ + public enum OcelotErrorCode + { UnauthenticatedError = 0, - UnknownError = 1, - DownstreampathTemplateAlreadyUsedError = 2, - UnableToFindDownstreamRouteError = 3, - CannotAddDataError = 4, - CannotFindDataError = 5, - UnableToCompleteRequestError = 6, - UnableToCreateAuthenticationHandlerError = 7, - UnsupportedAuthenticationProviderError = 8, - CannotFindClaimError = 9, - ParsingConfigurationHeaderError = 10, - NoInstructionsError = 11, - InstructionNotForClaimsError = 12, - UnauthorizedError = 13, - ClaimValueNotAuthorisedError = 14, - ScopeNotAuthorisedError = 15, - UserDoesNotHaveClaimError = 16, - DownstreamPathTemplateContainsSchemeError = 17, - DownstreamPathNullOrEmptyError = 18, - DownstreamSchemeNullOrEmptyError = 19, - DownstreamHostNullOrEmptyError = 20, - ServicesAreNullError = 21, - ServicesAreEmptyError = 22, - UnableToFindServiceDiscoveryProviderError = 23, - UnableToFindLoadBalancerError = 24, - RequestTimedOutError = 25, - UnableToFindQoSProviderError = 26, - UnmappableRequestError = 27, - RateLimitOptionsError = 28, - PathTemplateDoesntStartWithForwardSlash = 29, - FileValidationFailedError = 30, - UnableToFindDelegatingHandlerProviderError = 31, - CouldNotFindPlaceholderError = 32, - CouldNotFindAggregatorError = 33, - CannotAddPlaceholderError = 34, - CannotRemovePlaceholderError = 35, + UnknownError = 1, + DownstreampathTemplateAlreadyUsedError = 2, + UnableToFindDownstreamRouteError = 3, + CannotAddDataError = 4, + CannotFindDataError = 5, + UnableToCompleteRequestError = 6, + UnableToCreateAuthenticationHandlerError = 7, + UnsupportedAuthenticationProviderError = 8, + CannotFindClaimError = 9, + ParsingConfigurationHeaderError = 10, + NoInstructionsError = 11, + InstructionNotForClaimsError = 12, + UnauthorizedError = 13, + ClaimValueNotAuthorizedError = 14, + ScopeNotAuthorizedError = 15, + UserDoesNotHaveClaimError = 16, + DownstreamPathTemplateContainsSchemeError = 17, + DownstreamPathNullOrEmptyError = 18, + DownstreamSchemeNullOrEmptyError = 19, + DownstreamHostNullOrEmptyError = 20, + ServicesAreNullError = 21, + ServicesAreEmptyError = 22, + UnableToFindServiceDiscoveryProviderError = 23, + UnableToFindLoadBalancerError = 24, + RequestTimedOutError = 25, + UnableToFindQoSProviderError = 26, + UnmappableRequestError = 27, + RateLimitOptionsError = 28, + PathTemplateDoesntStartWithForwardSlash = 29, + FileValidationFailedError = 30, + UnableToFindDelegatingHandlerProviderError = 31, + CouldNotFindPlaceholderError = 32, + CouldNotFindAggregatorError = 33, + CannotAddPlaceholderError = 34, + CannotRemovePlaceholderError = 35, QuotaExceededError = 36, - RequestCanceled = 37, - } + RequestCanceled = 37, + ConnectionToDownstreamServiceError = 38, + CouldNotFindLoadBalancerCreator = 39, + ErrorInvokingLoadBalancerCreator = 40, + } } diff --git a/src/Ocelot/Headers/AddHeadersToRequest.cs b/src/Ocelot/Headers/AddHeadersToRequest.cs index 9b0afee75..7d830ec6d 100644 --- a/src/Ocelot/Headers/AddHeadersToRequest.cs +++ b/src/Ocelot/Headers/AddHeadersToRequest.cs @@ -1,17 +1,23 @@ -namespace Ocelot.Headers -{ - using Infrastructure; - using Logging; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.Primitives; - using Ocelot.Configuration; - using Ocelot.Configuration.Creator; - using Ocelot.Infrastructure.Claims.Parser; - using Ocelot.Request.Middleware; - using Ocelot.Responses; - using System.Collections.Generic; - using System.Linq; +using System.Collections.Generic; +using System.Linq; + +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; + +using Ocelot.Infrastructure; + +using Ocelot.Logging; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Request.Middleware; + +using Ocelot.Responses; + +namespace Ocelot.Headers +{ public class AddHeadersToRequest : IAddHeadersToRequest { private readonly IClaimsParser _claimsParser; @@ -60,7 +66,7 @@ public void SetHeadersOnDownstreamRequest(IEnumerable headers, HttpCo requestHeader.Remove(header.Key); } - if (header.Value.StartsWith("{") && header.Value.EndsWith("}")) + if (header.Value.StartsWith('{') && header.Value.EndsWith("}")) { var value = _placeholders.Get(header.Value); diff --git a/src/Ocelot/Headers/AddHeadersToResponse.cs b/src/Ocelot/Headers/AddHeadersToResponse.cs index a41518600..d59fd8a5e 100644 --- a/src/Ocelot/Headers/AddHeadersToResponse.cs +++ b/src/Ocelot/Headers/AddHeadersToResponse.cs @@ -1,12 +1,15 @@ +using System.Collections.Generic; + +using Ocelot.Configuration.Creator; + +using Ocelot.Infrastructure; + +using Ocelot.Logging; + +using Ocelot.Middleware; + namespace Ocelot.Headers { - using Ocelot.Configuration.Creator; - using Ocelot.Infrastructure; - using Ocelot.Infrastructure.Extensions; - using Ocelot.Logging; - using Ocelot.Middleware; - using System.Collections.Generic; - public class AddHeadersToResponse : IAddHeadersToResponse { private readonly IPlaceholders _placeholders; diff --git a/src/Ocelot/Headers/HttpContextRequestHeaderReplacer.cs b/src/Ocelot/Headers/HttpContextRequestHeaderReplacer.cs index 6709231d3..d92186dc7 100644 --- a/src/Ocelot/Headers/HttpContextRequestHeaderReplacer.cs +++ b/src/Ocelot/Headers/HttpContextRequestHeaderReplacer.cs @@ -1,7 +1,9 @@ +using System.Collections.Generic; + using Microsoft.AspNetCore.Http; + using Ocelot.Configuration; using Ocelot.Responses; -using System.Collections.Generic; namespace Ocelot.Headers { diff --git a/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs index a6992bf19..bc7fce9bf 100644 --- a/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs +++ b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs @@ -1,13 +1,19 @@ +using System.Collections.Generic; +using System.Linq; + +using Ocelot.Configuration; + +using Ocelot.Infrastructure; +using Ocelot.Infrastructure.Extensions; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Middleware; + +using Ocelot.Responses; + namespace Ocelot.Headers { - using Ocelot.Configuration; - using Ocelot.Infrastructure; - using Ocelot.Infrastructure.Extensions; - using Ocelot.Middleware; - using Ocelot.Responses; - using System.Collections.Generic; - using System.Linq; - public class HttpResponseHeaderReplacer : IHttpResponseHeaderReplacer { private readonly IPlaceholders _placeholders; @@ -17,10 +23,10 @@ public HttpResponseHeaderReplacer(IPlaceholders placeholders) _placeholders = placeholders; } - public Response Replace(DownstreamContext context, List fAndRs) + public Response Replace(HttpContext httpContext, List fAndRs) { - var response = context.DownstreamResponse; - var request = context.DownstreamRequest; + var response = httpContext.Items.DownstreamResponse(); + var request = httpContext.Items.DownstreamRequest(); foreach (var f in fAndRs) { diff --git a/src/Ocelot/Headers/IAddHeadersToRequest.cs b/src/Ocelot/Headers/IAddHeadersToRequest.cs index 9abbb3b42..1be56e74b 100644 --- a/src/Ocelot/Headers/IAddHeadersToRequest.cs +++ b/src/Ocelot/Headers/IAddHeadersToRequest.cs @@ -1,13 +1,15 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; + +using Ocelot.Request.Middleware; + +using Ocelot.Responses; namespace Ocelot.Headers { - using Ocelot.Configuration; - using Ocelot.Configuration.Creator; - using Ocelot.Request.Middleware; - using Ocelot.Responses; - using System.Collections.Generic; - public interface IAddHeadersToRequest { Response SetHeadersOnDownstreamRequest(List claimsToThings, IEnumerable claims, DownstreamRequest downstreamRequest); diff --git a/src/Ocelot/Headers/IAddHeadersToResponse.cs b/src/Ocelot/Headers/IAddHeadersToResponse.cs index bbd37f4d4..057548022 100644 --- a/src/Ocelot/Headers/IAddHeadersToResponse.cs +++ b/src/Ocelot/Headers/IAddHeadersToResponse.cs @@ -1,10 +1,10 @@ using Ocelot.Middleware; +using System.Collections.Generic; + +using Ocelot.Configuration.Creator; namespace Ocelot.Headers { - using Ocelot.Configuration.Creator; - using System.Collections.Generic; - public interface IAddHeadersToResponse { void Add(List addHeaders, DownstreamResponse response); diff --git a/src/Ocelot/Headers/IHttpContextRequestHeaderReplacer.cs b/src/Ocelot/Headers/IHttpContextRequestHeaderReplacer.cs index ba5ba3483..4e23045f2 100644 --- a/src/Ocelot/Headers/IHttpContextRequestHeaderReplacer.cs +++ b/src/Ocelot/Headers/IHttpContextRequestHeaderReplacer.cs @@ -1,7 +1,9 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration; -using Ocelot.Responses; using System.Collections.Generic; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Configuration; +using Ocelot.Responses; namespace Ocelot.Headers { diff --git a/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs b/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs index 5ef5c84c9..985a8d90d 100644 --- a/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs +++ b/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs @@ -1,12 +1,15 @@ -namespace Ocelot.Headers -{ - using Ocelot.Configuration; - using Ocelot.Middleware; - using Ocelot.Responses; - using System.Collections.Generic; - - public interface IHttpResponseHeaderReplacer - { - Response Replace(DownstreamContext context, List fAndRs); - } +using System.Collections.Generic; + +using Ocelot.Configuration; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Responses; + +namespace Ocelot.Headers +{ + public interface IHttpResponseHeaderReplacer + { + public Response Replace(HttpContext httpContext, List fAndRs); + } } diff --git a/src/Ocelot/Headers/IRemoveOutputHeaders.cs b/src/Ocelot/Headers/IRemoveOutputHeaders.cs index bf2f7ff4c..c53b9f42d 100644 --- a/src/Ocelot/Headers/IRemoveOutputHeaders.cs +++ b/src/Ocelot/Headers/IRemoveOutputHeaders.cs @@ -1,6 +1,7 @@ -using Ocelot.Middleware; -using Ocelot.Responses; -using System.Collections.Generic; +using System.Collections.Generic; + +using Ocelot.Middleware; +using Ocelot.Responses; namespace Ocelot.Headers { diff --git a/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddleware.cs b/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddleware.cs index e45bb4bb3..aa79b8435 100644 --- a/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddleware.cs +++ b/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddleware.cs @@ -1,44 +1,52 @@ -using Ocelot.Logging; -using Ocelot.Middleware; -using System.Linq; +using System.Linq; using System.Threading.Tasks; -namespace Ocelot.Headers.Middleware -{ - public class ClaimsToHeadersMiddleware : OcelotMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly IAddHeadersToRequest _addHeadersToRequest; - - public ClaimsToHeadersMiddleware(OcelotRequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IAddHeadersToRequest addHeadersToRequest) - : base(loggerFactory.CreateLogger()) - { - _next = next; - _addHeadersToRequest = addHeadersToRequest; - } - - public async Task Invoke(DownstreamContext context) - { - if (context.DownstreamReRoute.ClaimsToHeaders.Any()) - { - Logger.LogInformation($"{context.DownstreamReRoute.DownstreamPathTemplate.Value} has instructions to convert claims to headers"); - - var response = _addHeadersToRequest.SetHeadersOnDownstreamRequest(context.DownstreamReRoute.ClaimsToHeaders, context.HttpContext.User.Claims, context.DownstreamRequest); +using Ocelot.Logging; - if (response.IsError) - { - Logger.LogWarning("Error setting headers on context, setting pipeline error"); +using Microsoft.AspNetCore.Http; - SetPipelineError(context, response.Errors); - return; - } - - Logger.LogInformation("headers have been set on context"); - } - - await _next.Invoke(context); - } - } -} +using Ocelot.Middleware; + +namespace Ocelot.Headers.Middleware +{ + public class ClaimsToHeadersMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IAddHeadersToRequest _addHeadersToRequest; + + public ClaimsToHeadersMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IAddHeadersToRequest addHeadersToRequest) + : base(loggerFactory.CreateLogger()) + { + _next = next; + _addHeadersToRequest = addHeadersToRequest; + } + + public async Task Invoke(HttpContext httpContext) + { + var downstreamRoute = httpContext.Items.DownstreamRoute(); + + if (downstreamRoute.ClaimsToHeaders.Any()) + { + Logger.LogInformation($"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to headers"); + + var downstreamRequest = httpContext.Items.DownstreamRequest(); + + var response = _addHeadersToRequest.SetHeadersOnDownstreamRequest(downstreamRoute.ClaimsToHeaders, httpContext.User.Claims, downstreamRequest); + + if (response.IsError) + { + Logger.LogWarning("Error setting headers on context, setting pipeline error"); + + httpContext.Items.UpsertErrors(response.Errors); + return; + } + + Logger.LogInformation("headers have been set on context"); + } + + await _next.Invoke(httpContext); + } + } +} diff --git a/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddlewareExtensions.cs b/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddlewareExtensions.cs index 91a5fe902..4923c27f4 100644 --- a/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddlewareExtensions.cs +++ b/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddlewareExtensions.cs @@ -1,11 +1,10 @@ using Microsoft.AspNetCore.Builder; -using Ocelot.Middleware.Pipeline; namespace Ocelot.Headers.Middleware { public static class ClaimsToHeadersMiddlewareExtensions { - public static IOcelotPipelineBuilder UseClaimsToHeadersMiddleware(this IOcelotPipelineBuilder builder) + public static IApplicationBuilder UseClaimsToHeadersMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware(); } diff --git a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs index 796843853..c6dc243e5 100644 --- a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs +++ b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs @@ -1,23 +1,28 @@ +using System.Threading.Tasks; + using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; + using Ocelot.Middleware; -using System.Threading.Tasks; namespace Ocelot.Headers.Middleware { public class HttpHeadersTransformationMiddleware : OcelotMiddleware { - private readonly OcelotRequestDelegate _next; + private readonly RequestDelegate _next; private readonly IHttpContextRequestHeaderReplacer _preReplacer; private readonly IHttpResponseHeaderReplacer _postReplacer; private readonly IAddHeadersToResponse _addHeadersToResponse; private readonly IAddHeadersToRequest _addHeadersToRequest; - public HttpHeadersTransformationMiddleware(OcelotRequestDelegate next, + public HttpHeadersTransformationMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IHttpContextRequestHeaderReplacer preReplacer, IHttpResponseHeaderReplacer postReplacer, IAddHeadersToResponse addHeadersToResponse, - IAddHeadersToRequest addHeadersToRequest) + IAddHeadersToRequest addHeadersToRequest + ) : base(loggerFactory.CreateLogger()) { _addHeadersToResponse = addHeadersToResponse; @@ -27,27 +32,33 @@ public HttpHeadersTransformationMiddleware(OcelotRequestDelegate next, _preReplacer = preReplacer; } - public async Task Invoke(DownstreamContext context) + public async Task Invoke(HttpContext httpContext) { - var preFAndRs = context.DownstreamReRoute.UpstreamHeadersFindAndReplace; + var downstreamRoute = httpContext.Items.DownstreamRoute(); + + var preFAndRs = downstreamRoute.UpstreamHeadersFindAndReplace; //todo - this should be on httprequestmessage not httpcontext? - _preReplacer.Replace(context.HttpContext, preFAndRs); + _preReplacer.Replace(httpContext, preFAndRs); - _addHeadersToRequest.SetHeadersOnDownstreamRequest(context.DownstreamReRoute.AddHeadersToUpstream, context.HttpContext); + _addHeadersToRequest.SetHeadersOnDownstreamRequest(downstreamRoute.AddHeadersToUpstream, httpContext); - await _next.Invoke(context); + await _next.Invoke(httpContext); - if (context.IsError) + // todo check errors is ok + //todo put this check on the base class? + if (httpContext.Items.Errors().Count > 0) { return; } - var postFAndRs = context.DownstreamReRoute.DownstreamHeadersFindAndReplace; + var postFAndRs = downstreamRoute.DownstreamHeadersFindAndReplace; + + _postReplacer.Replace(httpContext, postFAndRs); - _postReplacer.Replace(context, postFAndRs); + var downstreamResponse = httpContext.Items.DownstreamResponse(); - _addHeadersToResponse.Add(context.DownstreamReRoute.AddHeadersToDownstream, context.DownstreamResponse); + _addHeadersToResponse.Add(downstreamRoute.AddHeadersToDownstream, downstreamResponse); } } } diff --git a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddlewareExtensions.cs b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddlewareExtensions.cs index 4cfec462d..badfbca42 100644 --- a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddlewareExtensions.cs +++ b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddlewareExtensions.cs @@ -1,13 +1,12 @@ -using Microsoft.AspNetCore.Builder; -using Ocelot.Middleware.Pipeline; - -namespace Ocelot.Headers.Middleware -{ - public static class HttpHeadersTransformationMiddlewareExtensions - { - public static IOcelotPipelineBuilder UseHttpHeadersTransformationMiddleware(this IOcelotPipelineBuilder builder) - { - return builder.UseMiddleware(); - } - } +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Headers.Middleware +{ + public static class HttpHeadersTransformationMiddlewareExtensions + { + public static IApplicationBuilder UseHttpHeadersTransformationMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } } diff --git a/src/Ocelot/Headers/RemoveOutputHeaders.cs b/src/Ocelot/Headers/RemoveOutputHeaders.cs index 82cd0051b..1a6f4d989 100644 --- a/src/Ocelot/Headers/RemoveOutputHeaders.cs +++ b/src/Ocelot/Headers/RemoveOutputHeaders.cs @@ -1,7 +1,8 @@ -using Ocelot.Middleware; -using Ocelot.Responses; using System.Collections.Generic; using System.Linq; + +using Ocelot.Middleware; +using Ocelot.Responses; namespace Ocelot.Headers { @@ -10,11 +11,11 @@ public class RemoveOutputHeaders : IRemoveOutputHeaders /// /// Some webservers return headers that cannot be forwarded to the client /// in a given context such as transfer encoding chunked when ASP.NET is not - /// returning the response in this manner + /// returning the response in this manner. /// private readonly string[] _unsupportedRequestHeaders = { - "Transfer-Encoding" + "Transfer-Encoding", }; public Response Remove(List
headers) diff --git a/src/Ocelot/Infrastructure/CannotAddPlaceholderError.cs b/src/Ocelot/Infrastructure/CannotAddPlaceholderError.cs index 5ff14542f..28bdbf7a7 100644 --- a/src/Ocelot/Infrastructure/CannotAddPlaceholderError.cs +++ b/src/Ocelot/Infrastructure/CannotAddPlaceholderError.cs @@ -1,12 +1,12 @@ -using Ocelot.Errors; - -namespace Ocelot.Infrastructure -{ - public class CannotAddPlaceholderError : Error - { - public CannotAddPlaceholderError(string message) - : base(message, OcelotErrorCode.CannotAddPlaceholderError) - { - } - } +using Ocelot.Errors; + +namespace Ocelot.Infrastructure +{ + public class CannotAddPlaceholderError : Error + { + public CannotAddPlaceholderError(string message) + : base(message, OcelotErrorCode.CannotAddPlaceholderError, 404) + { + } + } } diff --git a/src/Ocelot/Infrastructure/CannotRemovePlaceholderError.cs b/src/Ocelot/Infrastructure/CannotRemovePlaceholderError.cs index 335a8da12..ddaa5c09d 100644 --- a/src/Ocelot/Infrastructure/CannotRemovePlaceholderError.cs +++ b/src/Ocelot/Infrastructure/CannotRemovePlaceholderError.cs @@ -5,8 +5,8 @@ namespace Ocelot.Infrastructure public class CannotRemovePlaceholderError : Error { public CannotRemovePlaceholderError(string message) - : base(message, OcelotErrorCode.CannotRemovePlaceholderError) + : base(message, OcelotErrorCode.CannotRemovePlaceholderError, 404) { } } -} \ No newline at end of file +} diff --git a/src/Ocelot/Infrastructure/Claims/Parser/CannotFindClaimError.cs b/src/Ocelot/Infrastructure/Claims/Parser/CannotFindClaimError.cs index 302bba06b..53e2a2e18 100644 --- a/src/Ocelot/Infrastructure/Claims/Parser/CannotFindClaimError.cs +++ b/src/Ocelot/Infrastructure/Claims/Parser/CannotFindClaimError.cs @@ -1,12 +1,12 @@ -namespace Ocelot.Infrastructure.Claims.Parser -{ - using Errors; - - public class CannotFindClaimError : Error - { +using Ocelot.Errors; + +namespace Ocelot.Infrastructure.Claims.Parser +{ + public class CannotFindClaimError : Error + { public CannotFindClaimError(string message) - : base(message, OcelotErrorCode.CannotFindClaimError) - { - } - } + : base(message, OcelotErrorCode.CannotFindClaimError, 403) + { + } + } } diff --git a/src/Ocelot/Infrastructure/Claims/Parser/ClaimsParser.cs b/src/Ocelot/Infrastructure/Claims/Parser/ClaimsParser.cs index 157ebbd30..d935ad9f7 100644 --- a/src/Ocelot/Infrastructure/Claims/Parser/ClaimsParser.cs +++ b/src/Ocelot/Infrastructure/Claims/Parser/ClaimsParser.cs @@ -1,11 +1,13 @@ -namespace Ocelot.Infrastructure.Claims.Parser +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; + +using Microsoft.Extensions.Primitives; + +using Ocelot.Responses; + +namespace Ocelot.Infrastructure.Claims.Parser { - using Microsoft.Extensions.Primitives; - using Responses; - using System.Collections.Generic; - using System.Linq; - using System.Security.Claims; - public class ClaimsParser : IClaimsParser { public Response GetValue(IEnumerable claims, string key, string delimiter, int index) @@ -36,14 +38,12 @@ public Response GetValue(IEnumerable claims, string key, string d public Response> GetValuesByClaimType(IEnumerable claims, string claimType) { - List values = new List(); - - values.AddRange(claims.Where(x => x.Type == claimType).Select(x => x.Value).ToList()); + var values = claims.Where(x => x.Type == claimType).Select(x => x.Value).ToList(); return new OkResponse>(values); } - private Response GetValue(IEnumerable claims, string key) + private static Response GetValue(IEnumerable claims, string key) { var claimValues = claims.Where(c => c.Type == key).Select(c => c.Value).ToArray(); diff --git a/src/Ocelot/Infrastructure/Claims/Parser/IClaimsParser.cs b/src/Ocelot/Infrastructure/Claims/Parser/IClaimsParser.cs index 05ec4f5d2..545878818 100644 --- a/src/Ocelot/Infrastructure/Claims/Parser/IClaimsParser.cs +++ b/src/Ocelot/Infrastructure/Claims/Parser/IClaimsParser.cs @@ -1,9 +1,10 @@ -namespace Ocelot.Infrastructure.Claims.Parser -{ - using Responses; - using System.Collections.Generic; - using System.Security.Claims; +using System.Collections.Generic; +using System.Security.Claims; + +using Ocelot.Responses; +namespace Ocelot.Infrastructure.Claims.Parser +{ public interface IClaimsParser { Response GetValue(IEnumerable claims, string key, string delimiter, int index); diff --git a/src/Ocelot/Infrastructure/ConfigAwarePlaceholders.cs b/src/Ocelot/Infrastructure/ConfigAwarePlaceholders.cs index 254dd4a57..17ef058d3 100644 --- a/src/Ocelot/Infrastructure/ConfigAwarePlaceholders.cs +++ b/src/Ocelot/Infrastructure/ConfigAwarePlaceholders.cs @@ -1,11 +1,14 @@ +using System; +using System.Text.RegularExpressions; + +using Microsoft.Extensions.Configuration; + +using Ocelot.Request.Middleware; + +using Ocelot.Responses; + namespace Ocelot.Infrastructure { - using System; - using System.Text.RegularExpressions; - using Microsoft.Extensions.Configuration; - using Request.Middleware; - using Responses; - public class ConfigAwarePlaceholders : IPlaceholders { private readonly IConfiguration _configuration; @@ -16,7 +19,7 @@ public ConfigAwarePlaceholders(IConfiguration configuration, IPlaceholders place _configuration = configuration; _placeholders = placeholders; } - + public Response Get(string key) { var placeholderResponse = _placeholders.Get(key); @@ -32,7 +35,7 @@ public Response Get(string key) public Response Get(string key, DownstreamRequest request) { var placeholderResponse = _placeholders.Get(key, request); - + if (!placeholderResponse.IsError) { return placeholderResponse; @@ -47,14 +50,14 @@ public Response Add(string key, Func> func) public Response Remove(string key) => _placeholders.Remove(key); - private string CleanKey(string key) + private static string CleanKey(string key) => Regex.Replace(key, @"[{}]", string.Empty, RegexOptions.None); private Response GetFromConfig(string key) { var valueFromConfig = _configuration[key]; return valueFromConfig == null - ? (Response) new ErrorResponse(new CouldNotFindPlaceholderError(key)) + ? new ErrorResponse(new CouldNotFindPlaceholderError(key)) : new OkResponse(valueFromConfig); } } diff --git a/src/Ocelot/Infrastructure/CouldNotFindPlaceholderError.cs b/src/Ocelot/Infrastructure/CouldNotFindPlaceholderError.cs index 5c7121aa0..20bb88333 100644 --- a/src/Ocelot/Infrastructure/CouldNotFindPlaceholderError.cs +++ b/src/Ocelot/Infrastructure/CouldNotFindPlaceholderError.cs @@ -5,7 +5,7 @@ namespace Ocelot.Infrastructure public class CouldNotFindPlaceholderError : Error { public CouldNotFindPlaceholderError(string placeholder) - : base($"Unable to find placeholder called {placeholder}", OcelotErrorCode.CouldNotFindPlaceholderError) + : base($"Unable to find placeholder called {placeholder}", OcelotErrorCode.CouldNotFindPlaceholderError, 404) { } } diff --git a/src/Ocelot/Infrastructure/Extensions/ErrorListExtensions.cs b/src/Ocelot/Infrastructure/Extensions/ErrorListExtensions.cs index 5d9f00f1e..73776b2cd 100644 --- a/src/Ocelot/Infrastructure/Extensions/ErrorListExtensions.cs +++ b/src/Ocelot/Infrastructure/Extensions/ErrorListExtensions.cs @@ -1,6 +1,7 @@ -using Ocelot.Errors; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; + +using Ocelot.Errors; namespace Ocelot.Infrastructure.Extensions { @@ -8,8 +9,8 @@ public static class ErrorListExtensions { public static string ToErrorString(this List errors) { - var listOfErrorStrings = errors.Select(x => "Error Code: " + x.Code.ToString() + " Message: " + x.Message); - return string.Join(" ", listOfErrorStrings); + var listOfErrorStrings = errors.Select(x => "Error Code: " + x.Code + " Message: " + x.Message); + return string.Join(' ', listOfErrorStrings); } } } diff --git a/src/Ocelot/Infrastructure/Extensions/NetCoreSupportExtensions.cs b/src/Ocelot/Infrastructure/Extensions/NetCoreSupportExtensions.cs index 3dee5caf2..03ecfb57c 100644 --- a/src/Ocelot/Infrastructure/Extensions/NetCoreSupportExtensions.cs +++ b/src/Ocelot/Infrastructure/Extensions/NetCoreSupportExtensions.cs @@ -11,22 +11,13 @@ internal static class NetCoreSupportExtensions { internal static void AppendJoin(this StringBuilder builder, char separator, IEnumerable values) { - builder.Append(string.Join(separator.ToString(), values)); + builder.Append(string.Join(separator, values)); } - internal static string[] Split(this string input, string separator, StringSplitOptions options = StringSplitOptions.None) - { - return input.Split(new[] { separator }, options); - } + internal static string[] Split(this string input, string separator, StringSplitOptions options = StringSplitOptions.None) => input.Split(separator, options); - internal static bool StartsWith(this string input, char value) - { - return input.StartsWith(value.ToString()); - } + internal static bool StartsWith(this string input, char value) => input.StartsWith(value); - internal static bool EndsWith(this string input, char value) - { - return input.EndsWith(value.ToString()); - } + internal static bool EndsWith(this string input, char value) => input.EndsWith(value); } } diff --git a/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs b/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs index 099d32520..e3cdbafd3 100644 --- a/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs +++ b/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs @@ -11,7 +11,7 @@ public static string TrimStart(this string source, string trim, StringComparison return null; } - string s = source; + var s = source; while (s.StartsWith(trim, stringComparison)) { s = s.Substring(trim.Length); diff --git a/src/Ocelot/Infrastructure/Extensions/StringValuesExtensions.cs b/src/Ocelot/Infrastructure/Extensions/StringValuesExtensions.cs index df9d24da1..4cd72cc8a 100644 --- a/src/Ocelot/Infrastructure/Extensions/StringValuesExtensions.cs +++ b/src/Ocelot/Infrastructure/Extensions/StringValuesExtensions.cs @@ -1,6 +1,7 @@ -using Microsoft.Extensions.Primitives; using System.Linq; +using Microsoft.Extensions.Primitives; + namespace Ocelot.Infrastructure.Extensions { public static class StringValuesExtensions diff --git a/src/Ocelot/Infrastructure/IPlaceholders.cs b/src/Ocelot/Infrastructure/IPlaceholders.cs index 9d4b39ec8..9823b5706 100644 --- a/src/Ocelot/Infrastructure/IPlaceholders.cs +++ b/src/Ocelot/Infrastructure/IPlaceholders.cs @@ -1,6 +1,7 @@ +using System; + using Ocelot.Request.Middleware; using Ocelot.Responses; -using System; namespace Ocelot.Infrastructure { diff --git a/src/Ocelot/Infrastructure/InMemoryBus.cs b/src/Ocelot/Infrastructure/InMemoryBus.cs index 96ed92854..855145563 100644 --- a/src/Ocelot/Infrastructure/InMemoryBus.cs +++ b/src/Ocelot/Infrastructure/InMemoryBus.cs @@ -10,7 +10,7 @@ public class InMemoryBus : IBus { private readonly BlockingCollection> _queue; private readonly List> _subscriptions; - private Thread _processing; + private readonly Thread _processing; public InMemoryBus() { diff --git a/src/Ocelot/Infrastructure/Placeholders.cs b/src/Ocelot/Infrastructure/Placeholders.cs index 3c319e6df..96f1ae235 100644 --- a/src/Ocelot/Infrastructure/Placeholders.cs +++ b/src/Ocelot/Infrastructure/Placeholders.cs @@ -1,13 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Middleware; + +using Ocelot.Request.Middleware; + +using Ocelot.Infrastructure.RequestData; + +using Ocelot.Responses; + namespace Ocelot.Infrastructure { - using Microsoft.AspNetCore.Http; - using Ocelot.Infrastructure.RequestData; - using Ocelot.Middleware; - using Ocelot.Request.Middleware; - using Ocelot.Responses; - using System; - using System.Collections.Generic; - public class Placeholders : IPlaceholders { private readonly Dictionary>> _placeholders; @@ -25,12 +31,13 @@ public Placeholders(IBaseUrlFinder finder, IRequestScopedDataRepository repo, IH { { "{BaseUrl}", GetBaseUrl() }, { "{TraceId}", GetTraceId() }, - { "{RemoteIpAddress}", GetRemoteIpAddress() } + { "{RemoteIpAddress}", GetRemoteIpAddress() }, + { "{UpstreamHost}", GetUpstreamHost() }, }; _requestPlaceholders = new Dictionary> { - { "{DownstreamBaseUrl}", GetDownstreamBaseUrl() } + { "{DownstreamBaseUrl}", GetDownstreamBaseUrl() }, }; } @@ -97,7 +104,7 @@ private Func> GetRemoteIpAddress() }; } - private Func GetDownstreamBaseUrl() + private static Func GetDownstreamBaseUrl() { return x => { @@ -130,5 +137,25 @@ private Func> GetBaseUrl() { return () => new OkResponse(_finder.Find()); } + + private Func> GetUpstreamHost() + { + return () => + { + try + { + if (_httpContextAccessor.HttpContext.Request.Headers.TryGetValue("Host", out var upstreamHost)) + { + return new OkResponse(upstreamHost.First()); + } + + return new ErrorResponse(new CouldNotFindPlaceholderError("{UpstreamHost}")); + } + catch + { + return new ErrorResponse(new CouldNotFindPlaceholderError("{UpstreamHost}")); + } + }; + } } } diff --git a/src/Ocelot/Infrastructure/RequestData/CannotAddDataError.cs b/src/Ocelot/Infrastructure/RequestData/CannotAddDataError.cs index e411f0d49..d186435a9 100644 --- a/src/Ocelot/Infrastructure/RequestData/CannotAddDataError.cs +++ b/src/Ocelot/Infrastructure/RequestData/CannotAddDataError.cs @@ -1,11 +1,11 @@ -using Ocelot.Errors; - -namespace Ocelot.Infrastructure.RequestData -{ - public class CannotAddDataError : Error - { - public CannotAddDataError(string message) : base(message, OcelotErrorCode.CannotAddDataError) - { - } - } +using Ocelot.Errors; + +namespace Ocelot.Infrastructure.RequestData +{ + public class CannotAddDataError : Error + { + public CannotAddDataError(string message) : base(message, OcelotErrorCode.CannotAddDataError, 404) + { + } + } } diff --git a/src/Ocelot/Infrastructure/RequestData/CannotFindDataError.cs b/src/Ocelot/Infrastructure/RequestData/CannotFindDataError.cs index 907742df8..27bdc1f9a 100644 --- a/src/Ocelot/Infrastructure/RequestData/CannotFindDataError.cs +++ b/src/Ocelot/Infrastructure/RequestData/CannotFindDataError.cs @@ -1,11 +1,11 @@ -using Ocelot.Errors; - -namespace Ocelot.Infrastructure.RequestData -{ - public class CannotFindDataError : Error - { - public CannotFindDataError(string message) : base(message, OcelotErrorCode.CannotFindDataError) - { - } - } +using Ocelot.Errors; + +namespace Ocelot.Infrastructure.RequestData +{ + public class CannotFindDataError : Error + { + public CannotFindDataError(string message) : base(message, OcelotErrorCode.CannotFindDataError, 404) + { + } + } } diff --git a/src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs b/src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs index c14a1b786..b9c07e9d0 100644 --- a/src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs +++ b/src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs @@ -1,6 +1,8 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Responses; -using System; +using System; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Responses; namespace Ocelot.Infrastructure.RequestData { @@ -41,14 +43,12 @@ public Response Update(string key, T value) public Response Get(string key) { - object obj; - if (_httpContextAccessor.HttpContext == null || _httpContextAccessor.HttpContext.Items == null) { return new ErrorResponse(new CannotFindDataError($"Unable to find data for key: {key} because HttpContext or HttpContext.Items is null")); } - if (_httpContextAccessor.HttpContext.Items.TryGetValue(key, out obj)) + if (_httpContextAccessor.HttpContext.Items.TryGetValue(key, out var obj)) { var data = (T)obj; return new OkResponse(data); diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessions.cs b/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessions.cs index 775ed7ceb..f2050c6a6 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessions.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessions.cs @@ -1,13 +1,17 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; + +using Ocelot.Infrastructure; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Responses; + +using Ocelot.Values; + namespace Ocelot.LoadBalancer.LoadBalancers { - using Ocelot.Infrastructure; - using Ocelot.Middleware; - using Responses; - using System; - using System.Collections.Concurrent; - using System.Threading.Tasks; - using Values; - public class CookieStickySessions : ILoadBalancer { private readonly int _keyExpiryInMs; @@ -15,7 +19,7 @@ public class CookieStickySessions : ILoadBalancer private readonly ILoadBalancer _loadBalancer; private readonly ConcurrentDictionary _stored; private readonly IBus _bus; - private readonly object _lock = new object(); + private readonly object _lock = new(); public CookieStickySessions(ILoadBalancer loadBalancer, string key, int keyExpiryInMs, IBus bus) { @@ -41,9 +45,9 @@ public CookieStickySessions(ILoadBalancer loadBalancer, string key, int keyExpir }); } - public async Task> Lease(DownstreamContext context) + public async Task> Lease(HttpContext httpContext) { - var key = context.HttpContext.Request.Cookies[_key]; + var key = httpContext.Request.Cookies[_key]; lock (_lock) { @@ -61,7 +65,7 @@ public async Task> Lease(DownstreamContext context) } } - var next = await _loadBalancer.Lease(context); + var next = await _loadBalancer.Lease(httpContext); if (next.IsError) { diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessionsCreator.cs b/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessionsCreator.cs new file mode 100644 index 000000000..1f2c3a0b1 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/CookieStickySessionsCreator.cs @@ -0,0 +1,23 @@ +using Ocelot.Configuration; + +using Ocelot.Infrastructure; + +using Ocelot.Responses; + +using Ocelot.ServiceDiscovery.Providers; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class CookieStickySessionsCreator : ILoadBalancerCreator + { + public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) + { + var loadBalancer = new RoundRobin(async () => await serviceProvider.Get()); + var bus = new InMemoryBus(); + return new OkResponse(new CookieStickySessions(loadBalancer, route.LoadBalancerOptions.Key, + route.LoadBalancerOptions.ExpiryInMs, bus)); + } + + public string Type => nameof(CookieStickySessions); + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/CouldNotFindLoadBalancerCreator.cs b/src/Ocelot/LoadBalancer/LoadBalancers/CouldNotFindLoadBalancerCreator.cs new file mode 100644 index 000000000..0952f4323 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/CouldNotFindLoadBalancerCreator.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class CouldNotFindLoadBalancerCreator : Error + { + public CouldNotFindLoadBalancerCreator(string message) + : base(message, OcelotErrorCode.CouldNotFindLoadBalancerCreator, 404) + { + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/DelegateInvokingLoadBalancerCreator.cs b/src/Ocelot/LoadBalancer/LoadBalancers/DelegateInvokingLoadBalancerCreator.cs new file mode 100644 index 000000000..6298a10d6 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/DelegateInvokingLoadBalancerCreator.cs @@ -0,0 +1,36 @@ +using System; + +using Ocelot.Configuration; + +using Ocelot.Responses; + +using Ocelot.ServiceDiscovery.Providers; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class DelegateInvokingLoadBalancerCreator : ILoadBalancerCreator + where T : ILoadBalancer + { + private readonly Func _creatorFunc; + + public DelegateInvokingLoadBalancerCreator( + Func creatorFunc) + { + _creatorFunc = creatorFunc; + } + + public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) + { + try + { + return new OkResponse(_creatorFunc(route, serviceProvider)); + } + catch (Exception e) + { + return new ErrorResponse(new ErrorInvokingLoadBalancerCreator(e)); + } + } + + public string Type => typeof(T).Name; + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ErrorInvokingLoadBalancerCreator.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ErrorInvokingLoadBalancerCreator.cs new file mode 100644 index 000000000..8921bf2ed --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ErrorInvokingLoadBalancerCreator.cs @@ -0,0 +1,13 @@ +using System; + +using Ocelot.Errors; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class ErrorInvokingLoadBalancerCreator : Error + { + public ErrorInvokingLoadBalancerCreator(Exception e) : base($"Error when invoking user provided load balancer creator function, Message: {e.Message}, StackTrace: {e.StackTrace}", OcelotErrorCode.ErrorInvokingLoadBalancerCreator, 500) + { + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs index 4f8ec2b74..2f19e1a5f 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs @@ -1,13 +1,16 @@ -using Ocelot.Middleware; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + using Ocelot.Responses; + using Ocelot.Values; -using System.Threading.Tasks; namespace Ocelot.LoadBalancer.LoadBalancers { public interface ILoadBalancer { - Task> Lease(DownstreamContext context); + Task> Lease(HttpContext httpContext); void Release(ServiceHostAndPort hostAndPort); } diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerCreator.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerCreator.cs new file mode 100644 index 000000000..982292253 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerCreator.cs @@ -0,0 +1,14 @@ +using Ocelot.Configuration; + +using Ocelot.Responses; + +using Ocelot.ServiceDiscovery.Providers; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public interface ILoadBalancerCreator + { + Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider); + string Type { get; } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs index df115c410..13799c3ab 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs @@ -1,11 +1,11 @@ -namespace Ocelot.LoadBalancer.LoadBalancers -{ - using Ocelot.Configuration; - using Ocelot.Responses; - using System.Threading.Tasks; +using Ocelot.Configuration; - public interface ILoadBalancerFactory - { - Task> Get(DownstreamReRoute reRoute, ServiceProviderConfiguration config); - } -} +using Ocelot.Responses; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public interface ILoadBalancerFactory + { + Response Get(DownstreamRoute route, ServiceProviderConfiguration config); + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs index 92eb0e9ed..b05a6aa49 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs @@ -1,11 +1,10 @@ -using Ocelot.Configuration; -using Ocelot.Responses; -using System.Threading.Tasks; - -namespace Ocelot.LoadBalancer.LoadBalancers -{ - public interface ILoadBalancerHouse - { - Task> Get(DownstreamReRoute reRoute, ServiceProviderConfiguration config); - } -} +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public interface ILoadBalancerHouse + { + Response Get(DownstreamRoute route, ServiceProviderConfiguration config); + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/Lease.cs b/src/Ocelot/LoadBalancer/LoadBalancers/Lease.cs index 94bfd97e6..632837b3e 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/Lease.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/Lease.cs @@ -10,7 +10,7 @@ public Lease(ServiceHostAndPort hostAndPort, int connections) Connections = connections; } - public ServiceHostAndPort HostAndPort { get; private set; } - public int Connections { get; private set; } + public ServiceHostAndPort HostAndPort { get; } + public int Connections { get; } } } diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnection.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnection.cs index ee7c0fdd3..43eac6f23 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnection.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnection.cs @@ -1,11 +1,14 @@ -using Ocelot.Middleware; -using Ocelot.Responses; -using Ocelot.Values; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +using Ocelot.Responses; + +using Ocelot.Values; + namespace Ocelot.LoadBalancer.LoadBalancers { public class LeastConnection : ILoadBalancer @@ -13,7 +16,7 @@ public class LeastConnection : ILoadBalancer private readonly Func>> _services; private readonly List _leases; private readonly string _serviceName; - private static readonly object _syncLock = new object(); + private static readonly object SyncLock = new(); public LeastConnection(Func>> services, string serviceName) { @@ -22,7 +25,7 @@ public LeastConnection(Func>> services, string serviceName) _leases = new List(); } - public async Task> Lease(DownstreamContext downstreamContext) + public async Task> Lease(HttpContext httpContext) { var services = await _services.Invoke(); @@ -36,7 +39,7 @@ public async Task> Lease(DownstreamContext downstre return new ErrorResponse(new ServicesAreEmptyError($"services were empty for {_serviceName}")); } - lock (_syncLock) + lock (SyncLock) { //todo - maybe this should be moved somewhere else...? Maybe on a repeater on seperate thread? loop every second and update or something? UpdateServices(services); @@ -55,7 +58,7 @@ public async Task> Lease(DownstreamContext downstre public void Release(ServiceHostAndPort hostAndPort) { - lock (_syncLock) + lock (SyncLock) { var matchingLease = _leases.FirstOrDefault(l => l.HostAndPort.DownstreamHost == hostAndPort.DownstreamHost && l.HostAndPort.DownstreamPort == hostAndPort.DownstreamPort); @@ -71,7 +74,7 @@ public void Release(ServiceHostAndPort hostAndPort) } } - private Lease AddConnection(Lease lease) + private static Lease AddConnection(Lease lease) { return new Lease(lease.HostAndPort, lease.Connections + 1); } diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnectionCreator.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnectionCreator.cs new file mode 100644 index 000000000..a0156e578 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnectionCreator.cs @@ -0,0 +1,18 @@ +using Ocelot.Configuration; + +using Ocelot.Responses; + +using Ocelot.ServiceDiscovery.Providers; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class LeastConnectionCreator : ILoadBalancerCreator + { + public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) + { + return new OkResponse(new LeastConnection(async () => await serviceProvider.Get(), route.ServiceName)); + } + + public string Type => nameof(LeastConnection); + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs index 12725a57f..3a1334eab 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs @@ -1,47 +1,51 @@ -using Ocelot.Configuration; -using Ocelot.Infrastructure; -using Ocelot.Responses; -using Ocelot.ServiceDiscovery; -using System.Threading.Tasks; - -namespace Ocelot.LoadBalancer.LoadBalancers -{ - public class LoadBalancerFactory : ILoadBalancerFactory - { - private readonly IServiceDiscoveryProviderFactory _serviceProviderFactory; - - public LoadBalancerFactory(IServiceDiscoveryProviderFactory serviceProviderFactory) - { - _serviceProviderFactory = serviceProviderFactory; - } - - public async Task> Get(DownstreamReRoute reRoute, ServiceProviderConfiguration config) - { - var response = _serviceProviderFactory.Get(config, reRoute); +using System.Collections.Generic; +using System.Linq; - if (response.IsError) - { - return new ErrorResponse(response.Errors); - } +using Ocelot.Configuration; - var serviceProvider = response.Data; - - switch (reRoute.LoadBalancerOptions?.Type) - { - case nameof(RoundRobin): - return new OkResponse(new RoundRobin(async () => await serviceProvider.Get())); - - case nameof(LeastConnection): - return new OkResponse(new LeastConnection(async () => await serviceProvider.Get(), reRoute.ServiceName)); - - case nameof(CookieStickySessions): - var loadBalancer = new RoundRobin(async () => await serviceProvider.Get()); - var bus = new InMemoryBus(); - return new OkResponse(new CookieStickySessions(loadBalancer, reRoute.LoadBalancerOptions.Key, reRoute.LoadBalancerOptions.ExpiryInMs, bus)); +using Ocelot.Responses; - default: - return new OkResponse(new NoLoadBalancer(async () => await serviceProvider.Get())); - } - } - } -} +using Ocelot.ServiceDiscovery; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class LoadBalancerFactory : ILoadBalancerFactory + { + private readonly IServiceDiscoveryProviderFactory _serviceProviderFactory; + private readonly IEnumerable _loadBalancerCreators; + + public LoadBalancerFactory(IServiceDiscoveryProviderFactory serviceProviderFactory, IEnumerable loadBalancerCreators) + { + _serviceProviderFactory = serviceProviderFactory; + _loadBalancerCreators = loadBalancerCreators; + } + + public Response Get(DownstreamRoute route, ServiceProviderConfiguration config) + { + var serviceProviderFactoryResponse = _serviceProviderFactory.Get(config, route); + + if (serviceProviderFactoryResponse.IsError) + { + return new ErrorResponse(serviceProviderFactoryResponse.Errors); + } + + var serviceProvider = serviceProviderFactoryResponse.Data; + var requestedType = route.LoadBalancerOptions?.Type ?? nameof(NoLoadBalancer); + var applicableCreator = _loadBalancerCreators.SingleOrDefault(c => c.Type == requestedType); + + if (applicableCreator == null) + { + return new ErrorResponse(new CouldNotFindLoadBalancerCreator($"Could not find load balancer creator for Type: {requestedType}, please check your config specified the correct load balancer and that you have registered a class with the same name.")); + } + + var createdLoadBalancerResponse = applicableCreator.Create(route, serviceProvider); + + if (createdLoadBalancerResponse.IsError) + { + return new ErrorResponse(createdLoadBalancerResponse.Errors); + } + + return new OkResponse(createdLoadBalancerResponse.Data); + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs index 8f7e81352..5b8e90797 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs @@ -1,71 +1,71 @@ -using Ocelot.Configuration; -using Ocelot.Responses; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Ocelot.LoadBalancer.LoadBalancers -{ - public class LoadBalancerHouse : ILoadBalancerHouse - { - private readonly ILoadBalancerFactory _factory; - private readonly ConcurrentDictionary _loadBalancers; - - public LoadBalancerHouse(ILoadBalancerFactory factory) - { - _factory = factory; - _loadBalancers = new ConcurrentDictionary(); - } - - public async Task> Get(DownstreamReRoute reRoute, ServiceProviderConfiguration config) - { - try - { - Response result; - - if (_loadBalancers.TryGetValue(reRoute.LoadBalancerKey, out var loadBalancer)) - { - loadBalancer = _loadBalancers[reRoute.LoadBalancerKey]; - - if (reRoute.LoadBalancerOptions.Type != loadBalancer.GetType().Name) - { - result = await _factory.Get(reRoute, config); - if (result.IsError) - { - return new ErrorResponse(result.Errors); - } +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; - loadBalancer = result.Data; - AddLoadBalancer(reRoute.LoadBalancerKey, loadBalancer); - } - - return new OkResponse(loadBalancer); - } - - result = await _factory.Get(reRoute, config); - - if (result.IsError) - { - return new ErrorResponse(result.Errors); - } - - loadBalancer = result.Data; - AddLoadBalancer(reRoute.LoadBalancerKey, loadBalancer); - return new OkResponse(loadBalancer); - } - catch (Exception ex) - { - return new ErrorResponse(new List() - { - new UnableToFindLoadBalancerError($"unabe to find load balancer for {reRoute.LoadBalancerKey} exception is {ex}") - }); - } - } - - private void AddLoadBalancer(string key, ILoadBalancer loadBalancer) - { - _loadBalancers.AddOrUpdate(key, loadBalancer, (x, y) => loadBalancer); - } - } -} +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class LoadBalancerHouse : ILoadBalancerHouse + { + private readonly ILoadBalancerFactory _factory; + private readonly ConcurrentDictionary _loadBalancers; + + public LoadBalancerHouse(ILoadBalancerFactory factory) + { + _factory = factory; + _loadBalancers = new ConcurrentDictionary(); + } + + public Response Get(DownstreamRoute route, ServiceProviderConfiguration config) + { + try + { + Response result; + + if (_loadBalancers.TryGetValue(route.LoadBalancerKey, out var loadBalancer)) + { + loadBalancer = _loadBalancers[route.LoadBalancerKey]; + + if (route.LoadBalancerOptions.Type != loadBalancer.GetType().Name) + { + result = _factory.Get(route, config); + if (result.IsError) + { + return new ErrorResponse(result.Errors); + } + + loadBalancer = result.Data; + AddLoadBalancer(route.LoadBalancerKey, loadBalancer); + } + + return new OkResponse(loadBalancer); + } + + result = _factory.Get(route, config); + + if (result.IsError) + { + return new ErrorResponse(result.Errors); + } + + loadBalancer = result.Data; + AddLoadBalancer(route.LoadBalancerKey, loadBalancer); + return new OkResponse(loadBalancer); + } + catch (Exception ex) + { + return new ErrorResponse(new List + { + new UnableToFindLoadBalancerError($"unabe to find load balancer for {route.LoadBalancerKey} exception is {ex}"), + }); + } + } + + private void AddLoadBalancer(string key, ILoadBalancer loadBalancer) + { + _loadBalancers.AddOrUpdate(key, loadBalancer, (x, y) => loadBalancer); + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs b/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs index 112fd5bb8..0ab0582d8 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs @@ -1,11 +1,14 @@ -using Ocelot.Middleware; -using Ocelot.Responses; -using Ocelot.Values; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +using Ocelot.Responses; + +using Ocelot.Values; + namespace Ocelot.LoadBalancer.LoadBalancers { public class NoLoadBalancer : ILoadBalancer @@ -17,7 +20,7 @@ public NoLoadBalancer(Func>> services) _services = services; } - public async Task> Lease(DownstreamContext downstreamContext) + public async Task> Lease(HttpContext httpContext) { var services = await _services(); diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancerCreator.cs b/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancerCreator.cs new file mode 100644 index 000000000..dcc49b69c --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancerCreator.cs @@ -0,0 +1,18 @@ +using Ocelot.Configuration; + +using Ocelot.Responses; + +using Ocelot.ServiceDiscovery.Providers; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class NoLoadBalancerCreator : ILoadBalancerCreator + { + public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) + { + return new OkResponse(new NoLoadBalancer(async () => await serviceProvider.Get())); + } + + public string Type => nameof(NoLoadBalancer); + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobin.cs b/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobin.cs index 3500efe02..93436d984 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobin.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobin.cs @@ -1,16 +1,19 @@ -using Ocelot.Middleware; -using Ocelot.Responses; -using Ocelot.Values; -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +using Ocelot.Responses; + +using Ocelot.Values; + namespace Ocelot.LoadBalancer.LoadBalancers { public class RoundRobin : ILoadBalancer { private readonly Func>> _services; - private readonly object _lock = new object(); + private readonly object _lock = new(); private int _last; @@ -19,7 +22,7 @@ public RoundRobin(Func>> services) _services = services; } - public async Task> Lease(DownstreamContext downstreamContext) + public async Task> Lease(HttpContext httpContext) { var services = await _services(); lock (_lock) diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobinCreator.cs b/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobinCreator.cs new file mode 100644 index 000000000..5c4b36a4d --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobinCreator.cs @@ -0,0 +1,18 @@ +using Ocelot.Configuration; + +using Ocelot.Responses; + +using Ocelot.ServiceDiscovery.Providers; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class RoundRobinCreator : ILoadBalancerCreator + { + public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) + { + return new OkResponse(new RoundRobin(async () => await serviceProvider.Get())); + } + + public string Type => nameof(RoundRobin); + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreEmptyError.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreEmptyError.cs index b1a1e02cf..2823b53fa 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreEmptyError.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreEmptyError.cs @@ -5,8 +5,8 @@ namespace Ocelot.LoadBalancer.LoadBalancers public class ServicesAreEmptyError : Error { public ServicesAreEmptyError(string message) - : base(message, OcelotErrorCode.ServicesAreEmptyError) + : base(message, OcelotErrorCode.ServicesAreEmptyError, 404) { } } -} \ No newline at end of file +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreNullError.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreNullError.cs index 0116bee9c..498334c91 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreNullError.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreNullError.cs @@ -5,8 +5,8 @@ namespace Ocelot.LoadBalancer.LoadBalancers public class ServicesAreNullError : Error { public ServicesAreNullError(string message) - : base(message, OcelotErrorCode.ServicesAreNullError) + : base(message, OcelotErrorCode.ServicesAreNullError, 404) { } } -} \ No newline at end of file +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/StickySession.cs b/src/Ocelot/LoadBalancer/LoadBalancers/StickySession.cs index 6f487ec78..c65bd05bf 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/StickySession.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/StickySession.cs @@ -1,6 +1,7 @@ -using Ocelot.Values; using System; +using Ocelot.Values; + namespace Ocelot.LoadBalancer.LoadBalancers { public class StickySession diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs b/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs index 04a305f5a..5c3ba89bd 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs @@ -1,12 +1,12 @@ -using Ocelot.Errors; - -namespace Ocelot.LoadBalancer.LoadBalancers -{ - public class UnableToFindLoadBalancerError : Errors.Error - { +using Ocelot.Errors; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class UnableToFindLoadBalancerError : Error + { public UnableToFindLoadBalancerError(string message) - : base(message, OcelotErrorCode.UnableToFindLoadBalancerError) - { - } - } + : base(message, OcelotErrorCode.UnableToFindLoadBalancerError, 404) + { + } + } } diff --git a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs index 646af5fef..5bd4830fa 100644 --- a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs +++ b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs @@ -1,68 +1,81 @@ -using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.Logging; -using Ocelot.Middleware; -using System; +using System; using System.Threading.Tasks; -namespace Ocelot.LoadBalancer.Middleware -{ - public class LoadBalancingMiddleware : OcelotMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly ILoadBalancerHouse _loadBalancerHouse; - - public LoadBalancingMiddleware(OcelotRequestDelegate next, - IOcelotLoggerFactory loggerFactory, - ILoadBalancerHouse loadBalancerHouse) - : base(loggerFactory.CreateLogger()) - { - _next = next; - _loadBalancerHouse = loadBalancerHouse; - } - - public async Task Invoke(DownstreamContext context) - { - var loadBalancer = await _loadBalancerHouse.Get(context.DownstreamReRoute, context.Configuration.ServiceProviderConfiguration); - if (loadBalancer.IsError) - { - Logger.LogDebug("there was an error retriving the loadbalancer, setting pipeline error"); - SetPipelineError(context, loadBalancer.Errors); - return; - } +using Ocelot.LoadBalancer.LoadBalancers; - var hostAndPort = await loadBalancer.Data.Lease(context); - if (hostAndPort.IsError) - { - Logger.LogDebug("there was an error leasing the loadbalancer, setting pipeline error"); - SetPipelineError(context, hostAndPort.Errors); - return; - } - - context.DownstreamRequest.Host = hostAndPort.Data.DownstreamHost; - - if (hostAndPort.Data.DownstreamPort > 0) - { - context.DownstreamRequest.Port = hostAndPort.Data.DownstreamPort; - } +using Ocelot.Logging; - if (!string.IsNullOrEmpty(hostAndPort.Data.Scheme)) - { - context.DownstreamRequest.Scheme = hostAndPort.Data.Scheme; - } +using Microsoft.AspNetCore.Http; - try - { - await _next.Invoke(context); - } - catch (Exception) - { - Logger.LogDebug("Exception calling next middleware, exception will be thrown to global handler"); - throw; - } - finally - { - loadBalancer.Data.Release(hostAndPort.Data); - } - } - } -} +using Ocelot.Middleware; + +namespace Ocelot.LoadBalancer.Middleware +{ + public class LoadBalancingMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly ILoadBalancerHouse _loadBalancerHouse; + + public LoadBalancingMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + ILoadBalancerHouse loadBalancerHouse) + : base(loggerFactory.CreateLogger()) + { + _next = next; + _loadBalancerHouse = loadBalancerHouse; + } + + public async Task Invoke(HttpContext httpContext) + { + var downstreamRoute = httpContext.Items.DownstreamRoute(); + + var internalConfiguration = httpContext.Items.IInternalConfiguration(); + + var loadBalancer = _loadBalancerHouse.Get(downstreamRoute, internalConfiguration.ServiceProviderConfiguration); + + if (loadBalancer.IsError) + { + Logger.LogDebug("there was an error retriving the loadbalancer, setting pipeline error"); + httpContext.Items.UpsertErrors(loadBalancer.Errors); + return; + } + + var hostAndPort = await loadBalancer.Data.Lease(httpContext); + if (hostAndPort.IsError) + { + Logger.LogDebug("there was an error leasing the loadbalancer, setting pipeline error"); + httpContext.Items.UpsertErrors(hostAndPort.Errors); + return; + } + + var downstreamRequest = httpContext.Items.DownstreamRequest(); + + //todo check downstreamRequest is ok + downstreamRequest.Host = hostAndPort.Data.DownstreamHost; + + if (hostAndPort.Data.DownstreamPort > 0) + { + downstreamRequest.Port = hostAndPort.Data.DownstreamPort; + } + + if (!string.IsNullOrEmpty(hostAndPort.Data.Scheme)) + { + downstreamRequest.Scheme = hostAndPort.Data.Scheme; + } + + try + { + await _next.Invoke(httpContext); + } + catch (Exception) + { + Logger.LogDebug("Exception calling next middleware, exception will be thrown to global handler"); + throw; + } + finally + { + loadBalancer.Data.Release(hostAndPort.Data); + } + } + } +} diff --git a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddlewareExtensions.cs b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddlewareExtensions.cs index 8f6558c74..cd8be6582 100644 --- a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddlewareExtensions.cs +++ b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddlewareExtensions.cs @@ -1,13 +1,12 @@ -using Microsoft.AspNetCore.Builder; -using Ocelot.Middleware.Pipeline; - -namespace Ocelot.LoadBalancer.Middleware +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.LoadBalancer.Middleware { - public static class LoadBalancingMiddlewareExtensions - { - public static IOcelotPipelineBuilder UseLoadBalancingMiddleware(this IOcelotPipelineBuilder builder) - { - return builder.UseMiddleware(); - } - } + public static class LoadBalancingMiddlewareExtensions + { + public static IApplicationBuilder UseLoadBalancingMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } } diff --git a/src/Ocelot/Logging/AspDotNetLogger.cs b/src/Ocelot/Logging/AspDotNetLogger.cs index 35375d455..139fcbb52 100644 --- a/src/Ocelot/Logging/AspDotNetLogger.cs +++ b/src/Ocelot/Logging/AspDotNetLogger.cs @@ -1,6 +1,8 @@ -using Microsoft.Extensions.Logging; -using Ocelot.Infrastructure.RequestData; using System; + +using Microsoft.Extensions.Logging; + +using Ocelot.Infrastructure.RequestData; namespace Ocelot.Logging { @@ -14,17 +16,7 @@ public AspDotNetLogger(ILogger logger, IRequestScopedDataRepository scopedDataRe { _logger = logger; _scopedDataRepository = scopedDataRepository; - _func = (state, exception) => - { - if (exception == null) - { - return state; - } - else - { - return $"{state}, exception: {exception}"; - } - }; + _func = (state, exception) => exception == null ? state : $"{state}, exception: {exception}"; } public void LogTrace(string message) @@ -34,7 +26,7 @@ public void LogTrace(string message) var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - _logger.Log(LogLevel.Trace, default(EventId), state, null, _func); + _logger.Log(LogLevel.Trace, default, state, null, _func); } public void LogDebug(string message) @@ -44,7 +36,7 @@ public void LogDebug(string message) var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - _logger.Log(LogLevel.Debug, default(EventId), state, null, _func); + _logger.Log(LogLevel.Debug, default, state, null, _func); } public void LogInformation(string message) @@ -54,7 +46,7 @@ public void LogInformation(string message) var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - _logger.Log(LogLevel.Information, default(EventId), state, null, _func); + _logger.Log(LogLevel.Information, default, state, null, _func); } public void LogWarning(string message) @@ -64,7 +56,7 @@ public void LogWarning(string message) var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - _logger.Log(LogLevel.Warning, default(EventId), state, null, _func); + _logger.Log(LogLevel.Warning, default, state, null, _func); } public void LogError(string message, Exception exception) @@ -74,7 +66,7 @@ public void LogError(string message, Exception exception) var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - _logger.Log(LogLevel.Error,default(EventId), state, exception, _func); + _logger.Log(LogLevel.Error, default, state, exception, _func); } public void LogCritical(string message, Exception exception) @@ -84,31 +76,21 @@ public void LogCritical(string message, Exception exception) var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - _logger.Log(LogLevel.Critical, default(EventId), state, exception, _func); + _logger.Log(LogLevel.Critical, default, state, exception, _func); } private string GetOcelotRequestId() { var requestId = _scopedDataRepository.Get("RequestId"); - if (requestId == null || requestId.IsError) - { - return "no request id"; - } - - return requestId.Data; + return requestId == null || requestId.IsError ? "no request id" : requestId.Data; } private string GetOcelotPreviousRequestId() { var requestId = _scopedDataRepository.Get("PreviousRequestId"); - if (requestId == null || requestId.IsError) - { - return "no previous request id"; - } - - return requestId.Data; + return requestId == null || requestId.IsError ? "no previous request id" : requestId.Data; } } } diff --git a/src/Ocelot/Logging/AspDotNetLoggerFactory.cs b/src/Ocelot/Logging/AspDotNetLoggerFactory.cs index b988e09c4..b104633af 100644 --- a/src/Ocelot/Logging/AspDotNetLoggerFactory.cs +++ b/src/Ocelot/Logging/AspDotNetLoggerFactory.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; + using Ocelot.Infrastructure.RequestData; namespace Ocelot.Logging @@ -20,4 +21,4 @@ public IOcelotLogger CreateLogger() return new AspDotNetLogger(logger, _scopedDataRepository); } } -} \ No newline at end of file +} diff --git a/src/Ocelot/Logging/IOcelotLogger.cs b/src/Ocelot/Logging/IOcelotLogger.cs index 9ffced207..4d5111a27 100644 --- a/src/Ocelot/Logging/IOcelotLogger.cs +++ b/src/Ocelot/Logging/IOcelotLogger.cs @@ -3,7 +3,7 @@ namespace Ocelot.Logging { /// - /// Thin wrapper around the DotNet core logging framework, used to allow the scopedDataRepository to be injected giving access to the Ocelot RequestId + /// 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 { diff --git a/src/Ocelot/Logging/ITracer.cs b/src/Ocelot/Logging/ITracer.cs index 6dbf91499..be4affb6e 100644 --- a/src/Ocelot/Logging/ITracer.cs +++ b/src/Ocelot/Logging/ITracer.cs @@ -1,11 +1,12 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + namespace Ocelot.Logging { - using Microsoft.AspNetCore.Http; - using System; - using System.Net.Http; - using System.Threading; - using System.Threading.Tasks; - public interface ITracer { void Event(HttpContext httpContext, string @event); diff --git a/src/Ocelot/Logging/OcelotDiagnosticListener.cs b/src/Ocelot/Logging/OcelotDiagnosticListener.cs index 09beac49b..03fc989a8 100644 --- a/src/Ocelot/Logging/OcelotDiagnosticListener.cs +++ b/src/Ocelot/Logging/OcelotDiagnosticListener.cs @@ -1,8 +1,8 @@ -using Microsoft.AspNetCore.Http; +using System; + +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DiagnosticAdapter; -using Ocelot.Middleware; -using System; namespace Ocelot.Logging { @@ -17,27 +17,6 @@ public OcelotDiagnosticListener(IOcelotLoggerFactory factory, IServiceProvider s _tracer = serviceProvider.GetService(); } - [DiagnosticName("Ocelot.MiddlewareException")] - public virtual void OcelotMiddlewareException(Exception exception, DownstreamContext context, string name) - { - _logger.LogTrace($"Ocelot.MiddlewareException: {name}; {exception.Message};"); - Event(context.HttpContext, $"Ocelot.MiddlewareStarted: {name}; {context.HttpContext.Request.Path}"); - } - - [DiagnosticName("Ocelot.MiddlewareStarted")] - public virtual void OcelotMiddlewareStarted(DownstreamContext context, string name) - { - _logger.LogTrace($"Ocelot.MiddlewareStarted: {name}; {context.HttpContext.Request.Path}"); - Event(context.HttpContext, $"Ocelot.MiddlewareStarted: {name}; {context.HttpContext.Request.Path}"); - } - - [DiagnosticName("Ocelot.MiddlewareFinished")] - public virtual void OcelotMiddlewareFinished(DownstreamContext context, string name) - { - _logger.LogTrace($"Ocelot.MiddlewareFinished: {name}; {context.HttpContext.Request.Path}"); - Event(context.HttpContext, $"OcelotMiddlewareFinished: {name}; {context.HttpContext.Request.Path}"); - } - [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareStarting")] public virtual void OnMiddlewareStarting(HttpContext httpContext, string name) { diff --git a/src/Ocelot/Middleware/BaseUrlFinder.cs b/src/Ocelot/Middleware/BaseUrlFinder.cs index d65fb7698..805dbe7ba 100644 --- a/src/Ocelot/Middleware/BaseUrlFinder.cs +++ b/src/Ocelot/Middleware/BaseUrlFinder.cs @@ -1,23 +1,23 @@ -using Microsoft.Extensions.Configuration; - -namespace Ocelot.Middleware -{ - public class BaseUrlFinder : IBaseUrlFinder - { - private readonly IConfiguration _config; - - public BaseUrlFinder(IConfiguration config) - { - _config = config; - } - - public string Find() - { - //tries to get base url out of file... - var baseUrl = _config.GetValue("GlobalConfiguration:BaseUrl", ""); - - //falls back to memory config then finally default.. - return string.IsNullOrEmpty(baseUrl) ? _config.GetValue("BaseUrl", "http://localhost:5000") : baseUrl; - } - } +using Microsoft.Extensions.Configuration; + +namespace Ocelot.Middleware +{ + public class BaseUrlFinder : IBaseUrlFinder + { + private readonly IConfiguration _config; + + public BaseUrlFinder(IConfiguration config) + { + _config = config; + } + + public string Find() + { + //tries to get base url out of file... + var baseUrl = _config.GetValue("GlobalConfiguration:BaseUrl", string.Empty); + + //falls back to memory config then finally default.. + return string.IsNullOrEmpty(baseUrl) ? _config.GetValue("BaseUrl", "http://localhost:5000") : baseUrl; + } + } } diff --git a/src/Ocelot/Middleware/ConfigurationMiddleware.cs b/src/Ocelot/Middleware/ConfigurationMiddleware.cs new file mode 100644 index 000000000..f832c997b --- /dev/null +++ b/src/Ocelot/Middleware/ConfigurationMiddleware.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; + +using Ocelot.Configuration.Repository; + +using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Errors.Middleware; + +namespace Ocelot.Middleware +{ + public class ConfigurationMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IInternalConfigurationRepository _configRepo; + + public ConfigurationMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IInternalConfigurationRepository configRepo) + : base(loggerFactory.CreateLogger()) + { + _next = next; + _configRepo = configRepo; + } + + public async Task Invoke(HttpContext httpContext) + { + //todo check the config is actually ok? + var config = _configRepo.Get(); + + if (config.IsError) + { + throw new System.Exception("OOOOPS this should not happen raise an issue in GitHub"); + } + + httpContext.Items.SetIInternalConfiguration(config.Data); + + await _next.Invoke(httpContext); + } + } +} diff --git a/src/Ocelot/Middleware/DownstreamContext.cs b/src/Ocelot/Middleware/DownstreamContext.cs deleted file mode 100644 index 5a2dc34a4..000000000 --- a/src/Ocelot/Middleware/DownstreamContext.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration; -using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.Errors; -using Ocelot.Request.Middleware; -using System.Collections.Generic; - -namespace Ocelot.Middleware -{ - public class DownstreamContext - { - public DownstreamContext(HttpContext httpContext) - { - HttpContext = httpContext; - Errors = new List(); - } - - public List TemplatePlaceholderNameAndValues { get; set; } - - public HttpContext HttpContext { get; } - - public DownstreamReRoute DownstreamReRoute { get; set; } - - public DownstreamRequest DownstreamRequest { get; set; } - - public DownstreamResponse DownstreamResponse { get; set; } - - public List Errors { get; } - - public IInternalConfiguration Configuration { get; set; } - - public bool IsError => Errors.Count > 0; - } -} diff --git a/src/Ocelot/Middleware/DownstreamContextMiddlewareExtensions.cs b/src/Ocelot/Middleware/DownstreamContextMiddlewareExtensions.cs new file mode 100644 index 000000000..e68829801 --- /dev/null +++ b/src/Ocelot/Middleware/DownstreamContextMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Middleware +{ + public static class DownstreamContextMiddlewareExtensions + { + public static IApplicationBuilder UseDownstreamContextMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/src/Ocelot/Middleware/HttpItemsExtensions.cs b/src/Ocelot/Middleware/HttpItemsExtensions.cs new file mode 100644 index 000000000..6298d7242 --- /dev/null +++ b/src/Ocelot/Middleware/HttpItemsExtensions.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; + +using Ocelot.Configuration; + +using Ocelot.DownstreamRouteFinder.UrlMatcher; + +using Ocelot.Errors; + +using Ocelot.Request.Middleware; + +namespace Ocelot.Middleware +{ + public static class HttpItemsExtensions + { + public static void UpsertDownstreamRequest(this IDictionary input, DownstreamRequest downstreamRequest) + { + input.Upsert("DownstreamRequest", downstreamRequest); + } + + public static void UpsertDownstreamResponse(this IDictionary input, DownstreamResponse downstreamResponse) + { + input.Upsert("DownstreamResponse", downstreamResponse); + } + + public static void UpsertDownstreamRoute(this IDictionary input, DownstreamRoute downstreamRoute) + { + input.Upsert("DownstreamRoute", downstreamRoute); + } + + public static void UpsertTemplatePlaceholderNameAndValues(this IDictionary input, List tPNV) + { + input.Upsert("TemplatePlaceholderNameAndValues", tPNV); + } + + public static void UpsertDownstreamRoute(this IDictionary input, DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute) + { + input.Upsert("DownstreamRouteHolder", downstreamRoute); + } + + public static void UpsertErrors(this IDictionary input, List errors) + { + input.Upsert("Errors", errors); + } + + public static void SetError(this IDictionary input, Error error) + { + var errors = new List { error }; + input.Upsert("Errors", errors); + } + + public static void SetIInternalConfiguration(this IDictionary input, IInternalConfiguration config) + { + input.Upsert("IInternalConfiguration", config); + } + + public static IInternalConfiguration IInternalConfiguration(this IDictionary input) + { + return input.Get("IInternalConfiguration"); + } + + public static List Errors(this IDictionary input) + { + var errors = input.Get>("Errors"); + return errors ?? new List(); + } + + public static DownstreamRouteFinder.DownstreamRouteHolder + DownstreamRouteHolder(this IDictionary input) => + input.Get("DownstreamRouteHolder"); + + public static List + TemplatePlaceholderNameAndValues(this IDictionary input) => + input.Get>("TemplatePlaceholderNameAndValues"); + + public static DownstreamRequest DownstreamRequest(this IDictionary input) => + input.Get("DownstreamRequest"); + + public static DownstreamResponse DownstreamResponse(this IDictionary input) => + input.Get("DownstreamResponse"); + + public static DownstreamRoute DownstreamRoute(this IDictionary input) => + input.Get("DownstreamRoute"); + + private static T Get(this IDictionary input, string key) => + input.TryGetValue(key, out var value) ? (T)value : default; + + private static void Upsert(this IDictionary input, string key, T value) + { + if (input.DoesntExist(key)) + { + input.Add(key, value); + } + else + { + input.Remove(key); + input.Add(key, value); + } + } + + private static bool DoesntExist(this IDictionary input, string key) => !input.ContainsKey(key); + } +} diff --git a/src/Ocelot/Middleware/IBaseUrlFinder.cs b/src/Ocelot/Middleware/IBaseUrlFinder.cs index 23d20104c..18706e627 100644 --- a/src/Ocelot/Middleware/IBaseUrlFinder.cs +++ b/src/Ocelot/Middleware/IBaseUrlFinder.cs @@ -4,4 +4,4 @@ public interface IBaseUrlFinder { string Find(); } -} \ No newline at end of file +} diff --git a/src/Ocelot/Middleware/Multiplexer/IDefinedAggregator.cs b/src/Ocelot/Middleware/Multiplexer/IDefinedAggregator.cs deleted file mode 100644 index 48234782b..000000000 --- a/src/Ocelot/Middleware/Multiplexer/IDefinedAggregator.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Ocelot.Middleware.Multiplexer -{ - public interface IDefinedAggregator - { - Task Aggregate(List responses); - } -} diff --git a/src/Ocelot/Middleware/Multiplexer/IMultiplexer.cs b/src/Ocelot/Middleware/Multiplexer/IMultiplexer.cs deleted file mode 100644 index 57a724e2d..000000000 --- a/src/Ocelot/Middleware/Multiplexer/IMultiplexer.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Ocelot.Configuration; -using System.Threading.Tasks; - -namespace Ocelot.Middleware.Multiplexer -{ - public interface IMultiplexer - { - Task Multiplex(DownstreamContext context, ReRoute reRoute, OcelotRequestDelegate next); - } -} diff --git a/src/Ocelot/Middleware/Multiplexer/IResponseAggregator.cs b/src/Ocelot/Middleware/Multiplexer/IResponseAggregator.cs deleted file mode 100644 index bd56ca09c..000000000 --- a/src/Ocelot/Middleware/Multiplexer/IResponseAggregator.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Ocelot.Configuration; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Ocelot.Middleware.Multiplexer -{ - public interface IResponseAggregator - { - Task Aggregate(ReRoute reRoute, DownstreamContext originalContext, List downstreamResponses); - } -} diff --git a/src/Ocelot/Middleware/Multiplexer/Multiplexer.cs b/src/Ocelot/Middleware/Multiplexer/Multiplexer.cs deleted file mode 100644 index a49e20c1a..000000000 --- a/src/Ocelot/Middleware/Multiplexer/Multiplexer.cs +++ /dev/null @@ -1,152 +0,0 @@ -using Ocelot.Configuration; -using Ocelot.DownstreamRouteFinder.UrlMatcher; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Ocelot.Middleware.Multiplexer -{ - public class Multiplexer : IMultiplexer - { - private readonly IResponseAggregatorFactory _factory; - - public Multiplexer(IResponseAggregatorFactory factory) - { - _factory = factory; - } - - public async Task Multiplex(DownstreamContext context, ReRoute reRoute, OcelotRequestDelegate next) - { - var reRouteKeysConfigs = reRoute.DownstreamReRouteConfig; - if (reRouteKeysConfigs == null || !reRouteKeysConfigs.Any()) - { - var tasks = new Task[reRoute.DownstreamReRoute.Count]; - - for (var i = 0; i < reRoute.DownstreamReRoute.Count; i++) - { - var downstreamContext = new DownstreamContext(context.HttpContext) - { - TemplatePlaceholderNameAndValues = context.TemplatePlaceholderNameAndValues, - Configuration = context.Configuration, - DownstreamReRoute = reRoute.DownstreamReRoute[i], - }; - - tasks[i] = Fire(downstreamContext, next); - } - - await Task.WhenAll(tasks); - - var contexts = new List(); - - foreach (var task in tasks) - { - var finished = await task; - contexts.Add(finished); - } - - await Map(reRoute, context, contexts); - } - else - { - var downstreamContextMain = new DownstreamContext(context.HttpContext) - { - TemplatePlaceholderNameAndValues = context.TemplatePlaceholderNameAndValues, - Configuration = context.Configuration, - DownstreamReRoute = reRoute.DownstreamReRoute[0], - }; - var mainResponse = await Fire(downstreamContextMain, next); - - if (reRoute.DownstreamReRoute.Count == 1) - { - MapNotAggregate(context, new List() { mainResponse }); - return; - } - - var tasks = new List>(); - if (mainResponse.DownstreamResponse == null) - { - return; - } - - var content = await mainResponse.DownstreamResponse.Content.ReadAsStringAsync(); - var jObject = Newtonsoft.Json.Linq.JToken.Parse(content); - - for (var i = 1; i < reRoute.DownstreamReRoute.Count; i++) - { - var templatePlaceholderNameAndValues = context.TemplatePlaceholderNameAndValues; - var downstreamReRoute = reRoute.DownstreamReRoute[i]; - var matchAdvancedAgg = reRouteKeysConfigs.FirstOrDefault(q => q.ReRouteKey == downstreamReRoute.Key); - if (matchAdvancedAgg != null) - { - var values = jObject.SelectTokens(matchAdvancedAgg.JsonPath).Select(s => s.ToString()).Distinct().ToList(); - - foreach (var value in values) - { - var downstreamContext = new DownstreamContext(context.HttpContext) - { - TemplatePlaceholderNameAndValues = new List(templatePlaceholderNameAndValues), - Configuration = context.Configuration, - DownstreamReRoute = downstreamReRoute, - }; - downstreamContext.TemplatePlaceholderNameAndValues.Add(new PlaceholderNameAndValue("{" + matchAdvancedAgg.Parameter + "}", value.ToString())); - tasks.Add(Fire(downstreamContext, next)); - } - } - else - { - var downstreamContext = new DownstreamContext(context.HttpContext) - { - TemplatePlaceholderNameAndValues = new List(templatePlaceholderNameAndValues), - Configuration = context.Configuration, - DownstreamReRoute = downstreamReRoute, - }; - tasks.Add(Fire(downstreamContext, next)); - } - } - - await Task.WhenAll(tasks); - - var contexts = new List() { mainResponse }; - - foreach (var task in tasks) - { - var finished = await task; - contexts.Add(finished); - } - - await Map(reRoute, context, contexts); - } - } - - private async Task Map(ReRoute reRoute, DownstreamContext context, List contexts) - { - if (reRoute.DownstreamReRoute.Count > 1) - { - var aggregator = _factory.Get(reRoute); - await aggregator.Aggregate(reRoute, context, contexts); - } - else - { - MapNotAggregate(context, contexts); - } - } - - private void MapNotAggregate(DownstreamContext originalContext, List downstreamContexts) - { - //assume at least one..if this errors then it will be caught by global exception handler - var finished = downstreamContexts.First(); - - originalContext.Errors.AddRange(finished.Errors); - - originalContext.DownstreamRequest = finished.DownstreamRequest; - - originalContext.DownstreamResponse = finished.DownstreamResponse; - } - - private async Task Fire(DownstreamContext context, OcelotRequestDelegate next) - { - await next.Invoke(context); - return context; - } - } -} diff --git a/src/Ocelot/Middleware/Multiplexer/SimpleJsonResponseAggregator.cs b/src/Ocelot/Middleware/Multiplexer/SimpleJsonResponseAggregator.cs deleted file mode 100644 index f03b52c54..000000000 --- a/src/Ocelot/Middleware/Multiplexer/SimpleJsonResponseAggregator.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Ocelot.Configuration; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading.Tasks; - -namespace Ocelot.Middleware.Multiplexer -{ - public class SimpleJsonResponseAggregator : IResponseAggregator - { - public async Task Aggregate(ReRoute reRoute, DownstreamContext originalContext, List downstreamContexts) - { - await MapAggregateContent(originalContext, downstreamContexts); - } - - private static async Task MapAggregateContent(DownstreamContext originalContext, List downstreamContexts) - { - var contentBuilder = new StringBuilder(); - - contentBuilder.Append("{"); - - var responseKeys = downstreamContexts.Select(s => s.DownstreamReRoute.Key).Distinct().ToList(); - - for (var k = 0; k < responseKeys.Count; k++) - { - var contexts = downstreamContexts.Where(w => w.DownstreamReRoute.Key == responseKeys[k]).ToList(); - if (contexts.Count == 1) - { - if (contexts[0].IsError) - { - MapAggregateError(originalContext, contexts[0]); - return; - } - - var content = await contexts[0].DownstreamResponse.Content.ReadAsStringAsync(); - contentBuilder.Append($"\"{responseKeys[k]}\":{content}"); - } - else - { - contentBuilder.Append($"\"{responseKeys[k]}\":"); - contentBuilder.Append("["); - - for (var i = 0; i < contexts.Count; i++) - { - if (contexts[i].IsError) - { - MapAggregateError(originalContext, contexts[i]); - return; - } - - var content = await contexts[i].DownstreamResponse.Content.ReadAsStringAsync(); - if (string.IsNullOrWhiteSpace(content)) - { - continue; - } - - contentBuilder.Append($"{content}"); - - if (i + 1 < contexts.Count) - { - contentBuilder.Append(","); - } - } - - contentBuilder.Append("]"); - } - - if (k + 1 < responseKeys.Count) - { - contentBuilder.Append(","); - } - } - - contentBuilder.Append("}"); - - var stringContent = new StringContent(contentBuilder.ToString()) - { - Headers = { ContentType = new MediaTypeHeaderValue("application/json") } - }; - - originalContext.DownstreamResponse = new DownstreamResponse(stringContent, HttpStatusCode.OK, new List>>(), "cannot return from aggregate..which reason phrase would you use?"); - } - - private static void MapAggregateError(DownstreamContext originalContext, DownstreamContext downstreamContext) - { - originalContext.Errors.AddRange(downstreamContext.Errors); - originalContext.DownstreamResponse = downstreamContext.DownstreamResponse; - } - } -} diff --git a/src/Ocelot/Middleware/OcelotMiddleware.cs b/src/Ocelot/Middleware/OcelotMiddleware.cs index 0bf57afc7..813332c97 100644 --- a/src/Ocelot/Middleware/OcelotMiddleware.cs +++ b/src/Ocelot/Middleware/OcelotMiddleware.cs @@ -1,33 +1,16 @@ -using Ocelot.Errors; -using Ocelot.Logging; -using System.Collections.Generic; - -namespace Ocelot.Middleware -{ - public abstract class OcelotMiddleware +using Ocelot.Logging; + +namespace Ocelot.Middleware +{ + public abstract class OcelotMiddleware { - protected OcelotMiddleware(IOcelotLogger logger) - { - Logger = logger; - MiddlewareName = this.GetType().Name; - } - - public IOcelotLogger Logger { get; } - - public string MiddlewareName { get; } - - public void SetPipelineError(DownstreamContext context, List errors) - { - foreach (var error in errors) - { - SetPipelineError(context, error); - } - } - - public void SetPipelineError(DownstreamContext context, Error error) - { - Logger.LogWarning(error.Message); - context.Errors.Add(error); - } - } + protected OcelotMiddleware(IOcelotLogger logger) + { + Logger = logger; + MiddlewareName = GetType().Name; + } + + public IOcelotLogger Logger { get; } + public string MiddlewareName { get; } + } } diff --git a/src/Ocelot/Middleware/OcelotMiddlewareConfigurationDelegate.cs b/src/Ocelot/Middleware/OcelotMiddlewareConfigurationDelegate.cs index f5e2061d1..ffa7f8c40 100644 --- a/src/Ocelot/Middleware/OcelotMiddlewareConfigurationDelegate.cs +++ b/src/Ocelot/Middleware/OcelotMiddlewareConfigurationDelegate.cs @@ -1,7 +1,8 @@ -namespace Ocelot.Middleware -{ - using Microsoft.AspNetCore.Builder; - using System.Threading.Tasks; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Builder; +namespace Ocelot.Middleware +{ public delegate Task OcelotMiddlewareConfigurationDelegate(IApplicationBuilder builder); } diff --git a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs index 42bc6b713..70b9d53a5 100644 --- a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs +++ b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs @@ -1,23 +1,27 @@ -namespace Ocelot.Middleware -{ - using DependencyInjection; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Options; - using Ocelot.Configuration; - using Ocelot.Configuration.Creator; - using Ocelot.Configuration.File; - using Ocelot.Configuration.Repository; - using Ocelot.Configuration.Setter; - using Ocelot.Logging; - using Ocelot.Middleware.Pipeline; - using Ocelot.Responses; - using System; - using System.Diagnostics; - using System.Linq; - using System.Threading.Tasks; +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Repository; +using Ocelot.Configuration.Setter; + +using Ocelot.DependencyInjection; + +using Ocelot.Logging; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +using Ocelot.Responses; + +namespace Ocelot.Middleware +{ public static class OcelotMiddlewareExtensions { public static async Task UseOcelot(this IApplicationBuilder builder) @@ -42,37 +46,25 @@ public static async Task UseOcelot(this IApplicationBuilder return CreateOcelotPipeline(builder, pipelineConfiguration); } - public static Task UseOcelot(this IApplicationBuilder app, Action builderAction) + public static Task UseOcelot(this IApplicationBuilder app, Action builderAction) => UseOcelot(app, builderAction, new OcelotPipelineConfiguration()); - public static async Task UseOcelot(this IApplicationBuilder app, Action builderAction, OcelotPipelineConfiguration configuration) + public static async Task UseOcelot(this IApplicationBuilder app, Action builderAction, OcelotPipelineConfiguration configuration) { - await CreateConfiguration(app); // initConfiguration + await CreateConfiguration(app); ConfigureDiagnosticListener(app); - var ocelotPipelineBuilder = new OcelotPipelineBuilder(app.ApplicationServices); - builderAction?.Invoke(ocelotPipelineBuilder, configuration ?? new OcelotPipelineConfiguration()); + builderAction?.Invoke(app, configuration ?? new OcelotPipelineConfiguration()); - var ocelotDelegate = ocelotPipelineBuilder.Build(); app.Properties["analysis.NextMiddlewareName"] = "TransitionToOcelotMiddleware"; - app.Use(async (context, task) => - { - var downstreamContext = new DownstreamContext(context); - await ocelotDelegate.Invoke(downstreamContext); - }); - return app; } private static IApplicationBuilder CreateOcelotPipeline(IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration) { - var pipelineBuilder = new OcelotPipelineBuilder(builder.ApplicationServices); - - pipelineBuilder.BuildOcelotPipeline(pipelineConfiguration); - - var firstDelegate = pipelineBuilder.Build(); + builder.BuildOcelotPipeline(pipelineConfiguration); /* inject first delegate into first piece of asp.net middleware..maybe not like this @@ -82,12 +74,6 @@ rest of asp.net.. builder.Properties["analysis.NextMiddlewareName"] = "TransitionToOcelotMiddleware"; - builder.Use(async (context, task) => - { - var downstreamContext = new DownstreamContext(context); - await firstDelegate.Invoke(downstreamContext); - }); - return builder; } @@ -173,7 +159,7 @@ private static IInternalConfiguration GetOcelotConfigAndReturn(IInternalConfigur private static void ThrowToStopOcelotStarting(Response config) { - throw new Exception($"Unable to start Ocelot, errors are: {string.Join(",", config.Errors.Select(x => x.ToString()))}"); + throw new Exception($"Unable to start Ocelot, errors are: {string.Join(',', config.Errors.Select(x => x.ToString()))}"); } private static void ConfigureDiagnosticListener(IApplicationBuilder builder) diff --git a/src/Ocelot/Middleware/OcelotPipelineConfiguration.cs b/src/Ocelot/Middleware/OcelotPipelineConfiguration.cs index 3dbb76a7d..012dc9ed7 100644 --- a/src/Ocelot/Middleware/OcelotPipelineConfiguration.cs +++ b/src/Ocelot/Middleware/OcelotPipelineConfiguration.cs @@ -1,10 +1,12 @@ -namespace Ocelot.Middleware -{ - using Ocelot.Middleware.Pipeline; - using System; - using System.Collections.Generic; - using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +namespace Ocelot.Middleware +{ public class OcelotPipelineConfiguration { /// @@ -12,38 +14,57 @@ public class OcelotPipelineConfiguration /// is the next thing called in the Ocelot pipeline. Anything after next.invoke is the last thing called /// in the Ocelot pipeline before we go to the global error handler. /// - public Func, Task> PreErrorResponderMiddleware { get; set; } + /// + /// A delegate object. + /// + public Func, Task> PreErrorResponderMiddleware { get; set; } /// - /// This is to allow the user to run any extra authentication before the Ocelot authentication - /// kicks in + /// This is to allow the user to run any extra authentication before the Ocelot authentication kicks in. /// - public Func, Task> PreAuthenticationMiddleware { get; set; } + /// + /// A delegate object. + /// + public Func, Task> PreAuthenticationMiddleware { get; set; } /// - /// This allows the user to completely override the ocelot authentication middleware + /// This allows the user to completely override the ocelot authentication middleware. /// - public Func, Task> AuthenticationMiddleware { get; set; } + /// + /// A delegate object. + /// + public Func, Task> AuthenticationMiddleware { get; set; } /// - /// This is to allow the user to run any extra authorisation before the Ocelot authentication - /// kicks in + /// This is to allow the user to run any extra authorization before the Ocelot authentication kicks in. /// - public Func, Task> PreAuthorisationMiddleware { get; set; } + /// + /// A delegate object. + /// + public Func, Task> PreAuthorizationMiddleware { get; set; } /// - /// This allows the user to completely override the ocelot authorisation middleware + /// This allows the user to completely override the ocelot authorization middleware. /// - public Func, Task> AuthorisationMiddleware { get; set; } + /// + /// A delegate object. + /// + public Func, Task> AuthorizationMiddleware { get; set; } /// - /// This allows the user to implement there own query string manipulation logic + /// This allows the user to implement there own query string manipulation logic. /// - public Func, Task> PreQueryStringBuilderMiddleware { get; set; } + /// + /// A delegate object. + /// + public Func, Task> PreQueryStringBuilderMiddleware { get; set; } /// - /// This is an extension that will branch to different pipes + /// This is an extension that will branch to different pipes. /// - public List>> MapWhenOcelotPipeline { get; } = new List>>(); + /// + /// A collection. + /// + public Dictionary, Action> MapWhenOcelotPipeline { get; } = new(); // TODO fix this data structure } } diff --git a/src/Ocelot/Middleware/Pipeline/OcelotPipelineExtensions.cs b/src/Ocelot/Middleware/OcelotPipelineExtensions.cs similarity index 56% rename from src/Ocelot/Middleware/Pipeline/OcelotPipelineExtensions.cs rename to src/Ocelot/Middleware/OcelotPipelineExtensions.cs index d52cfb0d0..e804ce1af 100644 --- a/src/Ocelot/Middleware/Pipeline/OcelotPipelineExtensions.cs +++ b/src/Ocelot/Middleware/OcelotPipelineExtensions.cs @@ -1,13 +1,21 @@ -using Ocelot.Authentication.Middleware; -using Ocelot.Authorisation.Middleware; +using System; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +using Ocelot.Multiplexer; + +using Ocelot.Authentication.Middleware; +using Ocelot.Authorization.Middleware; using Ocelot.Cache.Middleware; using Ocelot.Claims.Middleware; +using Ocelot.DownstreamPathManipulation.Middleware; using Ocelot.DownstreamRouteFinder.Middleware; using Ocelot.DownstreamUrlCreator.Middleware; using Ocelot.Errors.Middleware; using Ocelot.Headers.Middleware; using Ocelot.LoadBalancer.Middleware; -using Ocelot.PathManipulation.Middleware; using Ocelot.QueryStrings.Middleware; using Ocelot.RateLimit.Middleware; using Ocelot.Request.Middleware; @@ -16,129 +24,135 @@ using Ocelot.Responder.Middleware; using Ocelot.Security.Middleware; using Ocelot.WebSockets.Middleware; -using System; -using System.Threading.Tasks; -namespace Ocelot.Middleware.Pipeline +namespace Ocelot.Middleware { public static class OcelotPipelineExtensions { - public static OcelotRequestDelegate BuildOcelotPipeline(this IOcelotPipelineBuilder builder, + public static RequestDelegate BuildOcelotPipeline(this IApplicationBuilder app, OcelotPipelineConfiguration pipelineConfiguration) { + // this sets up the downstream context and gets the config + app.UseDownstreamContextMiddleware(); + // This is registered to catch any global exceptions that are not handled // It also sets the Request Id if anything is set globally - builder.UseExceptionHandlerMiddleware(); + app.UseExceptionHandlerMiddleware(); // If the request is for websockets upgrade we fork into a different pipeline - builder.MapWhen(context => context.HttpContext.WebSockets.IsWebSocketRequest, - app => + app.MapWhen(httpContext => httpContext.WebSockets.IsWebSocketRequest, + wenSocketsApp => { - app.UseDownstreamRouteFinderMiddleware(); - app.UseDownstreamRequestInitialiser(); - app.UseLoadBalancingMiddleware(); - app.UseDownstreamUrlCreatorMiddleware(); - app.UseWebSocketsProxyMiddleware(); + wenSocketsApp.UseDownstreamRouteFinderMiddleware(); + wenSocketsApp.UseMultiplexingMiddleware(); + wenSocketsApp.UseDownstreamRequestInitialiser(); + wenSocketsApp.UseLoadBalancingMiddleware(); + wenSocketsApp.UseDownstreamUrlCreatorMiddleware(); + wenSocketsApp.UseWebSocketsProxyMiddleware(); }); // Allow the user to respond with absolutely anything they want. - builder.UseIfNotNull(pipelineConfiguration.PreErrorResponderMiddleware); + app.UseIfNotNull(pipelineConfiguration.PreErrorResponderMiddleware); // This is registered first so it can catch any errors and issue an appropriate response - builder.UseResponderMiddleware(); + app.UseResponderMiddleware(); // Then we get the downstream route information - builder.UseDownstreamRouteFinderMiddleware(); + app.UseDownstreamRouteFinderMiddleware(); + + // Multiplex the request if required + app.UseMultiplexingMiddleware(); // This security module, IP whitelist blacklist, extended security mechanism - builder.UseSecurityMiddleware(); + app.UseSecurityMiddleware(); //Expand other branch pipes if (pipelineConfiguration.MapWhenOcelotPipeline != null) { foreach (var pipeline in pipelineConfiguration.MapWhenOcelotPipeline) { - builder.MapWhen(pipeline); + // todo why is this asking for an app app? + app.MapWhen(pipeline.Key, pipeline.Value); } } // Now we have the ds route we can transform headers and stuff? - builder.UseHttpHeadersTransformationMiddleware(); + app.UseHttpHeadersTransformationMiddleware(); // Initialises downstream request - builder.UseDownstreamRequestInitialiser(); + app.UseDownstreamRequestInitialiser(); // We check whether the request is ratelimit, and if there is no continue processing - builder.UseRateLimiting(); + app.UseRateLimiting(); // This adds or updates the request id (initally we try and set this based on global config in the error handling middleware) // If anything was set at global level and we have a different setting at re route level the global stuff will be overwritten // This means you can get a scenario where you have a different request id from the first piece of middleware to the request id middleware. - builder.UseRequestIdMiddleware(); + app.UseRequestIdMiddleware(); // Allow pre authentication logic. The idea being people might want to run something custom before what is built in. - builder.UseIfNotNull(pipelineConfiguration.PreAuthenticationMiddleware); + app.UseIfNotNull(pipelineConfiguration.PreAuthenticationMiddleware); // Now we know where the client is going to go we can authenticate them. // We allow the ocelot middleware to be overriden by whatever the // user wants if (pipelineConfiguration.AuthenticationMiddleware == null) { - builder.UseAuthenticationMiddleware(); + app.UseAuthenticationMiddleware(); } else { - builder.Use(pipelineConfiguration.AuthenticationMiddleware); + app.Use(pipelineConfiguration.AuthenticationMiddleware); } - // The next thing we do is look at any claims transforms in case this is important for authorisation - builder.UseClaimsToClaimsMiddleware(); + // The next thing we do is look at any claims transforms in case this is important for authorization + app.UseClaimsToClaimsMiddleware(); - // Allow pre authorisation logic. The idea being people might want to run something custom before what is built in. - builder.UseIfNotNull(pipelineConfiguration.PreAuthorisationMiddleware); + // Allow pre authorization logic. The idea being people might want to run something custom before what is built in. + app.UseIfNotNull(pipelineConfiguration.PreAuthorizationMiddleware); // Now we have authenticated and done any claims transformation we - // can authorise the request + // can authorize the request // We allow the ocelot middleware to be overriden by whatever the // user wants - if (pipelineConfiguration.AuthorisationMiddleware == null) + if (pipelineConfiguration.AuthorizationMiddleware == null) { - builder.UseAuthorisationMiddleware(); + app.UseAuthorizationMiddleware(); } else { - builder.Use(pipelineConfiguration.AuthorisationMiddleware); + app.Use(pipelineConfiguration.AuthorizationMiddleware); } // Now we can run the claims to headers transformation middleware - builder.UseClaimsToHeadersMiddleware(); + app.UseClaimsToHeadersMiddleware(); // Allow the user to implement their own query string manipulation logic - builder.UseIfNotNull(pipelineConfiguration.PreQueryStringBuilderMiddleware); + app.UseIfNotNull(pipelineConfiguration.PreQueryStringBuilderMiddleware); // Now we can run any claims to query string transformation middleware - builder.UseClaimsToQueryStringMiddleware(); + app.UseClaimsToQueryStringMiddleware(); - builder.UseClaimsToDownstreamPathMiddleware(); + app.UseClaimsToDownstreamPathMiddleware(); // Get the load balancer for this request - builder.UseLoadBalancingMiddleware(); + app.UseLoadBalancingMiddleware(); // This takes the downstream route we retrieved earlier and replaces any placeholders with the variables that should be used - builder.UseDownstreamUrlCreatorMiddleware(); + app.UseDownstreamUrlCreatorMiddleware(); // Not sure if this is the best place for this but we use the downstream url // as the basis for our cache key. - builder.UseOutputCacheMiddleware(); + app.UseOutputCacheMiddleware(); //We fire off the request and set the response on the scoped data repo - builder.UseHttpRequesterMiddleware(); + app.UseHttpRequesterMiddleware(); - return builder.Build(); + return app.Build(); } - private static void UseIfNotNull(this IOcelotPipelineBuilder builder, - Func, Task> middleware) + private static void UseIfNotNull(this IApplicationBuilder builder, + Func, Task> middleware) { if (middleware != null) { diff --git a/src/Ocelot/Middleware/OcelotRequestDelegate.cs b/src/Ocelot/Middleware/OcelotRequestDelegate.cs deleted file mode 100644 index 130dfd86c..000000000 --- a/src/Ocelot/Middleware/OcelotRequestDelegate.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System.Threading.Tasks; - -namespace Ocelot.Middleware -{ - public delegate Task OcelotRequestDelegate(DownstreamContext downstreamContext); -} diff --git a/src/Ocelot/Middleware/Pipeline/IOcelotPipelineBuilder.cs b/src/Ocelot/Middleware/Pipeline/IOcelotPipelineBuilder.cs deleted file mode 100644 index b7199e2ba..000000000 --- a/src/Ocelot/Middleware/Pipeline/IOcelotPipelineBuilder.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -// Removed code and changed RequestDelete to OcelotRequestDelete, HttpContext to DownstreamContext, removed some exception handling messages - -using System; - -namespace Ocelot.Middleware.Pipeline -{ - public interface IOcelotPipelineBuilder - { - IServiceProvider ApplicationServices { get; } - - IOcelotPipelineBuilder Use(Func middleware); - - OcelotRequestDelegate Build(); - - IOcelotPipelineBuilder New(); - } -} diff --git a/src/Ocelot/Middleware/Pipeline/LICENSE.txt b/src/Ocelot/Middleware/Pipeline/LICENSE.txt deleted file mode 100644 index 7b2956ece..000000000 --- a/src/Ocelot/Middleware/Pipeline/LICENSE.txt +++ /dev/null @@ -1,14 +0,0 @@ -Copyright (c) .NET Foundation and Contributors - -All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed -under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -CONDITIONS OF ANY KIND, either express or implied. See the License for the -specific language governing permissions and limitations under the License. diff --git a/src/Ocelot/Middleware/Pipeline/MapWhenMiddleware.cs b/src/Ocelot/Middleware/Pipeline/MapWhenMiddleware.cs deleted file mode 100644 index f05c35e45..000000000 --- a/src/Ocelot/Middleware/Pipeline/MapWhenMiddleware.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Ocelot.Middleware.Pipeline -{ - public class MapWhenMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly MapWhenOptions _options; - - public MapWhenMiddleware(OcelotRequestDelegate next, MapWhenOptions options) - { - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } - - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - _next = next; - _options = options; - } - - public async Task Invoke(DownstreamContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (_options.Predicate(context)) - { - await _options.Branch(context); - } - else - { - await _next(context); - } - } - } -} diff --git a/src/Ocelot/Middleware/Pipeline/MapWhenOptions.cs b/src/Ocelot/Middleware/Pipeline/MapWhenOptions.cs deleted file mode 100644 index 912688c32..000000000 --- a/src/Ocelot/Middleware/Pipeline/MapWhenOptions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; - -namespace Ocelot.Middleware.Pipeline -{ - public class MapWhenOptions - { - private Func _predicate; - - public Func Predicate - { - get - { - return _predicate; - } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - _predicate = value; - } - } - - public OcelotRequestDelegate Branch { get; set; } - } -} diff --git a/src/Ocelot/Middleware/Pipeline/OcelotPipelineBuilder.cs b/src/Ocelot/Middleware/Pipeline/OcelotPipelineBuilder.cs deleted file mode 100644 index 5e3ee32e1..000000000 --- a/src/Ocelot/Middleware/Pipeline/OcelotPipelineBuilder.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -// Removed code and changed RequestDelete to OcelotRequestDelete, HttpContext to DownstreamContext, removed some exception handling messages - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Ocelot.Middleware.Pipeline -{ - public class OcelotPipelineBuilder : IOcelotPipelineBuilder - { - private readonly IList> _middlewares; - - public OcelotPipelineBuilder(IServiceProvider provider) - { - ApplicationServices = provider; - _middlewares = new List>(); - } - - public OcelotPipelineBuilder(IOcelotPipelineBuilder builder) - { - ApplicationServices = builder.ApplicationServices; - _middlewares = new List>(); - } - - public IServiceProvider ApplicationServices { get; } - - public IOcelotPipelineBuilder Use(Func middleware) - { - _middlewares.Add(middleware); - return this; - } - - public OcelotRequestDelegate Build() - { - OcelotRequestDelegate app = context => - { - context.HttpContext.Response.StatusCode = 404; - return Task.CompletedTask; - }; - - foreach (var component in _middlewares.Reverse()) - { - app = component(app); - } - - return app; - } - - public IOcelotPipelineBuilder New() - { - return new OcelotPipelineBuilder(this); - } - } -} diff --git a/src/Ocelot/Middleware/Pipeline/OcelotPipelineBuilderExtensions.cs b/src/Ocelot/Middleware/Pipeline/OcelotPipelineBuilderExtensions.cs deleted file mode 100644 index 3dbc4a482..000000000 --- a/src/Ocelot/Middleware/Pipeline/OcelotPipelineBuilderExtensions.cs +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -// Removed code and changed RequestDelete to OcelotRequestDelete, HttpContext to DownstreamContext, removed some exception handling messages - -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Diagnostics; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Threading.Tasks; - -namespace Ocelot.Middleware.Pipeline -{ - using Predicate = Func; - - public static class OcelotPipelineBuilderExtensions - { - internal const string InvokeMethodName = "Invoke"; - internal const string InvokeAsyncMethodName = "InvokeAsync"; - private static readonly MethodInfo GetServiceInfo = typeof(OcelotPipelineBuilderExtensions).GetMethod(nameof(GetService), BindingFlags.NonPublic | BindingFlags.Static); - - public static IOcelotPipelineBuilder UseMiddleware(this IOcelotPipelineBuilder app, params object[] args) - { - return app.UseMiddleware(typeof(TMiddleware), args); - } - - public static IOcelotPipelineBuilder Use(this IOcelotPipelineBuilder app, Func, Task> middleware) - { - return app.Use(next => - { - return context => - { - Func simpleNext = () => next(context); - return middleware(context, simpleNext); - }; - }); - } - - public static IOcelotPipelineBuilder UseMiddleware(this IOcelotPipelineBuilder app, Type middleware, params object[] args) - { - return app.Use(next => - { - var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public); - var invokeMethods = methods.Where(m => - string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal) - || string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal) - ).ToArray(); - - if (invokeMethods.Length > 1) - { - throw new InvalidOperationException(); - } - - if (invokeMethods.Length == 0) - { - throw new InvalidOperationException(); - } - - var methodinfo = invokeMethods[0]; - if (!typeof(Task).IsAssignableFrom(methodinfo.ReturnType)) - { - throw new InvalidOperationException(); - } - - var parameters = methodinfo.GetParameters(); - if (parameters.Length == 0 || parameters[0].ParameterType != typeof(DownstreamContext)) - { - throw new InvalidOperationException(); - } - - var ctorArgs = new object[args.Length + 1]; - ctorArgs[0] = next; - Array.Copy(args, 0, ctorArgs, 1, args.Length); - var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs); - if (parameters.Length == 1) - { - var ocelotDelegate = (OcelotRequestDelegate)methodinfo.CreateDelegate(typeof(OcelotRequestDelegate), instance); - var diagnosticListener = (DiagnosticListener)app.ApplicationServices.GetService(typeof(DiagnosticListener)); - var middlewareName = ocelotDelegate.Target.GetType().Name; - - OcelotRequestDelegate wrapped = async context => - { - try - { - Write(diagnosticListener, "Ocelot.MiddlewareStarted", middlewareName, context); - await ocelotDelegate(context); - } - catch (Exception ex) - { - WriteException(diagnosticListener, ex, "Ocelot.MiddlewareException", middlewareName, context); - throw ex; - } - finally - { - Write(diagnosticListener, "Ocelot.MiddlewareFinished", middlewareName, context); - } - }; - - return wrapped; - } - - var factory = Compile(methodinfo, parameters); - - return context => - { - var serviceProvider = context.HttpContext.RequestServices ?? app.ApplicationServices; - if (serviceProvider == null) - { - throw new InvalidOperationException(); - } - - return factory(instance, context, serviceProvider); - }; - }); - } - - private static void Write(DiagnosticListener diagnosticListener, string message, string middlewareName, DownstreamContext context) - { - if (diagnosticListener != null) - { - diagnosticListener.Write(message, new { name = middlewareName, context = context }); - } - } - - private static void WriteException(DiagnosticListener diagnosticListener, Exception exception, string message, string middlewareName, DownstreamContext context) - { - if (diagnosticListener != null) - { - diagnosticListener.Write(message, new { name = middlewareName, context = context, exception = exception }); - } - } - - public static IOcelotPipelineBuilder MapWhen(this IOcelotPipelineBuilder app, Predicate predicate, Action configuration) - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - if (predicate == null) - { - throw new ArgumentNullException(nameof(predicate)); - } - - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } - - var branchBuilder = app.New(); - configuration(branchBuilder); - var branch = branchBuilder.Build(); - - var options = new MapWhenOptions - { - Predicate = predicate, - Branch = branch, - }; - return app.Use(next => new MapWhenMiddleware(next, options).Invoke); - } - - public static IOcelotPipelineBuilder MapWhen(this IOcelotPipelineBuilder app, Func pipelineBuilderFunc) - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - if (pipelineBuilderFunc == null) - { - throw new ArgumentNullException(nameof(pipelineBuilderFunc)); - } - - var branchBuilder = app.New(); - var predicate = pipelineBuilderFunc.Invoke(branchBuilder); - var branch = branchBuilder.Build(); - - var options = new MapWhenOptions - { - Predicate = predicate, - Branch = branch - }; - return app.Use(next => new MapWhenMiddleware(next, options).Invoke); - } - - private static Func Compile(MethodInfo methodinfo, ParameterInfo[] parameters) - { - var middleware = typeof(T); - var httpContextArg = Expression.Parameter(typeof(DownstreamContext), "downstreamContext"); - var providerArg = Expression.Parameter(typeof(IServiceProvider), "serviceProvider"); - var instanceArg = Expression.Parameter(middleware, "middleware"); - - var methodArguments = new Expression[parameters.Length]; - methodArguments[0] = httpContextArg; - for (int i = 1; i < parameters.Length; i++) - { - var parameterType = parameters[i].ParameterType; - if (parameterType.IsByRef) - { - throw new NotSupportedException(); - } - - var parameterTypeExpression = new Expression[] - { - providerArg, - Expression.Constant(parameterType, typeof(Type)) - }; - - var getServiceCall = Expression.Call(GetServiceInfo, parameterTypeExpression); - methodArguments[i] = Expression.Convert(getServiceCall, parameterType); - } - - Expression middlewareInstanceArg = instanceArg; - if (methodinfo.DeclaringType != typeof(T)) - { - middlewareInstanceArg = Expression.Convert(middlewareInstanceArg, methodinfo.DeclaringType); - } - - var body = Expression.Call(middlewareInstanceArg, methodinfo, methodArguments); - - var lambda = Expression.Lambda>(body, instanceArg, httpContextArg, providerArg); - - return lambda.Compile(); - } - - private static object GetService(IServiceProvider sp, Type type) - { - var service = sp.GetService(type); - if (service == null) - { - throw new InvalidOperationException(); - } - - return service; - } - } -} diff --git a/src/Ocelot/Middleware/UnauthenticatedError.cs b/src/Ocelot/Middleware/UnauthenticatedError.cs index f6ee444ba..4b1c766c2 100644 --- a/src/Ocelot/Middleware/UnauthenticatedError.cs +++ b/src/Ocelot/Middleware/UnauthenticatedError.cs @@ -1,11 +1,12 @@ -using Ocelot.Errors; - -namespace Ocelot.Middleware -{ - public class UnauthenticatedError : Error - { - public UnauthenticatedError(string message) : base(message, OcelotErrorCode.UnauthenticatedError) - { - } - } +using Ocelot.Errors; + +namespace Ocelot.Middleware +{ + public class UnauthenticatedError : Error + { + public UnauthenticatedError(string message) + : base(message, OcelotErrorCode.UnauthenticatedError, 401) + { + } + } } diff --git a/src/Ocelot/Middleware/Multiplexer/CouldNotFindAggregatorError.cs b/src/Ocelot/Multiplexer/CouldNotFindAggregatorError.cs similarity index 73% rename from src/Ocelot/Middleware/Multiplexer/CouldNotFindAggregatorError.cs rename to src/Ocelot/Multiplexer/CouldNotFindAggregatorError.cs index e32109b75..2024fbd99 100644 --- a/src/Ocelot/Middleware/Multiplexer/CouldNotFindAggregatorError.cs +++ b/src/Ocelot/Multiplexer/CouldNotFindAggregatorError.cs @@ -1,11 +1,11 @@ using Ocelot.Errors; -namespace Ocelot.Middleware.Multiplexer +namespace Ocelot.Multiplexer { public class CouldNotFindAggregatorError : Error { public CouldNotFindAggregatorError(string aggregator) - : base($"Could not find Aggregator: {aggregator}", OcelotErrorCode.CouldNotFindAggregatorError) + : base($"Could not find Aggregator: {aggregator}", OcelotErrorCode.CouldNotFindAggregatorError, 404) { } } diff --git a/src/Ocelot/Multiplexer/IDefinedAggregator.cs b/src/Ocelot/Multiplexer/IDefinedAggregator.cs new file mode 100644 index 000000000..40ed8d9a9 --- /dev/null +++ b/src/Ocelot/Multiplexer/IDefinedAggregator.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Middleware; + +namespace Ocelot.Multiplexer +{ + public interface IDefinedAggregator + { + Task Aggregate(List responses); + } +} diff --git a/src/Ocelot/Middleware/Multiplexer/IDefinedAggregatorProvider.cs b/src/Ocelot/Multiplexer/IDefinedAggregatorProvider.cs similarity index 54% rename from src/Ocelot/Middleware/Multiplexer/IDefinedAggregatorProvider.cs rename to src/Ocelot/Multiplexer/IDefinedAggregatorProvider.cs index 375ebc335..2e3941dcb 100644 --- a/src/Ocelot/Middleware/Multiplexer/IDefinedAggregatorProvider.cs +++ b/src/Ocelot/Multiplexer/IDefinedAggregatorProvider.cs @@ -1,10 +1,10 @@ using Ocelot.Configuration; using Ocelot.Responses; -namespace Ocelot.Middleware.Multiplexer +namespace Ocelot.Multiplexer { public interface IDefinedAggregatorProvider { - Response Get(ReRoute reRoute); + Response Get(Route route); } } diff --git a/src/Ocelot/Multiplexer/IResponseAggregator.cs b/src/Ocelot/Multiplexer/IResponseAggregator.cs new file mode 100644 index 000000000..76c1bc667 --- /dev/null +++ b/src/Ocelot/Multiplexer/IResponseAggregator.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +using Ocelot.Configuration; + +using Microsoft.AspNetCore.Http; + +namespace Ocelot.Multiplexer +{ + public interface IResponseAggregator + { + Task Aggregate(Route route, HttpContext originalContext, List downstreamResponses); + } +} diff --git a/src/Ocelot/Middleware/Multiplexer/IResponseAggregatorFactory.cs b/src/Ocelot/Multiplexer/IResponseAggregatorFactory.cs similarity index 50% rename from src/Ocelot/Middleware/Multiplexer/IResponseAggregatorFactory.cs rename to src/Ocelot/Multiplexer/IResponseAggregatorFactory.cs index b032ff1fd..203286216 100644 --- a/src/Ocelot/Middleware/Multiplexer/IResponseAggregatorFactory.cs +++ b/src/Ocelot/Multiplexer/IResponseAggregatorFactory.cs @@ -1,9 +1,9 @@ using Ocelot.Configuration; -namespace Ocelot.Middleware.Multiplexer +namespace Ocelot.Multiplexer { public interface IResponseAggregatorFactory { - IResponseAggregator Get(ReRoute reRoute); + IResponseAggregator Get(Route route); } } diff --git a/src/Ocelot/Middleware/Multiplexer/InMemoryResponseAggregatorFactory.cs b/src/Ocelot/Multiplexer/InMemoryResponseAggregatorFactory.cs similarity index 80% rename from src/Ocelot/Middleware/Multiplexer/InMemoryResponseAggregatorFactory.cs rename to src/Ocelot/Multiplexer/InMemoryResponseAggregatorFactory.cs index ce426d86b..8983caea9 100644 --- a/src/Ocelot/Middleware/Multiplexer/InMemoryResponseAggregatorFactory.cs +++ b/src/Ocelot/Multiplexer/InMemoryResponseAggregatorFactory.cs @@ -1,6 +1,6 @@ using Ocelot.Configuration; -namespace Ocelot.Middleware.Multiplexer +namespace Ocelot.Multiplexer { public class InMemoryResponseAggregatorFactory : IResponseAggregatorFactory { @@ -13,9 +13,9 @@ public InMemoryResponseAggregatorFactory(IDefinedAggregatorProvider provider, IR _simple = responseAggregator; } - public IResponseAggregator Get(ReRoute reRoute) + public IResponseAggregator Get(Route route) { - if (!string.IsNullOrEmpty(reRoute.Aggregator)) + if (!string.IsNullOrEmpty(route.Aggregator)) { return _userDefined; } diff --git a/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs b/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs new file mode 100644 index 000000000..9c8d1a612 --- /dev/null +++ b/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs @@ -0,0 +1,221 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Logging; +using Ocelot.Middleware; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.Multiplexer +{ + public class MultiplexingMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IResponseAggregatorFactory _factory; + + public MultiplexingMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IResponseAggregatorFactory factory + ) + : base(loggerFactory.CreateLogger()) + { + _factory = factory; + _next = next; + } + + public async Task Invoke(HttpContext httpContext) + { + if (httpContext.WebSockets.IsWebSocketRequest) + { + //todo this is obviously stupid + httpContext.Items.UpsertDownstreamRoute(httpContext.Items.DownstreamRouteHolder().Route.DownstreamRoute[0]); + await _next.Invoke(httpContext); + return; + } + + var routeKeysConfigs = httpContext.Items.DownstreamRouteHolder().Route.DownstreamRouteConfig; + if (routeKeysConfigs == null || !routeKeysConfigs.Any()) + { + var downstreamRouteHolder = httpContext.Items.DownstreamRouteHolder(); + + var tasks = new Task[downstreamRouteHolder.Route.DownstreamRoute.Count]; + + for (var i = 0; i < downstreamRouteHolder.Route.DownstreamRoute.Count; i++) + { + var newHttpContext = Copy(httpContext); + + newHttpContext.Items + .Add("RequestId", httpContext.Items["RequestId"]); + newHttpContext.Items + .SetIInternalConfiguration(httpContext.Items.IInternalConfiguration()); + newHttpContext.Items + .UpsertTemplatePlaceholderNameAndValues(httpContext.Items.TemplatePlaceholderNameAndValues()); + newHttpContext.Items + .UpsertDownstreamRoute(downstreamRouteHolder.Route.DownstreamRoute[i]); + + tasks[i] = Fire(newHttpContext, _next); + } + + await Task.WhenAll(tasks); + + var contexts = new List(); + + foreach (var task in tasks) + { + var finished = await task; + contexts.Add(finished); + } + + await Map(httpContext, downstreamRouteHolder.Route, contexts); + } + else + { + httpContext.Items.UpsertDownstreamRoute(httpContext.Items.DownstreamRouteHolder().Route.DownstreamRoute[0]); + var mainResponse = await Fire(httpContext, _next); + + if (httpContext.Items.DownstreamRouteHolder().Route.DownstreamRoute.Count == 1) + { + MapNotAggregate(httpContext, new List { mainResponse }); + return; + } + + var tasks = new List>(); + + if (mainResponse.Items.DownstreamResponse() == null) + { + return; + } + + var content = await mainResponse.Items.DownstreamResponse().Content.ReadAsStringAsync(); + + var jObject = Newtonsoft.Json.Linq.JToken.Parse(content); + + for (var i = 1; i < httpContext.Items.DownstreamRouteHolder().Route.DownstreamRoute.Count; i++) + { + var templatePlaceholderNameAndValues = httpContext.Items.TemplatePlaceholderNameAndValues(); + + var downstreamRoute = httpContext.Items.DownstreamRouteHolder().Route.DownstreamRoute[i]; + + var matchAdvancedAgg = routeKeysConfigs + .FirstOrDefault(q => q.RouteKey == downstreamRoute.Key); + + if (matchAdvancedAgg != null) + { + var values = jObject.SelectTokens(matchAdvancedAgg.JsonPath).Select(s => s.ToString()).Distinct(); + + foreach (var value in values) + { + var newHttpContext = Copy(httpContext); + + var tPnv = httpContext.Items.TemplatePlaceholderNameAndValues(); + tPnv.Add(new PlaceholderNameAndValue('{' + matchAdvancedAgg.Parameter + '}', value)); + + newHttpContext.Items + .Add("RequestId", httpContext.Items["RequestId"]); + + newHttpContext.Items + .SetIInternalConfiguration(httpContext.Items.IInternalConfiguration()); + + newHttpContext.Items + .UpsertTemplatePlaceholderNameAndValues(tPnv); + + newHttpContext.Items + .UpsertDownstreamRoute(downstreamRoute); + + tasks.Add(Fire(newHttpContext, _next)); + } + } + else + { + var newHttpContext = Copy(httpContext); + + newHttpContext.Items + .Add("RequestId", httpContext.Items["RequestId"]); + + newHttpContext.Items + .SetIInternalConfiguration(httpContext.Items.IInternalConfiguration()); + + newHttpContext.Items + .UpsertTemplatePlaceholderNameAndValues(templatePlaceholderNameAndValues); + + newHttpContext.Items + .UpsertDownstreamRoute(downstreamRoute); + + tasks.Add(Fire(newHttpContext, _next)); + } + } + + await Task.WhenAll(tasks); + + var contexts = new List { mainResponse }; + + foreach (var task in tasks) + { + var finished = await task; + contexts.Add(finished); + } + + await Map(httpContext, httpContext.Items.DownstreamRouteHolder().Route, contexts); + } + } + + private static HttpContext Copy(HttpContext source) + { + var target = new DefaultHttpContext(); + + foreach (var header in source.Request.Headers) + { + target.Request.Headers.TryAdd(header.Key, header.Value); + } + + target.Request.Body = source.Request.Body; + target.Request.ContentLength = source.Request.ContentLength; + target.Request.ContentType = source.Request.ContentType; + target.Request.Host = source.Request.Host; + target.Request.Method = source.Request.Method; + target.Request.Path = source.Request.Path; + target.Request.PathBase = source.Request.PathBase; + target.Request.Protocol = source.Request.Protocol; + target.Request.Query = source.Request.Query; + target.Request.QueryString = source.Request.QueryString; + target.Request.Scheme = source.Request.Scheme; + target.Request.IsHttps = source.Request.IsHttps; + target.Request.RouteValues = source.Request.RouteValues; + target.Connection.RemoteIpAddress = source.Connection.RemoteIpAddress; + target.RequestServices = source.RequestServices; + return target; + } + + private async Task Map(HttpContext httpContext, Route route, List contexts) + { + if (route.DownstreamRoute.Count > 1) + { + var aggregator = _factory.Get(route); + await aggregator.Aggregate(route, httpContext, contexts); + } + else + { + MapNotAggregate(httpContext, contexts); + } + } + + private static void MapNotAggregate(HttpContext httpContext, List downstreamContexts) + { + //assume at least one..if this errors then it will be caught by global exception handler + var finished = downstreamContexts.First(); + + httpContext.Items.UpsertErrors(finished.Items.Errors()); + + httpContext.Items.UpsertDownstreamRequest(finished.Items.DownstreamRequest()); + + httpContext.Items.UpsertDownstreamResponse(finished.Items.DownstreamResponse()); + } + + private static async Task Fire(HttpContext httpContext, RequestDelegate next) + { + await next.Invoke(httpContext); + return httpContext; + } + } +} diff --git a/src/Ocelot/Multiplexer/MultiplexingMiddlewareExtensions.cs b/src/Ocelot/Multiplexer/MultiplexingMiddlewareExtensions.cs new file mode 100644 index 000000000..bdd7a9770 --- /dev/null +++ b/src/Ocelot/Multiplexer/MultiplexingMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Multiplexer +{ + public static class MultiplexingMiddlewareExtensions + { + public static IApplicationBuilder UseMultiplexingMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/src/Ocelot/Middleware/Multiplexer/ServiceLocatorDefinedAggregatorProvider.cs b/src/Ocelot/Multiplexer/ServiceLocatorDefinedAggregatorProvider.cs similarity index 74% rename from src/Ocelot/Middleware/Multiplexer/ServiceLocatorDefinedAggregatorProvider.cs rename to src/Ocelot/Multiplexer/ServiceLocatorDefinedAggregatorProvider.cs index c02a4bf74..cce17b2c7 100644 --- a/src/Ocelot/Middleware/Multiplexer/ServiceLocatorDefinedAggregatorProvider.cs +++ b/src/Ocelot/Multiplexer/ServiceLocatorDefinedAggregatorProvider.cs @@ -1,29 +1,31 @@ -using Microsoft.Extensions.DependencyInjection; -using Ocelot.Configuration; -using Ocelot.Responses; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Ocelot.Middleware.Multiplexer -{ - public class ServiceLocatorDefinedAggregatorProvider : IDefinedAggregatorProvider - { - private readonly Dictionary _aggregators; +using System; +using System.Collections.Generic; +using System.Linq; - public ServiceLocatorDefinedAggregatorProvider(IServiceProvider services) - { - _aggregators = services.GetServices().ToDictionary(x => x.GetType().Name); - } - - public Response Get(ReRoute reRoute) - { - if (_aggregators.ContainsKey(reRoute.Aggregator)) - { - return new OkResponse(_aggregators[reRoute.Aggregator]); - } +using Microsoft.Extensions.DependencyInjection; - return new ErrorResponse(new CouldNotFindAggregatorError(reRoute.Aggregator)); - } - } -} +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.Multiplexer +{ + public class ServiceLocatorDefinedAggregatorProvider : IDefinedAggregatorProvider + { + private readonly Dictionary _aggregators; + + public ServiceLocatorDefinedAggregatorProvider(IServiceProvider services) + { + _aggregators = services.GetServices().ToDictionary(x => x.GetType().Name); + } + + public Response Get(Route route) + { + if (_aggregators.ContainsKey(route.Aggregator)) + { + return new OkResponse(_aggregators[route.Aggregator]); + } + + return new ErrorResponse(new CouldNotFindAggregatorError(route.Aggregator)); + } + } +} diff --git a/src/Ocelot/Multiplexer/SimpleJsonResponseAggregator.cs b/src/Ocelot/Multiplexer/SimpleJsonResponseAggregator.cs new file mode 100644 index 000000000..8b1e198ef --- /dev/null +++ b/src/Ocelot/Multiplexer/SimpleJsonResponseAggregator.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Configuration; +using Ocelot.Middleware; + +namespace Ocelot.Multiplexer +{ + public class SimpleJsonResponseAggregator : IResponseAggregator + { + public async Task Aggregate(Route route, HttpContext originalContext, List downstreamContexts) + { + await MapAggregateContent(originalContext, downstreamContexts); + } + + private static async Task MapAggregateContent(HttpContext originalContext, List downstreamContexts) + { + var contentBuilder = new StringBuilder(); + + contentBuilder.Append('{'); + + var responseKeys = downstreamContexts.Select(s => s.Items.DownstreamRoute().Key).Distinct().ToArray(); + + for (var k = 0; k < responseKeys.Length; k++) + { + var contexts = downstreamContexts.Where(w => w.Items.DownstreamRoute().Key == responseKeys[k]).ToArray(); + if (contexts.Length == 1) + { + if (contexts[0].Items.Errors().Count > 0) + { + MapAggregateError(originalContext, contexts[0]); + return; + } + + var content = await contexts[0].Items.DownstreamResponse().Content.ReadAsStringAsync(); + contentBuilder.Append($"\"{responseKeys[k]}\":{content}"); + } + else + { + contentBuilder.Append($"\"{responseKeys[k]}\":"); + contentBuilder.Append('['); + + for (var i = 0; i < contexts.Length; i++) + { + if (contexts[i].Items.Errors().Count > 0) + { + MapAggregateError(originalContext, contexts[i]); + return; + } + + var content = await contexts[i].Items.DownstreamResponse().Content.ReadAsStringAsync(); + if (string.IsNullOrWhiteSpace(content)) + { + continue; + } + + contentBuilder.Append($"{content}"); + + if (i + 1 < contexts.Length) + { + contentBuilder.Append(','); + } + } + + contentBuilder.Append(']'); + } + + if (k + 1 < responseKeys.Length) + { + contentBuilder.Append(','); + } + } + + contentBuilder.Append('}'); + + var stringContent = new StringContent(contentBuilder.ToString()) + { + Headers = { ContentType = new MediaTypeHeaderValue("application/json") }, + }; + + originalContext.Items.UpsertDownstreamResponse(new DownstreamResponse(stringContent, HttpStatusCode.OK, new List>>(), "cannot return from aggregate..which reason phrase would you use?")); + } + + private static void MapAggregateError(HttpContext originalContext, HttpContext downstreamContext) + { + originalContext.Items.UpsertErrors(downstreamContext.Items.Errors()); + originalContext.Items.UpsertDownstreamResponse(downstreamContext.Items.DownstreamResponse()); + } + } +} diff --git a/src/Ocelot/Middleware/Multiplexer/UserDefinedResponseAggregator.cs b/src/Ocelot/Multiplexer/UserDefinedResponseAggregator.cs similarity index 60% rename from src/Ocelot/Middleware/Multiplexer/UserDefinedResponseAggregator.cs rename to src/Ocelot/Multiplexer/UserDefinedResponseAggregator.cs index 47c07c002..2e31a5b91 100644 --- a/src/Ocelot/Middleware/Multiplexer/UserDefinedResponseAggregator.cs +++ b/src/Ocelot/Multiplexer/UserDefinedResponseAggregator.cs @@ -1,8 +1,13 @@ -using Ocelot.Configuration; using System.Collections.Generic; using System.Threading.Tasks; -namespace Ocelot.Middleware.Multiplexer +using Ocelot.Configuration; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Middleware; + +namespace Ocelot.Multiplexer { public class UserDefinedResponseAggregator : IResponseAggregator { @@ -13,20 +18,20 @@ public UserDefinedResponseAggregator(IDefinedAggregatorProvider provider) _provider = provider; } - public async Task Aggregate(ReRoute reRoute, DownstreamContext originalContext, List downstreamResponses) + public async Task Aggregate(Route route, HttpContext originalContext, List downstreamResponses) { - var aggregator = _provider.Get(reRoute); + var aggregator = _provider.Get(route); if (!aggregator.IsError) { var aggregateResponse = await aggregator.Data .Aggregate(downstreamResponses); - originalContext.DownstreamResponse = aggregateResponse; + originalContext.Items.UpsertDownstreamResponse(aggregateResponse); } else { - originalContext.Errors.AddRange(aggregator.Errors); + originalContext.Items.UpsertErrors(aggregator.Errors); } } } diff --git a/src/Ocelot/Ocelot.csproj b/src/Ocelot/Ocelot.csproj index 7c290d0ac..29063e93d 100644 --- a/src/Ocelot/Ocelot.csproj +++ b/src/Ocelot/Ocelot.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1 + net7.0 true Ocelot is an API Gateway. The project is aimed at people using .NET running a micro services / service orientated architecture that need a unified point of entry into their system. In particular I want easy integration with IdentityServer reference and bearer tokens. reference tokens. Ocelot is a bunch of middlewares in a specific order. Ocelot manipulates the HttpRequest object into a state specified by its configuration until it reaches a request builder middleware where it creates a HttpRequestMessage object which is used to make a request to a downstream service. The middleware that makes the request is the last thing in the Ocelot pipeline. It does not call the next middleware. The response from the downstream service is stored in a per request scoped repository and retrived as the requests goes back up the Ocelot pipeline. There is a piece of middleware that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client. That is basically it with a bunch of other features. Ocelot @@ -9,8 +9,7 @@ Ocelot API Gateway;.NET core https://github.com/ThreeMammals/Ocelot - https://github.com/ThreeMammals/Ocelot - http://threemammals.com/images/ocelot_logo.png + https://raw.githubusercontent.com/ThreeMammals/Ocelot/develop/images/ocelot_logo.png win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 false false @@ -18,6 +17,8 @@ false Tom Pallister ..\..\codeanalysis.ruleset + True + 1591 full @@ -25,13 +26,13 @@ - - - + + + NU1701 - - + + all diff --git a/src/Ocelot/Properties/AssemblyInfo.cs b/src/Ocelot/Properties/AssemblyInfo.cs index c2a11a5bf..cf28a5101 100644 --- a/src/Ocelot/Properties/AssemblyInfo.cs +++ b/src/Ocelot/Properties/AssemblyInfo.cs @@ -15,4 +15,4 @@ [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("d6df4206-0dba-41d8-884d-c3e08290fdbb")] +[assembly: Guid("d6df4206-0dba-41d8-884d-c3e08290fdbb")] diff --git a/src/Ocelot/QueryStrings/AddQueriesToRequest.cs b/src/Ocelot/QueryStrings/AddQueriesToRequest.cs index 3df7ffb7a..d77d5ebdc 100644 --- a/src/Ocelot/QueryStrings/AddQueriesToRequest.cs +++ b/src/Ocelot/QueryStrings/AddQueriesToRequest.cs @@ -1,12 +1,14 @@ -using Microsoft.Extensions.Primitives; -using Ocelot.Configuration; -using Ocelot.Infrastructure.Claims.Parser; -using Ocelot.Request.Middleware; -using Ocelot.Responses; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text; + +using Microsoft.Extensions.Primitives; + +using Ocelot.Configuration; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Request.Middleware; +using Ocelot.Responses; namespace Ocelot.QueryStrings { @@ -49,7 +51,7 @@ public Response SetQueriesOnDownstreamRequest(List claimsToThings, return new OkResponse(); } - private Dictionary ConvertQueryStringToDictionary(string queryString) + private static Dictionary ConvertQueryStringToDictionary(string queryString) { var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers .ParseQuery(queryString); @@ -57,29 +59,29 @@ private Dictionary ConvertQueryStringToDictionary(string q return query; } - private string ConvertDictionaryToQueryString(Dictionary queryDictionary) + private static string ConvertDictionaryToQueryString(Dictionary queryDictionary) { var builder = new StringBuilder(); - builder.Append("?"); + builder.Append('?'); - int outerCount = 0; + var outerCount = 0; foreach (var query in queryDictionary) { - for (int innerCount = 0; innerCount < query.Value.Count; innerCount++) + for (var innerCount = 0; innerCount < query.Value.Count; innerCount++) { builder.Append($"{query.Key}={query.Value[innerCount]}"); if (innerCount < (query.Value.Count - 1)) { - builder.Append("&"); + builder.Append('&'); } } if (outerCount < (queryDictionary.Count - 1)) { - builder.Append("&"); + builder.Append('&'); } outerCount++; diff --git a/src/Ocelot/QueryStrings/IAddQueriesToRequest.cs b/src/Ocelot/QueryStrings/IAddQueriesToRequest.cs index 03c634ba7..86b4af307 100644 --- a/src/Ocelot/QueryStrings/IAddQueriesToRequest.cs +++ b/src/Ocelot/QueryStrings/IAddQueriesToRequest.cs @@ -1,8 +1,9 @@ -using Ocelot.Configuration; -using Ocelot.Request.Middleware; -using Ocelot.Responses; -using System.Collections.Generic; +using System.Collections.Generic; using System.Security.Claims; + +using Ocelot.Configuration; +using Ocelot.Request.Middleware; +using Ocelot.Responses; namespace Ocelot.QueryStrings { diff --git a/src/Ocelot/QueryStrings/Middleware/ClaimsToQueryStringMiddleware.cs b/src/Ocelot/QueryStrings/Middleware/ClaimsToQueryStringMiddleware.cs index 4e1a12184..1fbe2c350 100644 --- a/src/Ocelot/QueryStrings/Middleware/ClaimsToQueryStringMiddleware.cs +++ b/src/Ocelot/QueryStrings/Middleware/ClaimsToQueryStringMiddleware.cs @@ -1,42 +1,50 @@ -namespace Ocelot.QueryStrings.Middleware -{ - using Ocelot.Logging; - using Ocelot.Middleware; - using System.Linq; - using System.Threading.Tasks; +using System.Linq; +using System.Threading.Tasks; - public class ClaimsToQueryStringMiddleware : OcelotMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly IAddQueriesToRequest _addQueriesToRequest; +using Ocelot.Logging; - public ClaimsToQueryStringMiddleware(OcelotRequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IAddQueriesToRequest addQueriesToRequest) - : base(loggerFactory.CreateLogger()) - { - _next = next; - _addQueriesToRequest = addQueriesToRequest; - } +using Microsoft.AspNetCore.Http; - public async Task Invoke(DownstreamContext context) - { - if (context.DownstreamReRoute.ClaimsToQueries.Any()) - { - Logger.LogInformation($"{context.DownstreamReRoute.DownstreamPathTemplate.Value} has instructions to convert claims to queries"); - - var response = _addQueriesToRequest.SetQueriesOnDownstreamRequest(context.DownstreamReRoute.ClaimsToQueries, context.HttpContext.User.Claims, context.DownstreamRequest); - - if (response.IsError) - { - Logger.LogWarning("there was an error setting queries on context, setting pipeline error"); - - SetPipelineError(context, response.Errors); - return; - } - } - - await _next.Invoke(context); - } - } -} +using Ocelot.Middleware; + +namespace Ocelot.QueryStrings.Middleware +{ + public class ClaimsToQueryStringMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IAddQueriesToRequest _addQueriesToRequest; + + public ClaimsToQueryStringMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IAddQueriesToRequest addQueriesToRequest) + : base(loggerFactory.CreateLogger()) + { + _next = next; + _addQueriesToRequest = addQueriesToRequest; + } + + public async Task Invoke(HttpContext httpContext) + { + var downstreamRoute = httpContext.Items.DownstreamRoute(); + + if (downstreamRoute.ClaimsToQueries.Any()) + { + Logger.LogInformation($"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to queries"); + + var downstreamRequest = httpContext.Items.DownstreamRequest(); + + var response = _addQueriesToRequest.SetQueriesOnDownstreamRequest(downstreamRoute.ClaimsToQueries, httpContext.User.Claims, downstreamRequest); + + if (response.IsError) + { + Logger.LogWarning("there was an error setting queries on context, setting pipeline error"); + + httpContext.Items.UpsertErrors(response.Errors); + return; + } + } + + await _next.Invoke(httpContext); + } + } +} diff --git a/src/Ocelot/QueryStrings/Middleware/ClaimsToQueryStringMiddlewareExtensions.cs b/src/Ocelot/QueryStrings/Middleware/ClaimsToQueryStringMiddlewareExtensions.cs index e54dd9761..d0bfa74cf 100644 --- a/src/Ocelot/QueryStrings/Middleware/ClaimsToQueryStringMiddlewareExtensions.cs +++ b/src/Ocelot/QueryStrings/Middleware/ClaimsToQueryStringMiddlewareExtensions.cs @@ -1,11 +1,10 @@ -namespace Ocelot.QueryStrings.Middleware -{ - using Microsoft.AspNetCore.Builder; - using Ocelot.Middleware.Pipeline; +using Microsoft.AspNetCore.Builder; +namespace Ocelot.QueryStrings.Middleware +{ public static class ClaimsToQueryStringMiddlewareExtensions { - public static IOcelotPipelineBuilder UseClaimsToQueryStringMiddleware(this IOcelotPipelineBuilder builder) + public static IApplicationBuilder UseClaimsToQueryStringMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware(); } diff --git a/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs b/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs index 06393ddb7..38e46b029 100644 --- a/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs +++ b/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs @@ -1,18 +1,18 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration; -using System; +using System; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Configuration; namespace Ocelot.RateLimit { public class ClientRateLimitProcessor { - private readonly IRateLimitCounterHandler _counterHandler; private readonly RateLimitCore _core; public ClientRateLimitProcessor(IRateLimitCounterHandler counterHandler) { - _counterHandler = counterHandler; - _core = new RateLimitCore(_counterHandler); + _core = new RateLimitCore(counterHandler); } public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option) diff --git a/src/Ocelot/RateLimit/ClientRequestIdentity.cs b/src/Ocelot/RateLimit/ClientRequestIdentity.cs index f112c71e9..b67b7c5a9 100644 --- a/src/Ocelot/RateLimit/ClientRequestIdentity.cs +++ b/src/Ocelot/RateLimit/ClientRequestIdentity.cs @@ -9,10 +9,10 @@ public ClientRequestIdentity(string clientId, string path, string httpverb) HttpVerb = httpverb; } - public string ClientId { get; private set; } + public string ClientId { get; } - public string Path { get; private set; } + public string Path { get; } - public string HttpVerb { get; private set; } + public string HttpVerb { get; } } -} \ No newline at end of file +} diff --git a/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs b/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs index d9eddfe34..e119c612b 100644 --- a/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs +++ b/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs @@ -1,6 +1,8 @@ -using Microsoft.Extensions.Caching.Distributed; -using Newtonsoft.Json; -using System; +using System; + +using Microsoft.Extensions.Caching.Distributed; + +using Newtonsoft.Json; namespace Ocelot.RateLimit { diff --git a/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs b/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs index f71766c8f..328f98b50 100644 --- a/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs +++ b/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs @@ -1,5 +1,6 @@ -using Microsoft.Extensions.Caching.Memory; -using System; +using System; + +using Microsoft.Extensions.Caching.Memory; namespace Ocelot.RateLimit { @@ -17,22 +18,9 @@ public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) _memoryCache.Set(id, counter, new MemoryCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); } - public bool Exists(string id) - { - RateLimitCounter counter; - return _memoryCache.TryGetValue(id, out counter); - } + public bool Exists(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter); - public RateLimitCounter? Get(string id) - { - RateLimitCounter counter; - if (_memoryCache.TryGetValue(id, out counter)) - { - return counter; - } - - return null; - } + public RateLimitCounter? Get(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter) ? counter : null; public void Remove(string id) { diff --git a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs b/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs index 5437ca11d..63d6b2f95 100644 --- a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs +++ b/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs @@ -1,149 +1,159 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration; -using Ocelot.Logging; -using Ocelot.Middleware; -using System.Linq; -using System.Threading.Tasks; - -namespace Ocelot.RateLimit.Middleware -{ - public class ClientRateLimitMiddleware : OcelotMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly IRateLimitCounterHandler _counterHandler; - private readonly ClientRateLimitProcessor _processor; - - public ClientRateLimitMiddleware(OcelotRequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IRateLimitCounterHandler counterHandler) - : base(loggerFactory.CreateLogger()) - { - _next = next; - _counterHandler = counterHandler; - _processor = new ClientRateLimitProcessor(counterHandler); - } - - public async Task Invoke(DownstreamContext context) - { - var options = context.DownstreamReRoute.RateLimitOptions; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +using Ocelot.Configuration; + +using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Middleware; + +namespace Ocelot.RateLimit.Middleware +{ + public class ClientRateLimitMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly ClientRateLimitProcessor _processor; + + public ClientRateLimitMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IRateLimitCounterHandler counterHandler) + : base(loggerFactory.CreateLogger()) + { + _next = next; + _processor = new ClientRateLimitProcessor(counterHandler); + } + + public async Task Invoke(HttpContext httpContext) + { + var downstreamRoute = httpContext.Items.DownstreamRoute(); + + var options = downstreamRoute.RateLimitOptions; // check if rate limiting is enabled - if (!context.DownstreamReRoute.EnableEndpointEndpointRateLimiting) - { - Logger.LogInformation($"EndpointRateLimiting is not enabled for {context.DownstreamReRoute.DownstreamPathTemplate.Value}"); - await _next.Invoke(context); - return; - } - - // compute identity from request - var identity = SetIdentity(context.HttpContext, options); - - // check white list - if (IsWhitelisted(identity, options)) - { - Logger.LogInformation($"{context.DownstreamReRoute.DownstreamPathTemplate.Value} is white listed from rate limiting"); - await _next.Invoke(context); - return; - } - - var rule = options.RateLimitRule; - if (rule.Limit > 0) - { - // increment counter - var counter = _processor.ProcessRequest(identity, options); - - // check if limit is reached - if (counter.TotalRequests > rule.Limit) - { - //compute retry after value - var retryAfter = _processor.RetryAfterFrom(counter.Timestamp, rule); - - // log blocked request - LogBlockedRequest(context.HttpContext, identity, counter, rule, context.DownstreamReRoute); - + if (!downstreamRoute.EnableEndpointEndpointRateLimiting) + { + Logger.LogInformation($"EndpointRateLimiting is not enabled for {downstreamRoute.DownstreamPathTemplate.Value}"); + await _next.Invoke(httpContext); + return; + } + + // compute identity from request + var identity = SetIdentity(httpContext, options); + + // check white list + if (IsWhitelisted(identity, options)) + { + Logger.LogInformation($"{downstreamRoute.DownstreamPathTemplate.Value} is white listed from rate limiting"); + await _next.Invoke(httpContext); + return; + } + + var rule = options.RateLimitRule; + if (rule.Limit > 0) + { + // increment counter + var counter = _processor.ProcessRequest(identity, options); + + // check if limit is reached + if (counter.TotalRequests > rule.Limit) + { + //compute retry after value + var retryAfter = _processor.RetryAfterFrom(counter.Timestamp, rule); + + // log blocked request + LogBlockedRequest(httpContext, identity, counter, rule, downstreamRoute); + var retrystring = retryAfter.ToString(System.Globalization.CultureInfo.InvariantCulture); // break execution - await ReturnQuotaExceededResponse(context.HttpContext, options, retrystring); + var ds = ReturnQuotaExceededResponse(httpContext, options, retrystring); + httpContext.Items.UpsertDownstreamResponse(ds); // Set Error - context.Errors.Add(new QuotaExceededError(this.GetResponseMessage(options))); - - return; - } - } - - //set X-Rate-Limit headers for the longest period - if (!options.DisableRateLimitHeaders) - { - var headers = _processor.GetRateLimitHeaders(context.HttpContext, identity, options); - context.HttpContext.Response.OnStarting(SetRateLimitHeaders, state: headers); - } - - await _next.Invoke(context); - } - - public virtual ClientRequestIdentity SetIdentity(HttpContext httpContext, RateLimitOptions option) - { - var clientId = "client"; - if (httpContext.Request.Headers.Keys.Contains(option.ClientIdHeader)) - { - clientId = httpContext.Request.Headers[option.ClientIdHeader].First(); - } - - return new ClientRequestIdentity( - clientId, - httpContext.Request.Path.ToString().ToLowerInvariant(), - httpContext.Request.Method.ToLowerInvariant() - ); - } - - public bool IsWhitelisted(ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - if (option.ClientWhitelist.Contains(requestIdentity.ClientId)) - { - return true; - } - - return false; - } - - public virtual void LogBlockedRequest(HttpContext httpContext, ClientRequestIdentity identity, RateLimitCounter counter, RateLimitRule rule, DownstreamReRoute downstreamReRoute) - { - 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 { downstreamReRoute.UpstreamPathTemplate.OriginalValue }, TraceIdentifier {httpContext.TraceIdentifier}."); + httpContext.Items.SetError(new QuotaExceededError(GetResponseMessage(options), options.HttpStatusCode)); + + return; + } + } + + //set X-Rate-Limit headers for the longest period + if (!options.DisableRateLimitHeaders) + { + var headers = _processor.GetRateLimitHeaders(httpContext, identity, options); + httpContext.Response.OnStarting(SetRateLimitHeaders, state: headers); + } + + await _next.Invoke(httpContext); } - public virtual Task ReturnQuotaExceededResponse(HttpContext httpContext, RateLimitOptions option, string retryAfter) - { - var message = this.GetResponseMessage(option); - - if (!option.DisableRateLimitHeaders) - { - httpContext.Response.Headers["Retry-After"] = retryAfter; - } - - httpContext.Response.StatusCode = option.HttpStatusCode; - return httpContext.Response.WriteAsync(message); - } - - private string GetResponseMessage(RateLimitOptions option) - { - var message = string.IsNullOrEmpty(option.QuotaExceededMessage) - ? $"API calls quota exceeded! maximum admitted {option.RateLimitRule.Limit} per {option.RateLimitRule.Period}." - : option.QuotaExceededMessage; - return message; - } - - private Task SetRateLimitHeaders(object rateLimitHeaders) - { - var headers = (RateLimitHeaders)rateLimitHeaders; - - headers.Context.Response.Headers["X-Rate-Limit-Limit"] = headers.Limit; - headers.Context.Response.Headers["X-Rate-Limit-Remaining"] = headers.Remaining; - headers.Context.Response.Headers["X-Rate-Limit-Reset"] = headers.Reset; - - return Task.CompletedTask; - } - } + public virtual ClientRequestIdentity SetIdentity(HttpContext httpContext, RateLimitOptions option) + { + var clientId = "client"; + if (httpContext.Request.Headers.Keys.Contains(option.ClientIdHeader)) + { + clientId = httpContext.Request.Headers[option.ClientIdHeader].First(); + } + + return new ClientRequestIdentity( + clientId, + httpContext.Request.Path.ToString().ToLowerInvariant(), + httpContext.Request.Method.ToLowerInvariant() + ); + } + + public bool IsWhitelisted(ClientRequestIdentity requestIdentity, RateLimitOptions option) + { + if (option.ClientWhitelist.Contains(requestIdentity.ClientId)) + { + return true; + } + + return false; + } + + 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}."); + } + + public virtual DownstreamResponse ReturnQuotaExceededResponse(HttpContext httpContext, RateLimitOptions option, string retryAfter) + { + var message = GetResponseMessage(option); + + var http = new HttpResponseMessage((HttpStatusCode)option.HttpStatusCode); + + http.Content = new StringContent(message); + + if (!option.DisableRateLimitHeaders) + { + http.Headers.TryAddWithoutValidation("Retry-After", retryAfter); + } + + return new DownstreamResponse(http); + } + + private static string GetResponseMessage(RateLimitOptions option) + { + var message = string.IsNullOrEmpty(option.QuotaExceededMessage) + ? $"API calls quota exceeded! maximum admitted {option.RateLimitRule.Limit} per {option.RateLimitRule.Period}." + : option.QuotaExceededMessage; + return message; + } + + private static Task SetRateLimitHeaders(object rateLimitHeaders) + { + var headers = (RateLimitHeaders)rateLimitHeaders; + + headers.Context.Response.Headers["X-Rate-Limit-Limit"] = headers.Limit; + headers.Context.Response.Headers["X-Rate-Limit-Remaining"] = headers.Remaining; + headers.Context.Response.Headers["X-Rate-Limit-Reset"] = headers.Reset; + + return Task.CompletedTask; + } + } } diff --git a/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs b/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs index 3d6861967..91609c67f 100644 --- a/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs +++ b/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs @@ -1,12 +1,12 @@ -using Ocelot.Middleware.Pipeline; - -namespace Ocelot.RateLimit.Middleware -{ - public static class RateLimitMiddlewareExtensions - { - public static IOcelotPipelineBuilder UseRateLimiting(this IOcelotPipelineBuilder builder) - { - return builder.UseMiddleware(); - } - } +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.RateLimit.Middleware +{ + public static class RateLimitMiddlewareExtensions + { + public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } } diff --git a/src/Ocelot/RateLimit/QuotaExceededError.cs b/src/Ocelot/RateLimit/QuotaExceededError.cs index f025161cd..9c98dc5a6 100644 --- a/src/Ocelot/RateLimit/QuotaExceededError.cs +++ b/src/Ocelot/RateLimit/QuotaExceededError.cs @@ -4,8 +4,8 @@ namespace Ocelot.RateLimit { public class QuotaExceededError : Error { - public QuotaExceededError(string message) - : base(message, OcelotErrorCode.QuotaExceededError) + public QuotaExceededError(string message, int httpStatusCode) + : base(message, OcelotErrorCode.QuotaExceededError, httpStatusCode) { } } diff --git a/src/Ocelot/RateLimit/RateLimitCore.cs b/src/Ocelot/RateLimit/RateLimitCore.cs index e2e93e01d..fba124874 100644 --- a/src/Ocelot/RateLimit/RateLimitCore.cs +++ b/src/Ocelot/RateLimit/RateLimitCore.cs @@ -1,16 +1,18 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration; -using System; +using System; using System.Globalization; using System.Security.Cryptography; using System.Text; +using Microsoft.AspNetCore.Http; + +using Ocelot.Configuration; + namespace Ocelot.RateLimit { public class RateLimitCore { private readonly IRateLimitCounterHandler _counterHandler; - private static readonly object _processLocker = new object(); + private static readonly object ProcessLocker = new(); public RateLimitCore(IRateLimitCounterHandler counterStore) { @@ -19,13 +21,13 @@ public RateLimitCore(IRateLimitCounterHandler counterStore) public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option) { - RateLimitCounter counter = new RateLimitCounter(DateTime.UtcNow, 1); + var counter = new RateLimitCounter(DateTime.UtcNow, 1); var rule = option.RateLimitRule; var counterId = ComputeCounterKey(requestIdentity, option); // serial reads and writes - lock (_processLocker) + lock (ProcessLocker) { var entry = _counterHandler.Get(counterId); if (entry.HasValue) @@ -76,7 +78,7 @@ public void SaveRateLimitCounter(ClientRequestIdentity requestIdentity, RateLimi public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option) { var rule = option.RateLimitRule; - RateLimitHeaders headers = null; + RateLimitHeaders headers; var counterId = ComputeCounterKey(requestIdentity, option); var entry = _counterHandler.Get(counterId); if (entry.HasValue) diff --git a/src/Ocelot/RateLimit/RateLimitCounter.cs b/src/Ocelot/RateLimit/RateLimitCounter.cs index ac355e39e..097afb732 100644 --- a/src/Ocelot/RateLimit/RateLimitCounter.cs +++ b/src/Ocelot/RateLimit/RateLimitCounter.cs @@ -1,10 +1,11 @@ -using Newtonsoft.Json; -using System; +using System; + +using Newtonsoft.Json; namespace Ocelot.RateLimit { /// - /// Stores the initial access time and the numbers of calls made from that point + /// Stores the initial access time and the numbers of calls made from that point. /// public struct RateLimitCounter { @@ -15,8 +16,8 @@ public RateLimitCounter(DateTime timestamp, long totalRequests) TotalRequests = totalRequests; } - public DateTime Timestamp { get; private set; } + public DateTime Timestamp { get; } - public long TotalRequests { get; private set; } + public long TotalRequests { get; } } } diff --git a/src/Ocelot/RateLimit/RateLimitHeaders.cs b/src/Ocelot/RateLimit/RateLimitHeaders.cs index b93cf04db..67d7596ce 100644 --- a/src/Ocelot/RateLimit/RateLimitHeaders.cs +++ b/src/Ocelot/RateLimit/RateLimitHeaders.cs @@ -12,12 +12,12 @@ public RateLimitHeaders(HttpContext context, string limit, string remaining, str Reset = reset; } - public HttpContext Context { get; private set; } + public HttpContext Context { get; } - public string Limit { get; private set; } + public string Limit { get; } - public string Remaining { get; private set; } + public string Remaining { get; } - public string Reset { get; private set; } + public string Reset { get; } } } diff --git a/src/Ocelot/Request/Creator/DownstreamRequestCreator.cs b/src/Ocelot/Request/Creator/DownstreamRequestCreator.cs index 560b6735e..03d9ff6da 100644 --- a/src/Ocelot/Request/Creator/DownstreamRequestCreator.cs +++ b/src/Ocelot/Request/Creator/DownstreamRequestCreator.cs @@ -1,9 +1,9 @@ +using Ocelot.Infrastructure; +using Ocelot.Request.Middleware; +using System.Net.Http; + namespace Ocelot.Request.Creator { - using Ocelot.Infrastructure; - using Ocelot.Request.Middleware; - using System.Net.Http; - public class DownstreamRequestCreator : IDownstreamRequestCreator { private readonly IFrameworkDescription _framework; @@ -14,15 +14,17 @@ public DownstreamRequestCreator(IFrameworkDescription framework) _framework = framework; } + /// + /// According to https://tools.ietf.org/html/rfc7231 + /// GET,HEAD,DELETE,CONNECT,TRACE + /// Can have body but server can reject the request. + /// And MS HttpClient in Full Framework actually rejects it. + /// See #366 issue. + /// + /// The HTTP request. + /// A object. public DownstreamRequest Create(HttpRequestMessage request) { - /** - * According to https://tools.ietf.org/html/rfc7231 - * GET,HEAD,DELETE,CONNECT,TRACE - * Can have body but server can reject the request. - * And MS HttpClient in Full Framework actually rejects it. - * see #366 issue - **/ if (_framework.Get().Contains(DotNetFramework)) { if (request.Method == HttpMethod.Get || diff --git a/src/Ocelot/Request/Creator/IDownstreamRequestCreator.cs b/src/Ocelot/Request/Creator/IDownstreamRequestCreator.cs index dd87d5c8a..0065837e4 100644 --- a/src/Ocelot/Request/Creator/IDownstreamRequestCreator.cs +++ b/src/Ocelot/Request/Creator/IDownstreamRequestCreator.cs @@ -1,8 +1,9 @@ +using System.Net.Http; + +using Ocelot.Request.Middleware; + namespace Ocelot.Request.Creator { - using Ocelot.Request.Middleware; - using System.Net.Http; - public interface IDownstreamRequestCreator { DownstreamRequest Create(HttpRequestMessage request); diff --git a/src/Ocelot/Request/Mapper/IRequestMapper.cs b/src/Ocelot/Request/Mapper/IRequestMapper.cs index d16a2d064..56b9178f8 100644 --- a/src/Ocelot/Request/Mapper/IRequestMapper.cs +++ b/src/Ocelot/Request/Mapper/IRequestMapper.cs @@ -1,12 +1,16 @@ -namespace Ocelot.Request.Mapper -{ - using Microsoft.AspNetCore.Http; - using Ocelot.Responses; - using System.Net.Http; - using System.Threading.Tasks; +using System.Net.Http; +using System.Threading.Tasks; - public interface IRequestMapper - { - Task> Map(HttpRequest request); - } -} +using Ocelot.Configuration; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Responses; + +namespace Ocelot.Request.Mapper +{ + public interface IRequestMapper + { + Task> Map(HttpRequest request, DownstreamRoute downstreamRoute); + } +} diff --git a/src/Ocelot/Request/Mapper/RequestMapper.cs b/src/Ocelot/Request/Mapper/RequestMapper.cs index f669c2863..8a5bba465 100644 --- a/src/Ocelot/Request/Mapper/RequestMapper.cs +++ b/src/Ocelot/Request/Mapper/RequestMapper.cs @@ -1,112 +1,119 @@ -namespace Ocelot.Request.Mapper -{ - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Http.Extensions; - using Microsoft.Extensions.Primitives; - using Ocelot.Responses; - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Net.Http; - using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; - public class RequestMapper : IRequestMapper - { - private readonly string[] _unsupportedHeaders = { "host" }; +using Ocelot.Configuration; - public async Task> Map(HttpRequest request) - { - try - { - var requestMessage = new HttpRequestMessage() - { - Content = await MapContent(request), - Method = MapMethod(request), - RequestUri = MapUri(request) - }; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.Primitives; - MapHeaders(request, requestMessage); - - return new OkResponse(requestMessage); - } - catch (Exception ex) - { - return new ErrorResponse(new UnmappableRequestError(ex)); - } - } - - private async Task MapContent(HttpRequest request) - { - if (request.Body == null || (request.Body.CanSeek && request.Body.Length <= 0)) - { - return null; - } - - // Never change this to StreamContent again, I forgot it doesnt work in #464. - var content = new ByteArrayContent(await ToByteArray(request.Body)); - - if (!string.IsNullOrEmpty(request.ContentType)) - { - content.Headers - .TryAddWithoutValidation("Content-Type", new[] { request.ContentType }); - } - - AddHeaderIfExistsOnRequest("Content-Language", content, request); - AddHeaderIfExistsOnRequest("Content-Location", content, request); - AddHeaderIfExistsOnRequest("Content-Range", content, request); - AddHeaderIfExistsOnRequest("Content-MD5", content, request); - AddHeaderIfExistsOnRequest("Content-Disposition", content, request); - AddHeaderIfExistsOnRequest("Content-Encoding", content, request); - - return content; - } - - private void AddHeaderIfExistsOnRequest(string key, HttpContent content, HttpRequest request) - { - if (request.Headers.ContainsKey(key)) - { - content.Headers - .TryAddWithoutValidation(key, request.Headers[key].ToList()); - } - } - - private HttpMethod MapMethod(HttpRequest request) - { - return new HttpMethod(request.Method); - } - - private Uri MapUri(HttpRequest request) - { - return new Uri(request.GetEncodedUrl()); - } - - private void MapHeaders(HttpRequest request, HttpRequestMessage requestMessage) - { - foreach (var header in request.Headers) - { - if (IsSupportedHeader(header)) - { - requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); - } - } - } - - private bool IsSupportedHeader(KeyValuePair header) - { - return !_unsupportedHeaders.Contains(header.Key.ToLower()); - } - - private async Task ToByteArray(Stream stream) - { - using (stream) - { - using (var memStream = new MemoryStream()) - { - await stream.CopyToAsync(memStream); - return memStream.ToArray(); - } - } - } - } -} +using Ocelot.Responses; + +namespace Ocelot.Request.Mapper +{ + public class RequestMapper : IRequestMapper + { + private readonly string[] _unsupportedHeaders = { "host" }; + + public async Task> Map(HttpRequest request, DownstreamRoute downstreamRoute) + { + try + { + var requestMessage = new HttpRequestMessage + { + Content = await MapContent(request), + Method = MapMethod(request, downstreamRoute), + RequestUri = MapUri(request), + Version = downstreamRoute.DownstreamHttpVersion, + }; + + MapHeaders(request, requestMessage); + + return new OkResponse(requestMessage); + } + catch (Exception ex) + { + return new ErrorResponse(new UnmappableRequestError(ex)); + } + } + + private static async Task MapContent(HttpRequest request) + { + if (request.Body == null || (request.Body.CanSeek && request.Body.Length <= 0)) + { + return null; + } + + // Never change this to StreamContent again, I forgot it doesnt work in #464. + var content = new ByteArrayContent(await ToByteArray(request.Body)); + + if (!string.IsNullOrEmpty(request.ContentType)) + { + content.Headers + .TryAddWithoutValidation("Content-Type", new[] { request.ContentType }); + } + + AddHeaderIfExistsOnRequest("Content-Language", content, request); + AddHeaderIfExistsOnRequest("Content-Location", content, request); + AddHeaderIfExistsOnRequest("Content-Range", content, request); + AddHeaderIfExistsOnRequest("Content-MD5", content, request); + AddHeaderIfExistsOnRequest("Content-Disposition", content, request); + AddHeaderIfExistsOnRequest("Content-Encoding", content, request); + + return content; + } + + private static void AddHeaderIfExistsOnRequest(string key, HttpContent content, HttpRequest request) + { + if (request.Headers.ContainsKey(key)) + { + content.Headers + .TryAddWithoutValidation(key, request.Headers[key].ToArray()); + } + } + + private static HttpMethod MapMethod(HttpRequest request, DownstreamRoute downstreamRoute) + { + if (!string.IsNullOrEmpty(downstreamRoute?.DownstreamHttpMethod)) + { + return new HttpMethod(downstreamRoute.DownstreamHttpMethod); + } + + return new HttpMethod(request.Method); + } + + private static Uri MapUri(HttpRequest request) => new(request.GetEncodedUrl()); + + private void MapHeaders(HttpRequest request, HttpRequestMessage requestMessage) + { + foreach (var header in request.Headers) + { + if (IsSupportedHeader(header)) + { + requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); + } + } + } + + private bool IsSupportedHeader(KeyValuePair header) + { + return !_unsupportedHeaders.Contains(header.Key.ToLower()); + } + + private static async Task ToByteArray(Stream stream) + { + await using (stream) + { + using (var memStream = new MemoryStream()) + { + await stream.CopyToAsync(memStream); + return memStream.ToArray(); + } + } + } + } +} diff --git a/src/Ocelot/Request/Mapper/UnmappableRequestError.cs b/src/Ocelot/Request/Mapper/UnmappableRequestError.cs index fa5a602af..44563f7f8 100644 --- a/src/Ocelot/Request/Mapper/UnmappableRequestError.cs +++ b/src/Ocelot/Request/Mapper/UnmappableRequestError.cs @@ -1,12 +1,13 @@ -namespace Ocelot.Request.Mapper -{ - using Ocelot.Errors; - using System; - - public class UnmappableRequestError : Error - { - public UnmappableRequestError(Exception exception) : base($"Error when parsing incoming request, exception: {exception}", OcelotErrorCode.UnmappableRequestError) - { - } - } +using System; + +using Ocelot.Errors; + +namespace Ocelot.Request.Mapper +{ + public class UnmappableRequestError : Error + { + public UnmappableRequestError(Exception exception) : base($"Error when parsing incoming request, exception: {exception}", OcelotErrorCode.UnmappableRequestError, 404) + { + } + } } diff --git a/src/Ocelot/Request/Middleware/DownstreamRequest.cs b/src/Ocelot/Request/Middleware/DownstreamRequest.cs index 256436e52..9b4e0b755 100644 --- a/src/Ocelot/Request/Middleware/DownstreamRequest.cs +++ b/src/Ocelot/Request/Middleware/DownstreamRequest.cs @@ -1,9 +1,9 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; + namespace Ocelot.Request.Middleware { - using System; - using System.Net.Http; - using System.Net.Http.Headers; - public class DownstreamRequest { private readonly HttpRequestMessage _request; @@ -48,10 +48,11 @@ public HttpRequestMessage ToHttpRequestMessage() Host = Host, Path = AbsolutePath, Query = RemoveLeadingQuestionMark(Query), - Scheme = Scheme + Scheme = Scheme, }; _request.RequestUri = uriBuilder.Uri; + _request.Method = new HttpMethod(Method); return _request; } @@ -63,7 +64,7 @@ public string ToUri() Host = Host, Path = AbsolutePath, Query = RemoveLeadingQuestionMark(Query), - Scheme = Scheme + Scheme = Scheme, }; return uriBuilder.Uri.AbsoluteUri; @@ -74,9 +75,9 @@ public override string ToString() return ToUri(); } - private string RemoveLeadingQuestionMark(string query) + private static string RemoveLeadingQuestionMark(string query) { - if (!string.IsNullOrEmpty(query) && query.StartsWith("?")) + if (!string.IsNullOrEmpty(query) && query.StartsWith('?')) { return query.Substring(1); } diff --git a/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs b/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs index 442bb4b99..4d78fa309 100644 --- a/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs +++ b/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs @@ -1,17 +1,22 @@ +using System.Threading.Tasks; + +using Ocelot.Request.Creator; + +using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Middleware; + namespace Ocelot.Request.Middleware { - using Ocelot.Logging; - using Ocelot.Middleware; - using Ocelot.Request.Creator; - using System.Threading.Tasks; - public class DownstreamRequestInitialiserMiddleware : OcelotMiddleware { - private readonly OcelotRequestDelegate _next; + private readonly RequestDelegate _next; private readonly Mapper.IRequestMapper _requestMapper; private readonly IDownstreamRequestCreator _creator; - public DownstreamRequestInitialiserMiddleware(OcelotRequestDelegate next, + public DownstreamRequestInitialiserMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, Mapper.IRequestMapper requestMapper, IDownstreamRequestCreator creator) @@ -22,19 +27,23 @@ public DownstreamRequestInitialiserMiddleware(OcelotRequestDelegate next, _creator = creator; } - public async Task Invoke(DownstreamContext context) + public async Task Invoke(HttpContext httpContext) { - var downstreamRequest = await _requestMapper.Map(context.HttpContext.Request); + var downstreamRoute = httpContext.Items.DownstreamRoute(); - if (downstreamRequest.IsError) + var httpRequestMessage = await _requestMapper.Map(httpContext.Request, downstreamRoute); + + if (httpRequestMessage.IsError) { - SetPipelineError(context, downstreamRequest.Errors); + httpContext.Items.UpsertErrors(httpRequestMessage.Errors); return; } - context.DownstreamRequest = _creator.Create(downstreamRequest.Data); + var downstreamRequest = _creator.Create(httpRequestMessage.Data); + + httpContext.Items.UpsertDownstreamRequest(downstreamRequest); - await _next.Invoke(context); + await _next.Invoke(httpContext); } } } diff --git a/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddlewareExtensions.cs b/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddlewareExtensions.cs index 2d803e1e5..6be68781f 100644 --- a/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddlewareExtensions.cs +++ b/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddlewareExtensions.cs @@ -1,13 +1,12 @@ -using Microsoft.AspNetCore.Builder; -using Ocelot.Middleware.Pipeline; - -namespace Ocelot.Request.Middleware -{ - public static class HttpRequestBuilderMiddlewareExtensions - { - public static IOcelotPipelineBuilder UseDownstreamRequestInitialiser(this IOcelotPipelineBuilder builder) - { - return builder.UseMiddleware(); - } - } +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Request.Middleware +{ + public static class HttpRequestBuilderMiddlewareExtensions + { + public static IApplicationBuilder UseDownstreamRequestInitialiser(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } } diff --git a/src/Ocelot/RequestId/Middleware/ReRouteRequestIdMiddleware.cs b/src/Ocelot/RequestId/Middleware/ReRouteRequestIdMiddleware.cs deleted file mode 100644 index 4c802a027..000000000 --- a/src/Ocelot/RequestId/Middleware/ReRouteRequestIdMiddleware.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Ocelot.Infrastructure.RequestData; -using Ocelot.Logging; -using Ocelot.Middleware; -using Ocelot.Request.Middleware; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http.Headers; -using System.Threading.Tasks; - -namespace Ocelot.RequestId.Middleware -{ - public class ReRouteRequestIdMiddleware : OcelotMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly IRequestScopedDataRepository _requestScopedDataRepository; - - public ReRouteRequestIdMiddleware(OcelotRequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IRequestScopedDataRepository requestScopedDataRepository) - : base(loggerFactory.CreateLogger()) - { - _next = next; - _requestScopedDataRepository = requestScopedDataRepository; - } - - public async Task Invoke(DownstreamContext context) - { - SetOcelotRequestId(context); - await _next.Invoke(context); - } - - private void SetOcelotRequestId(DownstreamContext context) - { - var key = context.DownstreamReRoute.RequestIdKey ?? DefaultRequestIdKey.Value; - - if (context.HttpContext.Request.Headers.TryGetValue(key, out var upstreamRequestIds)) - { - context.HttpContext.TraceIdentifier = upstreamRequestIds.First(); - - var previousRequestId = _requestScopedDataRepository.Get("RequestId"); - if (!previousRequestId.IsError && !string.IsNullOrEmpty(previousRequestId.Data) && previousRequestId.Data != context.HttpContext.TraceIdentifier) - { - _requestScopedDataRepository.Add("PreviousRequestId", previousRequestId.Data); - _requestScopedDataRepository.Update("RequestId", context.HttpContext.TraceIdentifier); - } - else - { - _requestScopedDataRepository.Add("RequestId", context.HttpContext.TraceIdentifier); - } - } - - var requestId = new RequestId(context.DownstreamReRoute.RequestIdKey, context.HttpContext.TraceIdentifier); - - if (ShouldAddRequestId(requestId, context.DownstreamRequest.Headers)) - { - AddRequestIdHeader(requestId, context.DownstreamRequest); - } - } - - private bool ShouldAddRequestId(RequestId requestId, HttpRequestHeaders headers) - { - return !string.IsNullOrEmpty(requestId?.RequestIdKey) - && !string.IsNullOrEmpty(requestId.RequestIdValue) - && !RequestIdInHeaders(requestId, headers); - } - - private bool RequestIdInHeaders(RequestId requestId, HttpRequestHeaders headers) - { - IEnumerable value; - return headers.TryGetValues(requestId.RequestIdKey, out value); - } - - private void AddRequestIdHeader(RequestId requestId, DownstreamRequest httpRequestMessage) - { - httpRequestMessage.Headers.Add(requestId.RequestIdKey, requestId.RequestIdValue); - } - } -} diff --git a/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs b/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs new file mode 100644 index 000000000..5d5f45b97 --- /dev/null +++ b/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs @@ -0,0 +1,84 @@ +using System.Linq; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +using Ocelot.Infrastructure.RequestData; + +using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Middleware; +using Ocelot.Request.Middleware; + +namespace Ocelot.RequestId.Middleware +{ + public class RequestIdMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IRequestScopedDataRepository _requestScopedDataRepository; + public RequestIdMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IRequestScopedDataRepository requestScopedDataRepository) + : base(loggerFactory.CreateLogger()) + { + _next = next; + _requestScopedDataRepository = requestScopedDataRepository; + } + + public async Task Invoke(HttpContext httpContext) + { + SetOcelotRequestId(httpContext); + await _next.Invoke(httpContext); + } + + private void SetOcelotRequestId(HttpContext httpContext) + { + var downstreamRoute = httpContext.Items.DownstreamRoute(); + + var key = downstreamRoute.RequestIdKey ?? DefaultRequestIdKey.Value; + + if (httpContext.Request.Headers.TryGetValue(key, out var upstreamRequestIds)) + { + httpContext.TraceIdentifier = upstreamRequestIds.First(); + + var previousRequestId = _requestScopedDataRepository.Get("RequestId"); + if (!previousRequestId.IsError && !string.IsNullOrEmpty(previousRequestId.Data) && previousRequestId.Data != httpContext.TraceIdentifier) + { + _requestScopedDataRepository.Add("PreviousRequestId", previousRequestId.Data); + _requestScopedDataRepository.Update("RequestId", httpContext.TraceIdentifier); + } + else + { + _requestScopedDataRepository.Add("RequestId", httpContext.TraceIdentifier); + } + } + + var requestId = new RequestId(downstreamRoute.RequestIdKey, httpContext.TraceIdentifier); + + var downstreamRequest = httpContext.Items.DownstreamRequest(); + + if (ShouldAddRequestId(requestId, downstreamRequest.Headers)) + { + AddRequestIdHeader(requestId, downstreamRequest); + } + } + + private static bool ShouldAddRequestId(RequestId requestId, HttpRequestHeaders headers) + { + return !string.IsNullOrEmpty(requestId?.RequestIdKey) + && !string.IsNullOrEmpty(requestId.RequestIdValue) + && !RequestIdInHeaders(requestId, headers); + } + + private static bool RequestIdInHeaders(RequestId requestId, HttpRequestHeaders headers) + { + return headers.TryGetValues(requestId.RequestIdKey, out var value); + } + + private static void AddRequestIdHeader(RequestId requestId, DownstreamRequest httpRequestMessage) + { + httpRequestMessage.Headers.Add(requestId.RequestIdKey, requestId.RequestIdValue); + } + } +} diff --git a/src/Ocelot/RequestId/Middleware/RequestIdMiddlewareExtensions.cs b/src/Ocelot/RequestId/Middleware/RequestIdMiddlewareExtensions.cs index ce4e892de..9b614e061 100644 --- a/src/Ocelot/RequestId/Middleware/RequestIdMiddlewareExtensions.cs +++ b/src/Ocelot/RequestId/Middleware/RequestIdMiddlewareExtensions.cs @@ -1,13 +1,12 @@ -using Microsoft.AspNetCore.Builder; -using Ocelot.Middleware.Pipeline; - -namespace Ocelot.RequestId.Middleware -{ - public static class RequestIdMiddlewareExtensions - { - public static IOcelotPipelineBuilder UseRequestIdMiddleware(this IOcelotPipelineBuilder builder) - { - return builder.UseMiddleware(); - } - } +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.RequestId.Middleware +{ + public static class RequestIdMiddlewareExtensions + { + public static IApplicationBuilder UseRequestIdMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } } diff --git a/src/Ocelot/RequestId/RequestId.cs b/src/Ocelot/RequestId/RequestId.cs index 5d24088e6..004957c5b 100644 --- a/src/Ocelot/RequestId/RequestId.cs +++ b/src/Ocelot/RequestId/RequestId.cs @@ -8,7 +8,7 @@ public RequestId(string requestIdKey, string requestIdValue) RequestIdValue = requestIdValue; } - public string RequestIdKey { get; private set; } - public string RequestIdValue { get; private set; } + public string RequestIdKey { get; } + public string RequestIdValue { get; } } } diff --git a/src/Ocelot/Requester/ConnectionToDownstreamServiceError.cs b/src/Ocelot/Requester/ConnectionToDownstreamServiceError.cs new file mode 100644 index 000000000..1ae9cbe2b --- /dev/null +++ b/src/Ocelot/Requester/ConnectionToDownstreamServiceError.cs @@ -0,0 +1,14 @@ +using System; + +using Ocelot.Errors; + +namespace Ocelot.Requester +{ + public class ConnectionToDownstreamServiceError : Error + { + public ConnectionToDownstreamServiceError(Exception exception) + : base($"Error connecting to downstream service, exception: {exception}", OcelotErrorCode.ConnectionToDownstreamServiceError, 502) + { + } + } +} diff --git a/src/Ocelot/Requester/DelegatingHandlerHandlerFactory.cs b/src/Ocelot/Requester/DelegatingHandlerHandlerFactory.cs index 3e868b322..5aa6f21a9 100644 --- a/src/Ocelot/Requester/DelegatingHandlerHandlerFactory.cs +++ b/src/Ocelot/Requester/DelegatingHandlerHandlerFactory.cs @@ -1,106 +1,106 @@ -namespace Ocelot.Requester -{ - using Logging; - using Microsoft.Extensions.DependencyInjection; - using Ocelot.Configuration; - using Ocelot.Responses; - using QoS; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; - public class DelegatingHandlerHandlerFactory : IDelegatingHandlerHandlerFactory - { - private readonly ITracingHandlerFactory _tracingFactory; - private readonly IQoSFactory _qoSFactory; - private readonly IServiceProvider _serviceProvider; - private readonly IOcelotLogger _logger; +using Ocelot.Configuration; - public DelegatingHandlerHandlerFactory( - ITracingHandlerFactory tracingFactory, - IQoSFactory qoSFactory, - IServiceProvider serviceProvider, - IOcelotLoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - _serviceProvider = serviceProvider; - _tracingFactory = tracingFactory; - _qoSFactory = qoSFactory; - } +using Ocelot.Logging; - public Response>> Get(DownstreamReRoute downstreamReRoute) - { - var globalDelegatingHandlers = _serviceProvider - .GetServices() - .ToList(); +using Microsoft.Extensions.DependencyInjection; - var reRouteSpecificHandlers = _serviceProvider - .GetServices() - .ToList(); +using Ocelot.Requester.QoS; - var handlers = new List>(); - - foreach (var handler in globalDelegatingHandlers) - { - if (GlobalIsInHandlersConfig(downstreamReRoute, handler)) - { - reRouteSpecificHandlers.Add(handler.DelegatingHandler); - } - else - { - handlers.Add(() => handler.DelegatingHandler); - } - } - - if (downstreamReRoute.DelegatingHandlers.Any()) - { - var sorted = SortByConfigOrder(downstreamReRoute, reRouteSpecificHandlers); - - foreach (var handler in sorted) - { - handlers.Add(() => handler); - } - } - - if (downstreamReRoute.HttpHandlerOptions.UseTracing) - { - handlers.Add(() => (DelegatingHandler)_tracingFactory.Get()); - } - - if (downstreamReRoute.QosOptions.UseQos) - { - var handler = _qoSFactory.Get(downstreamReRoute); - - if (handler != null && !handler.IsError) - { - handlers.Add(() => handler.Data); - } - else - { - _logger.LogWarning($"ReRoute {downstreamReRoute.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()); - } - } - - return new OkResponse>>(handlers); - } - - private List SortByConfigOrder(DownstreamReRoute request, List reRouteSpecificHandlers) - { - return reRouteSpecificHandlers - .Where(x => request.DelegatingHandlers.Contains(x.GetType().Name)) - .OrderBy(d => - { - var type = d.GetType().Name; - var pos = request.DelegatingHandlers.IndexOf(type); - return pos; - }).ToList(); - } - - private bool GlobalIsInHandlersConfig(DownstreamReRoute request, GlobalDelegatingHandler handler) - { - return request.DelegatingHandlers.Contains(handler.DelegatingHandler.GetType().Name); - } - } -} +using Ocelot.Responses; + +namespace Ocelot.Requester +{ + public class DelegatingHandlerHandlerFactory : IDelegatingHandlerHandlerFactory + { + private readonly ITracingHandlerFactory _tracingFactory; + private readonly IQoSFactory _qoSFactory; + private readonly IServiceProvider _serviceProvider; + private readonly IOcelotLogger _logger; + + public DelegatingHandlerHandlerFactory( + ITracingHandlerFactory tracingFactory, + IQoSFactory qoSFactory, + IServiceProvider serviceProvider, + IOcelotLoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + _serviceProvider = serviceProvider; + _tracingFactory = tracingFactory; + _qoSFactory = qoSFactory; + } + + public Response>> Get(DownstreamRoute downstreamRoute) + { + var globalDelegatingHandlers = _serviceProvider + .GetServices() + .ToArray(); + + var routeSpecificHandlers = _serviceProvider + .GetServices() + .ToList(); + + var handlers = new List>(); + + foreach (var handler in globalDelegatingHandlers) + { + if (GlobalIsInHandlersConfig(downstreamRoute, handler)) + { + routeSpecificHandlers.Add(handler.DelegatingHandler); + } + else + { + handlers.Add(() => handler.DelegatingHandler); + } + } + + if (downstreamRoute.DelegatingHandlers.Any()) + { + var sorted = SortByConfigOrder(downstreamRoute, routeSpecificHandlers); + + handlers.AddRange(sorted.Select(handler => (Func)(() => handler))); + } + + if (downstreamRoute.HttpHandlerOptions.UseTracing) + { + handlers.Add(() => (DelegatingHandler)_tracingFactory.Get()); + } + + if (downstreamRoute.QosOptions.UseQos) + { + var handler = _qoSFactory.Get(downstreamRoute); + + if (handler?.IsError == false) + { + handlers.Add(() => handler.Data); + } + 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!"); + handlers.Add(() => new NoQosDelegatingHandler()); + } + } + + return new OkResponse>>(handlers); + } + + private static IEnumerable SortByConfigOrder(DownstreamRoute request, IEnumerable routeSpecificHandlers) + { + return routeSpecificHandlers + .Where(x => request.DelegatingHandlers.Contains(x.GetType().Name)) + .OrderBy(d => + { + var type = d.GetType().Name; + var pos = request.DelegatingHandlers.IndexOf(type); + return pos; + }).ToArray(); + } + + private static bool GlobalIsInHandlersConfig(DownstreamRoute request, GlobalDelegatingHandler handler) => + request.DelegatingHandlers.Contains(handler.DelegatingHandler.GetType().Name); + } +} diff --git a/src/Ocelot/Requester/GlobalDelegatingHandler.cs b/src/Ocelot/Requester/GlobalDelegatingHandler.cs index ba5e1c5f4..3ef15e9a1 100644 --- a/src/Ocelot/Requester/GlobalDelegatingHandler.cs +++ b/src/Ocelot/Requester/GlobalDelegatingHandler.cs @@ -9,6 +9,6 @@ public GlobalDelegatingHandler(DelegatingHandler delegatingHandler) DelegatingHandler = delegatingHandler; } - public DelegatingHandler DelegatingHandler { get; private set; } + public DelegatingHandler DelegatingHandler { get; } } } diff --git a/src/Ocelot/Requester/HttpClientBuilder.cs b/src/Ocelot/Requester/HttpClientBuilder.cs index e7d458578..89ee7ef18 100644 --- a/src/Ocelot/Requester/HttpClientBuilder.cs +++ b/src/Ocelot/Requester/HttpClientBuilder.cs @@ -1,11 +1,12 @@ -using Ocelot.Configuration; -using Ocelot.Logging; -using Ocelot.Middleware; -using System; +using System; using System.Linq; using System.Net; using System.Net.Http; +using Ocelot.Configuration; + +using Ocelot.Logging; + namespace Ocelot.Requester { public class HttpClientBuilder : IHttpClientBuilder @@ -13,7 +14,7 @@ public class HttpClientBuilder : IHttpClientBuilder private readonly IDelegatingHandlerHandlerFactory _factory; private readonly IHttpClientCache _cacheHandlers; private readonly IOcelotLogger _logger; - private DownstreamReRoute _cacheKey; + private DownstreamRoute _cacheKey; private HttpClient _httpClient; private IHttpClient _client; private readonly TimeSpan _defaultTimeout; @@ -32,9 +33,9 @@ public HttpClientBuilder( _defaultTimeout = TimeSpan.FromSeconds(90); } - public IHttpClient Create(DownstreamContext context) + public IHttpClient Create(DownstreamRoute downstreamRoute) { - _cacheKey = context.DownstreamReRoute; + _cacheKey = downstreamRoute; var httpClient = _cacheHandlers.Get(_cacheKey); @@ -44,23 +45,24 @@ public IHttpClient Create(DownstreamContext context) return httpClient; } - var handler = CreateHandler(context); + var handler = CreateHandler(downstreamRoute); - if (context.DownstreamReRoute.DangerousAcceptAnyServerCertificateValidator) + if (downstreamRoute.DangerousAcceptAnyServerCertificateValidator) { - handler.ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) => true; + handler.ServerCertificateCustomValidationCallback = + HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; _logger - .LogWarning($"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamReRoute, UpstreamPathTemplate: {context.DownstreamReRoute.UpstreamPathTemplate}, DownstreamPathTemplate: {context.DownstreamReRoute.DownstreamPathTemplate}"); + .LogWarning($"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamRoute, UpstreamPathTemplate: {downstreamRoute.UpstreamPathTemplate}, DownstreamPathTemplate: {downstreamRoute.DownstreamPathTemplate}"); } - var timeout = context.DownstreamReRoute.QosOptions.TimeoutValue == 0 + var timeout = downstreamRoute.QosOptions.TimeoutValue == 0 ? _defaultTimeout - : TimeSpan.FromMilliseconds(context.DownstreamReRoute.QosOptions.TimeoutValue); + : TimeSpan.FromMilliseconds(downstreamRoute.QosOptions.TimeoutValue); - _httpClient = new HttpClient(CreateHttpMessageHandler(handler, context.DownstreamReRoute)) + _httpClient = new HttpClient(CreateHttpMessageHandler(handler, downstreamRoute)) { - Timeout = timeout + Timeout = timeout, }; _client = new HttpClientWrapper(_httpClient); @@ -68,43 +70,35 @@ public IHttpClient Create(DownstreamContext context) return _client; } - private HttpClientHandler CreateHandler(DownstreamContext context) + private static HttpClientHandler CreateHandler(DownstreamRoute downstreamRoute) { // Dont' create the CookieContainer if UseCookies is not set or the HttpClient will complain // under .Net Full Framework - bool useCookies = context.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer; + var useCookies = downstreamRoute.HttpHandlerOptions.UseCookieContainer; - if (useCookies) - { - return UseCookiesHandler(context); - } - else - { - return UseNonCookiesHandler(context); - } + return useCookies ? UseCookiesHandler(downstreamRoute) : UseNonCookiesHandler(downstreamRoute); } - private HttpClientHandler UseNonCookiesHandler(DownstreamContext context) + private static HttpClientHandler UseNonCookiesHandler(DownstreamRoute downstreamRoute) { return new HttpClientHandler { - AllowAutoRedirect = context.DownstreamReRoute.HttpHandlerOptions.AllowAutoRedirect, - UseCookies = context.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer, - UseProxy = context.DownstreamReRoute.HttpHandlerOptions.UseProxy, - MaxConnectionsPerServer = context.DownstreamReRoute.HttpHandlerOptions.MaxConnectionsPerServer - + AllowAutoRedirect = downstreamRoute.HttpHandlerOptions.AllowAutoRedirect, + UseCookies = downstreamRoute.HttpHandlerOptions.UseCookieContainer, + UseProxy = downstreamRoute.HttpHandlerOptions.UseProxy, + MaxConnectionsPerServer = downstreamRoute.HttpHandlerOptions.MaxConnectionsPerServer, }; } - private HttpClientHandler UseCookiesHandler(DownstreamContext context) + private static HttpClientHandler UseCookiesHandler(DownstreamRoute downstreamRoute) { return new HttpClientHandler { - AllowAutoRedirect = context.DownstreamReRoute.HttpHandlerOptions.AllowAutoRedirect, - UseCookies = context.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer, - UseProxy = context.DownstreamReRoute.HttpHandlerOptions.UseProxy, - MaxConnectionsPerServer = context.DownstreamReRoute.HttpHandlerOptions.MaxConnectionsPerServer, - CookieContainer = new CookieContainer() + AllowAutoRedirect = downstreamRoute.HttpHandlerOptions.AllowAutoRedirect, + UseCookies = downstreamRoute.HttpHandlerOptions.UseCookieContainer, + UseProxy = downstreamRoute.HttpHandlerOptions.UseProxy, + MaxConnectionsPerServer = downstreamRoute.HttpHandlerOptions.MaxConnectionsPerServer, + CookieContainer = new CookieContainer(), }; } @@ -113,7 +107,7 @@ public void Save() _cacheHandlers.Set(_cacheKey, _client, TimeSpan.FromHours(24)); } - private HttpMessageHandler CreateHttpMessageHandler(HttpMessageHandler httpMessageHandler, DownstreamReRoute request) + private HttpMessageHandler CreateHttpMessageHandler(HttpMessageHandler httpMessageHandler, DownstreamRoute request) { //todo handle error var handlers = _factory.Get(request).Data; diff --git a/src/Ocelot/Requester/HttpClientHttpRequester.cs b/src/Ocelot/Requester/HttpClientHttpRequester.cs index eecf37db7..1d03e1c72 100644 --- a/src/Ocelot/Requester/HttpClientHttpRequester.cs +++ b/src/Ocelot/Requester/HttpClientHttpRequester.cs @@ -1,10 +1,15 @@ -using Ocelot.Logging; -using Ocelot.Middleware; -using Ocelot.Responses; using System; using System.Net.Http; using System.Threading.Tasks; +using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Middleware; + +using Ocelot.Responses; + namespace Ocelot.Requester { public class HttpClientHttpRequester : IHttpRequester @@ -25,15 +30,19 @@ public HttpClientHttpRequester(IOcelotLoggerFactory loggerFactory, _mapper = mapper; } - public async Task> GetResponse(DownstreamContext context) + public async Task> GetResponse(HttpContext httpContext) { var builder = new HttpClientBuilder(_factory, _cacheHandlers, _logger); - var httpClient = builder.Create(context); + var downstreamRoute = httpContext.Items.DownstreamRoute(); + + var downstreamRequest = httpContext.Items.DownstreamRequest(); + + var httpClient = builder.Create(downstreamRoute); try { - var response = await httpClient.SendAsync(context.DownstreamRequest.ToHttpRequestMessage(), context.HttpContext.RequestAborted); + var response = await httpClient.SendAsync(downstreamRequest.ToHttpRequestMessage(), httpContext.RequestAborted); return new OkResponse(response); } catch (Exception exception) diff --git a/src/Ocelot/Requester/HttpExeptionToErrorMapper.cs b/src/Ocelot/Requester/HttpExeptionToErrorMapper.cs index f2fbad2dc..e11475684 100644 --- a/src/Ocelot/Requester/HttpExeptionToErrorMapper.cs +++ b/src/Ocelot/Requester/HttpExeptionToErrorMapper.cs @@ -1,10 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; + +using Ocelot.Errors; + +using Microsoft.Extensions.DependencyInjection; + namespace Ocelot.Requester { - using Ocelot.Errors; - using Microsoft.Extensions.DependencyInjection; - using System; - using System.Collections.Generic; - public class HttpExeptionToErrorMapper : IExceptionToErrorMapper { private readonly Dictionary> _mappers; @@ -28,6 +31,11 @@ public Error Map(Exception exception) return new RequestCanceledError(exception.Message); } + if (type == typeof(HttpRequestException)) + { + return new ConnectionToDownstreamServiceError(exception); + } + return new UnableToCompleteRequestError(exception); } } diff --git a/src/Ocelot/Requester/IDelegatingHandlerHandlerFactory.cs b/src/Ocelot/Requester/IDelegatingHandlerHandlerFactory.cs index cf0b6d07a..a63e897d4 100644 --- a/src/Ocelot/Requester/IDelegatingHandlerHandlerFactory.cs +++ b/src/Ocelot/Requester/IDelegatingHandlerHandlerFactory.cs @@ -1,13 +1,15 @@ -namespace Ocelot.Requester -{ - using Ocelot.Configuration; - using Ocelot.Responses; - using System; - using System.Collections.Generic; - using System.Net.Http; +using System; +using System.Collections.Generic; +using System.Net.Http; - public interface IDelegatingHandlerHandlerFactory - { - Response>> Get(DownstreamReRoute downstreamReRoute); - } -} +using Ocelot.Configuration; + +using Ocelot.Responses; + +namespace Ocelot.Requester +{ + public interface IDelegatingHandlerHandlerFactory + { + Response>> Get(DownstreamRoute downstreamRoute); + } +} diff --git a/src/Ocelot/Requester/IExceptionToErrorMapper.cs b/src/Ocelot/Requester/IExceptionToErrorMapper.cs index dd23f10bd..aa29bdbbe 100644 --- a/src/Ocelot/Requester/IExceptionToErrorMapper.cs +++ b/src/Ocelot/Requester/IExceptionToErrorMapper.cs @@ -1,5 +1,6 @@ -using Ocelot.Errors; -using System; +using System; + +using Ocelot.Errors; namespace Ocelot.Requester { diff --git a/src/Ocelot/Requester/IHttpClientBuilder.cs b/src/Ocelot/Requester/IHttpClientBuilder.cs index 40bb2949e..b284a69ba 100644 --- a/src/Ocelot/Requester/IHttpClientBuilder.cs +++ b/src/Ocelot/Requester/IHttpClientBuilder.cs @@ -1,10 +1,10 @@ -using Ocelot.Middleware; +using Ocelot.Configuration; namespace Ocelot.Requester { public interface IHttpClientBuilder { - IHttpClient Create(DownstreamContext request); + IHttpClient Create(DownstreamRoute downstreamRoute); void Save(); } diff --git a/src/Ocelot/Requester/IHttpClientCache.cs b/src/Ocelot/Requester/IHttpClientCache.cs index ac4cd2fd6..6c1bf8903 100644 --- a/src/Ocelot/Requester/IHttpClientCache.cs +++ b/src/Ocelot/Requester/IHttpClientCache.cs @@ -1,12 +1,13 @@ -namespace Ocelot.Requester -{ - using Configuration; - using System; +using System; + +using Ocelot.Configuration; +namespace Ocelot.Requester +{ public interface IHttpClientCache { - IHttpClient Get(DownstreamReRoute key); + IHttpClient Get(DownstreamRoute key); - void Set(DownstreamReRoute key, IHttpClient handler, TimeSpan expirationTime); + void Set(DownstreamRoute key, IHttpClient handler, TimeSpan expirationTime); } } diff --git a/src/Ocelot/Requester/IHttpRequester.cs b/src/Ocelot/Requester/IHttpRequester.cs index 18f5ed78b..30824312b 100644 --- a/src/Ocelot/Requester/IHttpRequester.cs +++ b/src/Ocelot/Requester/IHttpRequester.cs @@ -1,12 +1,14 @@ -using Ocelot.Middleware; -using Ocelot.Responses; -using System.Net.Http; -using System.Threading.Tasks; - -namespace Ocelot.Requester -{ - public interface IHttpRequester - { - Task> GetResponse(DownstreamContext context); - } +using System.Net.Http; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Responses; + +namespace Ocelot.Requester +{ + public interface IHttpRequester + { + Task> GetResponse(HttpContext httpContext); + } } diff --git a/src/Ocelot/Requester/MemoryHttpClientCache.cs b/src/Ocelot/Requester/MemoryHttpClientCache.cs index 433d4ac68..5fc629867 100644 --- a/src/Ocelot/Requester/MemoryHttpClientCache.cs +++ b/src/Ocelot/Requester/MemoryHttpClientCache.cs @@ -1,27 +1,28 @@ -namespace Ocelot.Requester -{ - using Configuration; - using System; - using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; - public class MemoryHttpClientCache : IHttpClientCache - { - private readonly ConcurrentDictionary _httpClientsCache; - - public MemoryHttpClientCache() - { - _httpClientsCache = new ConcurrentDictionary(); - } - - public void Set(DownstreamReRoute key, IHttpClient client, TimeSpan expirationTime) - { - _httpClientsCache.AddOrUpdate(key, client, (k, oldValue) => client); - } - - public IHttpClient Get(DownstreamReRoute key) - { - //todo handle error? - return _httpClientsCache.TryGetValue(key, out var client) ? client : null; - } - } -} +using Ocelot.Configuration; + +namespace Ocelot.Requester +{ + public class MemoryHttpClientCache : IHttpClientCache + { + private readonly ConcurrentDictionary _httpClientsCache; + + public MemoryHttpClientCache() + { + _httpClientsCache = new ConcurrentDictionary(); + } + + public void Set(DownstreamRoute key, IHttpClient client, TimeSpan expirationTime) + { + _httpClientsCache.AddOrUpdate(key, client, (k, oldValue) => client); + } + + public IHttpClient Get(DownstreamRoute key) + { + //todo handle error? + return _httpClientsCache.TryGetValue(key, out var client) ? client : null; + } + } +} diff --git a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs index a48aa84d1..f347e2f9a 100644 --- a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs +++ b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs @@ -1,59 +1,66 @@ -using System.Net; -using System.Net.Http; -using Ocelot.Logging; -using Ocelot.Middleware; -using System.Threading.Tasks; -using Ocelot.Responses; - -namespace Ocelot.Requester.Middleware -{ - public class HttpRequesterMiddleware : OcelotMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly IHttpRequester _requester; - - public HttpRequesterMiddleware(OcelotRequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IHttpRequester requester) - : base(loggerFactory.CreateLogger()) - { - _next = next; - _requester = requester; - } - - public async Task Invoke(DownstreamContext context) - { - var response = await _requester.GetResponse(context); - - CreateLogBasedOnResponse(response); - - if (response.IsError) - { - Logger.LogDebug("IHttpRequester returned an error, setting pipeline error"); - - SetPipelineError(context, response.Errors); - return; - } - - Logger.LogDebug("setting http response message"); - - context.DownstreamResponse = new DownstreamResponse(response.Data); - - await _next.Invoke(context); - } - - private void CreateLogBasedOnResponse(Response response) - { - if (response.Data?.StatusCode <= HttpStatusCode.BadRequest) - { - 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}"); - } - } - } +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Middleware; + +using Ocelot.Responses; + +namespace Ocelot.Requester.Middleware +{ + public class HttpRequesterMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IHttpRequester _requester; + + public HttpRequesterMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IHttpRequester requester) + : base(loggerFactory.CreateLogger()) + { + _next = next; + _requester = requester; + } + + public async Task Invoke(HttpContext httpContext) + { + var downstreamRoute = httpContext.Items.DownstreamRoute(); + + var response = await _requester.GetResponse(httpContext); + + CreateLogBasedOnResponse(response); + + if (response.IsError) + { + Logger.LogDebug("IHttpRequester returned an error, setting pipeline error"); + + httpContext.Items.UpsertErrors(response.Errors); + return; + } + + Logger.LogDebug("setting http response message"); + + httpContext.Items.UpsertDownstreamResponse(new DownstreamResponse(response.Data)); + + await _next.Invoke(httpContext); + } + + private void CreateLogBasedOnResponse(Response response) + { + if (response.Data?.StatusCode <= HttpStatusCode.BadRequest) + { + 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}"); + } + } + } } diff --git a/src/Ocelot/Requester/Middleware/HttpRequesterMiddlewareExtensions.cs b/src/Ocelot/Requester/Middleware/HttpRequesterMiddlewareExtensions.cs index d92f7909a..722aeeb6b 100644 --- a/src/Ocelot/Requester/Middleware/HttpRequesterMiddlewareExtensions.cs +++ b/src/Ocelot/Requester/Middleware/HttpRequesterMiddlewareExtensions.cs @@ -1,13 +1,12 @@ -using Microsoft.AspNetCore.Builder; -using Ocelot.Middleware.Pipeline; - -namespace Ocelot.Requester.Middleware -{ - public static class HttpRequesterMiddlewareExtensions - { - public static IOcelotPipelineBuilder UseHttpRequesterMiddleware(this IOcelotPipelineBuilder builder) - { - return builder.UseMiddleware(); - } - } +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Requester.Middleware +{ + public static class HttpRequesterMiddlewareExtensions + { + public static IApplicationBuilder UseHttpRequesterMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } } diff --git a/src/Ocelot/Requester/NoQosDelegatingHandler.cs b/src/Ocelot/Requester/NoQosDelegatingHandler.cs index 995b50eda..a50e2c764 100644 --- a/src/Ocelot/Requester/NoQosDelegatingHandler.cs +++ b/src/Ocelot/Requester/NoQosDelegatingHandler.cs @@ -1,7 +1,7 @@ +using System.Net.Http; + namespace Ocelot.Requester { - using System.Net.Http; - public class NoQosDelegatingHandler : DelegatingHandler { } diff --git a/src/Ocelot/Requester/OcelotHttpTracingHandler.cs b/src/Ocelot/Requester/OcelotHttpTracingHandler.cs index 0bf569227..f41687d93 100644 --- a/src/Ocelot/Requester/OcelotHttpTracingHandler.cs +++ b/src/Ocelot/Requester/OcelotHttpTracingHandler.cs @@ -1,12 +1,14 @@ -namespace Ocelot.Requester -{ - using Logging; - using Ocelot.Infrastructure.RequestData; - using System; - using System.Net.Http; - using System.Threading; - using System.Threading.Tasks; +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; + +namespace Ocelot.Requester +{ public class OcelotHttpTracingHandler : DelegatingHandler, ITracingHandler { private readonly ITracer _tracer; diff --git a/src/Ocelot/Requester/QoS/IQosFactory.cs b/src/Ocelot/Requester/QoS/IQosFactory.cs index 5a3477cbb..b386cc4bc 100644 --- a/src/Ocelot/Requester/QoS/IQosFactory.cs +++ b/src/Ocelot/Requester/QoS/IQosFactory.cs @@ -1,11 +1,13 @@ -namespace Ocelot.Requester.QoS -{ - using Configuration; - using Responses; - using System.Net.Http; +using System.Net.Http; - public interface IQoSFactory - { - Response Get(DownstreamReRoute request); - } -} +using Ocelot.Configuration; + +using Ocelot.Responses; + +namespace Ocelot.Requester.QoS +{ + public interface IQoSFactory + { + Response Get(DownstreamRoute request); + } +} diff --git a/src/Ocelot/Requester/QoS/QosFactory.cs b/src/Ocelot/Requester/QoS/QosFactory.cs index 711bc2c79..47e7b3ec4 100644 --- a/src/Ocelot/Requester/QoS/QosFactory.cs +++ b/src/Ocelot/Requester/QoS/QosFactory.cs @@ -1,33 +1,37 @@ -namespace Ocelot.Requester.QoS -{ - using Configuration; - using Logging; - using Microsoft.Extensions.DependencyInjection; - using Responses; - using System; - using System.Net.Http; +using System; +using System.Net.Http; - public class QoSFactory : IQoSFactory - { - private readonly IServiceProvider _serviceProvider; - private readonly IOcelotLoggerFactory _ocelotLoggerFactory; +using Ocelot.Configuration; - public QoSFactory(IServiceProvider serviceProvider, IOcelotLoggerFactory ocelotLoggerFactory) - { - _serviceProvider = serviceProvider; - _ocelotLoggerFactory = ocelotLoggerFactory; - } +using Ocelot.Logging; - public Response Get(DownstreamReRoute request) - { - var handler = _serviceProvider.GetService(); +using Microsoft.Extensions.DependencyInjection; - if (handler != null) - { - return new OkResponse(handler(request, _ocelotLoggerFactory)); - } - - return new ErrorResponse(new UnableToFindQoSProviderError($"could not find qosProvider for {request.DownstreamScheme}{request.DownstreamAddresses}{request.DownstreamPathTemplate}")); - } - } -} +using Ocelot.Responses; + +namespace Ocelot.Requester.QoS +{ + public class QoSFactory : IQoSFactory + { + private readonly IServiceProvider _serviceProvider; + private readonly IOcelotLoggerFactory _ocelotLoggerFactory; + + public QoSFactory(IServiceProvider serviceProvider, IOcelotLoggerFactory ocelotLoggerFactory) + { + _serviceProvider = serviceProvider; + _ocelotLoggerFactory = ocelotLoggerFactory; + } + + public Response Get(DownstreamRoute request) + { + var handler = _serviceProvider.GetService(); + + if (handler != null) + { + return new OkResponse(handler(request, _ocelotLoggerFactory)); + } + + return new ErrorResponse(new UnableToFindQoSProviderError($"could not find qosProvider for {request.DownstreamScheme}{request.DownstreamAddresses}{request.DownstreamPathTemplate}")); + } + } +} diff --git a/src/Ocelot/Requester/QoS/UnableToFindQoSProviderError.cs b/src/Ocelot/Requester/QoS/UnableToFindQoSProviderError.cs index de0882ac3..87ead7b93 100644 --- a/src/Ocelot/Requester/QoS/UnableToFindQoSProviderError.cs +++ b/src/Ocelot/Requester/QoS/UnableToFindQoSProviderError.cs @@ -1,12 +1,12 @@ -namespace Ocelot.Requester.QoS -{ - using Ocelot.Errors; +using Ocelot.Errors; - public class UnableToFindQoSProviderError : Error - { +namespace Ocelot.Requester.QoS +{ + public class UnableToFindQoSProviderError : Error + { public UnableToFindQoSProviderError(string message) - : base(message, OcelotErrorCode.UnableToFindQoSProviderError) - { - } - } + : base(message, OcelotErrorCode.UnableToFindQoSProviderError, 404) + { + } + } } diff --git a/src/Ocelot/Requester/QosDelegatingHandlerDelegate.cs b/src/Ocelot/Requester/QosDelegatingHandlerDelegate.cs index 7b76c4c9a..326a63d9b 100644 --- a/src/Ocelot/Requester/QosDelegatingHandlerDelegate.cs +++ b/src/Ocelot/Requester/QosDelegatingHandlerDelegate.cs @@ -1,8 +1,10 @@ -namespace Ocelot.Requester -{ - using Configuration; - using Logging; - using System.Net.Http; +using System.Net.Http; - public delegate DelegatingHandler QosDelegatingHandlerDelegate(DownstreamReRoute reRoute, IOcelotLoggerFactory logger); -} +using Ocelot.Configuration; + +using Ocelot.Logging; + +namespace Ocelot.Requester +{ + public delegate DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute route, IOcelotLoggerFactory logger); +} diff --git a/src/Ocelot/Requester/RequestCanceledError.cs b/src/Ocelot/Requester/RequestCanceledError.cs index 1944fada5..d778451bc 100644 --- a/src/Ocelot/Requester/RequestCanceledError.cs +++ b/src/Ocelot/Requester/RequestCanceledError.cs @@ -4,7 +4,16 @@ namespace Ocelot.Requester { public class RequestCanceledError : Error { - public RequestCanceledError(string message) : base(message, OcelotErrorCode.RequestCanceled) + /// + /// Initializes a new instance of the class. + /// Creates object by the message. + /// Status code refer to: + /// https://stackoverflow.com/questions/46234679/what-is-the-correct-http-status-code-for-a-cancelled-request?answertab=votes#tab-top . + /// https://httpstatuses.com/499 . + /// + /// The message text. + public RequestCanceledError(string message) + : base(message, OcelotErrorCode.RequestCanceled, 499) // https://httpstatuses.com/499 { } } diff --git a/src/Ocelot/Requester/TracingHandlerFactory.cs b/src/Ocelot/Requester/TracingHandlerFactory.cs index afb268458..986defa68 100644 --- a/src/Ocelot/Requester/TracingHandlerFactory.cs +++ b/src/Ocelot/Requester/TracingHandlerFactory.cs @@ -1,10 +1,13 @@ +using System; + +using Ocelot.Infrastructure.RequestData; + +using Ocelot.Logging; + +using Microsoft.Extensions.DependencyInjection; + namespace Ocelot.Requester { - using Logging; - using Microsoft.Extensions.DependencyInjection; - using Ocelot.Infrastructure.RequestData; - using System; - public class TracingHandlerFactory : ITracingHandlerFactory { private readonly ITracer _tracer; diff --git a/src/Ocelot/Requester/UnableToCompleteRequestError.cs b/src/Ocelot/Requester/UnableToCompleteRequestError.cs index 26302039f..f5afe175f 100644 --- a/src/Ocelot/Requester/UnableToCompleteRequestError.cs +++ b/src/Ocelot/Requester/UnableToCompleteRequestError.cs @@ -1,13 +1,14 @@ -using Ocelot.Errors; -using System; - -namespace Ocelot.Requester -{ - public class UnableToCompleteRequestError : Error - { +using System; + +using Ocelot.Errors; + +namespace Ocelot.Requester +{ + public class UnableToCompleteRequestError : Error + { public UnableToCompleteRequestError(Exception exception) - : base($"Error making http request, exception: {exception}", OcelotErrorCode.UnableToCompleteRequestError) - { - } - } + : base($"Error making http request, exception: {exception}", OcelotErrorCode.UnableToCompleteRequestError, 500) + { + } + } } diff --git a/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs b/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs index c50daa273..128c54024 100644 --- a/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs +++ b/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs @@ -1,51 +1,64 @@ -using Ocelot.Errors; -using System.Collections.Generic; -using System.Linq; - -namespace Ocelot.Responder -{ - public class ErrorsToHttpStatusCodeMapper : IErrorsToHttpStatusCodeMapper - { - public int Map(List errors) - { - if (errors.Any(e => e.Code == OcelotErrorCode.UnauthenticatedError)) - { - return 401; - } - +using System.Collections.Generic; +using System.Linq; + +using Ocelot.Errors; + +namespace Ocelot.Responder +{ + public class ErrorsToHttpStatusCodeMapper : IErrorsToHttpStatusCodeMapper + { + public int Map(List errors) + { + if (errors.Any(e => e.Code == OcelotErrorCode.UnauthenticatedError)) + { + return 401; + } + if (errors.Any(e => e.Code == OcelotErrorCode.UnauthorizedError - || e.Code == OcelotErrorCode.ClaimValueNotAuthorisedError - || e.Code == OcelotErrorCode.ScopeNotAuthorisedError - || e.Code == OcelotErrorCode.UserDoesNotHaveClaimError - || e.Code == OcelotErrorCode.CannotFindClaimError)) - { - return 403; - } - - if (errors.Any(e => e.Code == OcelotErrorCode.RequestTimedOutError)) - { - return 503; - } - - if (errors.Any(e => e.Code == OcelotErrorCode.RequestCanceled)) - { - // status code refer to + || e.Code == OcelotErrorCode.ClaimValueNotAuthorizedError + || e.Code == OcelotErrorCode.ScopeNotAuthorizedError + || e.Code == OcelotErrorCode.UserDoesNotHaveClaimError + || e.Code == OcelotErrorCode.CannotFindClaimError)) + { + return 403; + } + + if (errors.Any(e => e.Code == OcelotErrorCode.QuotaExceededError)) + { + return errors.Single(e => e.Code == OcelotErrorCode.QuotaExceededError).HttpStatusCode; + } + + if (errors.Any(e => e.Code == OcelotErrorCode.RequestTimedOutError)) + { + return 503; + } + + if (errors.Any(e => e.Code == OcelotErrorCode.RequestCanceled)) + { + // status code refer to // https://stackoverflow.com/questions/46234679/what-is-the-correct-http-status-code-for-a-cancelled-request?answertab=votes#tab-top - // https://httpstatuses.com/499 - return 499; - } - - if (errors.Any(e => e.Code == OcelotErrorCode.UnableToFindDownstreamRouteError)) - { - return 404; - } - - if (errors.Any(e => e.Code == OcelotErrorCode.UnableToCompleteRequestError)) - { - return 500; - } - - return 404; - } - } + // https://httpstatuses.com/499 + return 499; + } + + if (errors.Any(e => e.Code == OcelotErrorCode.UnableToFindDownstreamRouteError)) + { + return 404; + } + + if (errors.Any(e => e.Code == OcelotErrorCode.ConnectionToDownstreamServiceError)) + { + return 502; + } + + if (errors.Any(e => e.Code == OcelotErrorCode.UnableToCompleteRequestError + || e.Code == OcelotErrorCode.CouldNotFindLoadBalancerCreator + || e.Code == OcelotErrorCode.ErrorInvokingLoadBalancerCreator)) + { + return 500; + } + + return 404; + } + } } diff --git a/src/Ocelot/Responder/HttpContextResponder.cs b/src/Ocelot/Responder/HttpContextResponder.cs index 509df689f..e072846f0 100644 --- a/src/Ocelot/Responder/HttpContextResponder.cs +++ b/src/Ocelot/Responder/HttpContextResponder.cs @@ -1,17 +1,19 @@ -using Microsoft.AspNetCore.Http; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; + using Ocelot.Headers; using Ocelot.Middleware; -using System.Linq; -using System.Net; -using System.Threading.Tasks; namespace Ocelot.Responder { /// /// Cannot unit test things in this class due to methods not being implemented - /// on .net concretes used for testing + /// on .net concretes used for testing. /// public class HttpContextResponder : IHttpResponder { @@ -52,7 +54,7 @@ public async Task SetResponseOnHttpContext(HttpContext context, DownstreamRespon AddHeaderIfDoesntExist(context, new Header("Content-Length", new[] { response.Content.Headers.ContentLength.ToString() })); } - using (content) + await using (content) { if (response.StatusCode != HttpStatusCode.NotModified && context.Response.ContentLength != 0) { @@ -66,7 +68,25 @@ public void SetErrorResponseOnContext(HttpContext context, int statusCode) SetStatusCode(context, statusCode); } - private void SetStatusCode(HttpContext context, int statusCode) + public async Task SetErrorResponseOnContext(HttpContext context, DownstreamResponse response) + { + var content = await response.Content.ReadAsStreamAsync(); + + if (response.Content.Headers.ContentLength != null) + { + AddHeaderIfDoesntExist(context, new Header("Content-Length", new[] { response.Content.Headers.ContentLength.ToString() })); + } + + await using (content) + { + if (context.Response.ContentLength != 0) + { + await content.CopyToAsync(context.Response.Body); + } + } + } + + private static void SetStatusCode(HttpContext context, int statusCode) { if (!context.Response.HasStarted) { diff --git a/src/Ocelot/Responder/IErrorsToHttpStatusCodeMapper.cs b/src/Ocelot/Responder/IErrorsToHttpStatusCodeMapper.cs index 26da864f6..766b5a0a2 100644 --- a/src/Ocelot/Responder/IErrorsToHttpStatusCodeMapper.cs +++ b/src/Ocelot/Responder/IErrorsToHttpStatusCodeMapper.cs @@ -1,10 +1,11 @@ -using Ocelot.Errors; -using System.Collections.Generic; +using System.Collections.Generic; +using Ocelot.Errors; + namespace Ocelot.Responder -{ +{ /// - /// Map a list OceoltErrors to a single appropriate HTTP status code + /// Map a list OceoltErrors to a single appropriate HTTP status code. /// public interface IErrorsToHttpStatusCodeMapper { diff --git a/src/Ocelot/Responder/IHttpResponder.cs b/src/Ocelot/Responder/IHttpResponder.cs index 5a32950a8..8d44a1c89 100644 --- a/src/Ocelot/Responder/IHttpResponder.cs +++ b/src/Ocelot/Responder/IHttpResponder.cs @@ -1,13 +1,17 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Middleware; -using System.Threading.Tasks; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Middleware; namespace Ocelot.Responder -{ +{ public interface IHttpResponder { Task SetResponseOnHttpContext(HttpContext context, DownstreamResponse response); - void SetErrorResponseOnContext(HttpContext context, int statusCode); + void SetErrorResponseOnContext(HttpContext context, int statusCode); + + Task SetErrorResponseOnContext(HttpContext context, DownstreamResponse response); } } diff --git a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs index 43f01acba..2dd84139f 100644 --- a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs +++ b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs @@ -1,10 +1,16 @@ -using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + using Ocelot.Errors; + using Ocelot.Infrastructure.Extensions; + using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; + using Ocelot.Middleware; -using System.Collections.Generic; -using System.Threading.Tasks; namespace Ocelot.Responder.Middleware { @@ -13,11 +19,11 @@ namespace Ocelot.Responder.Middleware /// public class ResponderMiddleware : OcelotMiddleware { - private readonly OcelotRequestDelegate _next; + private readonly RequestDelegate _next; private readonly IHttpResponder _responder; private readonly IErrorsToHttpStatusCodeMapper _codeMapper; - public ResponderMiddleware(OcelotRequestDelegate next, + public ResponderMiddleware(RequestDelegate next, IHttpResponder responder, IOcelotLoggerFactory loggerFactory, IErrorsToHttpStatusCodeMapper codeMapper @@ -29,27 +35,40 @@ IErrorsToHttpStatusCodeMapper codeMapper _codeMapper = codeMapper; } - public async Task Invoke(DownstreamContext context) + public async Task Invoke(HttpContext httpContext) { - await _next.Invoke(context); + await _next.Invoke(httpContext); - if (context.IsError) + var errors = httpContext.Items.Errors(); + + // todo check errors is ok + if (errors.Count > 0) { - Logger.LogWarning($"{context.Errors.ToErrorString()} errors found in {MiddlewareName}. Setting error response for request path:{context.HttpContext.Request.Path}, request method: {context.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(context.HttpContext, context.Errors); + SetErrorResponse(httpContext, errors); } else { Logger.LogDebug("no pipeline errors, setting and returning completed response"); - await _responder.SetResponseOnHttpContext(context.HttpContext, context.DownstreamResponse); + + var downstreamResponse = httpContext.Items.DownstreamResponse(); + + await _responder.SetResponseOnHttpContext(httpContext, downstreamResponse); } } private void SetErrorResponse(HttpContext context, List errors) { + //todo - refactor this all teh way down because its shit var statusCode = _codeMapper.Map(errors); _responder.SetErrorResponseOnContext(context, statusCode); + + if (errors.Any(e => e.Code == OcelotErrorCode.QuotaExceededError)) + { + var downstreamResponse = context.Items.DownstreamResponse(); + _responder.SetErrorResponseOnContext(context, downstreamResponse); + } } } } diff --git a/src/Ocelot/Responder/Middleware/ResponderMiddlewareExtensions.cs b/src/Ocelot/Responder/Middleware/ResponderMiddlewareExtensions.cs index 9e8904129..a129fe228 100644 --- a/src/Ocelot/Responder/Middleware/ResponderMiddlewareExtensions.cs +++ b/src/Ocelot/Responder/Middleware/ResponderMiddlewareExtensions.cs @@ -1,11 +1,10 @@ using Microsoft.AspNetCore.Builder; -using Ocelot.Middleware.Pipeline; namespace Ocelot.Responder.Middleware { public static class ResponderMiddlewareExtensions { - public static IOcelotPipelineBuilder UseResponderMiddleware(this IOcelotPipelineBuilder builder) + public static IApplicationBuilder UseResponderMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware(); } diff --git a/src/Ocelot/Responses/ErrorResponse.cs b/src/Ocelot/Responses/ErrorResponse.cs index 56a3eb23f..e23d3fac6 100644 --- a/src/Ocelot/Responses/ErrorResponse.cs +++ b/src/Ocelot/Responses/ErrorResponse.cs @@ -1,5 +1,6 @@ -using Ocelot.Errors; using System.Collections.Generic; + +using Ocelot.Errors; namespace Ocelot.Responses { diff --git a/src/Ocelot/Responses/ErrorResponseGeneric.cs b/src/Ocelot/Responses/ErrorResponseGeneric.cs index 0dbf42729..71abbac69 100644 --- a/src/Ocelot/Responses/ErrorResponseGeneric.cs +++ b/src/Ocelot/Responses/ErrorResponseGeneric.cs @@ -1,5 +1,6 @@ -using Ocelot.Errors; using System.Collections.Generic; + +using Ocelot.Errors; namespace Ocelot.Responses { diff --git a/src/Ocelot/Responses/OkResponse.cs b/src/Ocelot/Responses/OkResponse.cs index 8ad234000..2a8b7b183 100644 --- a/src/Ocelot/Responses/OkResponse.cs +++ b/src/Ocelot/Responses/OkResponse.cs @@ -6,4 +6,4 @@ public OkResponse() { } } -} \ No newline at end of file +} diff --git a/src/Ocelot/Responses/Response.cs b/src/Ocelot/Responses/Response.cs index a10d480b9..5ec3f1381 100644 --- a/src/Ocelot/Responses/Response.cs +++ b/src/Ocelot/Responses/Response.cs @@ -1,22 +1,23 @@ -using Ocelot.Errors; -using System.Collections.Generic; - -namespace Ocelot.Responses -{ - public abstract class Response - { - protected Response() - { - Errors = new List(); - } - - protected Response(List errors) - { - Errors = errors ?? new List(); +using System.Collections.Generic; + +using Ocelot.Errors; + +namespace Ocelot.Responses +{ + public abstract class Response + { + protected Response() + { + Errors = new List(); } - public List Errors { get; } - - public bool IsError => Errors.Count > 0; - } + protected Response(List errors) + { + Errors = errors ?? new List(); + } + + public List Errors { get; } + + public bool IsError => Errors.Count > 0; + } } diff --git a/src/Ocelot/Responses/ResponseGeneric.cs b/src/Ocelot/Responses/ResponseGeneric.cs index 2deeb1138..f05ceb224 100644 --- a/src/Ocelot/Responses/ResponseGeneric.cs +++ b/src/Ocelot/Responses/ResponseGeneric.cs @@ -1,22 +1,23 @@ -using Ocelot.Errors; -using System.Collections.Generic; - -namespace Ocelot.Responses -{ -#pragma warning disable SA1649 // File name must match first type name - - public abstract class Response : Response -#pragma warning restore SA1649 // File name must match first type name - { - protected Response(T data) - { - Data = data; - } - - protected Response(List errors) : base(errors) - { - } - - public T Data { get; private set; } - } +using System.Collections.Generic; + +using Ocelot.Errors; + +namespace Ocelot.Responses +{ +#pragma warning disable SA1649 // File name must match first type name + + public abstract class Response : Response +#pragma warning restore SA1649 // File name must match first type name + { + protected Response(T data) + { + Data = data; + } + + protected Response(List errors) : base(errors) + { + } + + public T Data { get; } + } } diff --git a/src/Ocelot/Security/IPSecurity/IPSecurityPolicy.cs b/src/Ocelot/Security/IPSecurity/IPSecurityPolicy.cs index 19c75e593..ab1222335 100644 --- a/src/Ocelot/Security/IPSecurity/IPSecurityPolicy.cs +++ b/src/Ocelot/Security/IPSecurity/IPSecurityPolicy.cs @@ -1,17 +1,21 @@ -using Ocelot.Configuration; +using System.Threading.Tasks; + +using Ocelot.Configuration; + +using Microsoft.AspNetCore.Http; + using Ocelot.Middleware; + using Ocelot.Responses; -using System.Net; -using System.Threading.Tasks; namespace Ocelot.Security.IPSecurity { public class IPSecurityPolicy : ISecurityPolicy { - public async Task Security(DownstreamContext context) + public async Task Security(DownstreamRoute downstreamRoute, HttpContext httpContext) { - IPAddress clientIp = context.HttpContext.Connection.RemoteIpAddress; - SecurityOptions securityOptions = context.DownstreamReRoute.SecurityOptions; + var clientIp = httpContext.Connection.RemoteIpAddress; + var securityOptions = downstreamRoute.SecurityOptions; if (securityOptions == null) { return new OkResponse(); @@ -21,16 +25,16 @@ public async Task Security(DownstreamContext context) { if (securityOptions.IPBlockedList.Exists(f => f == clientIp.ToString())) { - var error = new UnauthenticatedError($" This request rejects access to {clientIp.ToString()} IP"); + var error = new UnauthenticatedError($" This request rejects access to {clientIp} IP"); return new ErrorResponse(error); } } - if (securityOptions.IPAllowedList != null && securityOptions.IPAllowedList.Count > 0) + if (securityOptions.IPAllowedList?.Count > 0) { if (!securityOptions.IPAllowedList.Exists(f => f == clientIp.ToString())) { - var error = new UnauthenticatedError($"{clientIp.ToString()} does not allow access, the request is invalid"); + var error = new UnauthenticatedError($"{clientIp} does not allow access, the request is invalid"); return new ErrorResponse(error); } } diff --git a/src/Ocelot/Security/ISecurityPolicy.cs b/src/Ocelot/Security/ISecurityPolicy.cs index c68546101..a82fa70f7 100644 --- a/src/Ocelot/Security/ISecurityPolicy.cs +++ b/src/Ocelot/Security/ISecurityPolicy.cs @@ -1,11 +1,15 @@ -using Ocelot.Middleware; +using System.Threading.Tasks; + +using Ocelot.Configuration; + +using Microsoft.AspNetCore.Http; + using Ocelot.Responses; -using System.Threading.Tasks; namespace Ocelot.Security { public interface ISecurityPolicy { - Task Security(DownstreamContext context); + Task Security(DownstreamRoute downstreamRoute, HttpContext httpContext); } } diff --git a/src/Ocelot/Security/Middleware/SecurityMiddleware.cs b/src/Ocelot/Security/Middleware/SecurityMiddleware.cs index 657445b82..138c78dd9 100644 --- a/src/Ocelot/Security/Middleware/SecurityMiddleware.cs +++ b/src/Ocelot/Security/Middleware/SecurityMiddleware.cs @@ -1,44 +1,49 @@ -using Ocelot.Logging; -using Ocelot.Middleware; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; +using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Middleware; + namespace Ocelot.Security.Middleware { public class SecurityMiddleware : OcelotMiddleware { - private readonly OcelotRequestDelegate _next; - private readonly IOcelotLogger _logger; + private readonly RequestDelegate _next; private readonly IEnumerable _securityPolicies; - public SecurityMiddleware(IOcelotLoggerFactory loggerFactory, - IEnumerable securityPolicies, - OcelotRequestDelegate next) + public SecurityMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IEnumerable securityPolicies + ) : base(loggerFactory.CreateLogger()) { - _logger = loggerFactory.CreateLogger(); _securityPolicies = securityPolicies; _next = next; } - public async Task Invoke(DownstreamContext context) + public async Task Invoke(HttpContext httpContext) { + var downstreamRoute = httpContext.Items.DownstreamRoute(); + if (_securityPolicies != null) { - foreach (var policie in _securityPolicies) + foreach (var policy in _securityPolicies) { - var result = await policie.Security(context); + var result = await policy.Security(downstreamRoute, httpContext); if (!result.IsError) { continue; } - this.SetPipelineError(context, result.Errors); + httpContext.Items.UpsertErrors(result.Errors); return; } } - await _next.Invoke(context); + await _next.Invoke(httpContext); } } } diff --git a/src/Ocelot/Security/Middleware/SecurityMiddlewareExtensions.cs b/src/Ocelot/Security/Middleware/SecurityMiddlewareExtensions.cs index 6230dae45..a91d94858 100644 --- a/src/Ocelot/Security/Middleware/SecurityMiddlewareExtensions.cs +++ b/src/Ocelot/Security/Middleware/SecurityMiddlewareExtensions.cs @@ -1,10 +1,10 @@ -using Ocelot.Middleware.Pipeline; +using Microsoft.AspNetCore.Builder; namespace Ocelot.Security.Middleware { public static class SecurityMiddlewareExtensions { - public static IOcelotPipelineBuilder UseSecurityMiddleware(this IOcelotPipelineBuilder builder) + public static IApplicationBuilder UseSecurityMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware(); } diff --git a/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProviderFactory.cs index a3c3b18be..eae3ba416 100644 --- a/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProviderFactory.cs +++ b/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProviderFactory.cs @@ -1,11 +1,13 @@ -namespace Ocelot.ServiceDiscovery -{ - using Ocelot.Configuration; - using Ocelot.Responses; - using Ocelot.ServiceDiscovery.Providers; +using Ocelot.Configuration; - public interface IServiceDiscoveryProviderFactory - { - Response Get(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute); - } -} +using Ocelot.ServiceDiscovery.Providers; + +using Ocelot.Responses; + +namespace Ocelot.ServiceDiscovery +{ + public interface IServiceDiscoveryProviderFactory + { + Response Get(ServiceProviderConfiguration serviceConfig, DownstreamRoute route); + } +} diff --git a/src/Ocelot/ServiceDiscovery/Providers/ConfigurationServiceProvider.cs b/src/Ocelot/ServiceDiscovery/Providers/ConfigurationServiceProvider.cs index f494e0870..52246bc9d 100644 --- a/src/Ocelot/ServiceDiscovery/Providers/ConfigurationServiceProvider.cs +++ b/src/Ocelot/ServiceDiscovery/Providers/ConfigurationServiceProvider.cs @@ -1,7 +1,8 @@ -using Ocelot.Values; using System.Collections.Generic; using System.Threading.Tasks; +using Ocelot.Values; + namespace Ocelot.ServiceDiscovery.Providers { public class ConfigurationServiceProvider : IServiceDiscoveryProvider diff --git a/src/Ocelot/ServiceDiscovery/Providers/IServiceDiscoveryProvider.cs b/src/Ocelot/ServiceDiscovery/Providers/IServiceDiscoveryProvider.cs index 440a9cae3..0ec626b1c 100644 --- a/src/Ocelot/ServiceDiscovery/Providers/IServiceDiscoveryProvider.cs +++ b/src/Ocelot/ServiceDiscovery/Providers/IServiceDiscoveryProvider.cs @@ -1,7 +1,8 @@ -using Ocelot.Values; using System.Collections.Generic; using System.Threading.Tasks; +using Ocelot.Values; + namespace Ocelot.ServiceDiscovery.Providers { public interface IServiceDiscoveryProvider diff --git a/src/Ocelot/ServiceDiscovery/Providers/ServiceFabricServiceDiscoveryProvider.cs b/src/Ocelot/ServiceDiscovery/Providers/ServiceFabricServiceDiscoveryProvider.cs index a397b3229..8dfd10b9d 100644 --- a/src/Ocelot/ServiceDiscovery/Providers/ServiceFabricServiceDiscoveryProvider.cs +++ b/src/Ocelot/ServiceDiscovery/Providers/ServiceFabricServiceDiscoveryProvider.cs @@ -1,8 +1,9 @@ -using Ocelot.ServiceDiscovery.Configuration; -using Ocelot.Values; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; +using Ocelot.ServiceDiscovery.Configuration; +using Ocelot.Values; + namespace Ocelot.ServiceDiscovery.Providers { public class ServiceFabricServiceDiscoveryProvider : IServiceDiscoveryProvider @@ -18,11 +19,11 @@ public Task> Get() { return Task.FromResult(new List { - new Service(_configuration.ServiceName, + new(_configuration.ServiceName, new ServiceHostAndPort(_configuration.HostName, _configuration.Port), "doesnt matter with service fabric", "doesnt matter with service fabric", - new List()) + new List()), }); } } diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs index a3cf84608..b9cfa3bcd 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs @@ -1,8 +1,10 @@ -namespace Ocelot.ServiceDiscovery -{ - using Ocelot.Configuration; - using Providers; - using System; +using System; - public delegate IServiceDiscoveryProvider ServiceDiscoveryFinderDelegate(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamReRoute reRoute); -} +using Ocelot.Configuration; + +using Ocelot.ServiceDiscovery.Providers; + +namespace Ocelot.ServiceDiscovery +{ + public delegate IServiceDiscoveryProvider ServiceDiscoveryFinderDelegate(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route); +} diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs index 7a5808722..ed04c3532 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs @@ -1,66 +1,73 @@ -namespace Ocelot.ServiceDiscovery -{ - 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 System; - using System.Collections.Generic; +using System; +using System.Collections.Generic; - public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory - { - private readonly IOcelotLoggerFactory _factory; - private readonly ServiceDiscoveryFinderDelegate _delegates; - private readonly IServiceProvider _provider; +using Ocelot.ServiceDiscovery.Configuration; - public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IServiceProvider provider) - { - _factory = factory; - _provider = provider; - _delegates = provider.GetService(); - } +using Ocelot.Logging; - public Response Get(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute) - { - if (reRoute.UseServiceDiscovery) - { - return GetServiceDiscoveryProvider(serviceConfig, reRoute); - } +using Microsoft.Extensions.DependencyInjection; - var services = new List(); +using Ocelot.Configuration; - foreach (var downstreamAddress in reRoute.DownstreamAddresses) - { - var service = new Service(reRoute.ServiceName, new ServiceHostAndPort(downstreamAddress.Host, downstreamAddress.Port, reRoute.DownstreamScheme), string.Empty, string.Empty, new string[0]); +using Ocelot.ServiceDiscovery.Providers; - services.Add(service); - } +using Ocelot.Responses; - return new OkResponse(new ConfigurationServiceProvider(services)); - } - - private Response GetServiceDiscoveryProvider(ServiceProviderConfiguration config, DownstreamReRoute reRoute) - { - if (config.Type?.ToLower() == "servicefabric") - { - var sfConfig = new ServiceFabricConfiguration(config.Host, config.Port, reRoute.ServiceName); - return new OkResponse(new ServiceFabricServiceDiscoveryProvider(sfConfig)); - } - - if (_delegates != null) - { - var provider = _delegates?.Invoke(_provider, config, reRoute); - - if (provider.GetType().Name.ToLower() == config.Type.ToLower()) - { - return new OkResponse(provider); - } - } - - return new ErrorResponse(new UnableToFindServiceDiscoveryProviderError($"Unable to find service discovery provider for type: {config.Type}")); - } - } -} +using Ocelot.Values; + +namespace Ocelot.ServiceDiscovery +{ + public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory + { + private readonly IOcelotLoggerFactory _factory; + private readonly ServiceDiscoveryFinderDelegate _delegates; + private readonly IServiceProvider _provider; + + public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IServiceProvider provider) + { + _factory = factory; + _provider = provider; + _delegates = provider.GetService(); + } + + public Response Get(ServiceProviderConfiguration serviceConfig, DownstreamRoute route) + { + if (route.UseServiceDiscovery) + { + return GetServiceDiscoveryProvider(serviceConfig, route); + } + + var services = new List(); + + foreach (var downstreamAddress in route.DownstreamAddresses) + { + var service = new Service(route.ServiceName, new ServiceHostAndPort(downstreamAddress.Host, downstreamAddress.Port, route.DownstreamScheme), string.Empty, string.Empty, Array.Empty()); + + services.Add(service); + } + + return new OkResponse(new ConfigurationServiceProvider(services)); + } + + private Response GetServiceDiscoveryProvider(ServiceProviderConfiguration config, DownstreamRoute route) + { + if (config.Type?.ToLower() == "servicefabric") + { + var sfConfig = new ServiceFabricConfiguration(config.Host, config.Port, route.ServiceName); + return new OkResponse(new ServiceFabricServiceDiscoveryProvider(sfConfig)); + } + + if (_delegates != null) + { + var provider = _delegates?.Invoke(_provider, config, route); + + if (provider.GetType().Name.ToLower() == config.Type.ToLower()) + { + return new OkResponse(provider); + } + } + + return new ErrorResponse(new UnableToFindServiceDiscoveryProviderError($"Unable to find service discovery provider for type: {config.Type}")); + } + } +} diff --git a/src/Ocelot/ServiceDiscovery/UnableToFindServiceDiscoveryProviderError.cs b/src/Ocelot/ServiceDiscovery/UnableToFindServiceDiscoveryProviderError.cs index 3ac838d20..1b4789271 100644 --- a/src/Ocelot/ServiceDiscovery/UnableToFindServiceDiscoveryProviderError.cs +++ b/src/Ocelot/ServiceDiscovery/UnableToFindServiceDiscoveryProviderError.cs @@ -1,10 +1,10 @@ +using Ocelot.Errors; + namespace Ocelot.ServiceDiscovery { - using Ocelot.Errors; - public class UnableToFindServiceDiscoveryProviderError : Error { - public UnableToFindServiceDiscoveryProviderError(string message) : base(message, OcelotErrorCode.UnableToFindServiceDiscoveryProviderError) + public UnableToFindServiceDiscoveryProviderError(string message) : base(message, OcelotErrorCode.UnableToFindServiceDiscoveryProviderError, 404) { } } diff --git a/src/Ocelot/WebSockets/Middleware/WebSocketsProxyMiddleware.cs b/src/Ocelot/WebSockets/Middleware/WebSocketsProxyMiddleware.cs index bc567e36d..b86dab086 100644 --- a/src/Ocelot/WebSockets/Middleware/WebSocketsProxyMiddleware.cs +++ b/src/Ocelot/WebSockets/Middleware/WebSocketsProxyMiddleware.cs @@ -2,15 +2,18 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Modified https://github.com/aspnet/Proxy websockets class to use in Ocelot. -using Microsoft.AspNetCore.Http; -using Ocelot.Logging; -using Ocelot.Middleware; using System; using System.Linq; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; +using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Middleware; + namespace Ocelot.WebSockets.Middleware { public class WebSocketsProxyMiddleware : OcelotMiddleware @@ -18,9 +21,9 @@ public class WebSocketsProxyMiddleware : OcelotMiddleware private static readonly string[] NotForwardedWebSocketHeaders = new[] { "Connection", "Host", "Upgrade", "Sec-WebSocket-Accept", "Sec-WebSocket-Protocol", "Sec-WebSocket-Key", "Sec-WebSocket-Version", "Sec-WebSocket-Extensions" }; private const int DefaultWebSocketBufferSize = 4096; private const int StreamCopyBufferSize = 81920; - private readonly OcelotRequestDelegate _next; + private readonly RequestDelegate _next; - public WebSocketsProxyMiddleware(OcelotRequestDelegate next, + public WebSocketsProxyMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory) : base(loggerFactory.CreateLogger()) { @@ -54,6 +57,7 @@ private static async Task PumpWebSocket(WebSocket source, WebSocket destination, await destination.CloseOutputAsync(WebSocketCloseStatus.EndpointUnavailable, null, cancellationToken); return; } + throw; } @@ -67,12 +71,13 @@ private static async Task PumpWebSocket(WebSocket source, WebSocket destination, } } - public async Task Invoke(DownstreamContext context) + public async Task Invoke(HttpContext httpContext) { - await Proxy(context.HttpContext, context.DownstreamRequest.ToUri()); + var uri = httpContext.Items.DownstreamRequest().ToUri(); + await Proxy(httpContext, uri); } - private async Task Proxy(HttpContext context, string serverEndpoint) + private static async Task Proxy(HttpContext context, string serverEndpoint) { if (context == null) { diff --git a/src/Ocelot/WebSockets/Middleware/WebSocketsProxyMiddlewareExtensions.cs b/src/Ocelot/WebSockets/Middleware/WebSocketsProxyMiddlewareExtensions.cs index e973dfc3c..f190c00c6 100644 --- a/src/Ocelot/WebSockets/Middleware/WebSocketsProxyMiddlewareExtensions.cs +++ b/src/Ocelot/WebSockets/Middleware/WebSocketsProxyMiddlewareExtensions.cs @@ -1,10 +1,10 @@ -using Ocelot.Middleware.Pipeline; +using Microsoft.AspNetCore.Builder; namespace Ocelot.WebSockets.Middleware { public static class WebSocketsProxyMiddlewareExtensions { - public static IOcelotPipelineBuilder UseWebSocketsProxyMiddleware(this IOcelotPipelineBuilder builder) + public static IApplicationBuilder UseWebSocketsProxyMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware(); } diff --git a/test/Ocelot.AcceptanceTests/AggregateTests.cs b/test/Ocelot.AcceptanceTests/AggregateTests.cs index 2bd2f09f7..b420274df 100644 --- a/test/Ocelot.AcceptanceTests/AggregateTests.cs +++ b/test/Ocelot.AcceptanceTests/AggregateTests.cs @@ -1,656 +1,674 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.File; -using Ocelot.Middleware; -using Ocelot.Middleware.Multiplexer; -using Shouldly; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.AcceptanceTests -{ - public class AggregateTests : IDisposable - { - private readonly Steps _steps; - private string _downstreamPathOne; - private string _downstreamPathTwo; - private readonly ServiceHandler _serviceHandler; - - public AggregateTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - - [Fact] - public void should_fix_issue_597() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/values?MailId={userid}", - UpstreamPathTemplate = "/key1data/{userid}", - UpstreamHttpMethod = new List {"Get"}, - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 8571 - } - }, - Key = "key1" - }, - new FileReRoute - { - DownstreamPathTemplate = "/api/values?MailId={userid}", - UpstreamPathTemplate = "/key2data/{userid}", - UpstreamHttpMethod = new List {"Get"}, - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 8571 - } - }, - Key = "key2" - }, - new FileReRoute - { - DownstreamPathTemplate = "/api/values?MailId={userid}", - UpstreamPathTemplate = "/key3data/{userid}", - UpstreamHttpMethod = new List {"Get"}, - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 8571 - } - }, - Key = "key3" - }, - new FileReRoute - { - DownstreamPathTemplate = "/api/values?MailId={userid}", - UpstreamPathTemplate = "/key4data/{userid}", - UpstreamHttpMethod = new List {"Get"}, - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 8571 - } - }, - Key = "key4" - }, - }, - Aggregates = new List - { - new FileAggregateReRoute - { - ReRouteKeys = new List{ - "key1", - "key2", - "key3", - "key4" - }, - UpstreamPathTemplate = "/EmpDetail/IN/{userid}" - }, - new FileAggregateReRoute - { - ReRouteKeys = new List{ - "key1", - "key2", - }, - UpstreamPathTemplate = "/EmpDetail/US/{userid}" - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - RequestIdKey = "CorrelationID" - } - }; - - var expected = "{\"key1\":some_data,\"key2\":some_data}"; - - this.Given(x => x.GivenServiceIsRunning("http://localhost:8571", 200, "some_data")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EmpDetail/US/1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_advanced_aggregate_configs() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51889, - } - }, - UpstreamPathTemplate = "/Comments", - UpstreamHttpMethod = new List { "Get" }, - Key = "Comments" - }, - new FileReRoute - { - DownstreamPathTemplate = "/users/{userId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 54030, - } - }, - UpstreamPathTemplate = "/UserDetails", - UpstreamHttpMethod = new List { "Get" }, - Key = "UserDetails" - }, - new FileReRoute - { - DownstreamPathTemplate = "/posts/{postId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51887, - } - }, - UpstreamPathTemplate = "/PostDetails", - UpstreamHttpMethod = new List { "Get" }, - Key = "PostDetails" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Comments", - "UserDetails", - "PostDetails" - }, - ReRouteKeysConfig = new List() - { - new AggregateReRouteConfig(){ReRouteKey = "UserDetails",JsonPath = "$[*].writerId",Parameter = "userId"}, - new AggregateReRouteConfig(){ReRouteKey = "PostDetails",JsonPath = "$[*].postId",Parameter = "postId"} - }, - } - } - }; - - var userDetailsResponseContent = @"{""id"":1,""firstName"":""abolfazl"",""lastName"":""rajabpour""}"; - var postDetailsResponseContent = @"{""id"":1,""title"":""post1""}"; - var commentsResponseContent = @"[{""id"":1,""writerId"":1,""postId"":2,""text"":""text1""},{""id"":2,""writerId"":1,""postId"":2,""text"":""text2""}]"; - - var expected = "{\"Comments\":" + commentsResponseContent + ",\"UserDetails\":" + userDetailsResponseContent + ",\"PostDetails\":" + postDetailsResponseContent + "}"; - - this.Given(x => x.GivenServiceOneIsRunning("http://localhost:51889", "/", 200, commentsResponseContent)) - .Given(x => x.GivenServiceTwoIsRunning("http://localhost:54030", "/users/1", 200, userDetailsResponseContent)) - .Given(x => x.GivenServiceTwoIsRunning("http://localhost:51887", "/posts/2", 200, postDetailsResponseContent)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_simple_url_user_defined_aggregate() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51885, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51886, - } - }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Laura", - "Tom" - }, - Aggregator = "FakeDefinedAggregator" - } - } - }; - - var expected = "Bye from Laura, Bye from Tom"; - - this.Given(x => x.GivenServiceOneIsRunning("http://localhost:51885", "/", 200, "{Hello from Laura}")) - .Given(x => x.GivenServiceTwoIsRunning("http://localhost:51886", "/", 200, "{Hello from Tom}")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithSpecficAggregatorsRegisteredInDi()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) - .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_simple_url() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51875, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 52476, - } - }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Laura", - "Tom" - } - } - } - }; - - var expected = "{\"Laura\":{Hello from Laura},\"Tom\":{Hello from Tom}}"; - - this.Given(x => x.GivenServiceOneIsRunning("http://localhost:51875", "/", 200, "{Hello from Laura}")) - .Given(x => x.GivenServiceTwoIsRunning("http://localhost:52476", "/", 200, "{Hello from Tom}")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) - .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_simple_url_one_service_404() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51881, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51889, - } - }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Laura", - "Tom" - } - } - } - }; - - var expected = "{\"Laura\":,\"Tom\":{Hello from Tom}}"; - - this.Given(x => x.GivenServiceOneIsRunning("http://localhost:51881", "/", 404, "")) - .Given(x => x.GivenServiceTwoIsRunning("http://localhost:51889", "/", 200, "{Hello from Tom}")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) - .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_simple_url_both_service_404() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51883, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51884, - } - }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Laura", - "Tom" - } - } - } - }; - - var expected = "{\"Laura\":,\"Tom\":}"; - - this.Given(x => x.GivenServiceOneIsRunning("http://localhost:51883", "/", 404, "")) - .Given(x => x.GivenServiceTwoIsRunning("http://localhost:51884", "/", 404, "")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) - .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) - .BDDfy(); - } - - [Fact] - public void should_be_thread_safe() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Laura", - "Tom" - } - } - } - }; - - this.Given(x => x.GivenServiceOneIsRunning("http://localhost:51878", "/", 200, "{Hello from Laura}")) - .Given(x => x.GivenServiceTwoIsRunning("http://localhost:51880", "/", 200, "{Hello from Tom}")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIMakeLotsOfDifferentRequestsToTheApiGateway()) - .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) - .BDDfy(); - } - - private void GivenServiceIsRunning(string baseUrl, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, async context => - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - } - - private void GivenServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPathOne = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPathOne != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); - } - - private void GivenServiceTwoIsRunning(string baseUrl, string basePath, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPathTwo = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPathTwo != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); - } - - internal void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPathOne, string expectedDownstreamPath) - { - _downstreamPathOne.ShouldBe(expectedDownstreamPathOne); - _downstreamPathTwo.ShouldBe(expectedDownstreamPath); - } - - public void Dispose() - { - _serviceHandler.Dispose(); - _steps.Dispose(); - } - } - - public class FakeDepdendency - { - } - - public class FakeDefinedAggregator : IDefinedAggregator - { - private readonly FakeDepdendency _dep; - - public FakeDefinedAggregator(FakeDepdendency dep) - { - _dep = dep; - } - - public async Task Aggregate(List responses) - { - var one = await responses[0].DownstreamResponse.Content.ReadAsStringAsync(); - var two = await responses[1].DownstreamResponse.Content.ReadAsStringAsync(); - - var merge = $"{one}, {two}"; - merge = merge.Replace("Hello", "Bye").Replace("{", "").Replace("}", ""); - var headers = responses.SelectMany(x => x.DownstreamResponse.Headers).ToList(); - return new DownstreamResponse(new StringContent(merge), HttpStatusCode.OK, headers, "some reason"); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Configuration.File; +using Ocelot.Middleware; +using Ocelot.Multiplexer; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class AggregateTests : IDisposable + { + private readonly Steps _steps; + private string _downstreamPathOne; + private string _downstreamPathTwo; + private readonly ServiceHandler _serviceHandler; + + public AggregateTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_fix_issue_597() + { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/values?MailId={userid}", + UpstreamPathTemplate = "/key1data/{userid}", + UpstreamHttpMethod = new List {"Get"}, + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + Key = "key1", + }, + new() + { + DownstreamPathTemplate = "/api/values?MailId={userid}", + UpstreamPathTemplate = "/key2data/{userid}", + UpstreamHttpMethod = new List {"Get"}, + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + Key = "key2", + }, + new() + { + DownstreamPathTemplate = "/api/values?MailId={userid}", + UpstreamPathTemplate = "/key3data/{userid}", + UpstreamHttpMethod = new List {"Get"}, + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + Key = "key3", + }, + new() + { + DownstreamPathTemplate = "/api/values?MailId={userid}", + UpstreamPathTemplate = "/key4data/{userid}", + UpstreamHttpMethod = new List {"Get"}, + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + Key = "key4", + }, + }, + Aggregates = new List + { + new() + { + RouteKeys = new List{ + "key1", + "key2", + "key3", + "key4", + }, + UpstreamPathTemplate = "/EmpDetail/IN/{userid}", + }, + new() + { + RouteKeys = new List{ + "key1", + "key2", + }, + UpstreamPathTemplate = "/EmpDetail/US/{userid}", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + RequestIdKey = "CorrelationID", + }, + }; + + var expected = "{\"key1\":some_data,\"key2\":some_data}"; + + this.Given(x => x.GivenServiceIsRunning($"http://localhost:{port}", 200, "some_data")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EmpDetail/US/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_advanced_aggregate_configs() + { + var port1 = RandomPortFinder.GetRandomPort(); + var port2 = RandomPortFinder.GetRandomPort(); + var port3 = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port1, + }, + }, + UpstreamPathTemplate = "/Comments", + UpstreamHttpMethod = new List { "Get" }, + Key = "Comments", + }, + new() + { + DownstreamPathTemplate = "/users/{userId}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port2, + }, + }, + UpstreamPathTemplate = "/UserDetails", + UpstreamHttpMethod = new List { "Get" }, + Key = "UserDetails", + }, + new() + { + DownstreamPathTemplate = "/posts/{postId}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port3, + }, + }, + UpstreamPathTemplate = "/PostDetails", + UpstreamHttpMethod = new List { "Get" }, + Key = "PostDetails", + }, + }, + Aggregates = new List + { + new() + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = new List + { + "Comments", + "UserDetails", + "PostDetails", + }, + RouteKeysConfig = new List() + { + new(){RouteKey = "UserDetails",JsonPath = "$[*].writerId",Parameter = "userId"}, + new(){RouteKey = "PostDetails",JsonPath = "$[*].postId",Parameter = "postId"}, + }, + }, + }, + }; + + var userDetailsResponseContent = @"{""id"":1,""firstName"":""abolfazl"",""lastName"":""rajabpour""}"; + var postDetailsResponseContent = @"{""id"":1,""title"":""post1""}"; + var commentsResponseContent = @"[{""id"":1,""writerId"":1,""postId"":2,""text"":""text1""},{""id"":2,""writerId"":1,""postId"":2,""text"":""text2""}]"; + + var expected = "{\"Comments\":" + commentsResponseContent + ",\"UserDetails\":" + userDetailsResponseContent + ",\"PostDetails\":" + postDetailsResponseContent + "}"; + + this.Given(x => x.GivenServiceOneIsRunning($"http://localhost:{port1}", "/", 200, commentsResponseContent)) + .Given(x => x.GivenServiceTwoIsRunning($"http://localhost:{port2}", "/users/1", 200, userDetailsResponseContent)) + .Given(x => x.GivenServiceTwoIsRunning($"http://localhost:{port3}", "/posts/2", 200, postDetailsResponseContent)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_simple_url_user_defined_aggregate() + { + 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 = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port2, + }, + }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new List { "Get" }, + Key = "Tom", + }, + }, + Aggregates = new List + { + new() + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = new List + { + "Laura", + "Tom", + }, + Aggregator = "FakeDefinedAggregator", + }, + }, + }; + + var expected = "Bye from Laura, Bye from Tom"; + + 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()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) + .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_simple_url() + { + 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 = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port2, + }, + }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new List { "Get" }, + Key = "Tom", + }, + }, + Aggregates = new List + { + new() + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = new List + { + "Laura", + "Tom", + }, + }, + }, + }; + + var expected = "{\"Laura\":{Hello from Laura},\"Tom\":{Hello from Tom}}"; + + 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.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) + .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_simple_url_one_service_404() + { + 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 = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port2, + }, + }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new List { "Get" }, + Key = "Tom", + }, + }, + Aggregates = new List + { + new() + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = new List + { + "Laura", + "Tom", + }, + }, + }, + }; + + var expected = "{\"Laura\":,\"Tom\":{Hello from Tom}}"; + + this.Given(x => x.GivenServiceOneIsRunning($"http://localhost:{port1}", "/", 404, "")) + .Given(x => x.GivenServiceTwoIsRunning($"http://localhost:{port2}", "/", 200, "{Hello from Tom}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) + .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_simple_url_both_service_404() + { + 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 = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port2, + }, + }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new List { "Get" }, + Key = "Tom", + }, + }, + Aggregates = new List + { + new() + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = new List + { + "Laura", + "Tom", + }, + }, + }, + }; + + var expected = "{\"Laura\":,\"Tom\":}"; + + this.Given(x => x.GivenServiceOneIsRunning($"http://localhost:{port1}", "/", 404, "")) + .Given(x => x.GivenServiceTwoIsRunning($"http://localhost:{port2}", "/", 404, "")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) + .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) + .BDDfy(); + } + + [Fact] + public void should_be_thread_safe() + { + 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 = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port2, + }, + }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new List { "Get" }, + Key = "Tom", + }, + }, + Aggregates = new List + { + new() + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = new List + { + "Laura", + "Tom", + }, + }, + }, + }; + + 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.GivenOcelotIsRunning()) + .When(x => _steps.WhenIMakeLotsOfDifferentRequestsToTheApiGateway()) + .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) + .BDDfy(); + } + + private void GivenServiceIsRunning(string baseUrl, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + } + + private void GivenServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPathOne = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPathOne != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + } + + private void GivenServiceTwoIsRunning(string baseUrl, string basePath, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPathTwo = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPathTwo != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + } + + internal void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPathOne, string expectedDownstreamPath) + { + _downstreamPathOne.ShouldBe(expectedDownstreamPathOne); + _downstreamPathTwo.ShouldBe(expectedDownstreamPath); + } + + public void Dispose() + { + _serviceHandler.Dispose(); + _steps.Dispose(); + } + } + + public class FakeDepdendency + { + } + + public class FakeDefinedAggregator : IDefinedAggregator + { + private readonly FakeDepdendency _dep; + + public FakeDefinedAggregator(FakeDepdendency dep) + { + _dep = dep; + } + + public async Task Aggregate(List responses) + { + var one = await responses[0].Items.DownstreamResponse().Content.ReadAsStringAsync(); + var two = await responses[1].Items.DownstreamResponse().Content.ReadAsStringAsync(); + + var merge = $"{one}, {two}"; + merge = merge.Replace("Hello", "Bye").Replace("{", "").Replace("}", ""); + var headers = responses.SelectMany(x => x.Items.DownstreamResponse().Headers).ToList(); + return new DownstreamResponse(new StringContent(merge), HttpStatusCode.OK, headers, "some reason"); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/AuthenticationTests.cs b/test/Ocelot.AcceptanceTests/AuthenticationTests.cs index 65d9a8782..f72fdac45 100644 --- a/test/Ocelot.AcceptanceTests/AuthenticationTests.cs +++ b/test/Ocelot.AcceptanceTests/AuthenticationTests.cs @@ -1,117 +1,124 @@ -namespace Ocelot.AcceptanceTests -{ - using IdentityServer4.AccessTokenValidation; - using IdentityServer4.Models; - using IdentityServer4.Test; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.DependencyInjection; - using Ocelot.Configuration.File; - using System; - using System.Collections.Generic; - using System.IO; - using System.Net; - using System.Security.Claims; - using TestStack.BDDfy; - using Xunit; - - public class AuthenticationTests : IDisposable - { - private readonly Steps _steps; - private IWebHost _identityServerBuilder; - private string _identityServerRootUrl = "http://localhost:51888"; - private string _downstreamServicePath = "/"; - private string _downstreamServiceHost = "localhost"; - private string _downstreamServiceScheme = "http"; - private string _downstreamServiceUrl = "http://localhost:"; - private readonly Action _options; - private readonly ServiceHandler _serviceHandler; - - public AuthenticationTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - _options = o => - { - o.Authority = _identityServerRootUrl; - o.ApiName = "api"; - o.RequireHttpsMetadata = false; - o.SupportedTokens = SupportedTokens.Both; - o.ApiSecret = "secret"; - }; - } - - [Fact] - public void should_return_401_using_identity_server_access_token() - { - int port = 54329; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Security.Claims; + +using Ocelot.Configuration.File; + +using IdentityServer4.AccessTokenValidation; +using IdentityServer4.Models; +using IdentityServer4.Test; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class AuthenticationTests : IDisposable + { + private readonly Steps _steps; + private IWebHost _identityServerBuilder; + private readonly string _identityServerRootUrl; + private readonly string _downstreamServicePath = "/"; + private readonly string _downstreamServiceHost = "localhost"; + private readonly string _downstreamServiceScheme = "http"; + private readonly string _downstreamServiceUrl = "http://localhost:"; + private readonly Action _options; + private readonly ServiceHandler _serviceHandler; + + public AuthenticationTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + var identityServerPort = RandomPortFinder.GetRandomPort(); + _identityServerRootUrl = $"http://localhost:{identityServerPort}"; + _options = o => + { + o.Authority = _identityServerRootUrl; + o.ApiName = "api"; + o.RequireHttpsMetadata = false; + o.SupportedTokens = SupportedTokens.Both; + o.ApiSecret = "secret"; + }; + } + + [Fact] + public void should_return_401_using_identity_server_access_token() + { + var port = RandomPortFinder.GetRandomPort(); var configuration = new FileConfiguration { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = _downstreamServicePath, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host =_downstreamServiceHost, - Port = port, - } - }, - DownstreamScheme = _downstreamServiceScheme, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Post" }, - AuthenticationOptions = new FileAuthenticationOptions - { - AuthenticationProviderKey = "Test" - } - } - } - }; - - this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", "api2", AccessTokenType.Jwt)) - .And(x => x.GivenThereIsAServiceRunningOn($"{_downstreamServiceUrl}{port}", 201, string.Empty)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_using_identity_server() - { - int port = 54099; + Routes = new List + { + new() + { + DownstreamPathTemplate = _downstreamServicePath, + DownstreamHostAndPorts = new List + { + new() + { + Host =_downstreamServiceHost, + Port = port, + }, + }, + DownstreamScheme = _downstreamServiceScheme, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Post" }, + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", "api2", AccessTokenType.Jwt)) + .And(x => x.GivenThereIsAServiceRunningOn($"{_downstreamServiceUrl}{port}", 201, string.Empty)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_using_identity_server() + { + var port = RandomPortFinder.GetRandomPort(); var configuration = new FileConfiguration { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = _downstreamServicePath, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host =_downstreamServiceHost, - Port = port, - } - }, - DownstreamScheme = _downstreamServiceScheme, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - AuthenticationOptions = new FileAuthenticationOptions - { - AuthenticationProviderKey = "Test" - } - } - } + Routes = new List + { + new() + { + DownstreamPathTemplate = _downstreamServicePath, + DownstreamHostAndPorts = new List + { + new() + { + Host =_downstreamServiceHost, + Port = port, + }, + }, + DownstreamScheme = _downstreamServiceScheme, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + }, + }, + }, }; this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", "api2", AccessTokenType.Jwt)) @@ -123,38 +130,38 @@ public void should_return_response_200_using_identity_server() .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_401_using_identity_server_with_token_requested_for_other_api() - { - int port = 54196; - + .BDDfy(); + } + + [Fact] + public void should_return_response_401_using_identity_server_with_token_requested_for_other_api() + { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = _downstreamServicePath, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host =_downstreamServiceHost, - Port = port, - } - }, - DownstreamScheme = _downstreamServiceScheme, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - AuthenticationOptions = new FileAuthenticationOptions - { - AuthenticationProviderKey = "Test" - } - } - } + Routes = new List + { + new() + { + DownstreamPathTemplate = _downstreamServicePath, + DownstreamHostAndPorts = new List + { + new() + { + Host =_downstreamServiceHost, + Port = port, + }, + }, + DownstreamScheme = _downstreamServiceScheme, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + }, + }, + }, }; this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", "api2", AccessTokenType.Jwt)) @@ -165,38 +172,38 @@ public void should_return_response_401_using_identity_server_with_token_requeste .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)) - .BDDfy(); - } - - [Fact] - public void should_return_201_using_identity_server_access_token() - { - int port = 52226; - + .BDDfy(); + } + + [Fact] + public void should_return_201_using_identity_server_access_token() + { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = _downstreamServicePath, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host =_downstreamServiceHost, - Port = port, - } - }, - DownstreamScheme = _downstreamServiceScheme, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Post" }, - AuthenticationOptions = new FileAuthenticationOptions - { - AuthenticationProviderKey = "Test" - } - } - } + Routes = new List + { + new() + { + DownstreamPathTemplate = _downstreamServicePath, + DownstreamHostAndPorts = new List + { + new() + { + Host =_downstreamServiceHost, + Port = port, + }, + }, + DownstreamScheme = _downstreamServiceScheme, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Post" }, + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + }, + }, + }, }; this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", "api2", AccessTokenType.Jwt)) @@ -208,38 +215,38 @@ public void should_return_201_using_identity_server_access_token() .And(x => _steps.GivenThePostHasContent("postContent")) .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) - .BDDfy(); - } - - [Fact] - public void should_return_201_using_identity_server_reference_token() - { - int port = 52222; - + .BDDfy(); + } + + [Fact] + public void should_return_201_using_identity_server_reference_token() + { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = _downstreamServicePath, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host =_downstreamServiceHost, - Port = port, - } - }, - DownstreamScheme = _downstreamServiceScheme, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Post" }, - AuthenticationOptions = new FileAuthenticationOptions - { - AuthenticationProviderKey = "Test" - } - } - } + Routes = new List + { + new() + { + DownstreamPathTemplate = _downstreamServicePath, + DownstreamHostAndPorts = new List + { + new() + { + Host =_downstreamServiceHost, + Port = port, + }, + }, + DownstreamScheme = _downstreamServiceScheme, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Post" }, + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + }, + }, + }, }; this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", "api2", AccessTokenType.Reference)) @@ -251,126 +258,131 @@ public void should_return_201_using_identity_server_reference_token() .And(x => _steps.GivenThePostHasContent("postContent")) .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - } - - private void GivenThereIsAnIdentityServerOn(string url, string apiName, string api2Name, AccessTokenType tokenType) - { - _identityServerBuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .ConfigureServices(services => - { - services.AddLogging(); - services.AddIdentityServer() - .AddDeveloperSigningCredential() - .AddInMemoryApiResources(new List - { - new ApiResource - { - Name = apiName, - Description = "My API", - Enabled = true, - DisplayName = "test", - Scopes = new List() - { - new Scope("api"), - new Scope("api.readOnly"), - new Scope("openid"), - new Scope("offline_access") - }, - ApiSecrets = new List() - { - new Secret - { - Value = "secret".Sha256() - } - }, - UserClaims = new List() - { - "CustomerId", "LocationId" - } - }, - new ApiResource - { - Name = api2Name, - Description = "My second API", - Enabled = true, - DisplayName = "second test", - Scopes = new List() - { - new Scope("api2"), - new Scope("api2.readOnly"), - }, - ApiSecrets = new List() - { - new Secret - { - Value = "secret".Sha256() - } - }, - UserClaims = new List() - { - "CustomerId", "LocationId" - } - }, - }) - .AddInMemoryClients(new List - { - new Client - { - ClientId = "client", - AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, - ClientSecrets = new List {new Secret("secret".Sha256())}, - AllowedScopes = new List { apiName, api2Name, "api.readOnly", "openid", "offline_access" }, - AccessTokenType = tokenType, - Enabled = true, - RequireClientSecret = false - } - }) - .AddTestUsers(new List - { - new TestUser - { - Username = "test", - Password = "test", - SubjectId = "registered|1231231", - Claims = new List - { - new Claim("CustomerId", "123"), - new Claim("LocationId", "321") - } - } - }); - }) - .Configure(app => - { - app.UseIdentityServer(); - }) - .Build(); - - _identityServerBuilder.Start(); - - _steps.VerifyIdentiryServerStarted(url); - } - - public void Dispose() - { - _serviceHandler.Dispose(); - _steps.Dispose(); - _identityServerBuilder?.Dispose(); - } - } + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + } + + private void GivenThereIsAnIdentityServerOn(string url, string apiName, string api2Name, AccessTokenType tokenType) + { + _identityServerBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .ConfigureServices(services => + { + services.AddLogging(); + services.AddIdentityServer() + .AddDeveloperSigningCredential() + .AddInMemoryApiScopes(new List + { + new(apiName, "test"), + new(api2Name, "test"), + }) + .AddInMemoryApiResources(new List + { + new() + { + Name = apiName, + Description = "My API", + Enabled = true, + DisplayName = "test", + Scopes = new List + { + "api", + "api.readOnly", + "openid", + "offline_access", + }, + ApiSecrets = new List + { + new() + { + Value = "secret".Sha256(), + }, + }, + UserClaims = new List + { + "CustomerId", "LocationId", + }, + }, + new() + { + Name = api2Name, + Description = "My second API", + Enabled = true, + DisplayName = "second test", + Scopes = new List + { + "api2", + "api2.readOnly", + }, + ApiSecrets = new List + { + new() + { + Value = "secret".Sha256(), + }, + }, + UserClaims = new List + { + "CustomerId", "LocationId", + }, + }, + }) + .AddInMemoryClients(new List + { + new() + { + ClientId = "client", + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List {new("secret".Sha256())}, + AllowedScopes = new List { apiName, api2Name, "api.readOnly", "openid", "offline_access" }, + AccessTokenType = tokenType, + Enabled = true, + RequireClientSecret = false, + }, + }) + .AddTestUsers(new List + { + new() + { + Username = "test", + Password = "test", + SubjectId = "registered|1231231", + Claims = new List + { + new("CustomerId", "123"), + new("LocationId", "321"), + }, + }, + }); + }) + .Configure(app => + { + app.UseIdentityServer(); + }) + .Build(); + + _identityServerBuilder.Start(); + + _steps.VerifyIdentiryServerStarted(url); + } + + public void Dispose() + { + _serviceHandler.Dispose(); + _steps.Dispose(); + _identityServerBuilder?.Dispose(); + } + } } diff --git a/test/Ocelot.AcceptanceTests/AuthorisationTests.cs b/test/Ocelot.AcceptanceTests/AuthorisationTests.cs deleted file mode 100644 index 6df6755cc..000000000 --- a/test/Ocelot.AcceptanceTests/AuthorisationTests.cs +++ /dev/null @@ -1,470 +0,0 @@ -namespace Ocelot.AcceptanceTests -{ - using IdentityServer4.AccessTokenValidation; - using IdentityServer4.Models; - using IdentityServer4.Test; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.DependencyInjection; - using Ocelot.Configuration.File; - using System; - using System.Collections.Generic; - using System.IO; - using System.Net; - using System.Security.Claims; - using TestStack.BDDfy; - using Xunit; - - public class AuthorisationTests : IDisposable - { - private IWebHost _identityServerBuilder; - private readonly Steps _steps; - private readonly Action _options; - private string _identityServerRootUrl = "http://localhost:51888"; - private readonly ServiceHandler _serviceHandler; - - public AuthorisationTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - _options = o => - { - o.Authority = _identityServerRootUrl; - o.ApiName = "api"; - o.RequireHttpsMetadata = false; - o.SupportedTokens = SupportedTokens.Both; - o.ApiSecret = "secret"; - }; - } - - [Fact] - public void should_return_response_200_authorising_route() - { - int port = 52875; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = port, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - AuthenticationOptions = new FileAuthenticationOptions - { - AuthenticationProviderKey = "Test" - }, - AddHeadersToRequest = - { - {"CustomerId", "Claims[CustomerId] > value"}, - {"LocationId", "Claims[LocationId] > value"}, - {"UserType", "Claims[sub] > value[0] > |"}, - {"UserId", "Claims[sub] > value[1] > |"} - }, - AddClaimsToRequest = - { - {"CustomerId", "Claims[CustomerId] > value"}, - {"UserType", "Claims[sub] > value[0] > |"}, - {"UserId", "Claims[sub] > value[1] > |"} - }, - RouteClaimsRequirement = - { - {"UserType", "registered"} - } - } - } - }; - - this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:51888", "api", AccessTokenType.Jwt)) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "Hello from Laura")) - .And(x => _steps.GivenIHaveAToken("http://localhost:51888")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) - .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_403_authorising_route() - { - int port = 59471; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = port, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - AuthenticationOptions = new FileAuthenticationOptions - { - AuthenticationProviderKey = "Test" - }, - AddHeadersToRequest = - { - {"CustomerId", "Claims[CustomerId] > value"}, - {"LocationId", "Claims[LocationId] > value"}, - {"UserType", "Claims[sub] > value[0] > |"}, - {"UserId", "Claims[sub] > value[1] > |"} - }, - AddClaimsToRequest = - { - {"CustomerId", "Claims[CustomerId] > value"}, - {"UserId", "Claims[sub] > value[1] > |"} - }, - RouteClaimsRequirement = - { - {"UserType", "registered"} - } - } - } - }; - - this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:51888", "api", AccessTokenType.Jwt)) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "Hello from Laura")) - .And(x => _steps.GivenIHaveAToken("http://localhost:51888")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) - .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Forbidden)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_using_identity_server_with_allowed_scope() - { - int port = 63471; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = port, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - AuthenticationOptions = new FileAuthenticationOptions - { - AuthenticationProviderKey = "Test", - AllowedScopes = new List{ "api", "api.readOnly", "openid", "offline_access" }, - }, - } - } - }; - - this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:51888", "api", AccessTokenType.Jwt)) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "Hello from Laura")) - .And(x => _steps.GivenIHaveATokenForApiReadOnlyScope("http://localhost:51888")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) - .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void should_return_response_403_using_identity_server_with_scope_not_allowed() - { - int port = 60571; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = port, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - AuthenticationOptions = new FileAuthenticationOptions - { - AuthenticationProviderKey = "Test", - AllowedScopes = new List{ "api", "openid", "offline_access" }, - }, - } - } - }; - - this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:51888", "api", AccessTokenType.Jwt)) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "Hello from Laura")) - .And(x => _steps.GivenIHaveATokenForApiReadOnlyScope("http://localhost:51888")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) - .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Forbidden)) - .BDDfy(); - } - - [Fact] - public void should_fix_issue_240() - { - int port = 61071; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = port, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - AuthenticationOptions = new FileAuthenticationOptions - { - AuthenticationProviderKey = "Test" - }, - RouteClaimsRequirement = - { - {"Role", "User"} - } - } - } - }; - - var users = new List - { - new TestUser - { - Username = "test", - Password = "test", - SubjectId = "registered|1231231", - Claims = new List - { - new Claim("Role", "AdminUser"), - new Claim("Role", "User") - }, - } - }; - - this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:51888", "api", AccessTokenType.Jwt, users)) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "Hello from Laura")) - .And(x => _steps.GivenIHaveAToken("http://localhost:51888")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) - .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - } - - private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTokenType tokenType) - { - _identityServerBuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .ConfigureServices(services => - { - services.AddLogging(); - services.AddIdentityServer() - .AddDeveloperSigningCredential() - .AddInMemoryApiResources(new List - { - new ApiResource - { - Name = apiName, - Description = "My API", - Enabled = true, - DisplayName = "test", - Scopes = new List() - { - new Scope("api"), - new Scope("api.readOnly"), - new Scope("openid"), - new Scope("offline_access") - }, - ApiSecrets = new List() - { - new Secret - { - Value = "secret".Sha256() - } - }, - UserClaims = new List() - { - "CustomerId", "LocationId", "UserType", "UserId" - } - }, - }) - .AddInMemoryClients(new List - { - new Client - { - ClientId = "client", - AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, - ClientSecrets = new List {new Secret("secret".Sha256())}, - AllowedScopes = new List { apiName, "api.readOnly", "openid", "offline_access" }, - AccessTokenType = tokenType, - Enabled = true, - RequireClientSecret = false - } - }) - .AddTestUsers(new List - { - new TestUser - { - Username = "test", - Password = "test", - SubjectId = "registered|1231231", - Claims = new List - { - new Claim("CustomerId", "123"), - new Claim("LocationId", "321") - } - } - }); - }) - .Configure(app => - { - app.UseIdentityServer(); - }) - .Build(); - - _identityServerBuilder.Start(); - - _steps.VerifyIdentiryServerStarted(url); - } - - private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTokenType tokenType, List users) - { - _identityServerBuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .ConfigureServices(services => - { - services.AddLogging(); - services.AddIdentityServer() - .AddDeveloperSigningCredential() - .AddInMemoryApiResources(new List - { - new ApiResource - { - Name = apiName, - Description = "My API", - Enabled = true, - DisplayName = "test", - Scopes = new List() - { - new Scope("api"), - new Scope("api.readOnly"), - new Scope("openid"), - new Scope("offline_access"), - }, - ApiSecrets = new List() - { - new Secret - { - Value = "secret".Sha256() - } - }, - UserClaims = new List() - { - "CustomerId", "LocationId", "UserType", "UserId", "Role" - } - }, - }) - .AddInMemoryClients(new List - { - new Client - { - ClientId = "client", - AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, - ClientSecrets = new List {new Secret("secret".Sha256())}, - AllowedScopes = new List { apiName, "api.readOnly", "openid", "offline_access" }, - AccessTokenType = tokenType, - Enabled = true, - RequireClientSecret = false, - } - }) - .AddTestUsers(users); - }) - .Configure(app => - { - app.UseIdentityServer(); - }) - .Build(); - - _identityServerBuilder.Start(); - - _steps.VerifyIdentiryServerStarted(url); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - _identityServerBuilder?.Dispose(); - } - } -} diff --git a/test/Ocelot.AcceptanceTests/AuthorizationTests.cs b/test/Ocelot.AcceptanceTests/AuthorizationTests.cs new file mode 100644 index 000000000..ed276377a --- /dev/null +++ b/test/Ocelot.AcceptanceTests/AuthorizationTests.cs @@ -0,0 +1,488 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Security.Claims; + +using Ocelot.Configuration.File; + +using IdentityServer4.AccessTokenValidation; +using IdentityServer4.Models; +using IdentityServer4.Test; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class AuthorizationTests : IDisposable + { + private IWebHost _identityServerBuilder; + private readonly Steps _steps; + private readonly Action _options; + private readonly string _identityServerRootUrl; + private readonly ServiceHandler _serviceHandler; + + public AuthorizationTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + var identityServerPort = RandomPortFinder.GetRandomPort(); + _identityServerRootUrl = $"http://localhost:{identityServerPort}"; + _options = o => + { + o.Authority = _identityServerRootUrl; + o.ApiName = "api"; + o.RequireHttpsMetadata = false; + o.SupportedTokens = SupportedTokens.Both; + o.ApiSecret = "secret"; + }; + } + + [Fact] + public void should_return_response_200_authorizing_route() + { + 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 { "Get" }, + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + }, + AddHeadersToRequest = + { + {"CustomerId", "Claims[CustomerId] > value"}, + {"LocationId", "Claims[LocationId] > value"}, + {"UserType", "Claims[sub] > value[0] > |"}, + {"UserId", "Claims[sub] > value[1] > |"}, + }, + AddClaimsToRequest = + { + {"CustomerId", "Claims[CustomerId] > value"}, + {"UserType", "Claims[sub] > value[0] > |"}, + {"UserId", "Claims[sub] > value[1] > |"}, + }, + RouteClaimsRequirement = + { + {"UserType", "registered"}, + }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "Hello from Laura")) + .And(x => _steps.GivenIHaveAToken(_identityServerRootUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) + .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_403_authorizing_route() + { + 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 { "Get" }, + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + }, + AddHeadersToRequest = + { + {"CustomerId", "Claims[CustomerId] > value"}, + {"LocationId", "Claims[LocationId] > value"}, + {"UserType", "Claims[sub] > value[0] > |"}, + {"UserId", "Claims[sub] > value[1] > |"}, + }, + AddClaimsToRequest = + { + {"CustomerId", "Claims[CustomerId] > value"}, + {"UserId", "Claims[sub] > value[1] > |"}, + }, + RouteClaimsRequirement = + { + {"UserType", "registered"}, + }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "Hello from Laura")) + .And(x => _steps.GivenIHaveAToken(_identityServerRootUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) + .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Forbidden)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_using_identity_server_with_allowed_scope() + { + 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 { "Get" }, + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + AllowedScopes = new List{ "api", "api.readOnly", "openid", "offline_access" }, + }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "Hello from Laura")) + .And(x => _steps.GivenIHaveATokenForApiReadOnlyScope(_identityServerRootUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) + .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_return_response_403_using_identity_server_with_scope_not_allowed() + { + 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 { "Get" }, + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + AllowedScopes = new List{ "api", "openid", "offline_access" }, + }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "Hello from Laura")) + .And(x => _steps.GivenIHaveATokenForApiReadOnlyScope(_identityServerRootUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) + .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Forbidden)) + .BDDfy(); + } + + [Fact] + public void should_fix_issue_240() + { + 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 { "Get" }, + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + }, + RouteClaimsRequirement = + { + {"Role", "User"}, + }, + }, + }, + }; + + var users = new List + { + new() + { + Username = "test", + Password = "test", + SubjectId = "registered|1231231", + Claims = new List + { + new("Role", "AdminUser"), + new("Role", "User"), + }, + }, + }; + + this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt, users)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "Hello from Laura")) + .And(x => _steps.GivenIHaveAToken(_identityServerRootUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) + .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + } + + private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTokenType tokenType) + { + _identityServerBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .ConfigureServices(services => + { + services.AddLogging(); + services.AddIdentityServer() + .AddDeveloperSigningCredential() + .AddInMemoryApiScopes(new List + { + new(apiName, "test"), + new("openid", "test"), + new("offline_access", "test"), + new("api.readOnly", "test"), + }) + .AddInMemoryApiResources(new List + { + new() + { + Name = apiName, + Description = "My API", + Enabled = true, + DisplayName = "test", + Scopes = new List + { + "api", + "api.readOnly", + "openid", + "offline_access", + }, + ApiSecrets = new List + { + new() + { + Value = "secret".Sha256(), + }, + }, + UserClaims = new List + { + "CustomerId", "LocationId", "UserType", "UserId", + }, + }, + }) + .AddInMemoryClients(new List + { + new() + { + ClientId = "client", + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List {new("secret".Sha256())}, + AllowedScopes = new List { apiName, "api.readOnly", "openid", "offline_access" }, + AccessTokenType = tokenType, + Enabled = true, + RequireClientSecret = false, + }, + }) + .AddTestUsers(new List + { + new() + { + Username = "test", + Password = "test", + SubjectId = "registered|1231231", + Claims = new List + { + new("CustomerId", "123"), + new("LocationId", "321"), + }, + }, + }); + }) + .Configure(app => + { + app.UseIdentityServer(); + }) + .Build(); + + _identityServerBuilder.Start(); + + _steps.VerifyIdentiryServerStarted(url); + } + + private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTokenType tokenType, List users) + { + _identityServerBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .ConfigureServices(services => + { + services.AddLogging(); + services.AddIdentityServer() + .AddDeveloperSigningCredential() + .AddInMemoryApiScopes(new List + { + new(apiName, "test"), + }) + .AddInMemoryApiResources(new List + { + new() + { + Name = apiName, + Description = "My API", + Enabled = true, + DisplayName = "test", + Scopes = new List + { + "api", + "api.readOnly", + "openid", + "offline_access", + }, + ApiSecrets = new List + { + new() + { + Value = "secret".Sha256(), + }, + }, + UserClaims = new List + { + "CustomerId", "LocationId", "UserType", "UserId", "Role", + }, + }, + }) + .AddInMemoryClients(new List + { + new() + { + ClientId = "client", + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List {new("secret".Sha256())}, + AllowedScopes = new List { apiName, "api.readOnly", "openid", "offline_access" }, + AccessTokenType = tokenType, + Enabled = true, + RequireClientSecret = false, + }, + }) + .AddTestUsers(users); + }) + .Configure(app => + { + app.UseIdentityServer(); + }) + .Build(); + + _identityServerBuilder.Start(); + + _steps.VerifyIdentiryServerStarted(url); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + _identityServerBuilder?.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs b/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs index 1b587cb82..45deb6791 100644 --- a/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs +++ b/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs @@ -1,261 +1,271 @@ -namespace Ocelot.AcceptanceTests -{ - using Butterfly.Client.AspNetCore; - using Configuration.File; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Rafty.Infrastructure; - using Shouldly; - using System; - using System.Collections.Generic; - using System.IO; - using System.Net; - using TestStack.BDDfy; - using Xunit; - using Xunit.Abstractions; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; - public class ButterflyTracingTests : IDisposable - { - private IWebHost _serviceOneBuilder; - private IWebHost _serviceTwoBuilder; - private IWebHost _fakeButterfly; - private readonly Steps _steps; - private string _downstreamPathOne; - private string _downstreamPathTwo; - private int _butterflyCalled; - private readonly ITestOutputHelper _output; +using Butterfly.Client.AspNetCore; - public ButterflyTracingTests(ITestOutputHelper output) - { - _output = output; - _steps = new Steps(); - } +using Ocelot.Configuration.File; - [Fact] - public void should_forward_tracing_information_from_ocelot_and_downstream_services() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/values", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51887, - } - }, - UpstreamPathTemplate = "/api001/values", - UpstreamHttpMethod = new List { "Get" }, - HttpHandlerOptions = new FileHttpHandlerOptions - { - UseTracing = true - } - }, - new FileReRoute - { - DownstreamPathTemplate = "/api/values", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51388, - } - }, - UpstreamPathTemplate = "/api002/values", - UpstreamHttpMethod = new List { "Get" }, - HttpHandlerOptions = new FileHttpHandlerOptions - { - UseTracing = true - } - } - } - }; - - var butterflyUrl = "http://localhost:9618"; - - this.Given(x => GivenFakeButterfly(butterflyUrl)) - .And(x => GivenServiceOneIsRunning("http://localhost:51887", "/api/values", 200, "Hello from Laura", butterflyUrl)) - .And(x => GivenServiceTwoIsRunning("http://localhost:51388", "/api/values", 200, "Hello from Tom", butterflyUrl)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingButterfly(butterflyUrl)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api001/values")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api002/values")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) - .BDDfy(); - - var commandOnAllStateMachines = Wait.WaitFor(10000).Until(() => _butterflyCalled >= 4); +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; - _output.WriteLine($"_butterflyCalled is {_butterflyCalled}"); +using Shouldly; - commandOnAllStateMachines.ShouldBeTrue(); - } +using TestStack.BDDfy; - [Fact] - public void should_return_tracing_header() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/values", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51387, - } - }, - UpstreamPathTemplate = "/api001/values", - UpstreamHttpMethod = new List { "Get" }, - HttpHandlerOptions = new FileHttpHandlerOptions - { - UseTracing = true - }, - DownstreamHeaderTransform = new Dictionary() - { - {"Trace-Id", "{TraceId}"}, - {"Tom", "Laura"} - } - } - } +using Xunit; +using Xunit.Abstractions; + +namespace Ocelot.AcceptanceTests +{ + public class ButterflyTracingTests : IDisposable + { + private IWebHost _serviceOneBuilder; + private IWebHost _serviceTwoBuilder; + private IWebHost _fakeButterfly; + private readonly Steps _steps; + private string _downstreamPathOne; + private string _downstreamPathTwo; + private int _butterflyCalled; + private readonly ITestOutputHelper _output; + + public ButterflyTracingTests(ITestOutputHelper output) + { + _output = output; + _steps = new Steps(); + } + + [Fact] + public void should_forward_tracing_information_from_ocelot_and_downstream_services() + { + var port1 = RandomPortFinder.GetRandomPort(); + var port2 = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port1, + }, + }, + UpstreamPathTemplate = "/api001/values", + UpstreamHttpMethod = new List { "Get" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true, + }, + }, + new() + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port2, + }, + }, + UpstreamPathTemplate = "/api002/values", + UpstreamHttpMethod = new List { "Get" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true, + }, + }, + }, }; - var butterflyUrl = "http://localhost:9618"; - - this.Given(x => GivenFakeButterfly(butterflyUrl)) - .And(x => GivenServiceOneIsRunning("http://localhost:51387", "/api/values", 200, "Hello from Laura", butterflyUrl)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingButterfly(butterflyUrl)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api001/values")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => _steps.ThenTheTraceHeaderIsSet("Trace-Id")) - .And(x => _steps.ThenTheResponseHeaderIs("Tom", "Laura")) - .BDDfy(); - } - - private void GivenServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody, string butterflyUrl) - { - _serviceOneBuilder = new WebHostBuilder() - .UseUrls(baseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .ConfigureServices(services => - { - services.AddButterfly(option => - { - option.CollectorUrl = butterflyUrl; - option.Service = "Service One"; - option.IgnoredRoutesRegexPatterns = new string[0]; - }); - }) - .Configure(app => - { - app.UsePathBase(basePath); - app.Run(async context => - { - _downstreamPathOne = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPathOne != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); - }) - .Build(); - - _serviceOneBuilder.Start(); - } - - private void GivenFakeButterfly(string baseUrl) - { - _fakeButterfly = new WebHostBuilder() - .UseUrls(baseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .Configure(app => - { - app.Run(async context => - { - _butterflyCalled++; - await context.Response.WriteAsync("OK..."); - }); - }) - .Build(); - - _fakeButterfly.Start(); - } - - private void GivenServiceTwoIsRunning(string baseUrl, string basePath, int statusCode, string responseBody, string butterflyUrl) - { - _serviceTwoBuilder = new WebHostBuilder() - .UseUrls(baseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .ConfigureServices(services => - { - services.AddButterfly(option => - { - option.CollectorUrl = butterflyUrl; - option.Service = "Service Two"; - option.IgnoredRoutesRegexPatterns = new string[0]; - }); - }) - .Configure(app => - { - app.UsePathBase(basePath); - app.Run(async context => - { - _downstreamPathTwo = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPathTwo != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); - }) - .Build(); - - _serviceTwoBuilder.Start(); - } - - public void Dispose() - { - _serviceOneBuilder?.Dispose(); - _serviceTwoBuilder?.Dispose(); - _fakeButterfly?.Dispose(); - _steps.Dispose(); - } - } -} + var butterflyPort = RandomPortFinder.GetRandomPort(); + var butterflyUrl = $"http://localhost:{butterflyPort}"; + + this.Given(x => GivenFakeButterfly(butterflyUrl)) + .And(x => GivenServiceOneIsRunning($"http://localhost:{port1}", "/api/values", 200, "Hello from Laura", butterflyUrl)) + .And(x => GivenServiceTwoIsRunning($"http://localhost:{port2}", "/api/values", 200, "Hello from Tom", butterflyUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingButterfly(butterflyUrl)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api001/values")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api002/values")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) + .BDDfy(); + + var commandOnAllStateMachines = Wait.WaitFor(10000).Until(() => _butterflyCalled >= 4); + + _output.WriteLine($"_butterflyCalled is {_butterflyCalled}"); + + commandOnAllStateMachines.ShouldBeTrue(); + } + + [Fact] + public void should_return_tracing_header() + { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/api001/values", + UpstreamHttpMethod = new List { "Get" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true, + }, + DownstreamHeaderTransform = new Dictionary + { + {"Trace-Id", "{TraceId}"}, + {"Tom", "Laura"}, + }, + }, + }, + }; + + var butterflyPort = RandomPortFinder.GetRandomPort(); + var butterflyUrl = $"http://localhost:{butterflyPort}"; + + this.Given(x => GivenFakeButterfly(butterflyUrl)) + .And(x => GivenServiceOneIsRunning($"http://localhost:{port}", "/api/values", 200, "Hello from Laura", butterflyUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingButterfly(butterflyUrl)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api001/values")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.ThenTheTraceHeaderIsSet("Trace-Id")) + .And(x => _steps.ThenTheResponseHeaderIs("Tom", "Laura")) + .BDDfy(); + } + + private void GivenServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody, string butterflyUrl) + { + _serviceOneBuilder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .ConfigureServices(services => + { + services.AddButterfly(option => + { + option.CollectorUrl = butterflyUrl; + option.Service = "Service One"; + option.IgnoredRoutesRegexPatterns = Array.Empty(); + }); + }) + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + _downstreamPathOne = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPathOne != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + }) + .Build(); + + _serviceOneBuilder.Start(); + } + + private void GivenFakeButterfly(string baseUrl) + { + _fakeButterfly = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.Run(async context => + { + _butterflyCalled++; + await context.Response.WriteAsync("OK..."); + }); + }) + .Build(); + + _fakeButterfly.Start(); + } + + private void GivenServiceTwoIsRunning(string baseUrl, string basePath, int statusCode, string responseBody, string butterflyUrl) + { + _serviceTwoBuilder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .ConfigureServices(services => + { + services.AddButterfly(option => + { + option.CollectorUrl = butterflyUrl; + option.Service = "Service Two"; + option.IgnoredRoutesRegexPatterns = Array.Empty(); + }); + }) + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + _downstreamPathTwo = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPathTwo != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + }) + .Build(); + + _serviceTwoBuilder.Start(); + } + + public void Dispose() + { + _serviceOneBuilder?.Dispose(); + _serviceTwoBuilder?.Dispose(); + _fakeButterfly?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Caching/InMemoryJsonHandle.cs b/test/Ocelot.AcceptanceTests/Caching/InMemoryJsonHandle.cs index 9c12ad609..5e7ddeaef 100644 --- a/test/Ocelot.AcceptanceTests/Caching/InMemoryJsonHandle.cs +++ b/test/Ocelot.AcceptanceTests/Caching/InMemoryJsonHandle.cs @@ -1,13 +1,14 @@ -namespace Ocelot.AcceptanceTests.Caching -{ - using CacheManager.Core; - using CacheManager.Core.Internal; - using CacheManager.Core.Logging; - using CacheManager.Core.Utility; - using System; - using System.Collections.Concurrent; - using System.Linq; +using System; +using System.Collections.Concurrent; +using System.Linq; + +using CacheManager.Core; +using CacheManager.Core.Internal; +using CacheManager.Core.Logging; +using CacheManager.Core.Utility; +namespace Ocelot.AcceptanceTests.Caching +{ public class InMemoryJsonHandle : BaseCacheHandle { private readonly ICacheSerializer _serializer; @@ -37,7 +38,7 @@ public override void ClearRegion(string region) var key = string.Concat(region, ":"); foreach (var item in _cache.Where(p => p.Key.StartsWith(key, StringComparison.OrdinalIgnoreCase))) { - _cache.TryRemove(item.Key, out Tuple val); + _cache.TryRemove(item.Key, out var val); } } @@ -74,13 +75,13 @@ protected override CacheItem GetCacheItemInternal(string key, strin CacheItem deserializedResult = null; - if (_cache.TryGetValue(fullKey, out Tuple result)) + if (_cache.TryGetValue(fullKey, out var result)) { deserializedResult = _serializer.DeserializeCacheItem(result.Item2, result.Item1); if (deserializedResult.ExpirationMode != ExpirationMode.None && IsExpired(deserializedResult, DateTime.UtcNow)) { - _cache.TryRemove(fullKey, out Tuple removeResult); + _cache.TryRemove(fullKey, out var removeResult); TriggerCacheSpecificRemove(key, region, CacheItemRemovedReason.Expired, deserializedResult.Value); return null; } @@ -93,7 +94,7 @@ protected override void PutInternalPrepared(CacheItem item) { Guard.NotNull(item, nameof(item)); - var serializedItem = _serializer.SerializeCacheItem(item); + var serializedItem = _serializer.SerializeCacheItem(item); _cache[GetKey(item.Key, item.Region)] = new Tuple(item.Value.GetType(), serializedItem); } @@ -103,7 +104,7 @@ protected override void PutInternalPrepared(CacheItem item) protected override bool RemoveInternal(string key, string region) { var fullKey = GetKey(key, region); - return _cache.TryRemove(fullKey, out Tuple val); + return _cache.TryRemove(fullKey, out var val); } private static string GetKey(string key, string region) diff --git a/test/Ocelot.AcceptanceTests/CachingTests.cs b/test/Ocelot.AcceptanceTests/CachingTests.cs index d8b1a05b1..10de27bef 100644 --- a/test/Ocelot.AcceptanceTests/CachingTests.cs +++ b/test/Ocelot.AcceptanceTests/CachingTests.cs @@ -1,14 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading; + +using Ocelot.Configuration.File; + +using Microsoft.AspNetCore.Http; + +using TestStack.BDDfy; + +using Xunit; + namespace Ocelot.AcceptanceTests { - using Configuration.File; - using Microsoft.AspNetCore.Http; - using System; - using System.Collections.Generic; - using System.Net; - using System.Threading; - using TestStack.BDDfy; - using Xunit; - public class CachingTests : IDisposable { private readonly Steps _steps; @@ -22,40 +26,42 @@ public CachingTests() [Fact] public void should_return_cached_response() - { + { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 57899, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, FileCacheOptions = new FileCacheOptions { - TtlSeconds = 100 - } - } - } + TtlSeconds = 100, + }, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:57899", 200, "Hello from Laura", null, null)) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "Hello from Laura", null, null)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => x.GivenTheServiceNowReturns("http://localhost:57899", 200, "Hello from Tom")) + .Given(x => x.GivenTheServiceNowReturns($"http://localhost:{port}", 200, "Hello from Tom")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) @@ -65,40 +71,42 @@ public void should_return_cached_response() [Fact] public void should_return_cached_response_with_expires_header() - { + { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 52839, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, FileCacheOptions = new FileCacheOptions { - TtlSeconds = 100 - } - } - } + TtlSeconds = 100, + }, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:52839", 200, "Hello from Laura", "Expires", "-1")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "Hello from Laura", "Expires", "-1")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => x.GivenTheServiceNowReturns("http://localhost:52839", 200, "Hello from Tom")) + .Given(x => x.GivenTheServiceNowReturns($"http://localhost:{port}", 200, "Hello from Tom")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) @@ -109,40 +117,42 @@ 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 configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 57879, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, FileCacheOptions = new FileCacheOptions { - TtlSeconds = 100 - } - } - } + TtlSeconds = 100, + }, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:57879", 200, "Hello from Laura", null, null)) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "Hello from Laura", null, null)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningUsingJsonSerializedCache()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => x.GivenTheServiceNowReturns("http://localhost:57879", 200, "Hello from Tom")) + .Given(x => x.GivenTheServiceNowReturns($"http://localhost:{port}", 200, "Hello from Tom")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) @@ -151,48 +161,50 @@ 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 configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 57873, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, FileCacheOptions = new FileCacheOptions { - TtlSeconds = 1 - } - } - } + TtlSeconds = 1, + }, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:57873", 200, "Hello from Laura", null, null)) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "Hello from Laura", null, null)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => x.GivenTheServiceNowReturns("http://localhost:57873", 200, "Hello from Tom")) - .And(x => x.GivenTheCacheExpires()) + .Given(x => x.GivenTheServiceNowReturns($"http://localhost:{port}", 200, "Hello from Tom")) + .And(x => GivenTheCacheExpires()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) .BDDfy(); } - private void GivenTheCacheExpires() + private static void GivenTheCacheExpires() { Thread.Sleep(1000); } @@ -211,6 +223,7 @@ private void GivenThereIsAServiceRunningOn(string url, int statusCode, string re { context.Response.Headers.Add(key, value); } + context.Response.StatusCode = statusCode; await context.Response.WriteAsync(responseBody); }); diff --git a/test/Ocelot.AcceptanceTests/CannotStartOcelotTests.cs b/test/Ocelot.AcceptanceTests/CannotStartOcelotTests.cs index c08d5eb5a..6a2bbdc71 100644 --- a/test/Ocelot.AcceptanceTests/CannotStartOcelotTests.cs +++ b/test/Ocelot.AcceptanceTests/CannotStartOcelotTests.cs @@ -1,221 +1,226 @@ -namespace Ocelot.AcceptanceTests -{ - using Ocelot.Configuration.File; - using Shouldly; - using System; - using System.Collections.Generic; - using Xunit; - - public class CannotStartOcelotTests : IDisposable - { - private readonly Steps _steps; - - public CannotStartOcelotTests() - { - _steps = new Steps(); - } - - [Fact] - public void should_throw_exception_if_cannot_start_because_service_discovery_provider_specified_in_config_but_no_service_discovery_provider_registered_with_dynamic_re_routes() - { - var invalidConfig = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Type = "consul", - Port = 8500 - } - } - }; - - Exception exception = null; - _steps.GivenThereIsAConfiguration(invalidConfig); - try - { - _steps.GivenOcelotIsRunning(); - } - catch (Exception ex) - { - exception = ex; - } - - exception.ShouldNotBeNull(); - exception.Message.ShouldBe("One or more errors occurred. (Unable to start Ocelot, errors are: Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?)"); - } - - [Fact] - public void should_throw_exception_if_cannot_start_because_service_discovery_provider_specified_in_config_but_no_service_discovery_provider_registered() - { - var invalidConfig = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "test" - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Type = "consul", - Port = 8500 - } - } - }; - - Exception exception = null; - _steps.GivenThereIsAConfiguration(invalidConfig); - try - { - _steps.GivenOcelotIsRunning(); - } - catch (Exception ex) - { - exception = ex; - } - - exception.ShouldNotBeNull(); - exception.Message.ShouldBe("One or more errors occurred. (Unable to start Ocelot, errors are: Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?,Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?)"); - } - - [Fact] - public void should_throw_exception_if_cannot_start_because_no_qos_delegate_registered_globally() - { - var invalidConfig = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura", - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - QoSOptions = new FileQoSOptions - { - TimeoutValue = 1, - ExceptionsAllowedBeforeBreaking = 1 - } - } - }; - - Exception exception = null; - _steps.GivenThereIsAConfiguration(invalidConfig); - try - { - _steps.GivenOcelotIsRunning(); - } - catch (Exception ex) - { - exception = ex; - } - - exception.ShouldNotBeNull(); - exception.Message.ShouldBe("One or more errors occurred. (Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?)"); - } - - [Fact] - public void should_throw_exception_if_cannot_start_because_no_qos_delegate_registered_for_re_route() - { - var invalidConfig = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura", - QoSOptions = new FileQoSOptions - { - TimeoutValue = 1, - ExceptionsAllowedBeforeBreaking = 1 - } - } - } - }; - - Exception exception = null; - _steps.GivenThereIsAConfiguration(invalidConfig); - try - { - _steps.GivenOcelotIsRunning(); - } - catch (Exception ex) - { - exception = ex; - } - - exception.ShouldNotBeNull(); - exception.Message.ShouldBe("One or more errors occurred. (Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?)"); - } - - [Fact] - public void should_throw_exception_if_cannot_start() - { - var invalidConfig = new FileConfiguration() - { - ReRoutes = new List - { - new FileReRoute - { - UpstreamPathTemplate = "api", - DownstreamPathTemplate = "test" - } - } - }; - - Exception exception = null; - _steps.GivenThereIsAConfiguration(invalidConfig); - try - { - _steps.GivenOcelotIsRunning(); - } - catch (Exception ex) - { - exception = ex; - } - - exception.ShouldNotBeNull(); - exception.Message.ShouldBe("One or more errors occurred. (Unable to start Ocelot, errors are: Downstream Path Template test doesnt start with forward slash,Upstream Path Template api doesnt start with forward slash,When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!)"); - } - - public void Dispose() - { - _steps.Dispose(); - } - } -} +using System; +using System.Collections.Generic; + +using Ocelot.Configuration.File; + +using Shouldly; + +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class CannotStartOcelotTests : IDisposable + { + private readonly Steps _steps; + + public CannotStartOcelotTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_throw_exception_if_cannot_start_because_service_discovery_provider_specified_in_config_but_no_service_discovery_provider_registered_with_dynamic_re_routes() + { + var invalidConfig = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "https", + Host = "localhost", + Type = "consul", + Port = 8500, + }, + }, + }; + + Exception exception = null; + _steps.GivenThereIsAConfiguration(invalidConfig); + try + { + _steps.GivenOcelotIsRunning(); + } + catch (Exception ex) + { + exception = ex; + } + + exception.ShouldNotBeNull(); + exception.Message.ShouldBe("One or more errors occurred. (Unable to start Ocelot, errors are: Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?)"); + } + + [Fact] + public void should_throw_exception_if_cannot_start_because_service_discovery_provider_specified_in_config_but_no_service_discovery_provider_registered() + { + var invalidConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "test", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "https", + Host = "localhost", + Type = "consul", + Port = 8500, + }, + }, + }; + + Exception exception = null; + _steps.GivenThereIsAConfiguration(invalidConfig); + try + { + _steps.GivenOcelotIsRunning(); + } + catch (Exception ex) + { + exception = ex; + } + + exception.ShouldNotBeNull(); + exception.Message.ShouldBe("One or more errors occurred. (Unable to start Ocelot, errors are: Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?,Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?)"); + } + + [Fact] + public void should_throw_exception_if_cannot_start_because_no_qos_delegate_registered_globally() + { + var invalidConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51878, + }, + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + QoSOptions = new FileQoSOptions + { + TimeoutValue = 1, + ExceptionsAllowedBeforeBreaking = 1, + }, + }, + }; + + Exception exception = null; + _steps.GivenThereIsAConfiguration(invalidConfig); + try + { + _steps.GivenOcelotIsRunning(); + } + catch (Exception ex) + { + exception = ex; + } + + exception.ShouldNotBeNull(); + exception.Message.ShouldBe("One or more errors occurred. (Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?)"); + } + + [Fact] + public void should_throw_exception_if_cannot_start_because_no_qos_delegate_registered_for_re_route() + { + var invalidConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51878, + }, + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + QoSOptions = new FileQoSOptions + { + TimeoutValue = 1, + ExceptionsAllowedBeforeBreaking = 1, + }, + }, + }, + }; + + Exception exception = null; + _steps.GivenThereIsAConfiguration(invalidConfig); + try + { + _steps.GivenOcelotIsRunning(); + } + catch (Exception ex) + { + exception = ex; + } + + exception.ShouldNotBeNull(); + exception.Message.ShouldBe("One or more errors occurred. (Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?)"); + } + + [Fact] + public void should_throw_exception_if_cannot_start() + { + var invalidConfig = new FileConfiguration + { + Routes = new List + { + new() + { + UpstreamPathTemplate = "api", + DownstreamPathTemplate = "test", + }, + }, + }; + + Exception exception = null; + _steps.GivenThereIsAConfiguration(invalidConfig); + try + { + _steps.GivenOcelotIsRunning(); + } + catch (Exception ex) + { + exception = ex; + } + + exception.ShouldNotBeNull(); + exception.Message.ShouldBe("One or more errors occurred. (Unable to start Ocelot, errors are: Downstream Path Template test doesnt start with forward slash,Upstream Path Template api doesnt start with forward slash,When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!)"); + } + + public void Dispose() + { + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs b/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs index 405a7c8ee..1d0f26c63 100644 --- a/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs @@ -1,13 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Net; + +using Ocelot.Configuration.File; + +using Microsoft.AspNetCore.Http; + +using TestStack.BDDfy; + +using Xunit; + namespace Ocelot.AcceptanceTests { - using Microsoft.AspNetCore.Http; - using Ocelot.Configuration.File; - using System; - using System.Collections.Generic; - using System.Net; - using TestStack.BDDfy; - using Xunit; - public class CaseSensitiveRoutingTests : IDisposable { private readonly Steps _steps; @@ -22,29 +26,31 @@ public CaseSensitiveRoutingTests() [Fact] public void should_return_response_200_when_global_ignore_case_sensitivity_set() { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/api/products/{productId}", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51877, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = new List { "Get" }, - } - } + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51877", "/api/products/1", 200, "Some Product")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/products/1", 200, "Some Product")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) @@ -53,32 +59,34 @@ public void should_return_response_200_when_global_ignore_case_sensitivity_set() } [Fact] - public void should_return_response_200_when_reroute_ignore_case_sensitivity_set() + public void should_return_response_200_when_route_ignore_case_sensitivity_set() { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/api/products/{productId}", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51877, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = new List { "Get" }, - ReRouteIsCaseSensitive = false, - } - } + RouteIsCaseSensitive = false, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51877", "/api/products/1", 200, "Some Product")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/products/1", 200, "Some Product")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) @@ -87,32 +95,34 @@ public void should_return_response_200_when_reroute_ignore_case_sensitivity_set( } [Fact] - public void should_return_response_404_when_reroute_respect_case_sensitivity_set() + public void should_return_response_404_when_route_respect_case_sensitivity_set() { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/api/products/{productId}", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51877, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = new List { "Get" }, - ReRouteIsCaseSensitive = true, - } - } + RouteIsCaseSensitive = true, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51877", "/api/products/1", 200, "Some Product")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/products/1", 200, "Some Product")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) @@ -121,32 +131,34 @@ public void should_return_response_404_when_reroute_respect_case_sensitivity_set } [Fact] - public void should_return_response_200_when_reroute_respect_case_sensitivity_set() + public void should_return_response_200_when_route_respect_case_sensitivity_set() { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/api/products/{productId}", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51877, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/PRODUCTS/{productId}", UpstreamHttpMethod = new List { "Get" }, - ReRouteIsCaseSensitive = true, - } - } + RouteIsCaseSensitive = true, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51877", "/api/products/1", 200, "Some Product")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/products/1", 200, "Some Product")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) @@ -157,30 +169,32 @@ public void should_return_response_200_when_reroute_respect_case_sensitivity_set [Fact] public void should_return_response_404_when_global_respect_case_sensitivity_set() { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/api/products/{productId}", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51877, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = new List { "Get" }, - ReRouteIsCaseSensitive = true, - } - } + RouteIsCaseSensitive = true, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51877", "/api/products/1", 200, "Some Product")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/products/1", 200, "Some Product")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) @@ -191,30 +205,32 @@ 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 configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/api/products/{productId}", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51877, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/PRODUCTS/{productId}", UpstreamHttpMethod = new List { "Get" }, - ReRouteIsCaseSensitive = true, - } - } + RouteIsCaseSensitive = true, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51877", "/api/products/1", 200, "Some Product")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/products/1", 200, "Some Product")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) @@ -237,4 +253,4 @@ public void Dispose() _steps.Dispose(); } } -} +} diff --git a/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs index 5e3f667f2..4626bfd95 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs @@ -1,33 +1,39 @@ using Xunit; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; + +using Ocelot.Configuration.File; + +using IdentityServer4.AccessTokenValidation; +using IdentityServer4.Models; +using IdentityServer4.Test; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +using Shouldly; + +using TestStack.BDDfy; namespace Ocelot.AcceptanceTests { - using IdentityServer4.AccessTokenValidation; - using IdentityServer4.Models; - using IdentityServer4.Test; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.DependencyInjection; - using Ocelot.Configuration.File; - using Shouldly; - using System; - using System.Collections.Generic; - using System.IO; - using System.Net; - using TestStack.BDDfy; - public class ClaimsToDownstreamPathTests : IDisposable { private IWebHost _servicebuilder; private IWebHost _identityServerBuilder; private readonly Steps _steps; - private Action _options; - private string _identityServerRootUrl = "http://localhost:57888"; + private readonly Action _options; + private readonly string _identityServerRootUrl; private string _downstreamFinalPath; public ClaimsToDownstreamPathTests() { + var identityServerPort = RandomPortFinder.GetRandomPort(); + _identityServerRootUrl = $"http://localhost:{identityServerPort}"; _steps = new Steps(); _options = o => { @@ -42,26 +48,28 @@ public ClaimsToDownstreamPathTests() [Fact] public void should_return_200_and_change_downstream_path() { - var user = new TestUser() + var user = new TestUser { Username = "test", Password = "test", SubjectId = "registered|1231231", }; + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/users/{userId}", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 57876, + Port = port, }, }, DownstreamScheme = "http", @@ -83,19 +91,24 @@ public void should_return_200_and_change_downstream_path() }, }; - this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:57888", "api", AccessTokenType.Jwt, user)) - .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:57876", 200)) - .And(x => _steps.GivenIHaveAToken("http://localhost:57888")) + this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt, user)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200)) + .And(x => _steps.GivenIHaveAToken(_identityServerRootUrl)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/users")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("UserId: 1231231")) - .And(x => _downstreamFinalPath.ShouldBe("/users/1231231")) + .And(x => ThenTheDownstreamPathIs("/users/1231231")) .BDDfy(); } + private void ThenTheDownstreamPathIs(string path) + { + _downstreamFinalPath.ShouldBe(path); + } + private void GivenThereIsAServiceRunningOn(string url, int statusCode) { _servicebuilder = new WebHostBuilder() @@ -110,7 +123,7 @@ private void GivenThereIsAServiceRunningOn(string url, int statusCode) { _downstreamFinalPath = context.Request.Path.Value; - string userId = _downstreamFinalPath.Replace("/users/", string.Empty); + var userId = _downstreamFinalPath.Replace("/users/", string.Empty); var responseBody = $"UserId: {userId}"; context.Response.StatusCode = statusCode; @@ -135,49 +148,56 @@ private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTo services.AddLogging(); services.AddIdentityServer() .AddDeveloperSigningCredential() + .AddInMemoryApiScopes(new List + { + new(apiName, "test"), + new("openid", "test"), + new("offline_access", "test"), + new("api.readOnly", "test"), + }) .AddInMemoryApiResources(new List { - new ApiResource + new() { Name = apiName, Description = "My API", Enabled = true, DisplayName = "test", - Scopes = new List() + Scopes = new List { - new Scope("api"), - new Scope("openid"), - new Scope("offline_access") + "api", + "openid", + "offline_access", }, - ApiSecrets = new List() + ApiSecrets = new List { - new Secret + new() { - Value = "secret".Sha256() - } + Value = "secret".Sha256(), + }, }, - UserClaims = new List() + UserClaims = new List { - "CustomerId", "LocationId", "UserType", "UserId" - } - } + "CustomerId", "LocationId", "UserType", "UserId", + }, + }, }) .AddInMemoryClients(new List { - new Client + new() { ClientId = "client", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, - ClientSecrets = new List {new Secret("secret".Sha256())}, + ClientSecrets = new List {new("secret".Sha256())}, AllowedScopes = new List { apiName, "openid", "offline_access" }, AccessTokenType = tokenType, Enabled = true, - RequireClientSecret = false - } + RequireClientSecret = false, + }, }) .AddTestUsers(new List { - user + user, }); }) .Configure(app => diff --git a/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs index 5f16c96f7..974264dbb 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs @@ -1,200 +1,214 @@ -using Xunit; - -[assembly: CollectionBehavior(DisableTestParallelization = true)] - -namespace Ocelot.AcceptanceTests -{ - using IdentityServer4.AccessTokenValidation; - using IdentityServer4.Models; - using IdentityServer4.Test; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.DependencyInjection; - using Ocelot.Configuration.File; - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Net; - using System.Security.Claims; - using TestStack.BDDfy; - - public class ClaimsToHeadersForwardingTests : IDisposable - { - private IWebHost _identityServerBuilder; - private readonly Steps _steps; - private Action _options; - private string _identityServerRootUrl = "http://localhost:52888"; - private readonly ServiceHandler _serviceHandler; - - public ClaimsToHeadersForwardingTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - _options = o => - { - o.Authority = _identityServerRootUrl; - o.ApiName = "api"; - o.RequireHttpsMetadata = false; - o.SupportedTokens = SupportedTokens.Both; - o.ApiSecret = "secret"; - }; - } - - [Fact] - public void should_return_response_200_and_foward_claim_as_header() +using Xunit; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Claims; + +using Ocelot.Configuration.File; + +using IdentityServer4.AccessTokenValidation; +using IdentityServer4.Models; +using IdentityServer4.Test; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +using TestStack.BDDfy; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace Ocelot.AcceptanceTests +{ + public class ClaimsToHeadersForwardingTests : IDisposable + { + private IWebHost _identityServerBuilder; + private readonly Steps _steps; + private readonly Action _options; + private readonly string _identityServerRootUrl; + private readonly ServiceHandler _serviceHandler; + + public ClaimsToHeadersForwardingTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + var identityServerPort = RandomPortFinder.GetRandomPort(); + _identityServerRootUrl = $"http://localhost:{identityServerPort}"; + _options = o => + { + o.Authority = _identityServerRootUrl; + o.ApiName = "api"; + o.RequireHttpsMetadata = false; + o.SupportedTokens = SupportedTokens.Both; + o.ApiSecret = "secret"; + }; + } + + [Fact] + public void should_return_response_200_and_foward_claim_as_header() { - var user = new TestUser() + var user = new TestUser { Username = "test", Password = "test", SubjectId = "registered|1231231", - Claims = new List - { - new Claim("CustomerId", "123"), - new Claim("LocationId", "1") - } + Claims = new List + { + new("CustomerId", "123"), + new("LocationId", "1"), + }, }; + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 52876, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - AuthenticationOptions = new FileAuthenticationOptions - { - AuthenticationProviderKey = "Test", - AllowedScopes = new List - { - "openid", "offline_access", "api" - }, - }, - AddHeadersToRequest = - { - {"CustomerId", "Claims[CustomerId] > value"}, - {"LocationId", "Claims[LocationId] > value"}, - {"UserType", "Claims[sub] > value[0] > |"}, - {"UserId", "Claims[sub] > value[1] > |"} - } - } - } + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + AllowedScopes = new List + { + "openid", "offline_access", "api", + }, + }, + AddHeadersToRequest = + { + {"CustomerId", "Claims[CustomerId] > value"}, + {"LocationId", "Claims[LocationId] > value"}, + {"UserType", "Claims[sub] > value[0] > |"}, + {"UserId", "Claims[sub] > value[1] > |"}, + }, + }, + }, }; - this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:52888", "api", AccessTokenType.Jwt, user)) - .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:52876", 200)) - .And(x => _steps.GivenIHaveAToken("http://localhost:52888")) + this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt, user)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200)) + .And(x => _steps.GivenIHaveAToken(_identityServerRootUrl)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("CustomerId: 123 LocationId: 1 UserType: registered UserId: 1231231")) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string url, int statusCode) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - var customerId = context.Request.Headers.First(x => x.Key == "CustomerId").Value.First(); - var locationId = context.Request.Headers.First(x => x.Key == "LocationId").Value.First(); - var userType = context.Request.Headers.First(x => x.Key == "UserType").Value.First(); - var userId = context.Request.Headers.First(x => x.Key == "UserId").Value.First(); - - var responseBody = $"CustomerId: {customerId} LocationId: {locationId} UserType: {userType} UserId: {userId}"; - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - } - - private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTokenType tokenType, TestUser user) - { - _identityServerBuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .ConfigureServices(services => - { - services.AddLogging(); - services.AddIdentityServer() - .AddDeveloperSigningCredential() - .AddInMemoryApiResources(new List - { - new ApiResource - { - Name = apiName, - Description = "My API", - Enabled = true, - DisplayName = "test", - Scopes = new List() - { - new Scope("api"), - new Scope("openid"), - new Scope("offline_access") - }, - ApiSecrets = new List() - { - new Secret - { - Value = "secret".Sha256() - } - }, - UserClaims = new List() - { - "CustomerId", "LocationId", "UserType", "UserId" - } - } - }) - .AddInMemoryClients(new List - { - new Client - { - ClientId = "client", - AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, - ClientSecrets = new List {new Secret("secret".Sha256())}, - AllowedScopes = new List { apiName, "openid", "offline_access" }, - AccessTokenType = tokenType, - Enabled = true, - RequireClientSecret = false - } - }) - .AddTestUsers(new List - { - user - }); - }) - .Configure(app => - { - app.UseIdentityServer(); - }) - .Build(); - - _identityServerBuilder.Start(); - - _steps.VerifyIdentiryServerStarted(url); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - _identityServerBuilder?.Dispose(); - } - } + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + var customerId = context.Request.Headers.First(x => x.Key == "CustomerId").Value.First(); + var locationId = context.Request.Headers.First(x => x.Key == "LocationId").Value.First(); + var userType = context.Request.Headers.First(x => x.Key == "UserType").Value.First(); + var userId = context.Request.Headers.First(x => x.Key == "UserId").Value.First(); + + var responseBody = $"CustomerId: {customerId} LocationId: {locationId} UserType: {userType} UserId: {userId}"; + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + } + + private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTokenType tokenType, TestUser user) + { + _identityServerBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .ConfigureServices(services => + { + services.AddLogging(); + services.AddIdentityServer() + .AddDeveloperSigningCredential() + .AddInMemoryApiScopes(new List + { + new(apiName, "test"), + new("openid", "test"), + new("offline_access", "test"), + new("api.readOnly", "test"), + }) + .AddInMemoryApiResources(new List + { + new() + { + Name = apiName, + Description = "My API", + Enabled = true, + DisplayName = "test", + Scopes = new List + { + "api", + "openid", + "offline_access", + }, + ApiSecrets = new List + { + new() + { + Value = "secret".Sha256(), + }, + }, + UserClaims = new List + { + "CustomerId", "LocationId", "UserType", "UserId", + }, + }, + }) + .AddInMemoryClients(new List + { + new() + { + ClientId = "client", + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List {new("secret".Sha256())}, + AllowedScopes = new List { apiName, "openid", "offline_access" }, + AccessTokenType = tokenType, + Enabled = true, + RequireClientSecret = false, + }, + }) + .AddTestUsers(new List + { + user, + }); + }) + .Configure(app => + { + app.UseIdentityServer(); + }) + .Build(); + + _identityServerBuilder.Start(); + + _steps.VerifyIdentiryServerStarted(url); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + _identityServerBuilder?.Dispose(); + } + } } diff --git a/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs index ad077eb7e..98f74ff4f 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs @@ -1,285 +1,303 @@ -using IdentityServer4.AccessTokenValidation; -using IdentityServer4.Models; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Primitives; -using Ocelot.Configuration.File; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Security.Claims; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.AcceptanceTests -{ - using IdentityServer4.Test; - using Shouldly; - - public class ClaimsToQueryStringForwardingTests : IDisposable - { - private IWebHost _servicebuilder; - private IWebHost _identityServerBuilder; - private readonly Steps _steps; - private Action _options; - private string _identityServerRootUrl = "http://localhost:57888"; - private string _downstreamQueryString; - - public ClaimsToQueryStringForwardingTests() - { - _steps = new Steps(); - _options = o => - { - o.Authority = _identityServerRootUrl; - o.ApiName = "api"; - o.RequireHttpsMetadata = false; - o.SupportedTokens = SupportedTokens.Both; - o.ApiSecret = "secret"; - }; - } - - [Fact] - public void should_return_response_200_and_foward_claim_as_query_string() +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Security.Claims; + +using Ocelot.Configuration.File; + +using IdentityServer4.AccessTokenValidation; +using IdentityServer4.Models; +using IdentityServer4.Test; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class ClaimsToQueryStringForwardingTests : IDisposable + { + private IWebHost _servicebuilder; + private IWebHost _identityServerBuilder; + private readonly Steps _steps; + private readonly Action _options; + private readonly string _identityServerRootUrl; + private string _downstreamQueryString; + + public ClaimsToQueryStringForwardingTests() { - var user = new TestUser() + _steps = new Steps(); + var identityServerPort = RandomPortFinder.GetRandomPort(); + _identityServerRootUrl = $"http://localhost:{identityServerPort}"; + _options = o => + { + o.Authority = _identityServerRootUrl; + o.ApiName = "api"; + o.RequireHttpsMetadata = false; + o.SupportedTokens = SupportedTokens.Both; + o.ApiSecret = "secret"; + }; + } + + [Fact] + public void should_return_response_200_and_foward_claim_as_query_string() + { + var user = new TestUser { Username = "test", Password = "test", SubjectId = "registered|1231231", - Claims = new List - { - new Claim("CustomerId", "123"), - new Claim("LocationId", "1") - } + Claims = new List + { + new("CustomerId", "123"), + new("LocationId", "1"), + }, }; + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 57876, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - AuthenticationOptions = new FileAuthenticationOptions - { - AuthenticationProviderKey = "Test", - AllowedScopes = new List - { - "openid", "offline_access", "api" - }, - }, - AddQueriesToRequest = - { - {"CustomerId", "Claims[CustomerId] > value"}, - {"LocationId", "Claims[LocationId] > value"}, - {"UserType", "Claims[sub] > value[0] > |"}, - {"UserId", "Claims[sub] > value[1] > |"} - } - } - } + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + AllowedScopes = new List + { + "openid", "offline_access", "api", + }, + }, + AddQueriesToRequest = + { + {"CustomerId", "Claims[CustomerId] > value"}, + {"LocationId", "Claims[LocationId] > value"}, + {"UserType", "Claims[sub] > value[0] > |"}, + {"UserId", "Claims[sub] > value[1] > |"}, + }, + }, + }, }; - this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:57888", "api", AccessTokenType.Jwt, user)) - .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:57876", 200)) - .And(x => _steps.GivenIHaveAToken("http://localhost:57888")) + this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt, user)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200)) + .And(x => _steps.GivenIHaveAToken(_identityServerRootUrl)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("CustomerId: 123 LocationId: 1 UserType: registered UserId: 1231231")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_and_foward_claim_as_query_string_and_preserve_original_string() + .BDDfy(); + } + + [Fact] + public void should_return_response_200_and_foward_claim_as_query_string_and_preserve_original_string() { - var user = new TestUser() + var user = new TestUser { Username = "test", Password = "test", SubjectId = "registered|1231231", - Claims = new List - { - new Claim("CustomerId", "123"), - new Claim("LocationId", "1") - } + Claims = new List + { + new("CustomerId", "123"), + new("LocationId", "1"), + }, }; + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 57876, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - AuthenticationOptions = new FileAuthenticationOptions - { - AuthenticationProviderKey = "Test", - AllowedScopes = new List - { - "openid", "offline_access", "api" - }, - }, - AddQueriesToRequest = - { - {"CustomerId", "Claims[CustomerId] > value"}, - {"LocationId", "Claims[LocationId] > value"}, - {"UserType", "Claims[sub] > value[0] > |"}, - {"UserId", "Claims[sub] > value[1] > |"} - } - } - } + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + AllowedScopes = new List + { + "openid", "offline_access", "api", + }, + }, + AddQueriesToRequest = + { + {"CustomerId", "Claims[CustomerId] > value"}, + {"LocationId", "Claims[LocationId] > value"}, + {"UserType", "Claims[sub] > value[0] > |"}, + {"UserId", "Claims[sub] > value[1] > |"}, + }, + }, + }, }; - this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:57888", "api", AccessTokenType.Jwt, user)) - .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:57876", 200)) - .And(x => _steps.GivenIHaveAToken("http://localhost:57888")) + this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt, user)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200)) + .And(x => _steps.GivenIHaveAToken(_identityServerRootUrl)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning(_options, "Test")) .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/?test=1&test=2")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("CustomerId: 123 LocationId: 1 UserType: registered UserId: 1231231")) - .And(_ => _downstreamQueryString.ShouldBe("?test=1&test=2&CustomerId=123&LocationId=1&UserId=1231231&UserType=registered")) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string url, int statusCode) - { - _servicebuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.Run(async context => - { - _downstreamQueryString = context.Request.QueryString.Value; - - StringValues customerId; - context.Request.Query.TryGetValue("CustomerId", out customerId); - - StringValues locationId; - context.Request.Query.TryGetValue("LocationId", out locationId); - - StringValues userType; - context.Request.Query.TryGetValue("UserType", out userType); - - StringValues userId; - context.Request.Query.TryGetValue("UserId", out userId); - - var responseBody = $"CustomerId: {customerId} LocationId: {locationId} UserType: {userType} UserId: {userId}"; - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - }) - .Build(); - - _servicebuilder.Start(); - } - - private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTokenType tokenType, TestUser user) - { - _identityServerBuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .ConfigureServices(services => - { - services.AddLogging(); - services.AddIdentityServer() - .AddDeveloperSigningCredential() - .AddInMemoryApiResources(new List - { - new ApiResource - { - Name = apiName, - Description = "My API", - Enabled = true, - DisplayName = "test", - Scopes = new List() - { - new Scope("api"), - new Scope("openid"), - new Scope("offline_access") - }, - ApiSecrets = new List() - { - new Secret - { - Value = "secret".Sha256() - } - }, - UserClaims = new List() - { - "CustomerId", "LocationId", "UserType", "UserId" - } - } - }) - .AddInMemoryClients(new List - { - new Client - { - ClientId = "client", - AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, - ClientSecrets = new List {new Secret("secret".Sha256())}, - AllowedScopes = new List { apiName, "openid", "offline_access" }, - AccessTokenType = tokenType, - Enabled = true, - RequireClientSecret = false - } - }) - .AddTestUsers(new List - { - user - }); - }) - .Configure(app => - { - app.UseIdentityServer(); - }) - .Build(); - - _identityServerBuilder.Start(); - - _steps.VerifyIdentiryServerStarted(url); - } - - public void Dispose() - { - _servicebuilder?.Dispose(); - _steps.Dispose(); - _identityServerBuilder?.Dispose(); - } - } + .And(_ => ThenTheQueryStringIs("?test=1&test=2&CustomerId=123&LocationId=1&UserId=1231231&UserType=registered")) + .BDDfy(); + } + + private void ThenTheQueryStringIs(string queryString) + { + _downstreamQueryString.ShouldBe(queryString); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode) + { + _servicebuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + _downstreamQueryString = context.Request.QueryString.Value; + + context.Request.Query.TryGetValue("CustomerId", out var customerId); + + context.Request.Query.TryGetValue("LocationId", out var locationId); + + context.Request.Query.TryGetValue("UserType", out var userType); + + context.Request.Query.TryGetValue("UserId", out var userId); + + var responseBody = $"CustomerId: {customerId} LocationId: {locationId} UserType: {userType} UserId: {userId}"; + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _servicebuilder.Start(); + } + + private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTokenType tokenType, TestUser user) + { + _identityServerBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .ConfigureServices(services => + { + services.AddLogging(); + services.AddIdentityServer() + .AddDeveloperSigningCredential() + .AddInMemoryApiScopes(new List + { + new(apiName, "test"), + new("openid", "test"), + new("offline_access", "test"), + new("api.readOnly", "test"), + }) + .AddInMemoryApiResources(new List + { + new() + { + Name = apiName, + Description = "My API", + Enabled = true, + DisplayName = "test", + Scopes = new List + { + "api", + "openid", + "offline_access", + }, + ApiSecrets = new List + { + new() + { + Value = "secret".Sha256(), + }, + }, + UserClaims = new List + { + "CustomerId", "LocationId", "UserType", "UserId", + }, + }, + }) + .AddInMemoryClients(new List + { + new() + { + ClientId = "client", + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List {new("secret".Sha256())}, + AllowedScopes = new List { apiName, "openid", "offline_access" }, + AccessTokenType = tokenType, + Enabled = true, + RequireClientSecret = false, + }, + }) + .AddTestUsers(new List + { + user, + }); + }) + .Configure(app => + { + app.UseIdentityServer(); + }) + .Build(); + + _identityServerBuilder.Start(); + + _steps.VerifyIdentiryServerStarted(url); + } + + public void Dispose() + { + _servicebuilder?.Dispose(); + _steps.Dispose(); + _identityServerBuilder?.Dispose(); + } + } } diff --git a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs index f598d4ad5..3d76f89ce 100644 --- a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs +++ b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs @@ -1,218 +1,226 @@ -namespace Ocelot.AcceptanceTests -{ - using Microsoft.AspNetCore.Http; - using Ocelot.Configuration.File; - using System; - using System.Collections.Generic; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; - public class ClientRateLimitTests : IDisposable - { - private readonly Steps _steps; - private int _counterOne; - private readonly ServiceHandler _serviceHandler; +using Ocelot.Configuration.File; - public ClientRateLimitTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } +using Microsoft.AspNetCore.Http; - [Fact] - public void should_call_withratelimiting() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/ClientRateLimit", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51876, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/ClientRateLimit", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = _steps.RequestIdKey, - RateLimitOptions = new FileRateLimitRule() - { - EnableRateLimiting = true, - ClientWhitelist = new List(), - Limit = 3, - Period = "1s", - PeriodTimespan = 1000 - } - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - RateLimitOptions = new FileRateLimitOptions() - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = "", - RateLimitCounterPrefix = "", - HttpStatusCode = 428 - }, - RequestIdKey = "oceclientrequest" - } - }; +using TestStack.BDDfy; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51876", "/api/ClientRateLimit")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .BDDfy(); - } - - [Fact] - public void should_wait_for_period_timespan_to_elapse_before_making_next_request() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/ClientRateLimit", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51926, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/ClientRateLimit", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = _steps.RequestIdKey, - - RateLimitOptions = new FileRateLimitRule() - { - EnableRateLimiting = true, - ClientWhitelist = new List(), - Limit = 3, - Period = "1s", - PeriodTimespan = 2 - } - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - RateLimitOptions = new FileRateLimitOptions() - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = "", - RateLimitCounterPrefix = "", - HttpStatusCode = 428 - }, - RequestIdKey = "oceclientrequest" - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51926", "/api/ClientRateLimit")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .And(x => _steps.GivenIWait(1000)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .And(x => _steps.GivenIWait(1000)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .BDDfy(); - } - - [Fact] - public void should_call_middleware_withWhitelistClient() - { - int port = 61876; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/ClientRateLimit", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = port, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/ClientRateLimit", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = _steps.RequestIdKey, - - RateLimitOptions = new FileRateLimitRule() - { - EnableRateLimiting = true, - ClientWhitelist = new List() { "ocelotclient1"}, - Limit = 3, - Period = "1s", - PeriodTimespan = 100 - } - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - RateLimitOptions = new FileRateLimitOptions() - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = "", - RateLimitCounterPrefix = "" - }, - RequestIdKey = "oceclientrequest" - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/ClientRateLimit")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 4)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, context => - { - _counterOne++; - context.Response.StatusCode = 200; - context.Response.WriteAsync(_counterOne.ToString()); - return Task.CompletedTask; - }); - } - - public void Dispose() - { - _steps.Dispose(); - } - } -} +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class ClientRateLimitTests : IDisposable + { + private readonly Steps _steps; + private int _counterOne; + private readonly ServiceHandler _serviceHandler; + + public ClientRateLimitTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_call_withratelimiting() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/ClientRateLimit", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/api/ClientRateLimit", + UpstreamHttpMethod = new List { "Get" }, + RequestIdKey = _steps.RequestIdKey, + RateLimitOptions = new FileRateLimitRule + { + EnableRateLimiting = true, + ClientWhitelist = new List(), + Limit = 3, + Period = "1s", + PeriodTimespan = 1000, + }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + RateLimitOptions = new FileRateLimitOptions + { + ClientIdHeader = "ClientId", + DisableRateLimitHeaders = false, + QuotaExceededMessage = string.Empty, + RateLimitCounterPrefix = string.Empty, + HttpStatusCode = 428, + }, + RequestIdKey = "oceclientrequest", + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/ClientRateLimit")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) + .BDDfy(); + } + + [Fact] + public void should_wait_for_period_timespan_to_elapse_before_making_next_request() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/ClientRateLimit", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/api/ClientRateLimit", + UpstreamHttpMethod = new List { "Get" }, + RequestIdKey = _steps.RequestIdKey, + + RateLimitOptions = new FileRateLimitRule + { + EnableRateLimiting = true, + ClientWhitelist = new List(), + Limit = 3, + Period = "1s", + PeriodTimespan = 2, + }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + RateLimitOptions = new FileRateLimitOptions + { + ClientIdHeader = "ClientId", + DisableRateLimitHeaders = false, + QuotaExceededMessage = string.Empty, + RateLimitCounterPrefix = string.Empty, + HttpStatusCode = 428, + }, + RequestIdKey = "oceclientrequest", + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/ClientRateLimit")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) + .And(x => _steps.GivenIWait(1000)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) + .And(x => _steps.GivenIWait(1000)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .BDDfy(); + } + + [Fact] + public void should_call_middleware_withWhitelistClient() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/ClientRateLimit", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/api/ClientRateLimit", + UpstreamHttpMethod = new List { "Get" }, + RequestIdKey = _steps.RequestIdKey, + + RateLimitOptions = new FileRateLimitRule + { + EnableRateLimiting = true, + ClientWhitelist = new List { "ocelotclient1"}, + Limit = 3, + Period = "1s", + PeriodTimespan = 100, + }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + RateLimitOptions = new FileRateLimitOptions + { + ClientIdHeader = "ClientId", + DisableRateLimitHeaders = false, + QuotaExceededMessage = string.Empty, + RateLimitCounterPrefix = string.Empty, + }, + RequestIdKey = "oceclientrequest", + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/ClientRateLimit")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 4)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, context => + { + _counterOne++; + context.Response.StatusCode = 200; + context.Response.WriteAsync(_counterOne.ToString()); + return Task.CompletedTask; + }); + } + + public void Dispose() + { + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs b/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs index e72aab335..3466d5077 100644 --- a/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs +++ b/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs @@ -1,183 +1,195 @@ -namespace Ocelot.AcceptanceTests -{ - using Configuration.File; - using Consul; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.Hosting; - using Newtonsoft.Json; - using System; - using System.Collections.Generic; - using System.IO; - using System.Net; - using System.Text; - using TestStack.BDDfy; - using Xunit; - - public class ConfigurationInConsulTests : IDisposable - { - private IHost _builder; - private readonly Steps _steps; - private IHost _fakeConsulBuilder; - private FileConfiguration _config; - private readonly List _consulServices; - - public ConfigurationInConsulTests() - { - _consulServices = new List(); - _steps = new Steps(); - } - - [Fact] - public void should_return_response_200_with_simple_url_when_using_jsonserialized_cache() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51779, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = 9502 - } - } - }; - - var fakeConsulServiceDiscoveryUrl = "http://localhost:9502"; - - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, "")) - .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51779", "", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) - { - _fakeConsulBuilder = Host.CreateDefaultBuilder() - .ConfigureWebHost(webBuilder => - { - webBuilder.UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.Run(async context => - { - if (context.Request.Method.ToLower() == "get" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") - { - var json = JsonConvert.SerializeObject(_config); - - var bytes = Encoding.UTF8.GetBytes(json); - - var base64 = Convert.ToBase64String(bytes); - - var kvp = new FakeConsulGetResponse(base64); - - await context.Response.WriteJsonAsync(new FakeConsulGetResponse[] { kvp }); - } - else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") - { - try - { - var reader = new StreamReader(context.Request.Body); - - // Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. - // var json = reader.ReadToEnd(); - var json = await reader.ReadToEndAsync(); - - _config = JsonConvert.DeserializeObject(json); - - var response = JsonConvert.SerializeObject(true); - - await context.Response.WriteAsync(response); - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } - } - else if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") - { - await context.Response.WriteJsonAsync(_consulServices); - } - }); - }); - }).Build(); - - _fakeConsulBuilder.Start(); - } - - public class FakeConsulGetResponse - { - public FakeConsulGetResponse(string value) - { - Value = value; - } - - public int CreateIndex => 100; - public int ModifyIndex => 200; - public int LockIndex => 200; - public string Key => "InternalConfiguration"; - public int Flags => 0; - public string Value { get; private set; } - public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e"; - } - - private void GivenThereIsAServiceRunningOn(string url, string basePath, int statusCode, string responseBody) - { - _builder = Host.CreateDefaultBuilder() - .ConfigureWebHost(webBuilder => - { - webBuilder.UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.UsePathBase(basePath); - app.Run(async context => - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - }); - }) - .Build(); - - _builder.Start(); - } - - public void Dispose() - { - _builder?.Dispose(); - _steps.Dispose(); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; + +using Ocelot.Configuration.File; + +using Consul; + +using IdentityServer4.Extensions; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; + +using Newtonsoft.Json; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class ConfigurationInConsulTests : IDisposable + { + private IHost _builder; + private readonly Steps _steps; + private IHost _fakeConsulBuilder; + private FileConfiguration _config; + private readonly List _consulServices; + + public ConfigurationInConsulTests() + { + _consulServices = new List(); + _steps = new Steps(); + } + + [Fact] + public void should_return_response_200_with_simple_url_when_using_jsonserialized_cache() + { + var consulPort = RandomPortFinder.GetRandomPort(); + var servicePort = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + + this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", string.Empty, 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) + { + _fakeConsulBuilder = Host.CreateDefaultBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder.UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + if (context.Request.Method.ToLower() == "get" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") + { + var json = JsonConvert.SerializeObject(_config); + + var bytes = Encoding.UTF8.GetBytes(json); + + var base64 = Convert.ToBase64String(bytes); + + var kvp = new FakeConsulGetResponse(base64); + + await context.Response.WriteJsonAsync(new[] { kvp }); + } + else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") + { + try + { + var reader = new StreamReader(context.Request.Body); + + // Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. + // var json = reader.ReadToEnd(); + var json = await reader.ReadToEndAsync(); + + _config = JsonConvert.DeserializeObject(json); + + var response = JsonConvert.SerializeObject(true); + + await context.Response.WriteAsync(response); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + else if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") + { + await context.Response.WriteJsonAsync(_consulServices); + } + }); + }); + }).Build(); + + _fakeConsulBuilder.Start(); + } + + public class FakeConsulGetResponse + { + public FakeConsulGetResponse(string value) + { + Value = value; + } + + public int CreateIndex => 100; + public int ModifyIndex => 200; + public int LockIndex => 200; + public string Key => "InternalConfiguration"; + public int Flags => 0; + public string Value { get; } + public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e"; + } + + private void GivenThereIsAServiceRunningOn(string url, string basePath, int statusCode, string responseBody) + { + _builder = Host.CreateDefaultBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder.UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }); + }) + .Build(); + + _builder.Start(); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ConfigurationReloadTests.cs b/test/Ocelot.AcceptanceTests/ConfigurationReloadTests.cs index 90d575044..9b673be9d 100644 --- a/test/Ocelot.AcceptanceTests/ConfigurationReloadTests.cs +++ b/test/Ocelot.AcceptanceTests/ConfigurationReloadTests.cs @@ -1,15 +1,19 @@ -using Ocelot.Configuration.File; -using System; +using System; + +using Ocelot.Configuration.ChangeTracking; +using Ocelot.Configuration.File; + using TestStack.BDDfy; + using Xunit; namespace Ocelot.AcceptanceTests { public class ConfigurationReloadTests : IDisposable { - private FileConfiguration _initialConfig; - private FileConfiguration _anotherConfig; - private Steps _steps; + private readonly FileConfiguration _initialConfig; + private readonly FileConfiguration _anotherConfig; + private readonly Steps _steps; public ConfigurationReloadTests() { @@ -19,16 +23,16 @@ public ConfigurationReloadTests() { GlobalConfiguration = new FileGlobalConfiguration { - RequestIdKey = "initialKey" - } + RequestIdKey = "initialKey", + }, }; _anotherConfig = new FileConfiguration { GlobalConfiguration = new FileGlobalConfiguration { - RequestIdKey = "someOtherKey" - } + RequestIdKey = "someOtherKey", + }, }; } @@ -38,7 +42,7 @@ public void should_reload_config_on_change() this.Given(x => _steps.GivenThereIsAConfiguration(_initialConfig)) .And(x => _steps.GivenOcelotIsRunningReloadingConfig(true)) .And(x => _steps.GivenThereIsAConfiguration(_anotherConfig)) - .And(x => _steps.GivenIWait(2500)) + .And(x => _steps.GivenIWait(5000)) .And(x => _steps.ThenConfigShouldBe(_anotherConfig)) .BDDfy(); } @@ -49,11 +53,38 @@ public void should_not_reload_config_on_change() this.Given(x => _steps.GivenThereIsAConfiguration(_initialConfig)) .And(x => _steps.GivenOcelotIsRunningReloadingConfig(false)) .And(x => _steps.GivenThereIsAConfiguration(_anotherConfig)) - .And(x => _steps.GivenIWait(2500)) + .And(x => _steps.GivenIWait(MillisecondsToWaitForChangeToken)) .And(x => _steps.ThenConfigShouldBe(_initialConfig)) .BDDfy(); } + [Fact] + public void should_trigger_change_token_on_change() + { + this.Given(x => _steps.GivenThereIsAConfiguration(_initialConfig)) + .And(x => _steps.GivenOcelotIsRunningReloadingConfig(true)) + .And(x => _steps.GivenIHaveAChangeToken()) + .And(x => _steps.GivenThereIsAConfiguration(_anotherConfig)) + .And(x => _steps.GivenIWait(MillisecondsToWaitForChangeToken)) + .Then(x => _steps.TheChangeTokenShouldBeActive(true)) + .BDDfy(); + } + + [Fact] + public void should_not_trigger_change_token_with_no_change() + { + this.Given(x => _steps.GivenThereIsAConfiguration(_initialConfig)) + .And(x => _steps.GivenOcelotIsRunningReloadingConfig(false)) + .And(x => _steps.GivenIHaveAChangeToken()) + .And(x => _steps.GivenIWait(MillisecondsToWaitForChangeToken)) // Wait for prior activation to expire. + .And(x => _steps.GivenThereIsAConfiguration(_anotherConfig)) + .And(x => _steps.GivenIWait(MillisecondsToWaitForChangeToken)) + .Then(x => _steps.TheChangeTokenShouldBeActive(false)) + .BDDfy(); + } + + private const int MillisecondsToWaitForChangeToken = (int)(OcelotConfigurationChangeToken.PollingIntervalSeconds * 1000) - 100; + public void Dispose() { _steps.Dispose(); diff --git a/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs b/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs index 976bd9095..08edaebfb 100644 --- a/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs +++ b/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs @@ -1,473 +1,494 @@ -namespace Ocelot.AcceptanceTests -{ - using Cache; - using Configuration.File; - using Consul; - using Infrastructure; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Newtonsoft.Json; - using Shouldly; - using System; - using System.Collections.Generic; - using System.IO; - using System.Net; - using System.Text; - using TestStack.BDDfy; - using Xunit; - - public class ConsulConfigurationInConsulTests : IDisposable - { - private IWebHost _builder; - private readonly Steps _steps; - private IWebHost _fakeConsulBuilder; - private FileConfiguration _config; - private readonly List _consulServices; - - public ConsulConfigurationInConsulTests() - { - _consulServices = new List(); - _steps = new Steps(); - } - - [Fact] - public void should_return_response_200_with_simple_url() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51779, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = 9500 - } - } - }; - - var fakeConsulServiceDiscoveryUrl = "http://localhost:9500"; - - this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, "")) - .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51779", "", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_load_configuration_out_of_consul() - { - var consulPort = 8500; - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration() - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = consulPort - } - } - }; - - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - - var consulConfig = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/status", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51779, - } - }, - UpstreamPathTemplate = "/cs/status", - UpstreamHttpMethod = new List {"Get"} - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = consulPort - } - } - }; - - this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) - .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, "")) - .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51779", "/status", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_load_configuration_out_of_consul_if_it_is_changed() - { - var consulPort = 8506; - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration() - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = consulPort - } - } - }; - - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - - var consulConfig = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/status", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51780, - } - }, - UpstreamPathTemplate = "/cs/status", - UpstreamHttpMethod = new List {"Get"} - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = consulPort - } - } - }; - - var secondConsulConfig = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/status", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51780, - } - }, - UpstreamPathTemplate = "/cs/status/awesome", - UpstreamHttpMethod = new List {"Get"} - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = consulPort - } - } - }; - - this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) - .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, "")) - .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51780", "/status", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .When(x => GivenTheConsulConfigurationIs(secondConsulConfig)) - .Then(x => ThenTheConfigIsUpdatedInOcelot()) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes_and_rate_limit() - { - const int consulPort = 8523; - const string serviceName = "web"; - const int downstreamServicePort = 8187; - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry() - { - Service = new AgentService() - { - Service = serviceName, - Address = "localhost", - Port = downstreamServicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" } - }, - }; - - var consulConfig = new FileConfiguration - { - DynamicReRoutes = new List - { - new FileDynamicReRoute - { - ServiceName = serviceName, - RateLimitRule = new FileRateLimitRule() - { - EnableRateLimiting = true, - ClientWhitelist = new List(), - Limit = 3, - Period = "1s", - PeriodTimespan = 1000 - } - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = consulPort - }, - RateLimitOptions = new FileRateLimitOptions() - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = "", - RateLimitCounterPrefix = "", - HttpStatusCode = 428 - }, - DownstreamScheme = "http", - } - }; - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = consulPort - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) - .And(x => GivenTheConsulConfigurationIs(consulConfig)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 2)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .BDDfy(); - } - - private void ThenTheConfigIsUpdatedInOcelot() - { - var result = Wait.WaitFor(20000).Until(() => - { - try - { - _steps.WhenIGetUrlOnTheApiGateway("/cs/status/awesome"); - _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK); - _steps.ThenTheResponseBodyShouldBe("Hello from Laura"); - return true; - } - catch (Exception) - { - return false; - } - }); - result.ShouldBeTrue(); - } - - private void GivenTheConsulConfigurationIs(FileConfiguration config) - { - _config = config; - } - - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) - { - foreach (var serviceEntry in serviceEntries) - { - _consulServices.Add(serviceEntry); - } - } - - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) - { - _fakeConsulBuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.Run(async context => - { - if (context.Request.Method.ToLower() == "get" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") - { - var json = JsonConvert.SerializeObject(_config); - - var bytes = Encoding.UTF8.GetBytes(json); - - var base64 = Convert.ToBase64String(bytes); - - var kvp = new FakeConsulGetResponse(base64); - json = JsonConvert.SerializeObject(new FakeConsulGetResponse[] { kvp }); - context.Response.Headers.Add("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") - { - try - { - var reader = new StreamReader(context.Request.Body); - - // Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. - // var json = reader.ReadToEnd(); - var json = await reader.ReadToEndAsync(); - - _config = JsonConvert.DeserializeObject(json); - - var response = JsonConvert.SerializeObject(true); - - await context.Response.WriteAsync(response); - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } - } - else if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") - { - var json = JsonConvert.SerializeObject(_consulServices); - context.Response.Headers.Add("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - }) - .Build(); - - _fakeConsulBuilder.Start(); - } - - public class FakeConsulGetResponse - { - public FakeConsulGetResponse(string value) - { - Value = value; - } - - public int CreateIndex => 100; - public int ModifyIndex => 200; - public int LockIndex => 200; - public string Key => "InternalConfiguration"; - public int Flags => 0; - public string Value { get; private set; } - public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e"; - } - - private void GivenThereIsAServiceRunningOn(string url, string basePath, int statusCode, string responseBody) - { - _builder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.UsePathBase(basePath); - - app.Run(async context => - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - }) - .Build(); - - _builder.Start(); - } - - public void Dispose() - { - _builder?.Dispose(); - _steps.Dispose(); - } - - private class FakeCache : IOcelotCache - { - public void Add(string key, FileConfiguration value, TimeSpan ttl, string region) - { - throw new NotImplementedException(); - } - - public FileConfiguration Get(string key, string region) - { - throw new NotImplementedException(); - } - - public void ClearRegion(string region) - { - throw new NotImplementedException(); - } - - public void AddAndDelete(string key, FileConfiguration value, TimeSpan ttl, string region) - { - throw new NotImplementedException(); - } - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; + +using Ocelot.Cache; + +using Ocelot.Configuration.File; + +using Consul; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; + +using Newtonsoft.Json; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class ConsulConfigurationInConsulTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + private IWebHost _fakeConsulBuilder; + private FileConfiguration _config; + private readonly List _consulServices; + + public ConsulConfigurationInConsulTests() + { + _consulServices = new List(); + _steps = new Steps(); + } + + [Fact] + public void should_return_response_200_with_simple_url() + { + var consulPort = RandomPortFinder.GetRandomPort(); + var servicePort = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + + this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", string.Empty, 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_load_configuration_out_of_consul() + { + var consulPort = RandomPortFinder.GetRandomPort(); + var servicePort = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + + var consulConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/status", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/cs/status", + UpstreamHttpMethod = new List {"Get"}, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) + .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", "/status", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_load_configuration_out_of_consul_if_it_is_changed() + { + var consulPort = RandomPortFinder.GetRandomPort(); + var servicePort = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + + var consulConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/status", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/cs/status", + UpstreamHttpMethod = new List {"Get"}, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + var secondConsulConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/status", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort, + }, + }, + UpstreamPathTemplate = "/cs/status/awesome", + UpstreamHttpMethod = new List {"Get"}, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) + .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{servicePort}", "/status", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(x => GivenTheConsulConfigurationIs(secondConsulConfig)) + .Then(x => ThenTheConfigIsUpdatedInOcelot()) + .BDDfy(); + } + + [Fact] + public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes_and_rate_limit() + { + var consulPort = RandomPortFinder.GetRandomPort(); + const string serviceName = "web"; + var downstreamServicePort = RandomPortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = downstreamServicePort, + ID = "web_90_0_2_224_8080", + Tags = new[] { "version-v1" }, + }, + }; + + var consulConfig = new FileConfiguration + { + DynamicRoutes = new List + { + new() + { + ServiceName = serviceName, + RateLimitRule = new FileRateLimitRule + { + EnableRateLimiting = true, + ClientWhitelist = new List(), + Limit = 3, + Period = "1s", + PeriodTimespan = 1000, + }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + RateLimitOptions = new FileRateLimitOptions + { + ClientIdHeader = "ClientId", + DisableRateLimitHeaders = false, + QuotaExceededMessage = string.Empty, + RateLimitCounterPrefix = string.Empty, + HttpStatusCode = 428, + }, + DownstreamScheme = "http", + }, + }; + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) + .And(x => GivenTheConsulConfigurationIs(consulConfig)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingConsulToStoreConfig()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 2)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/web/something", 1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) + .BDDfy(); + } + + private void ThenTheConfigIsUpdatedInOcelot() + { + var result = Wait.WaitFor(20000).Until(() => + { + try + { + _steps.WhenIGetUrlOnTheApiGateway("/cs/status/awesome"); + _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK); + _steps.ThenTheResponseBodyShouldBe("Hello from Laura"); + return true; + } + catch (Exception) + { + return false; + } + }); + result.ShouldBeTrue(); + } + + private void GivenTheConsulConfigurationIs(FileConfiguration config) + { + _config = config; + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach (var serviceEntry in serviceEntries) + { + _consulServices.Add(serviceEntry); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) + { + _fakeConsulBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + if (context.Request.Method.ToLower() == "get" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") + { + var json = JsonConvert.SerializeObject(_config); + + var bytes = Encoding.UTF8.GetBytes(json); + + var base64 = Convert.ToBase64String(bytes); + + var kvp = new FakeConsulGetResponse(base64); + json = JsonConvert.SerializeObject(new[] { kvp }); + context.Response.Headers.Add("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") + { + try + { + var reader = new StreamReader(context.Request.Body); + + // Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. + // var json = reader.ReadToEnd(); + var json = await reader.ReadToEndAsync(); + + _config = JsonConvert.DeserializeObject(json); + + var response = JsonConvert.SerializeObject(true); + + await context.Response.WriteAsync(response); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + else if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") + { + var json = JsonConvert.SerializeObject(_consulServices); + context.Response.Headers.Add("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + }) + .Build(); + + _fakeConsulBuilder.Start(); + } + + public class FakeConsulGetResponse + { + public FakeConsulGetResponse(string value) + { + Value = value; + } + + public int CreateIndex => 100; + public int ModifyIndex => 200; + public int LockIndex => 200; + public string Key => "InternalConfiguration"; + public int Flags => 0; + public string Value { get; } + public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e"; + } + + private void GivenThereIsAServiceRunningOn(string url, string basePath, int statusCode, string responseBody) + { + _builder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.UsePathBase(basePath); + + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _builder.Start(); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + + private class FakeCache : IOcelotCache + { + public void Add(string key, FileConfiguration value, TimeSpan ttl, string region) + { + throw new NotImplementedException(); + } + + public FileConfiguration Get(string key, string region) + { + throw new NotImplementedException(); + } + + public void ClearRegion(string region) + { + throw new NotImplementedException(); + } + + public void AddAndDelete(string key, FileConfiguration value, TimeSpan ttl, string region) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs b/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs index 7ebb47379..5524a532d 100644 --- a/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs +++ b/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs @@ -1,351 +1,353 @@ -namespace Ocelot.AcceptanceTests -{ - using Configuration.File; - using Consul; - using Microsoft.AspNetCore.Http; - using Newtonsoft.Json; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Net.WebSockets; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - - public class ConsulWebSocketTests : IDisposable - { - private readonly List _secondRecieved; - private readonly List _firstRecieved; - private readonly List _serviceEntries; - private readonly Steps _steps; - private readonly ServiceHandler _serviceHandler; - - public ConsulWebSocketTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - _firstRecieved = new List(); - _secondRecieved = new List(); - _serviceEntries = new List(); - } - - [Fact] - public void should_proxy_websocket_input_to_downstream_service_and_use_service_discovery_and_load_balancer() - { - var downstreamPort = 5007; - var downstreamHost = "localhost"; - - var secondDownstreamPort = 5008; - var secondDownstreamHost = "localhost"; - - var serviceName = "websockets"; - var consulPort = 8509; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry() - { - Service = new AgentService() - { - Service = serviceName, - Address = downstreamHost, - Port = downstreamPort, - ID = Guid.NewGuid().ToString(), - Tags = new string[0] - }, - }; - var serviceEntryTwo = new ServiceEntry() - { - Service = new AgentService() - { - Service = serviceName, - Address = secondDownstreamHost, - Port = secondDownstreamPort, - ID = Guid.NewGuid().ToString(), - Tags = new string[0] - }, - }; - - var config = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - UpstreamPathTemplate = "/", - DownstreamPathTemplate = "/ws", - DownstreamScheme = "ws", - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "RoundRobin" }, - ServiceName = serviceName, - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = consulPort, - Type = "consul" - } - } - }; - - this.Given(_ => _steps.GivenThereIsAConfiguration(config)) - .And(_ => _steps.StartFakeOcelotWithWebSocketsWithConsul()) - .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws")) - .And(_ => StartSecondFakeDownstreamService($"http://{secondDownstreamHost}:{secondDownstreamPort}", "/ws")) - .When(_ => WhenIStartTheClients()) - .Then(_ => ThenBothDownstreamServicesAreCalled()) - .BDDfy(); - } - - private void ThenBothDownstreamServicesAreCalled() - { - _firstRecieved.Count.ShouldBe(10); - _firstRecieved.ForEach(x => - { - x.ShouldBe("test"); - }); - - _secondRecieved.Count.ShouldBe(10); - _secondRecieved.ForEach(x => - { - x.ShouldBe("chocolate"); - }); - } - - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) - { - foreach (var serviceEntry in serviceEntries) - { - _serviceEntries.Add(serviceEntry); - } - } - - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") - { - var json = JsonConvert.SerializeObject(_serviceEntries); - context.Response.Headers.Add("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - } - - private async Task WhenIStartTheClients() - { - var firstClient = StartClient("ws://localhost:5000/"); - - var secondClient = StartSecondClient("ws://localhost:5000/"); - - await Task.WhenAll(firstClient, secondClient); - } - - private async Task StartClient(string url) - { - var client = new ClientWebSocket(); - - await client.ConnectAsync(new Uri(url), CancellationToken.None); - - var sending = Task.Run(async () => - { - string line = "test"; - for (int i = 0; i < 10; i++) - { - var bytes = Encoding.UTF8.GetBytes(line); - - await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, - CancellationToken.None); - await Task.Delay(10); - } - - await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); - }); - - var receiving = Task.Run(async () => - { - var buffer = new byte[1024 * 4]; - - while (true) - { - var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - if (result.MessageType == WebSocketMessageType.Text) - { - _firstRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); - } - else if (result.MessageType == WebSocketMessageType.Close) - { - if (client.State != WebSocketState.Closed) - { - // Last version, the client state is CloseReceived - // Valid states are: Open, CloseReceived, CloseSent - await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); - } - - break; - } - } - }); - - await Task.WhenAll(sending, receiving); - } - - private async Task StartSecondClient(string url) - { - await Task.Delay(500); - - var client = new ClientWebSocket(); - - await client.ConnectAsync(new Uri(url), CancellationToken.None); - - var sending = Task.Run(async () => - { - string line = "test"; - for (int i = 0; i < 10; i++) - { - var bytes = Encoding.UTF8.GetBytes(line); - - await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, - CancellationToken.None); - await Task.Delay(10); - } - - await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); - }); - - var receiving = Task.Run(async () => - { - var buffer = new byte[1024 * 4]; - - while (true) - { - var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - if (result.MessageType == WebSocketMessageType.Text) - { - _secondRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); - } - else if (result.MessageType == WebSocketMessageType.Close) - { - if (client.State != WebSocketState.Closed) - { - // Last version, the client state is CloseReceived - // Valid states are: Open, CloseReceived, CloseSent - await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); - } - - break; - } - } - }); - - await Task.WhenAll(sending, receiving); - } - - private async Task StartFakeDownstreamService(string url, string path) - { - await _serviceHandler.StartFakeDownstreamService(url, path, async (context, next) => - { - if (context.Request.Path == path) - { - if (context.WebSockets.IsWebSocketRequest) - { - var webSocket = await context.WebSockets.AcceptWebSocketAsync(); - await Echo(webSocket); - } - else - { - context.Response.StatusCode = 400; - } - } - else - { - await next(); - } - }); - } - - private async Task StartSecondFakeDownstreamService(string url, string path) - { - await _serviceHandler.StartFakeDownstreamService(url, path, async (context, next) => - { - if (context.Request.Path == path) - { - if (context.WebSockets.IsWebSocketRequest) - { - WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync(); - await Message(webSocket); - } - else - { - context.Response.StatusCode = 400; - } - } - else - { - await next(); - } - }); - } - - private async Task Echo(WebSocket webSocket) - { - try - { - var buffer = new byte[1024 * 4]; - - var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - while (!result.CloseStatus.HasValue) - { - await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); - - result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - } - - await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - - private async Task Message(WebSocket webSocket) - { - try - { - var buffer = new byte[1024 * 4]; - - var bytes = Encoding.UTF8.GetBytes("chocolate"); - - var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - while (!result.CloseStatus.HasValue) - { - await webSocket.SendAsync(new ArraySegment(bytes), result.MessageType, result.EndOfMessage, CancellationToken.None); - - result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - } - - await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - - public void Dispose() - { - _serviceHandler?.Dispose(); +using Consul; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class ConsulWebSocketTests : IDisposable + { + private readonly List _secondRecieved; + private readonly List _firstRecieved; + private readonly List _serviceEntries; + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public ConsulWebSocketTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + _firstRecieved = new List(); + _secondRecieved = new List(); + _serviceEntries = new List(); + } + + [Fact] + public void ShouldProxyWebsocketInputToDownstreamServiceAndUseServiceDiscoveryAndLoadBalancer() + { + var downstreamPort = RandomPortFinder.GetRandomPort(); + var downstreamHost = "localhost"; + + var secondDownstreamPort = RandomPortFinder.GetRandomPort(); + var secondDownstreamHost = "localhost"; + + var serviceName = "websockets"; + var consulPort = RandomPortFinder.GetRandomPort(); + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = downstreamHost, + Port = downstreamPort, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + var serviceEntryTwo = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = secondDownstreamHost, + Port = secondDownstreamPort, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + + var config = new FileConfiguration + { + Routes = new List + { + new() + { + UpstreamPathTemplate = "/", + DownstreamPathTemplate = "/ws", + DownstreamScheme = "ws", + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "RoundRobin" }, + ServiceName = serviceName, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + Type = "consul", + }, + }, + }; + + this.Given(_ => _steps.GivenThereIsAConfiguration(config)) + .And(_ => _steps.StartFakeOcelotWithWebSocketsWithConsul()) + .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws")) + .And(_ => StartSecondFakeDownstreamService($"http://{secondDownstreamHost}:{secondDownstreamPort}", "/ws")) + .When(_ => WhenIStartTheClients()) + .Then(_ => ThenBothDownstreamServicesAreCalled()) + .BDDfy(); + } + + private void ThenBothDownstreamServicesAreCalled() + { + _firstRecieved.Count.ShouldBe(10); + _firstRecieved.ForEach(x => + { + x.ShouldBe("test"); + }); + + _secondRecieved.Count.ShouldBe(10); + _secondRecieved.ForEach(x => + { + x.ShouldBe("chocolate"); + }); + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach (var serviceEntry in serviceEntries) + { + _serviceEntries.Add(serviceEntry); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") + { + var json = JsonConvert.SerializeObject(_serviceEntries); + context.Response.Headers.Add("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + } + + private async Task WhenIStartTheClients() + { + var firstClient = StartClient("ws://localhost:5000/"); + + var secondClient = StartSecondClient("ws://localhost:5000/"); + + await Task.WhenAll(firstClient, secondClient); + } + + private async Task StartClient(string url) + { + var client = new ClientWebSocket(); + + await client.ConnectAsync(new Uri(url), CancellationToken.None); + + var sending = Task.Run(async () => + { + var line = "test"; + for (var i = 0; i < 10; i++) + { + var bytes = Encoding.UTF8.GetBytes(line); + + await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, + CancellationToken.None); + await Task.Delay(10); + } + + await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + }); + + var receiving = Task.Run(async () => + { + var buffer = new byte[1024 * 4]; + + while (true) + { + var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + if (result.MessageType == WebSocketMessageType.Text) + { + _firstRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); + } + else if (result.MessageType == WebSocketMessageType.Close) + { + if (client.State != WebSocketState.Closed) + { + // Last version, the client state is CloseReceived + // Valid states are: Open, CloseReceived, CloseSent + await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + } + + break; + } + } + }); + + await Task.WhenAll(sending, receiving); + } + + private async Task StartSecondClient(string url) + { + await Task.Delay(500); + + var client = new ClientWebSocket(); + + await client.ConnectAsync(new Uri(url), CancellationToken.None); + + var sending = Task.Run(async () => + { + var line = "test"; + for (var i = 0; i < 10; i++) + { + var bytes = Encoding.UTF8.GetBytes(line); + + await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, + CancellationToken.None); + await Task.Delay(10); + } + + await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + }); + + var receiving = Task.Run(async () => + { + var buffer = new byte[1024 * 4]; + + while (true) + { + var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + if (result.MessageType == WebSocketMessageType.Text) + { + _secondRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); + } + else if (result.MessageType == WebSocketMessageType.Close) + { + if (client.State != WebSocketState.Closed) + { + // Last version, the client state is CloseReceived + // Valid states are: Open, CloseReceived, CloseSent + await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + } + + break; + } + } + }); + + await Task.WhenAll(sending, receiving); + } + + private async Task StartFakeDownstreamService(string url, string path) + { + await _serviceHandler.StartFakeDownstreamService(url, async (context, next) => + { + if (context.Request.Path == path) + { + if (context.WebSockets.IsWebSocketRequest) + { + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await Echo(webSocket); + } + else + { + context.Response.StatusCode = 400; + } + } + else + { + await next(); + } + }); + } + + private async Task StartSecondFakeDownstreamService(string url, string path) + { + await _serviceHandler.StartFakeDownstreamService(url, async (context, next) => + { + if (context.Request.Path == path) + { + if (context.WebSockets.IsWebSocketRequest) + { + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await Message(webSocket); + } + else + { + context.Response.StatusCode = 400; + } + } + else + { + await next(); + } + }); + } + + private static async Task Echo(WebSocket webSocket) + { + try + { + var buffer = new byte[1024 * 4]; + + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + private static async Task Message(WebSocket webSocket) + { + try + { + var buffer = new byte[1024 * 4]; + + var bytes = Encoding.UTF8.GetBytes("chocolate"); + + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(bytes), result.MessageType, result.EndOfMessage, CancellationToken.None); + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + public void Dispose() + { + _serviceHandler?.Dispose(); _steps.Dispose(); - } - } -} + GC.SuppressFinalize(this); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ContentTests.cs b/test/Ocelot.AcceptanceTests/ContentTests.cs index 7a4d83650..6a42c1513 100644 --- a/test/Ocelot.AcceptanceTests/ContentTests.cs +++ b/test/Ocelot.AcceptanceTests/ContentTests.cs @@ -1,176 +1,187 @@ -namespace Ocelot.AcceptanceTests -{ - using Microsoft.AspNetCore.Http; - using Ocelot.Configuration.File; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Net; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Net; - public class ContentTests : IDisposable - { - private readonly Steps _steps; - private string _contentType; - private long? _contentLength; - private bool _contentTypeHeaderExists; - private readonly ServiceHandler _serviceHandler; +using Ocelot.Configuration.File; - public ContentTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } +using Microsoft.AspNetCore.Http; - [Fact] - public void should_not_add_content_type_or_content_length_headers() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51339, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; +using Shouldly; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51339", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => ThenTheContentTypeShouldBeEmpty()) - .And(x => ThenTheContentLengthShouldBeZero()) - .BDDfy(); - } +using TestStack.BDDfy; - [Fact] - public void should_add_content_type_and_content_length_headers() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51349, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Post" }, - } - } - }; - - var contentType = "application/json"; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51349", "/", 201, string.Empty)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .And(x => _steps.GivenThePostHasContentType(contentType)) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) - .And(x => ThenTheContentLengthIs(11)) - .And(x => ThenTheContentTypeIsIs(contentType)) - .BDDfy(); - } - - [Fact] - public void should_add_default_content_type_header() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51359, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Post" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51359", "/", 201, string.Empty)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) - .And(x => ThenTheContentLengthIs(11)) - .And(x => ThenTheContentTypeIsIs("text/plain; charset=utf-8")) - .BDDfy(); - } - - private void ThenTheContentTypeIsIs(string expected) - { - _contentType.ShouldBe(expected); - } - - private void ThenTheContentLengthShouldBeZero() - { - _contentLength.ShouldBeEquivalentTo(0L); - } - - private void ThenTheContentLengthIs(int expected) - { - _contentLength.ShouldBe(expected); - } - - private void ThenTheContentTypeShouldBeEmpty() - { - _contentType.ShouldBeNullOrEmpty(); - _contentTypeHeaderExists.ShouldBe(false); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _contentType = context.Request.ContentType; - _contentLength = context.Request.ContentLength; - _contentTypeHeaderExists = context.Request.Headers.TryGetValue("Content-Type", out var value); - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } -} +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class ContentTests : IDisposable + { + private readonly Steps _steps; + private string _contentType; + private long? _contentLength; + private bool _contentTypeHeaderExists; + private readonly ServiceHandler _serviceHandler; + + public ContentTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_not_add_content_type_or_content_length_headers() + { + 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" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => ThenTheContentTypeShouldBeEmpty()) + .And(x => ThenTheContentLengthShouldBeZero()) + .BDDfy(); + } + + [Fact] + public void should_add_content_type_and_content_length_headers() + { + 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" }, + }, + }, + }; + + var contentType = "application/json"; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 201, string.Empty)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .And(x => _steps.GivenThePostHasContentType(contentType)) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) + .And(x => ThenTheContentLengthIs(11)) + .And(x => ThenTheContentTypeIsIs(contentType)) + .BDDfy(); + } + + [Fact] + public void should_add_default_content_type_header() + { + 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" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 201, string.Empty)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) + .And(x => ThenTheContentLengthIs(11)) + .And(x => ThenTheContentTypeIsIs("text/plain; charset=utf-8")) + .BDDfy(); + } + + private void ThenTheContentTypeIsIs(string expected) + { + _contentType.ShouldBe(expected); + } + + private void ThenTheContentLengthShouldBeZero() + { + _contentLength.ShouldBeNull(); + } + + private void ThenTheContentLengthIs(int expected) + { + _contentLength.ShouldBe(expected); + } + + private void ThenTheContentTypeShouldBeEmpty() + { + _contentType.ShouldBeNullOrEmpty(); + _contentTypeHeaderExists.ShouldBe(false); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _contentType = context.Request.ContentType; + _contentLength = context.Request.ContentLength; + _contentTypeHeaderExists = context.Request.Headers.TryGetValue("Content-Type", out var value); + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs b/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs index bbfce6001..69522552d 100644 --- a/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs +++ b/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs @@ -1,17 +1,23 @@ -namespace Ocelot.AcceptanceTests -{ - using Microsoft.AspNetCore.Http; - using Ocelot.Configuration.File; - using Ocelot.Middleware; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Net; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Threading.Tasks; + +using Ocelot.Configuration.File; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Middleware; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; +namespace Ocelot.AcceptanceTests +{ public class CustomMiddlewareTests : IDisposable { private readonly string _configurationPath; @@ -32,36 +38,38 @@ public void should_call_pre_query_string_builder_middleware() { var configuration = new OcelotPipelineConfiguration { - AuthorisationMiddleware = async (ctx, next) => + AuthorizationMiddleware = async (ctx, next) => { _counter++; await next.Invoke(); - } + }, }; + var port = RandomPortFinder.GetRandomPort(); + var fileConfiguration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 41879, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, - } - } + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41879", 200, "")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, string.Empty)) .And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath)) .And(x => _steps.GivenOcelotIsRunning(configuration)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -71,40 +79,42 @@ public void should_call_pre_query_string_builder_middleware() } [Fact] - public void should_call_authorisation_middleware() + public void should_call_authorization_middleware() { var configuration = new OcelotPipelineConfiguration { - AuthorisationMiddleware = async (ctx, next) => + AuthorizationMiddleware = async (ctx, next) => { _counter++; await next.Invoke(); - } + }, }; + var port = RandomPortFinder.GetRandomPort(); + var fileConfiguration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 41879, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, - } - } + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41879", 200, "")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, string.Empty)) .And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath)) .And(x => _steps.GivenOcelotIsRunning(configuration)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -122,32 +132,34 @@ public void should_call_authentication_middleware() { _counter++; await next.Invoke(); - } + }, }; + var port = RandomPortFinder.GetRandomPort(); + var fileConfiguration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/41879/", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 41879, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, - } - } + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41879", 200, "")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, string.Empty)) .And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath)) .And(x => _steps.GivenOcelotIsRunning(configuration)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -165,32 +177,34 @@ public void should_call_pre_error_middleware() { _counter++; await next.Invoke(); - } + }, }; + var port = RandomPortFinder.GetRandomPort(); + var fileConfiguration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 41879, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, - } - } + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41879", 200, "")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, string.Empty)) .And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath)) .And(x => _steps.GivenOcelotIsRunning(configuration)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -200,40 +214,42 @@ public void should_call_pre_error_middleware() } [Fact] - public void should_call_pre_authorisation_middleware() + public void should_call_pre_authorization_middleware() { var configuration = new OcelotPipelineConfiguration { - PreAuthorisationMiddleware = async (ctx, next) => + PreAuthorizationMiddleware = async (ctx, next) => { _counter++; await next.Invoke(); - } + }, }; + var port = RandomPortFinder.GetRandomPort(); + var fileConfiguration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 41879, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, - } - } + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41879", 200, "")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, string.Empty)) .And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath)) .And(x => _steps.GivenOcelotIsRunning(configuration)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -251,40 +267,42 @@ public void should_call_pre_http_authentication_middleware() { _counter++; await next.Invoke(); - } + }, }; + var port = RandomPortFinder.GetRandomPort(); + var fileConfiguration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 41879, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, - } - } + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41879", 200, "")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, string.Empty)) .And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath)) .And(x => _steps.GivenOcelotIsRunning(configuration)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => x.ThenTheCounterIs(1)) .BDDfy(); - } - + } + [Fact(Skip = "This is just an example to show how you could hook into Ocelot pipeline with your own middleware. At the moment you must use Response.OnCompleted callback and cannot change the response :( I will see if this can be changed one day!")] public void should_fix_issue_237() { @@ -293,7 +311,7 @@ public void should_fix_issue_237() var httpContext = (HttpContext)state; if (httpContext.Response.StatusCode > 400) - { + { Debug.WriteLine("COUNT CALLED"); Console.WriteLine("COUNT CALLED"); } @@ -301,29 +319,31 @@ public void should_fix_issue_237() return Task.CompletedTask; }; + var port = RandomPortFinder.GetRandomPort(); + var fileConfiguration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/west", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 41880, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, - } - } + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41880", 200, "/test")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "/test")) .And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath)) .And(x => _steps.GivenOcelotIsRunningWithMiddleareBeforePipeline(callback)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -362,8 +382,8 @@ public void Dispose() public class FakeMiddleware { private readonly RequestDelegate _next; - private readonly Func _callback; - + private readonly Func _callback; + public FakeMiddleware(RequestDelegate next, Func callback) { _next = next; @@ -372,10 +392,10 @@ public FakeMiddleware(RequestDelegate next, Func callback) public async Task Invoke(HttpContext context) { - await _next(context); - + await _next(context); + context.Response.OnCompleted(_callback, context); } } } -} +} diff --git a/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs index 3029249c7..1f1c7df3e 100644 --- a/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs @@ -1,283 +1,292 @@ -namespace Ocelot.AcceptanceTests -{ - using Configuration.File; - using Microsoft.AspNetCore.Http; - using Newtonsoft.Json; - using Steeltoe.Common.Discovery; - using System; - using System.Collections.Generic; - using System.Net; - using TestStack.BDDfy; - using Xunit; - - public class EurekaServiceDiscoveryTests : IDisposable - { - private readonly Steps _steps; - private readonly List _eurekaInstances; - private readonly ServiceHandler _serviceHandler; - - public EurekaServiceDiscoveryTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - _eurekaInstances = new List(); - } - - [Fact] - public void should_use_eureka_service_discovery_and_make_request() - { - var eurekaPort = 8761; - var serviceName = "product"; - var downstreamServicePort = 50371; - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeEurekaServiceDiscoveryUrl = $"http://localhost:{eurekaPort}"; - - var instanceOne = new FakeEurekaService(serviceName, "localhost", downstreamServicePort, false, - new Uri($"http://localhost:{downstreamServicePort}"), new Dictionary()); - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Type = "Eureka" - } - } - }; - - this.Given(x => x.GivenEurekaProductServiceOneIsRunning(downstreamServiceOneUrl)) - .And(x => x.GivenThereIsAFakeEurekaServiceDiscoveryProvider(fakeEurekaServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithEureka(instanceOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithEureka()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(_ => _steps.ThenTheResponseBodyShouldBe(nameof(EurekaServiceDiscoveryTests))) - .BDDfy(); - } - - private void GivenTheServicesAreRegisteredWithEureka(params IServiceInstance[] serviceInstances) - { - foreach (var instance in serviceInstances) - { - _eurekaInstances.Add(instance); - } - } - - private void GivenThereIsAFakeEurekaServiceDiscoveryProvider(string url, string serviceName) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - if (context.Request.Path.Value == "/eureka/apps/") - { - var apps = new List(); - - foreach (var serviceInstance in _eurekaInstances) - { - var a = new Application - { - name = serviceName, - instance = new List - { - new Instance - { - instanceId = $"{serviceInstance.Host}:{serviceInstance}", - hostName = serviceInstance.Host, - app = serviceName, - ipAddr = "127.0.0.1", - status = "UP", - overriddenstatus = "UNKNOWN", - port = new Port {value = serviceInstance.Port, enabled = "true"}, - securePort = new SecurePort {value = serviceInstance.Port, enabled = "true"}, - countryId = 1, - dataCenterInfo = new DataCenterInfo {value = "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", name = "MyOwn"}, - leaseInfo = new LeaseInfo - { - renewalIntervalInSecs = 30, - durationInSecs = 90, - registrationTimestamp = 1457714988223, - lastRenewalTimestamp= 1457716158319, - evictionTimestamp = 0, - serviceUpTimestamp = 1457714988223 - }, - metadata = new Metadata - { - value = "java.util.Collections$EmptyMap" - }, - homePageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", - statusPageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", - healthCheckUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", - vipAddress = serviceName, - isCoordinatingDiscoveryServer = "false", - lastUpdatedTimestamp = "1457714988223", - lastDirtyTimestamp = "1457714988172", - actionType = "ADDED" - } - } - }; - - apps.Add(a); - } - - var applications = new EurekaApplications - { - applications = new Applications - { - application = apps, - apps__hashcode = "UP_1_", - versions__delta = "1" - } - }; - - var json = JsonConvert.SerializeObject(applications); - context.Response.Headers.Add("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - } - - private void GivenEurekaProductServiceOneIsRunning(string url) +using System; +using System.Collections.Generic; +using System.Net; + +using Ocelot.Configuration.File; + +using Microsoft.AspNetCore.Http; + +using Newtonsoft.Json; + +using Steeltoe.Common.Discovery; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class EurekaServiceDiscoveryTests : IDisposable + { + private readonly Steps _steps; + private readonly List _eurekaInstances; + private readonly ServiceHandler _serviceHandler; + + public EurekaServiceDiscoveryTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + _eurekaInstances = new List(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void should_use_eureka_service_discovery_and_make_request(bool dotnetRunningInContainer) + { + Environment.SetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER", dotnetRunningInContainer.ToString()); + var eurekaPort = 8761; + var serviceName = "product"; + var downstreamServicePort = RandomPortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeEurekaServiceDiscoveryUrl = $"http://localhost:{eurekaPort}"; + + var instanceOne = new FakeEurekaService(serviceName, "localhost", downstreamServicePort, false, + new Uri($"http://localhost:{downstreamServicePort}"), new Dictionary()); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Type = "Eureka", + }, + }, + }; + + this.Given(x => x.GivenEurekaProductServiceOneIsRunning(downstreamServiceOneUrl)) + .And(x => x.GivenThereIsAFakeEurekaServiceDiscoveryProvider(fakeEurekaServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithEureka(instanceOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithEureka()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe(nameof(EurekaServiceDiscoveryTests))) + .BDDfy(); + } + + private void GivenTheServicesAreRegisteredWithEureka(params IServiceInstance[] serviceInstances) + { + foreach (var instance in serviceInstances) + { + _eurekaInstances.Add(instance); + } + } + + private void GivenThereIsAFakeEurekaServiceDiscoveryProvider(string url, string serviceName) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + if (context.Request.Path.Value == "/eureka/apps/") + { + var apps = new List(); + + foreach (var serviceInstance in _eurekaInstances) + { + var a = new Application + { + name = serviceName, + instance = new List + { + new() + { + instanceId = $"{serviceInstance.Host}:{serviceInstance}", + hostName = serviceInstance.Host, + app = serviceName, + ipAddr = "127.0.0.1", + status = "UP", + overriddenstatus = "UNKNOWN", + port = new Port {value = serviceInstance.Port, enabled = "true"}, + securePort = new SecurePort {value = serviceInstance.Port, enabled = "true"}, + countryId = 1, + dataCenterInfo = new DataCenterInfo {value = "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", name = "MyOwn"}, + leaseInfo = new LeaseInfo + { + renewalIntervalInSecs = 30, + durationInSecs = 90, + registrationTimestamp = 1457714988223, + lastRenewalTimestamp= 1457716158319, + evictionTimestamp = 0, + serviceUpTimestamp = 1457714988223, + }, + metadata = new Metadata + { + value = "java.util.Collections$EmptyMap", + }, + homePageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + statusPageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + healthCheckUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + vipAddress = serviceName, + isCoordinatingDiscoveryServer = "false", + lastUpdatedTimestamp = "1457714988223", + lastDirtyTimestamp = "1457714988172", + actionType = "ADDED", + }, + }, + }; + + apps.Add(a); + } + + var applications = new EurekaApplications + { + applications = new Applications + { + application = apps, + apps__hashcode = "UP_1_", + versions__delta = "1", + }, + }; + + var json = JsonConvert.SerializeObject(applications); + context.Response.Headers.Add("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + } + + private void GivenEurekaProductServiceOneIsRunning(string url) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync(nameof(EurekaServiceDiscoveryTests)); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } + + public void Dispose() { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - try - { - context.Response.StatusCode = 200; - await context.Response.WriteAsync(nameof(EurekaServiceDiscoveryTests)); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } - - public class FakeEurekaService : IServiceInstance - { - public FakeEurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary metadata) - { - ServiceId = serviceId; - Host = host; - Port = port; - IsSecure = isSecure; - Uri = uri; - Metadata = metadata; - } - - public string ServiceId { get; } - public string Host { get; } - public int Port { get; } - public bool IsSecure { get; } - public Uri Uri { get; } - public IDictionary Metadata { get; } - } - - public class Port - { - [JsonProperty("$")] - public int value { get; set; } - - [JsonProperty("@enabled")] - public string enabled { get; set; } - } - - public class SecurePort - { - [JsonProperty("$")] - public int value { get; set; } - - [JsonProperty("@enabled")] - public string enabled { get; set; } - } - - public class DataCenterInfo - { - [JsonProperty("@class")] - public string value { get; set; } - - public string name { get; set; } - } - - public class LeaseInfo - { - public int renewalIntervalInSecs { get; set; } - - public int durationInSecs { get; set; } - - public long registrationTimestamp { get; set; } - - public long lastRenewalTimestamp { get; set; } - - public int evictionTimestamp { get; set; } - - public long serviceUpTimestamp { get; set; } - } - - public class Metadata - { - [JsonProperty("@class")] - public string value { get; set; } - } - - public class Instance - { - public string instanceId { get; set; } - public string hostName { get; set; } - public string app { get; set; } - public string ipAddr { get; set; } - public string status { get; set; } - public string overriddenstatus { get; set; } - public Port port { get; set; } - public SecurePort securePort { get; set; } - public int countryId { get; set; } - public DataCenterInfo dataCenterInfo { get; set; } - public LeaseInfo leaseInfo { get; set; } - public Metadata metadata { get; set; } - public string homePageUrl { get; set; } - public string statusPageUrl { get; set; } - public string healthCheckUrl { get; set; } - public string vipAddress { get; set; } - public string isCoordinatingDiscoveryServer { get; set; } - public string lastUpdatedTimestamp { get; set; } - public string lastDirtyTimestamp { get; set; } - public string actionType { get; set; } - } - - public class Application - { - public string name { get; set; } - public List instance { get; set; } - } - - public class Applications - { - public string versions__delta { get; set; } - public string apps__hashcode { get; set; } - public List application { get; set; } - } - - public class EurekaApplications - { - public Applications applications { get; set; } - } -} + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } + + public class FakeEurekaService : IServiceInstance + { + public FakeEurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary metadata) + { + ServiceId = serviceId; + Host = host; + Port = port; + IsSecure = isSecure; + Uri = uri; + Metadata = metadata; + } + + public string ServiceId { get; } + public string Host { get; } + public int Port { get; } + public bool IsSecure { get; } + public Uri Uri { get; } + public IDictionary Metadata { get; } + } + + public class Port + { + [JsonProperty("$")] + public int value { get; set; } + + [JsonProperty("@enabled")] + public string enabled { get; set; } + } + + public class SecurePort + { + [JsonProperty("$")] + public int value { get; set; } + + [JsonProperty("@enabled")] + public string enabled { get; set; } + } + + public class DataCenterInfo + { + [JsonProperty("@class")] + public string value { get; set; } + + public string name { get; set; } + } + + public class LeaseInfo + { + public int renewalIntervalInSecs { get; set; } + + public int durationInSecs { get; set; } + + public long registrationTimestamp { get; set; } + + public long lastRenewalTimestamp { get; set; } + + public int evictionTimestamp { get; set; } + + public long serviceUpTimestamp { get; set; } + } + + public class Metadata + { + [JsonProperty("@class")] + public string value { get; set; } + } + + public class Instance + { + public string instanceId { get; set; } + public string hostName { get; set; } + public string app { get; set; } + public string ipAddr { get; set; } + public string status { get; set; } + public string overriddenstatus { get; set; } + public Port port { get; set; } + public SecurePort securePort { get; set; } + public int countryId { get; set; } + public DataCenterInfo dataCenterInfo { get; set; } + public LeaseInfo leaseInfo { get; set; } + public Metadata metadata { get; set; } + public string homePageUrl { get; set; } + public string statusPageUrl { get; set; } + public string healthCheckUrl { get; set; } + public string vipAddress { get; set; } + public string isCoordinatingDiscoveryServer { get; set; } + public string lastUpdatedTimestamp { get; set; } + public string lastDirtyTimestamp { get; set; } + public string actionType { get; set; } + } + + public class Application + { + public string name { get; set; } + public List instance { get; set; } + } + + public class Applications + { + public string versions__delta { get; set; } + public string apps__hashcode { get; set; } + public List application { get; set; } + } + + public class EurekaApplications + { + public Applications applications { get; set; } + } +} diff --git a/test/Ocelot.AcceptanceTests/GzipTests.cs b/test/Ocelot.AcceptanceTests/GzipTests.cs index a2b1e1b74..c521c7bf7 100644 --- a/test/Ocelot.AcceptanceTests/GzipTests.cs +++ b/test/Ocelot.AcceptanceTests/GzipTests.cs @@ -1,17 +1,22 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Configuration.File; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + namespace Ocelot.AcceptanceTests { - using Microsoft.AspNetCore.Http; - using Ocelot.Configuration.File; - using Shouldly; - using System; - using System.Collections.Generic; - using System.IO; - using System.IO.Compression; - using System.Linq; - using System.Net; - using TestStack.BDDfy; - using Xunit; - public class GzipTests : IDisposable { private readonly Steps _steps; @@ -25,32 +30,34 @@ public GzipTests() [Fact] public void should_return_response_200_with_simple_url() - { + { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51179, - } + Port = port, + }, }, UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Post" }, - } - } + }, + }, }; var input = "people"; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51179", "/", 200, "Hello from Laura", "\"people\"")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura", "\"people\"")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenThePostHasGzipContent(input)) diff --git a/test/Ocelot.AcceptanceTests/HeaderTests.cs b/test/Ocelot.AcceptanceTests/HeaderTests.cs index e7c0c2ca6..7ffd52907 100644 --- a/test/Ocelot.AcceptanceTests/HeaderTests.cs +++ b/test/Ocelot.AcceptanceTests/HeaderTests.cs @@ -1,15 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +using Ocelot.Configuration.File; + +using Microsoft.AspNetCore.Http; + +using TestStack.BDDfy; + +using Xunit; + namespace Ocelot.AcceptanceTests { - using Microsoft.AspNetCore.Http; - using Ocelot.Configuration.File; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - public class HeaderTests : IDisposable { private int _count; @@ -25,33 +29,35 @@ public HeaderTests() [Fact] public void should_transform_upstream_header() { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51871, - } + Port = port, + }, }, UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, UpstreamHeaderTransform = new Dictionary { - {"Laz", "D, GP"} - } - } - } + {"Laz", "D, GP"}, + }, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51871", "/", 200, "Laz")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Laz")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenIAddAHeader("Laz", "D")) @@ -64,33 +70,35 @@ public void should_transform_upstream_header() [Fact] public void should_transform_downstream_header() { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51871, - } + Port = port, + }, }, UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, DownstreamHeaderTransform = new Dictionary { - {"Location", "http://www.bbc.co.uk/, http://ocelot.com/"} - } - } - } + {"Location", "http://www.bbc.co.uk/, http://ocelot.com/"}, + }, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51871", "/", 200, "Location", "http://www.bbc.co.uk/")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Location", "http://www.bbc.co.uk/")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -102,37 +110,39 @@ public void should_transform_downstream_header() [Fact] public void should_fix_issue_190() { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 6773, - } + Port = port, + }, }, UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, DownstreamHeaderTransform = new Dictionary { - {"Location", "http://localhost:6773, {BaseUrl}"} + {"Location", $"http://localhost:{port}, {{BaseUrl}}"}, }, HttpHandlerOptions = new FileHttpHandlerOptions { - AllowAutoRedirect = false - } - } - } + AllowAutoRedirect = false, + }, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6773", "/", 302, "Location", "http://localhost:6773/pay/Receive")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 302, "Location", $"http://localhost:{port}/pay/Receive")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -144,37 +154,39 @@ public void should_fix_issue_190() [Fact] public void should_fix_issue_205() { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 6773, - } + Port = port, + }, }, UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, DownstreamHeaderTransform = new Dictionary { - {"Location", "{DownstreamBaseUrl}, {BaseUrl}"} + {"Location", "{DownstreamBaseUrl}, {BaseUrl}"}, }, HttpHandlerOptions = new FileHttpHandlerOptions { - AllowAutoRedirect = false - } - } - } + AllowAutoRedirect = false, + }, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6773", "/", 302, "Location", "http://localhost:6773/pay/Receive")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 302, "Location", $"http://localhost:{port}/pay/Receive")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -186,41 +198,43 @@ public void should_fix_issue_205() [Fact] public void should_fix_issue_417() { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 6773, - } + Port = port, + }, }, UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, DownstreamHeaderTransform = new Dictionary { - {"Location", "{DownstreamBaseUrl}, {BaseUrl}"} + {"Location", "{DownstreamBaseUrl}, {BaseUrl}"}, }, HttpHandlerOptions = new FileHttpHandlerOptions { - AllowAutoRedirect = false - } - } + AllowAutoRedirect = false, + }, + }, }, GlobalConfiguration = new FileGlobalConfiguration { - BaseUrl = "http://anotherapp.azurewebsites.net" - } + BaseUrl = "http://anotherapp.azurewebsites.net", + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6773", "/", 302, "Location", "http://localhost:6773/pay/Receive")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 302, "Location", $"http://localhost:{port}/pay/Receive")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -232,33 +246,35 @@ public void should_fix_issue_417() [Fact] public void request_should_reuse_cookies_with_cookie_container() { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/sso/{everything}", DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 6774, - } + Port = port, + }, }, UpstreamPathTemplate = "/sso/{everything}", UpstreamHttpMethod = new List { "Get", "Post", "Options" }, HttpHandlerOptions = new FileHttpHandlerOptions { - UseCookieContainer = true - } - } - } + UseCookieContainer = true, + }, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6774", "/sso/test", 200)) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/sso/test", 200)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) @@ -273,33 +289,35 @@ 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 configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/sso/{everything}", DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 6775, - } + Port = port, + }, }, UpstreamPathTemplate = "/sso/{everything}", UpstreamHttpMethod = new List { "Get", "Post", "Options" }, HttpHandlerOptions = new FileHttpHandlerOptions { - UseCookieContainer = false - } - } - } + UseCookieContainer = false, + }, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6775", "/sso/test", 200)) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/sso/test", 200)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) @@ -314,29 +332,31 @@ 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 configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 52866, - } + Port = port, + }, }, UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, - } - } + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:52866", "/", 200, "Accept")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Accept")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenIAddAHeader("Accept", "text/html,application/xhtml+xml,application/xml;")) @@ -349,29 +369,31 @@ 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 configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51874, - } + Port = port, + }, }, UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, - } - } + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51874", "/", 200, "Accept")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Accept")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenIAddAHeader("Accept", "text/html")) @@ -395,9 +417,18 @@ private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int return Task.CompletedTask; } - if (context.Request.Cookies.TryGetValue("test", out var cookieValue) || context.Request.Headers.TryGetValue("Set-Cookie", out var headerValue)) + if (context.Request.Cookies.TryGetValue("test", out var cookieValue)) + { + if (cookieValue == "0") + { + context.Response.StatusCode = statusCode; + return Task.CompletedTask; + } + } + + if (context.Request.Headers.TryGetValue("Set-Cookie", out var headerValue)) { - if (cookieValue == "0" || headerValue == "test=1; path=/") + if (headerValue == "test=1; path=/") { context.Response.StatusCode = statusCode; return Task.CompletedTask; diff --git a/test/Ocelot.AcceptanceTests/HttpClientCachingTests.cs b/test/Ocelot.AcceptanceTests/HttpClientCachingTests.cs index a3b8f49e6..28755107a 100644 --- a/test/Ocelot.AcceptanceTests/HttpClientCachingTests.cs +++ b/test/Ocelot.AcceptanceTests/HttpClientCachingTests.cs @@ -1,163 +1,171 @@ -namespace Ocelot.AcceptanceTests -{ - using Configuration; - using Microsoft.AspNetCore.Http; - using Ocelot.Configuration.File; - using Requester; - using Shouldly; - using System; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Net; - using TestStack.BDDfy; - using Xunit; - - public class HttpClientCachingTests : IDisposable - { - private readonly Steps _steps; - private string _downstreamPath; - private readonly ServiceHandler _serviceHandler; - - public HttpClientCachingTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - - [Fact] - public void should_cache_one_http_client_same_re_route() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 58814, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - var cache = new FakeHttpClientCache(); - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:58814", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithFakeHttpClientCache(cache)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => cache.Count.ShouldBe(1)) - .BDDfy(); - } - - [Fact] - public void should_cache_two_http_client_different_re_route() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 58817, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - }, - new FileReRoute - { - DownstreamPathTemplate = "/two", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 58817, - } - }, - UpstreamPathTemplate = "/two", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - var cache = new FakeHttpClientCache(); - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:58817", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithFakeHttpClientCache(cache)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/two")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/two")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/two")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => cache.Count.ShouldBe(2)) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, async context => - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - } - - public void Dispose() - { - _serviceHandler.Dispose(); - _steps.Dispose(); - } - - public class FakeHttpClientCache : IHttpClientCache - { - private readonly ConcurrentDictionary _httpClientsCache; - - public FakeHttpClientCache() - { - _httpClientsCache = new ConcurrentDictionary(); - } - - public void Set(DownstreamReRoute key, IHttpClient client, TimeSpan expirationTime) - { - _httpClientsCache.AddOrUpdate(key, client, (k, oldValue) => client); - } - - public IHttpClient Get(DownstreamReRoute key) - { - //todo handle error? - return _httpClientsCache.TryGetValue(key, out var client) ? client : null; - } - - public int Count => _httpClientsCache.Count; - } - } -} +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Configuration.File; +using Ocelot.Requester; +using Shouldly; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class HttpClientCachingTests : IDisposable + { + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public HttpClientCachingTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_cache_one_http_client_same_re_route() + { + 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" }, + }, + }, + }; + + var cache = new FakeHttpClientCache(); + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithFakeHttpClientCache(cache)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => ThenTheCountShouldBe(cache, 1)) + .BDDfy(); + } + + [Fact] + public void should_cache_two_http_client_different_re_route() + { + 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" }, + }, + new() + { + DownstreamPathTemplate = "/two", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/two", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + var cache = new FakeHttpClientCache(); + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithFakeHttpClientCache(cache)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/two")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/two")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/two")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => ThenTheCountShouldBe(cache, 2)) + .BDDfy(); + } + + private static void ThenTheCountShouldBe(FakeHttpClientCache cache, int count) + { + cache.Count.ShouldBe(count); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + } + + public void Dispose() + { + _serviceHandler.Dispose(); + _steps.Dispose(); + } + + public class FakeHttpClientCache : IHttpClientCache + { + private readonly ConcurrentDictionary _httpClientsCache; + + public FakeHttpClientCache() + { + _httpClientsCache = new ConcurrentDictionary(); + } + + public void Set(DownstreamRoute key, IHttpClient client, TimeSpan expirationTime) + { + _httpClientsCache.AddOrUpdate(key, client, (k, oldValue) => client); + } + + public IHttpClient Get(DownstreamRoute key) + { + //todo handle error? + return _httpClientsCache.TryGetValue(key, out var client) ? client : null; + } + + public int Count => _httpClientsCache.Count; + } + } +} diff --git a/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs index dc985e42b..47a4e3dac 100644 --- a/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs +++ b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs @@ -1,288 +1,301 @@ -namespace Ocelot.AcceptanceTests -{ - using Microsoft.AspNetCore.Http; - using Ocelot.Configuration.File; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Net; - using System.Net.Http; - using System.Threading; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; - public class HttpDelegatingHandlersTests : IDisposable - { - private readonly Steps _steps; - private string _downstreamPath; - private readonly ServiceHandler _serviceHandler; +using Ocelot.Configuration.File; - public HttpDelegatingHandlersTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } +using Microsoft.AspNetCore.Http; - [Fact] - public void should_call_re_route_ordered_specific_handlers() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 7197, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - DelegatingHandlers = new List - { - "FakeHandlerTwo", - "FakeHandler" - } - } - } - }; +using Shouldly; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:7197", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithSpecficHandlersRegisteredInDi()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => ThenTheOrderedHandlersAreCalledCorrectly()) - .BDDfy(); - } +using TestStack.BDDfy; - [Fact] - public void should_call_global_di_handlers() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 7187, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:7187", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => ThenTheHandlersAreCalledCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_call_global_di_handlers_multiple_times() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 9187, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:9187", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithGlobalHandlerRegisteredInDi()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_call_global_di_handlers_with_dependency() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 7188, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - var dependency = new FakeDependency(); - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:7188", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi(dependency)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => ThenTheDependencyIsCalled(dependency)) - .BDDfy(); - } - - private void ThenTheDependencyIsCalled(FakeDependency dependency) - { - dependency.Called.ShouldBeTrue(); - } - - private void ThenTheHandlersAreCalledCorrectly() - { - FakeHandler.TimeCalled.ShouldBeLessThan(FakeHandlerTwo.TimeCalled); - } - - private void ThenTheOrderedHandlersAreCalledCorrectly() - { - FakeHandlerTwo.TimeCalled.ShouldBeLessThan(FakeHandler.TimeCalled); - } - - public class FakeDependency - { - public bool Called; - } - - // ReSharper disable once ClassNeverInstantiated.Local - private class FakeHandlerWithDependency : DelegatingHandler - { - private readonly FakeDependency _dependency; - - public FakeHandlerWithDependency(FakeDependency dependency) - { - _dependency = dependency; - } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - _dependency.Called = true; - return base.SendAsync(request, cancellationToken); - } - } - - // ReSharper disable once ClassNeverInstantiated.Local - private class FakeHandler : DelegatingHandler - { - public static DateTime TimeCalled { get; private set; } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - TimeCalled = DateTime.Now; - return base.SendAsync(request, cancellationToken); - } - } - - // ReSharper disable once ClassNeverInstantiated.Local - private class FakeHandlerTwo : DelegatingHandler - { - public static DateTime TimeCalled { get; private set; } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - TimeCalled = DateTime.Now; - return base.SendAsync(request, cancellationToken); - } - } - - // ReSharper disable once ClassNeverInstantiated.Local - private class FakeHandlerAgain : DelegatingHandler - { - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - Console.WriteLine(request.RequestUri); - - //do stuff and optionally call the base handler.. - return await base.SendAsync(request, cancellationToken); - } - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPath != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); - } - - public void Dispose() - { - _steps?.Dispose(); - _serviceHandler?.Dispose(); - } - } -} +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class HttpDelegatingHandlersTests : IDisposable + { + private readonly Steps _steps; + private string _downstreamPath; + private readonly ServiceHandler _serviceHandler; + + public HttpDelegatingHandlersTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_call_re_route_ordered_specific_handlers() + { + 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" }, + DelegatingHandlers = new List + { + "FakeHandlerTwo", + "FakeHandler", + }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithSpecficHandlersRegisteredInDi()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => ThenTheOrderedHandlersAreCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_call_global_di_handlers() + { + 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" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => ThenTheHandlersAreCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_call_global_di_handlers_multiple_times() + { + 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" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithGlobalHandlerRegisteredInDi()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_call_global_di_handlers_with_dependency() + { + 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" }, + }, + }, + }; + + var dependency = new FakeDependency(); + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi(dependency)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => ThenTheDependencyIsCalled(dependency)) + .BDDfy(); + } + + private static void ThenTheDependencyIsCalled(FakeDependency dependency) + { + dependency.Called.ShouldBeTrue(); + } + + private static void ThenTheHandlersAreCalledCorrectly() + { + FakeHandler.TimeCalled.ShouldBeLessThan(FakeHandlerTwo.TimeCalled); + } + + private static void ThenTheOrderedHandlersAreCalledCorrectly() + { + FakeHandlerTwo.TimeCalled.ShouldBeLessThan(FakeHandler.TimeCalled); + } + + public class FakeDependency + { + public bool Called; + } + + // ReSharper disable once ClassNeverInstantiated.Local + private class FakeHandlerWithDependency : DelegatingHandler + { + private readonly FakeDependency _dependency; + + public FakeHandlerWithDependency(FakeDependency dependency) + { + _dependency = dependency; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + _dependency.Called = true; + return base.SendAsync(request, cancellationToken); + } + } + + // ReSharper disable once ClassNeverInstantiated.Local + private class FakeHandler : DelegatingHandler + { + public static DateTime TimeCalled { get; private set; } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + TimeCalled = DateTime.Now; + return base.SendAsync(request, cancellationToken); + } + } + + // ReSharper disable once ClassNeverInstantiated.Local + private class FakeHandlerTwo : DelegatingHandler + { + public static DateTime TimeCalled { get; private set; } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + TimeCalled = DateTime.Now; + return base.SendAsync(request, cancellationToken); + } + } + + // ReSharper disable once ClassNeverInstantiated.Local + private class FakeHandlerAgain : DelegatingHandler + { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Console.WriteLine(request.RequestUri); + + //do stuff and optionally call the base handler.. + return await base.SendAsync(request, cancellationToken); + } + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + } + + public void Dispose() + { + _steps?.Dispose(); + _serviceHandler?.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/HttpTests.cs b/test/Ocelot.AcceptanceTests/HttpTests.cs new file mode 100644 index 000000000..f7f7ecbc0 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/HttpTests.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; + +using Ocelot.Configuration.File; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class HttpTests : IDisposable + { + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public HttpTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_return_response_200_when_using_http_one() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + DownstreamHttpMethod = "POST", + DownstreamHttpVersion = "1.0", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/", port, HttpProtocols.Http1)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_when_using_http_one_point_one() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + DownstreamHttpMethod = "POST", + DownstreamHttpVersion = "1.1", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/", port, HttpProtocols.Http1)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_when_using_http_two_point_zero() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "https", + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + DownstreamHttpMethod = "POST", + DownstreamHttpVersion = "2.0", + DangerousAcceptAnyServerCertificateValidator = true, + }, + }, + }; + + const string expected = "here is some content"; + var httpContent = new StringContent(expected); + + this.Given(x => x.GivenThereIsAServiceUsingHttpsRunningOn($"http://localhost:{port}/", "/", port, HttpProtocols.Http2)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/", httpContent)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe(expected)) + .BDDfy(); + } + + [Fact] + public void should_return_response_502_when_using_http_one_to_talk_to_server_running_http_two() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "https", + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + DownstreamHttpMethod = "POST", + DownstreamHttpVersion = "1.1", + DangerousAcceptAnyServerCertificateValidator = true, + }, + }, + }; + + const string expected = "here is some content"; + var httpContent = new StringContent(expected); + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/", port, HttpProtocols.Http2)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/", httpContent)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) + .BDDfy(); + } + + //TODO: does this test make any sense? + [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 configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + DownstreamHttpMethod = "POST", + DownstreamHttpVersion = "1.1", + DangerousAcceptAnyServerCertificateValidator = true, + }, + }, + }; + + const string expected = "here is some content"; + var httpContent = new StringContent(expected); + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/", port, HttpProtocols.Http1)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/", httpContent)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe(expected)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int port, HttpProtocols protocols) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + context.Response.StatusCode = 200; + var reader = new StreamReader(context.Request.Body); + var body = await reader.ReadToEndAsync(); + await context.Response.WriteAsync(body); + }, port, protocols); + } + + private void GivenThereIsAServiceUsingHttpsRunningOn(string baseUrl, string basePath, int port, HttpProtocols protocols) + { + _serviceHandler.GivenThereIsAServiceRunningOnUsingHttps(baseUrl, basePath, async context => + { + context.Response.StatusCode = 200; + var reader = new StreamReader(context.Request.Body); + var body = await reader.ReadToEndAsync(); + await context.Response.WriteAsync(body); + }, port, protocols); + } + + public void Dispose() + { + _serviceHandler.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs b/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs index b3adf5316..9a67915be 100644 --- a/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs +++ b/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs @@ -1,20 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Ocelot.Configuration; +using Ocelot.Configuration.File; + +using Ocelot.LoadBalancer.LoadBalancers; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Responses; + +using Ocelot.ServiceDiscovery.Providers; + +using Shouldly; + +using TestStack.BDDfy; + +using Ocelot.Values; + +using Xunit; + namespace Ocelot.AcceptanceTests { - using Microsoft.AspNetCore.Http; - using Ocelot.Configuration.File; - using Ocelot.LoadBalancer.LoadBalancers; - using Shouldly; - using System; - using System.Collections.Generic; - using TestStack.BDDfy; - using Xunit; - public class LoadBalancerTests : IDisposable { private readonly Steps _steps; private int _counterOne; private int _counterTwo; - private static readonly object _syncLock = new object(); + private static readonly object SyncLock = new(); private readonly ServiceHandler _serviceHandler; public LoadBalancerTests() @@ -26,17 +40,17 @@ public LoadBalancerTests() [Fact] public void should_load_balance_request_with_least_connection() { - int portOne = 50591; - int portTwo = 51482; + var portOne = RandomPortFinder.GetRandomPort(); + var portTwo = RandomPortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{portOne}"; var downstreamServiceTwoUrl = $"http://localhost:{portTwo}"; var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "http", @@ -45,22 +59,20 @@ public void should_load_balance_request_with_least_connection() LoadBalancerOptions = new FileLoadBalancerOptions { Type = nameof(LeastConnection) }, DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = portOne + Port = portOne, }, - new FileHostAndPort + new() { Host = "localhost", - Port = portTwo - } - } - } + Port = portTwo, + }, + }, + }, }, - GlobalConfiguration = new FileGlobalConfiguration() - { - } + GlobalConfiguration = new FileGlobalConfiguration(), }; this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) @@ -76,16 +88,16 @@ public void should_load_balance_request_with_least_connection() [Fact] public void should_load_balance_request_with_round_robin() { - var downstreamPortOne = 51701; - var downstreamPortTwo = 53802; + var downstreamPortOne = RandomPortFinder.GetRandomPort(); + var downstreamPortTwo = RandomPortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "http", @@ -94,22 +106,20 @@ public void should_load_balance_request_with_round_robin() LoadBalancerOptions = new FileLoadBalancerOptions { Type = nameof(RoundRobin) }, DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = downstreamPortOne + Port = downstreamPortOne, }, - new FileHostAndPort + new() { Host = "localhost", - Port = downstreamPortTwo - } - } - } + Port = downstreamPortTwo, + }, + }, + }, }, - GlobalConfiguration = new FileGlobalConfiguration() - { - } + GlobalConfiguration = new FileGlobalConfiguration(), }; this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) @@ -122,6 +132,88 @@ public void should_load_balance_request_with_round_robin() .BDDfy(); } + [Fact] + public void should_load_balance_request_with_custom_load_balancer() + { + var downstreamPortOne = RandomPortFinder.GetRandomPort(); + var downstreamPortTwo = RandomPortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; + var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = nameof(CustomLoadBalancer) }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = downstreamPortOne, + }, + new() + { + Host = "localhost", + Port = downstreamPortTwo, + }, + }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration(), + }; + + Func loadBalancerFactoryFunc = (serviceProvider, route, serviceDiscoveryProvider) => new CustomLoadBalancer(serviceDiscoveryProvider.Get); + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithCustomLoadBalancer(loadBalancerFactoryFunc)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) + .BDDfy(); + } + + private class CustomLoadBalancer : ILoadBalancer + { + private readonly Func>> _services; + private readonly object _lock = new(); + + private int _last; + + public CustomLoadBalancer(Func>> services) + { + _services = services; + } + + public async Task> Lease(HttpContext httpContext) + { + var services = await _services(); + lock (_lock) + { + if (_last >= services.Count) + { + _last = 0; + } + + var next = services[_last]; + _last++; + return new OkResponse(next.HostAndPort); + } + } + + public void Release(ServiceHostAndPort hostAndPort) + { + } + } + private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) { _counterOne.ShouldBeInRange(bottom, top); @@ -140,8 +232,8 @@ private void GivenProductServiceOneIsRunning(string url, int statusCode) { try { - var response = string.Empty; - lock (_syncLock) + string response; + lock (SyncLock) { _counterOne++; response = _counterOne.ToString(); @@ -163,8 +255,8 @@ private void GivenProductServiceTwoIsRunning(string url, int statusCode) { try { - var response = string.Empty; - lock (_syncLock) + string response; + lock (SyncLock) { _counterTwo++; response = _counterTwo.ToString(); diff --git a/test/Ocelot.AcceptanceTests/MethodTests.cs b/test/Ocelot.AcceptanceTests/MethodTests.cs new file mode 100644 index 000000000..ce3e1711b --- /dev/null +++ b/test/Ocelot.AcceptanceTests/MethodTests.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; + +using Ocelot.Configuration.File; + +using Microsoft.AspNetCore.Http; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class MethodTests : IDisposable + { + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public MethodTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_return_response_200_when_get_converted_to_post() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + DownstreamHttpMethod = "POST", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/", "POST")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_when_get_converted_to_post_with_content() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + DownstreamHttpMethod = "POST", + }, + }, + }; + + const string expected = "here is some content"; + var httpContent = new StringContent(expected); + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/", "POST")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/", httpContent)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe(expected)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_when_get_converted_to_get_with_content() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Post" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + DownstreamHttpMethod = "GET", + }, + }, + }; + + const string expected = "here is some content"; + var httpContent = new StringContent(expected); + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/", "GET")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/", httpContent)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe(expected)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string expected) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + if (context.Request.Method == expected) + { + context.Response.StatusCode = 200; + var reader = new StreamReader(context.Request.Body); + var body = await reader.ReadToEndAsync(); + await context.Response.WriteAsync(body); + } + else + { + context.Response.StatusCode = 500; + } + }); + } + + public void Dispose() + { + _serviceHandler.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index 356561bb6..572cccd4a 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -1,75 +1,77 @@ - - - 0.0.0-dev - netcoreapp3.1 - Ocelot.AcceptanceTests - Exe - Ocelot.AcceptanceTests - true - osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64 - false - false - false - ..\..\codeanalysis.ruleset + + + 0.0.0-dev + net7.0 + Ocelot.AcceptanceTests + Exe + Ocelot.AcceptanceTests + true + osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64 + false + false + false + ..\..\codeanalysis.ruleset + True + 1591 - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - - - - - - - - - - - + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + + + - - - - - - all - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Ocelot.AcceptanceTests/OpenTracingTests.cs b/test/Ocelot.AcceptanceTests/OpenTracingTests.cs new file mode 100644 index 000000000..b6c274ca8 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/OpenTracingTests.cs @@ -0,0 +1,590 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Threading.Tasks; + +using Butterfly.Client.AspNetCore; + +using Ocelot.Configuration.File; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; + +using OpenTracing; +using OpenTracing.Propagation; +using OpenTracing.Tag; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; +using Xunit.Abstractions; + +namespace Ocelot.AcceptanceTests +{ + public class OpenTracingTests : IDisposable + { + private IWebHost _serviceOneBuilder; + private IWebHost _serviceTwoBuilder; + private IWebHost _fakeOpenTracing; + private readonly Steps _steps; + private string _downstreamPathOne; + private string _downstreamPathTwo; + private readonly ITestOutputHelper _output; + + public OpenTracingTests(ITestOutputHelper output) + { + _output = output; + _steps = new Steps(); + } + + [Fact] + public void should_forward_tracing_information_from_ocelot_and_downstream_services() + { + var port1 = RandomPortFinder.GetRandomPort(); + var port2 = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port1, + }, + }, + UpstreamPathTemplate = "/api001/values", + UpstreamHttpMethod = new List { "Get" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true, + }, + }, + new() + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port2, + }, + }, + UpstreamPathTemplate = "/api002/values", + UpstreamHttpMethod = new List { "Get" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true, + }, + }, + }, + }; + + var tracingPort = RandomPortFinder.GetRandomPort(); + var tracingUrl = $"http://localhost:{tracingPort}"; + + var fakeTracer = new FakeTracer(); + + this.Given(_ => GivenFakeOpenTracing(tracingUrl)) + .And(_ => GivenServiceOneIsRunning($"http://localhost:{port1}", "/api/values", 200, "Hello from Laura", tracingUrl)) + .And(_ => GivenServiceTwoIsRunning($"http://localhost:{port2}", "/api/values", 200, "Hello from Tom", tracingUrl)) + .And(_ => _steps.GivenThereIsAConfiguration(configuration)) + .And(_ => _steps.GivenOcelotIsRunningUsingOpenTracing(fakeTracer)) + .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/api001/values")) + .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/api002/values")) + .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) + .And(_ => ThenTheTracerIsCalled(fakeTracer)) + .BDDfy(); + } + + [Fact] + public void should_return_tracing_header() + { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/api001/values", + UpstreamHttpMethod = new List { "Get" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true, + }, + DownstreamHeaderTransform = new Dictionary + { + {"Trace-Id", "{TraceId}"}, + {"Tom", "Laura"}, + }, + }, + }, + }; + + var butterflyPort = RandomPortFinder.GetRandomPort(); + + var butterflyUrl = $"http://localhost:{butterflyPort}"; + + var fakeTracer = new FakeTracer(); + + this.Given(x => GivenFakeOpenTracing(butterflyUrl)) + .And(x => GivenServiceOneIsRunning($"http://localhost:{port}", "/api/values", 200, "Hello from Laura", butterflyUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingOpenTracing(fakeTracer)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api001/values")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.ThenTheTraceHeaderIsSet("Trace-Id")) + .And(x => _steps.ThenTheResponseHeaderIs("Tom", "Laura")) + .BDDfy(); + } + + private void ThenTheTracerIsCalled(FakeTracer fakeTracer) + { + var commandOnAllStateMachines = Wait.WaitFor(10000).Until(() => fakeTracer.BuildSpanCalled >= 2); + + _output.WriteLine($"fakeTracer.BuildSpanCalled is {fakeTracer.BuildSpanCalled}"); + + commandOnAllStateMachines.ShouldBeTrue(); + } + + private void GivenServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody, string butterflyUrl) + { + _serviceOneBuilder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .ConfigureServices(services => + { + services.AddButterfly(option => + { + option.CollectorUrl = butterflyUrl; + option.Service = "Service One"; + option.IgnoredRoutesRegexPatterns = Array.Empty(); + }); + }) + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + _downstreamPathOne = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPathOne != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + }) + .Build(); + + _serviceOneBuilder.Start(); + } + + private void GivenFakeOpenTracing(string baseUrl) + { + _fakeOpenTracing = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.Run(async context => + { + await context.Response.WriteAsync("OK..."); + }); + }) + .Build(); + + _fakeOpenTracing.Start(); + } + + private void GivenServiceTwoIsRunning(string baseUrl, string basePath, int statusCode, string responseBody, string butterflyUrl) + { + _serviceTwoBuilder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .ConfigureServices(services => + { + services.AddButterfly(option => + { + option.CollectorUrl = butterflyUrl; + option.Service = "Service Two"; + option.IgnoredRoutesRegexPatterns = Array.Empty(); + }); + }) + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + _downstreamPathTwo = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPathTwo != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + }) + .Build(); + + _serviceTwoBuilder.Start(); + } + + public void Dispose() + { + _serviceOneBuilder?.Dispose(); + _serviceTwoBuilder?.Dispose(); + _fakeOpenTracing?.Dispose(); + _steps.Dispose(); + } + } + + internal class FakeTracer : ITracer + { + public IScopeManager ScopeManager => throw new NotImplementedException(); + + public ISpan ActiveSpan => throw new NotImplementedException(); + + public ISpanBuilder BuildSpan(string operationName) + { + BuildSpanCalled++; + + return new FakeSpanBuilder(); + } + + public int BuildSpanCalled { get; set; } + + public ISpanContext Extract(IFormat format, TCarrier carrier) + { + ExtractCalled++; + + return null; + } + + public int ExtractCalled { get; set; } + + public void Inject(ISpanContext spanContext, IFormat format, TCarrier carrier) + { + InjectCalled++; + } + + public int InjectCalled { get; set; } + } + + internal class FakeSpanBuilder : ISpanBuilder + { + public ISpanBuilder AddReference(string referenceType, ISpanContext referencedContext) + { + throw new NotImplementedException(); + } + + public ISpanBuilder AsChildOf(ISpanContext parent) + { + throw new NotImplementedException(); + } + + public ISpanBuilder AsChildOf(ISpan parent) + { + throw new NotImplementedException(); + } + + public ISpanBuilder IgnoreActiveSpan() + { + throw new NotImplementedException(); + } + + public ISpan Start() + { + throw new NotImplementedException(); + } + + public IScope StartActive() + { + throw new NotImplementedException(); + } + + public IScope StartActive(bool finishSpanOnDispose) + { + return new FakeScope(finishSpanOnDispose); + } + + public ISpanBuilder WithStartTimestamp(DateTimeOffset timestamp) + { + throw new NotImplementedException(); + } + + public ISpanBuilder WithTag(string key, string value) + { + throw new NotImplementedException(); + } + + public ISpanBuilder WithTag(string key, bool value) + { + throw new NotImplementedException(); + } + + public ISpanBuilder WithTag(string key, int value) + { + throw new NotImplementedException(); + } + + public ISpanBuilder WithTag(string key, double value) + { + throw new NotImplementedException(); + } + + public ISpanBuilder WithTag(BooleanTag tag, bool value) + { + throw new NotImplementedException(); + } + + public ISpanBuilder WithTag(IntOrStringTag tag, string value) + { + throw new NotImplementedException(); + } + + public ISpanBuilder WithTag(IntTag tag, int value) + { + throw new NotImplementedException(); + } + + public ISpanBuilder WithTag(StringTag tag, string value) + { + throw new NotImplementedException(); + } + } + + internal class FakeScope : IScope + { + private readonly bool finishSpanOnDispose; + + public FakeScope(bool finishSpanOnDispose) + { + this.finishSpanOnDispose = finishSpanOnDispose; + } + + public ISpan Span { get; } = new FakeSpan(); + + public void Dispose() + { + if (finishSpanOnDispose) + { + Span.Finish(); + } + } + } + + internal class FakeSpan : ISpan + { + public ISpanContext Context => new FakeSpanContext(); + + public void Finish() + { + } + + public void Finish(DateTimeOffset finishTimestamp) + { + throw new NotImplementedException(); + } + + public string GetBaggageItem(string key) + { + throw new NotImplementedException(); + } + + public ISpan Log(IEnumerable> fields) + { + return this; + } + + public ISpan Log(DateTimeOffset timestamp, IEnumerable> fields) + { + throw new NotImplementedException(); + } + + public ISpan Log(string @event) + { + throw new NotImplementedException(); + } + + public ISpan Log(DateTimeOffset timestamp, string @event) + { + throw new NotImplementedException(); + } + + public ISpan SetBaggageItem(string key, string value) + { + throw new NotImplementedException(); + } + + public ISpan SetOperationName(string operationName) + { + throw new NotImplementedException(); + } + + public ISpan SetTag(string key, string value) + { + return this; + } + + public ISpan SetTag(string key, bool value) + { + return this; + } + + public ISpan SetTag(string key, int value) + { + return this; + } + + public ISpan SetTag(string key, double value) + { + return this; + } + + public ISpan SetTag(BooleanTag tag, bool value) + { + return this; + } + + public ISpan SetTag(IntOrStringTag tag, string value) + { + return this; + } + + public ISpan SetTag(IntTag tag, int value) + { + return this; + } + + public ISpan SetTag(StringTag tag, string value) + { + return this; + } + } + + internal class FakeSpanContext : ISpanContext + { + public static string FakeTraceId = "FakeTraceId"; + + public static string FakeSpanId = "FakeSpanId"; + + public string TraceId => FakeTraceId; + + public string SpanId => FakeSpanId; + + public IEnumerable> GetBaggageItems() + { + throw new NotImplementedException(); + } + } + + public class Wait + { + public static Waiter WaitFor(int milliSeconds) + { + return new Waiter(milliSeconds); + } + } + + public class Waiter + { + private readonly int _milliSeconds; + + public Waiter(int milliSeconds) + { + _milliSeconds = milliSeconds; + } + + public bool Until(Func condition) + { + var stopwatch = Stopwatch.StartNew(); + var passed = false; + while (stopwatch.ElapsedMilliseconds < _milliSeconds) + { + if (condition.Invoke()) + { + passed = true; + break; + } + } + + return passed; + } + + public async Task Until(Func> condition) + { + var stopwatch = Stopwatch.StartNew(); + var passed = false; + while (stopwatch.ElapsedMilliseconds < _milliSeconds) + { + if (await condition.Invoke()) + { + passed = true; + break; + } + } + + return passed; + } + + public bool Until(Func condition) + { + var stopwatch = Stopwatch.StartNew(); + var passed = false; + while (stopwatch.ElapsedMilliseconds < _milliSeconds) + { + if (condition.Invoke()) + { + passed = true; + break; + } + } + + return passed; + } + } +} diff --git a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs index d7cf7c2d6..f05e0e664 100644 --- a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs +++ b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs @@ -1,15 +1,19 @@ -namespace Ocelot.AcceptanceTests -{ - using Configuration.File; - using Microsoft.AspNetCore.Http; - using System; - using System.Collections.Generic; - using System.Net; - using System.Threading; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +using Ocelot.Configuration.File; + +using Microsoft.AspNetCore.Http; + +using TestStack.BDDfy; + +using Xunit; +namespace Ocelot.AcceptanceTests +{ public class PollyQoSTests : IDisposable { private readonly Steps _steps; @@ -25,20 +29,22 @@ public PollyQoSTests() [Fact] public void should_not_timeout() { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51569, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", @@ -46,13 +52,13 @@ public void should_not_timeout() QoSOptions = new FileQoSOptions { TimeoutValue = 1000, - ExceptionsAllowedBeforeBreaking = 10 - } - } - } + ExceptionsAllowedBeforeBreaking = 10, + }, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51569", 200, string.Empty, 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")) @@ -64,20 +70,22 @@ public void should_not_timeout() [Fact] public void should_timeout() { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51579, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", @@ -85,13 +93,13 @@ public void should_timeout() QoSOptions = new FileQoSOptions { TimeoutValue = 10, - ExceptionsAllowedBeforeBreaking = 10 - } - } - } + ExceptionsAllowedBeforeBreaking = 10, + }, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51579", 201, string.Empty, 1000)) + 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")) @@ -103,21 +111,23 @@ public void should_timeout() [Fact] public void should_open_circuit_breaker_then_close() { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51892, - } + Port = port, + }, }, UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, @@ -125,13 +135,13 @@ public void should_open_circuit_breaker_then_close() { ExceptionsAllowedBeforeBreaking = 1, TimeoutValue = 500, - DurationOfBreak = 1000 + DurationOfBreak = 1000, }, - } - } + }, + }, }; - this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51892", "Hello from Laura")) + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port}", "Hello from Laura")) .Given(x => _steps.GivenThereIsAConfiguration(configuration)) .Given(x => _steps.GivenOcelotIsRunningWithPolly()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -143,7 +153,7 @@ public void should_open_circuit_breaker_then_close() .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => x.GivenIWaitMilliseconds(3000)) + .Given(x => GivenIWaitMilliseconds(3000)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) @@ -151,23 +161,26 @@ public void should_open_circuit_breaker_then_close() } [Fact] - public void open_circuit_should_not_effect_different_reRoute() + public void open_circuit_should_not_effect_different_route() { + var port1 = RandomPortFinder.GetRandomPort(); + var port2 = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51870, - } + Port = port1, + }, }, UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, @@ -175,29 +188,29 @@ public void open_circuit_should_not_effect_different_reRoute() { ExceptionsAllowedBeforeBreaking = 1, TimeoutValue = 500, - DurationOfBreak = 1000 - } + DurationOfBreak = 1000, + }, }, - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51880, - } + Port = port2, + }, }, UpstreamPathTemplate = "/working", UpstreamHttpMethod = new List { "Get" }, - } - } + }, + }, }; - this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51870", "Hello from Laura")) - .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", 200, "Hello from Tom", 0)) + 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("/")) @@ -212,14 +225,14 @@ public void open_circuit_should_not_effect_different_reRoute() .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => x.GivenIWaitMilliseconds(3000)) + .And(x => GivenIWaitMilliseconds(3000)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } - private void GivenIWaitMilliseconds(int ms) + private static void GivenIWaitMilliseconds(int ms) { Thread.Sleep(ms); } diff --git a/test/Ocelot.AcceptanceTests/RandomPortFinder.cs b/test/Ocelot.AcceptanceTests/RandomPortFinder.cs new file mode 100644 index 000000000..34f3bbddb --- /dev/null +++ b/test/Ocelot.AcceptanceTests/RandomPortFinder.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Net; +using System.Net.Sockets; + +namespace Ocelot.AcceptanceTests +{ + public static class RandomPortFinder + { + private const int TrialNumber = 100; + private const int BeginPortRange = 20000; + private const int EndPortRange = 45000; + private static readonly Random Random = new(); + private static readonly ConcurrentBag UsedPorts = new(); + + public static int GetRandomPort() + { + for (var i = 0; i < TrialNumber; i++) + { + var randomPort = Random.Next(BeginPortRange, EndPortRange); + + if (!PortInUse(randomPort)) + { + try + { + return UsePort(randomPort); + } + catch (Exception) + { + // ignored + } + } + } + + throw new Exception("Cannot find available port to bind to."); + } + + 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; + } + } + + private static bool PortInUse(int randomPort) + { + return UsedPorts.Any(p => p == randomPort); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ReasonPhraseTests.cs b/test/Ocelot.AcceptanceTests/ReasonPhraseTests.cs index 448741152..b664cec65 100644 --- a/test/Ocelot.AcceptanceTests/ReasonPhraseTests.cs +++ b/test/Ocelot.AcceptanceTests/ReasonPhraseTests.cs @@ -1,74 +1,77 @@ -namespace Ocelot.AcceptanceTests -{ - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Http.Features; - using Ocelot.Configuration.File; - using System; - using System.Collections.Generic; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; - public class ReasonPhraseTests : IDisposable - { - private readonly Steps _steps; - private string _contentType; - private long? _contentLength; - private bool _contentTypeHeaderExists; - private readonly ServiceHandler _serviceHandler; +using Ocelot.Configuration.File; - public ReasonPhraseTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; - [Fact] - public void should_return_reason_phrase() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51339, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; +using TestStack.BDDfy; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51339", "/", "some reason")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(_ => _steps.ThenTheReasonPhraseIs("some reason")) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string reasonPhrase) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - context.Response.HttpContext.Features.Get().ReasonPhrase = reasonPhrase; - - await context.Response.WriteAsync("YOYO!"); - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } -} +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class ReasonPhraseTests : IDisposable + { + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public ReasonPhraseTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_return_reason_phrase() + { + 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" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", "some reason")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(_ => _steps.ThenTheReasonPhraseIs("some reason")) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string reasonPhrase) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + context.Response.HttpContext.Features.Get().ReasonPhrase = reasonPhrase; + + await context.Response.WriteAsync("YOYO!"); + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/RequestIdTests.cs b/test/Ocelot.AcceptanceTests/RequestIdTests.cs index bef0bc510..57d90d204 100644 --- a/test/Ocelot.AcceptanceTests/RequestIdTests.cs +++ b/test/Ocelot.AcceptanceTests/RequestIdTests.cs @@ -1,13 +1,16 @@ -namespace Ocelot.AcceptanceTests -{ - using Ocelot.Configuration.File; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Ocelot.Configuration.File; + +using TestStack.BDDfy; + +using Xunit; +namespace Ocelot.AcceptanceTests +{ public class RequestIdTests : IDisposable { private readonly Steps _steps; @@ -21,31 +24,33 @@ public RequestIdTests() [Fact] public void should_use_default_request_id_and_forward() - { + { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List + { + new() { - new FileReRoute + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List + new() { - new FileHostAndPort - { - Host = "localhost", - Port = 51873, - } + Host = "localhost", + Port = port, }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = _steps.RequestIdKey, - } - } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + RequestIdKey = _steps.RequestIdKey, + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51873")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -55,32 +60,34 @@ public void should_use_default_request_id_and_forward() [Fact] public void should_use_request_id_and_forward() - { + { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51873, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, - } - } + }, + }, }; var requestId = Guid.NewGuid().ToString(); - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51873")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/", requestId)) @@ -90,36 +97,38 @@ public void should_use_request_id_and_forward() [Fact] public void should_use_global_request_id_and_forward() - { + { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51873, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, - } + }, }, GlobalConfiguration = new FileGlobalConfiguration { - RequestIdKey = _steps.RequestIdKey - } + RequestIdKey = _steps.RequestIdKey, + }, }; var requestId = Guid.NewGuid().ToString(); - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51873")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/", requestId)) @@ -129,34 +138,36 @@ 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 configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51873, - } + Port = port, + }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, - } + }, }, GlobalConfiguration = new FileGlobalConfiguration { - RequestIdKey = _steps.RequestIdKey - } + RequestIdKey = _steps.RequestIdKey, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51873")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) diff --git a/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs b/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs index cd5be7a76..d026a149c 100644 --- a/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs +++ b/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs @@ -1,12 +1,13 @@ +using Ocelot.Configuration.File; +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using TestStack.BDDfy; +using Xunit; + namespace Ocelot.AcceptanceTests { - using Ocelot.Configuration.File; - using System; - using System.Collections.Generic; - using System.Net; - using TestStack.BDDfy; - using Xunit; - public class ResponseCodeTests : IDisposable { private readonly Steps _steps; @@ -19,31 +20,33 @@ public ResponseCodeTests() } [Fact] - public void should_return_response_304_when_service_returns_304() - { + public void ShouldReturnResponse304WhenServiceReturns304() + { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/{everything}", DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 51092, - } + Port = port, + }, }, UpstreamPathTemplate = "/{everything}", UpstreamHttpMethod = new List { "Get" }, - } - } + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51092", "/inline.132.bundle.js", 304)) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/inline.132.bundle.js", 304)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/inline.132.bundle.js")) @@ -53,16 +56,17 @@ public void should_return_response_304_when_service_returns_304() private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode) { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - context.Response.StatusCode = statusCode; - }); + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, (context) => Task.Run(() => + { + context.Response.StatusCode = statusCode; + })); } public void Dispose() { _serviceHandler?.Dispose(); - _steps.Dispose(); + _steps.Dispose(); + GC.SuppressFinalize(this); } } } diff --git a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs index fd04af61d..5a8affbc4 100644 --- a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs +++ b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs @@ -1,12 +1,15 @@ -namespace Ocelot.AcceptanceTests -{ - using Ocelot.Configuration.File; - using System; - using System.Collections.Generic; - using System.Net; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Net; + +using Ocelot.Configuration.File; + +using TestStack.BDDfy; + +using Xunit; +namespace Ocelot.AcceptanceTests +{ public class ReturnsErrorTests : IDisposable { private readonly Steps _steps; @@ -16,35 +19,68 @@ public ReturnsErrorTests() { _serviceHandler = new ServiceHandler(); _steps = new Steps(); + } + + [Fact] + public void should_return_bad_gateway_error_if_downstream_service_doesnt_respond() + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 53877, + }, + }, + DownstreamScheme = "http", + }, + }, + }; + + this.Given(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) + .BDDfy(); } [Fact] public void should_return_internal_server_error_if_downstream_service_returns_internal_server_error() { + var port = RandomPortFinder.GetRandomPort(); var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 53876, - } + Port = port, + }, }, DownstreamScheme = "http", - } - } + }, + }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:53876")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -55,21 +91,23 @@ 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() { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", - Port = 53876, + Port = port, }, }, DownstreamScheme = "http", @@ -77,7 +115,7 @@ public void should_log_warning_if_downstream_service_returns_internal_server_err }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:53876")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningWithLogger()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/RoutingTests.cs index 15eaf8d9f..df9296380 100644 --- a/test/Ocelot.AcceptanceTests/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingTests.cs @@ -1,1057 +1,1110 @@ -namespace Ocelot.AcceptanceTests -{ - using Microsoft.AspNetCore.Http; - using Ocelot.Configuration.File; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Net; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Net; - public class RoutingTests : IDisposable - { - private readonly Steps _steps; - private string _downstreamPath; - private readonly ServiceHandler _serviceHandler; +using Ocelot.Configuration.File; - public RoutingTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } +using Microsoft.AspNetCore.Http; - [Fact] - public void should_not_match_forward_slash_in_pattern_before_next_forward_slash() - { - var port = 31879; - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/v{apiVersion}/cards", - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/v{apiVersion}/cards", - UpstreamHttpMethod = new List { "Get" }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = port, - } - }, - Priority = 1 - } - } - }; +using Shouldly; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/api/v1/aaaaaaaaa/cards", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/v1/aaaaaaaaa/cards")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } +using TestStack.BDDfy; - [Fact] - public void should_return_response_404_when_no_configuration_at_all() - { - this.Given(x => _steps.GivenThereIsAConfiguration(new FileConfiguration())) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_forward_slash_and_placeholder_only() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/{url}", - DownstreamScheme = "http", - UpstreamPathTemplate = "/{url}", - UpstreamHttpMethod = new List { "Get" }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 57873, - } - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:57873/", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_favouring_forward_slash_with_path_route() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/{url}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/{url}", - UpstreamHttpMethod = new List { "Get" }, - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 50810, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", "/test", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/test")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_favouring_forward_slash() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/{url}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/{url}", - UpstreamHttpMethod = new List { "Get" }, - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 50810, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:50810/", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_favouring_forward_slash_route_because_it_is_first() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - }, - new FileReRoute - { - DownstreamPathTemplate = "/{url}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - UpstreamPathTemplate = "/{url}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_nothing_and_placeholder_only() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/{url}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51005, - } - }, - UpstreamPathTemplate = "/{url}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51005", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_simple_url() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 58589, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:58589", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void bug() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/v1/vacancy", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51874, - } - }, - UpstreamPathTemplate = "/vacancy/", - UpstreamHttpMethod = new List { "Options", "Put", "Get", "Post", "Delete" }, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" } - }, - new FileReRoute - { - DownstreamPathTemplate = "/api/v1/vacancy/{vacancyId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51874, - } - }, - UpstreamPathTemplate = "/vacancy/{vacancyId}", - UpstreamHttpMethod = new List { "Options", "Put", "Get", "Post", "Delete" }, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51874", "/api/v1/vacancy/1", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/vacancy/1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_when_path_missing_forward_slash_as_first_char() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51206, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51206", "/api/products", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_when_host_has_trailing_slash() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51990, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51990", "/api/products", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_ok_when_upstream_url_ends_with_forward_slash_but_template_does_not() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/products", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 58804, - } - }, - UpstreamPathTemplate = "/products/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:58804", "/products", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_not_found_when_upstream_url_ends_with_forward_slash_but_template_does_not() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/products", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 54015, - } - }, - UpstreamPathTemplate = "/products", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:54015", "/products", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - [Fact] - public void should_return_not_found() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/products", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 54072, - } - }, - UpstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:54072", "/products", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_complex_url() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/{productId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 55961, - } - }, - UpstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:55961", "/api/products/1", 200, "Some Product")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Some Product")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_complex_url_that_starts_with_placeholder() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/{variantId}/products/{productId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51116, - } - }, - UpstreamPathTemplate = "/{variantId}/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51116", "/api/23/products/1", 200, "Some Product")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("23/products/1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Some Product")) - .BDDfy(); - } - - [Fact] - public void should_not_add_trailing_slash_to_downstream_url() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/{productId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51809, - } - }, - UpstreamPathTemplate = "/products/{productId}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => GivenThereIsAServiceRunningOn("http://localhost:51809", "/api/products/1", 200, "Some Product")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/1")) - .Then(x => ThenTheDownstreamUrlPathShouldBe("/api/products/1")) - .BDDfy(); - } - - [Fact] - public void should_return_response_201_with_simple_url() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 56615, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Post" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:56615", "/", 201, string.Empty)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) - .BDDfy(); - } - - [Fact] - public void should_return_response_201_with_complex_query_string() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/newThing", - UpstreamPathTemplate = "/newThing", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 57771, - } - }, - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:57771", "/newThing", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/newThing?DeviceType=IphoneApp&Browser=moonpigIphone&BrowserString=-&CountryCode=123&DeviceName=iPhone 5 (GSM+CDMA)&OperatingSystem=iPhone OS 7.1.2&BrowserVersion=3708AdHoc&ipAddress=-")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_placeholder_for_final_url_path() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/{urlPath}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 55609, - } - }, - UpstreamPathTemplate = "/myApp1Name/api/{urlPath}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:55609", "/api/products/1", 200, "Some Product")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/myApp1Name/api/products/1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Some Product")) - .BDDfy(); - } - - [Fact] - public void should_return_response_201_with_simple_url_and_multiple_upstream_http_method() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 59911, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get", "Post" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:59911", "", 201, string.Empty)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_simple_url_and_any_upstream_http_method() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 59187, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List(), - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:59187", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_404_when_calling_upstream_route_with_no_matching_downstream_re_route_github_issue_134() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/v1/vacancy", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 54079, - } - }, - UpstreamPathTemplate = "/vacancy/", - UpstreamHttpMethod = new List { "Options", "Put", "Get", "Post", "Delete" }, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" } - }, - new FileReRoute - { - DownstreamPathTemplate = "/api/v1/vacancy/{vacancyId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 54079, - } - }, - UpstreamPathTemplate = "/vacancy/{vacancyId}", - UpstreamHttpMethod = new List { "Options", "Put", "Get", "Post", "Delete" }, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:54079", "/api/v1/vacancy/1", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("api/vacancy/1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - [Fact] - public void should_not_set_trailing_slash_on_url_template() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/{url}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51899, - } - }, - UpstreamPathTemplate = "/platform/{url}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", "/api/swagger/lib/backbone-min.js", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/platform/swagger/lib/backbone-min.js")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => ThenTheDownstreamUrlPathShouldBe("/api/swagger/lib/backbone-min.js")) - .BDDfy(); - } - - [Fact] - public void should_use_priority() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/goods/{url}", - DownstreamScheme = "http", - UpstreamPathTemplate = "/goods/{url}", - UpstreamHttpMethod = new List { "Get" }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 53879, - } - }, - Priority = 0 - }, - new FileReRoute - { - DownstreamPathTemplate = "/goods/delete", - DownstreamScheme = "http", - UpstreamPathTemplate = "/goods/delete", - UpstreamHttpMethod = new List { "Get" }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 52879, - } - }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:52879/", "/goods/delete", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/goods/delete")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_match_multiple_paths_with_catch_all() - { - var port = 61999; - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/{everything}", - DownstreamScheme = "http", - UpstreamPathTemplate = "/{everything}", - UpstreamHttpMethod = new List { "Get" }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = port, - } - }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/test/toot", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/test/toot")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_fix_issue_271() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/v1/{everything}", - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/v1/{everything}", - UpstreamHttpMethod = new List { "Get", "Put", "Post" }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 54879, - } - }, - }, - new FileReRoute - { - DownstreamPathTemplate = "/connect/token", - DownstreamScheme = "http", - UpstreamPathTemplate = "/connect/token", - UpstreamHttpMethod = new List { "Post" }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 5001, - } - }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:54879/", "/api/v1/modules/Test", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/v1/modules/Test")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPath != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); - } - - internal void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPath) - { - _downstreamPath.ShouldBe(expectedDownstreamPath); - } - - public void Dispose() - { - _serviceHandler.Dispose(); - _steps.Dispose(); - } - } -} +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class RoutingTests : IDisposable + { + private readonly Steps _steps; + private string _downstreamPath; + private readonly ServiceHandler _serviceHandler; + + public RoutingTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_not_match_forward_slash_in_pattern_before_next_forward_slash() + { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/v{apiVersion}/cards", + DownstreamScheme = "http", + UpstreamPathTemplate = "/api/v{apiVersion}/cards", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + Priority = 1, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/api/v1/aaaaaaaaa/cards", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/v1/aaaaaaaaa/cards")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void should_return_response_404_when_no_configuration_at_all() + { + this.Given(x => _steps.GivenThereIsAConfiguration(new FileConfiguration())) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_forward_slash_and_placeholder_only() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_favouring_forward_slash_with_path_route() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 50810, + }, + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/test", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/test")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_favouring_forward_slash() + { + var port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51880, + }, + }, + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_favouring_forward_slash_route_because_it_is_first() + { + 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" }, + }, + new() + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51879, + }, + }, + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_nothing_and_placeholder_only() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{url}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/{url}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway(string.Empty)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_simple_url() + { + 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" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void Bug() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/v1/vacancy", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/vacancy/", + UpstreamHttpMethod = new List { "Options", "Put", "Get", "Post", "Delete" }, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + new() + { + DownstreamPathTemplate = "/api/v1/vacancy/{vacancyId}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/vacancy/{vacancyId}", + UpstreamHttpMethod = new List { "Options", "Put", "Get", "Post", "Delete" }, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/v1/vacancy/1", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/vacancy/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_when_path_missing_forward_slash_as_first_char() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_when_host_has_trailing_slash() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_ok_when_upstream_url_ends_with_forward_slash_but_template_does_not() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/products/", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_not_found_when_upstream_url_ends_with_forward_slash_but_template_does_not() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/products", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void should_return_not_found() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_complex_url() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Some Product")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_complex_url_that_starts_with_placeholder() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/{variantId}/products/{productId}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/{variantId}/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/23/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("23/products/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Some Product")) + .BDDfy(); + } + + [Fact] + public void should_not_add_trailing_slash_to_downstream_url() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/1")) + .Then(x => ThenTheDownstreamUrlPathShouldBe("/api/products/1")) + .BDDfy(); + } + + [Fact] + public void should_return_response_201_with_simple_url() + { + 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" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 201, string.Empty)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) + .BDDfy(); + } + + [Fact] + public void should_return_response_201_with_complex_query_string() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/newThing", + UpstreamPathTemplate = "/newThing", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/newThing", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/newThing?DeviceType=IphoneApp&Browser=moonpigIphone&BrowserString=-&CountryCode=123&DeviceName=iPhone 5 (GSM+CDMA)&OperatingSystem=iPhone OS 7.1.2&BrowserVersion=3708AdHoc&ipAddress=-")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_placeholder_for_final_url_path() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/{urlPath}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/myApp1Name/api/{urlPath}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/myApp1Name/api/products/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Some Product")) + .BDDfy(); + } + + [Fact] + public void should_return_response_201_with_simple_url_and_multiple_upstream_http_method() + { + 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 { "Get", "Post" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", string.Empty, 201, string.Empty)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_simple_url_and_any_upstream_http_method() + { + 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(), + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_404_when_calling_upstream_route_with_no_matching_downstream_re_route_github_issue_134() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/v1/vacancy", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/vacancy/", + UpstreamHttpMethod = new List { "Options", "Put", "Get", "Post", "Delete" }, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + new() + { + DownstreamPathTemplate = "/api/v1/vacancy/{vacancyId}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/vacancy/{vacancyId}", + UpstreamHttpMethod = new List { "Options", "Put", "Get", "Post", "Delete" }, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/v1/vacancy/1", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("api/vacancy/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void should_not_set_trailing_slash_on_url_template() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/{url}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/platform/{url}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/api/swagger/lib/backbone-min.js", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/platform/swagger/lib/backbone-min.js")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => ThenTheDownstreamUrlPathShouldBe("/api/swagger/lib/backbone-min.js")) + .BDDfy(); + } + + [Fact] + public void should_use_priority() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/goods/{url}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/goods/{url}", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 53879, + }, + }, + Priority = 0, + }, + new() + { + DownstreamPathTemplate = "/goods/delete", + DownstreamScheme = "http", + UpstreamPathTemplate = "/goods/delete", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/goods/delete", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/goods/delete")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_match_multiple_paths_with_catch_all() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{everything}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/{everything}", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/test/toot", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/test/toot")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_fix_issue_271() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/v1/{everything}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/api/v1/{everything}", + UpstreamHttpMethod = new List { "Get", "Put", "Post" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + }, + new() + { + DownstreamPathTemplate = "/connect/token", + DownstreamScheme = "http", + UpstreamPathTemplate = "/connect/token", + UpstreamHttpMethod = new List { "Post" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 5001, + }, + }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}/", "/api/v1/modules/Test", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/v1/modules/Test")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + } + + internal void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPath) + { + _downstreamPath.ShouldBe(expectedDownstreamPath); + } + + public void Dispose() + { + _serviceHandler.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs b/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs index 58e54b2f9..5aa72fc72 100644 --- a/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs @@ -1,266 +1,275 @@ -namespace Ocelot.AcceptanceTests -{ - using Microsoft.AspNetCore.Http; - using Ocelot.Configuration.File; - using System; - using System.Collections.Generic; - using System.Net; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Net; - public class RoutingWithQueryStringTests : IDisposable - { - private readonly Steps _steps; - private readonly ServiceHandler _serviceHandler; +using Ocelot.Configuration.File; - public RoutingWithQueryStringTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } +using Microsoft.AspNetCore.Http; - [Fact] - public void should_return_response_200_with_query_string_template() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); +using TestStack.BDDfy; - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 61879, - } - }, - UpstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:61879", $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/{unitId}/updates")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_odata_query_string() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - var port = 57359; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/{everything}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = port, - } - }, - UpstreamPathTemplate = "/{everything}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/odata/customers", "?$filter=Name%20eq%20'Sam'", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/odata/customers?$filter=Name eq 'Sam' ")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_query_string_upstream_template() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 64879, - } - }, - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:64879", $"/api/units/{subscriptionId}/{unitId}/updates", "", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - 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 configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 64879, - } - }, - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:64879", $"/api/units/{subscriptionId}/{unitId}/updates", "", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - [Fact] - 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 configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 64879, - } - }, - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:64879", $"/api/units/{subscriptionId}/{unitId}/updates", "", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?test=1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_query_string_upstream_template_multiple_params() - { - var subscriptionId = Guid.NewGuid().ToString(); - var unitId = Guid.NewGuid().ToString(); - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 64879, - } - }, - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:64879", $"/api/units/{subscriptionId}/{unitId}/updates", "?productId=1", 200, "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(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string queryString, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - if ((context.Request.PathBase.Value != basePath) || context.Request.QueryString.Value != queryString) - { - context.Response.StatusCode = 500; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } -} +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class RoutingWithQueryStringTests : IDisposable + { + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public RoutingWithQueryStringTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + 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 configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/{unitId}/updates")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + 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 configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{everything}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/{everything}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/odata/customers", "?$filter=Name%20eq%20'Sam'", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/odata/customers?$filter=Name eq 'Sam' ")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + 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 configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + 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 configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + 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 configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?test=1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + 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 configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/units/{subscriptionId}/{unitId}/updates", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", "?productId=1", 200, "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(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string queryString, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + if ((context.Request.PathBase.Value != basePath) || context.Request.QueryString.Value != queryString) + { + context.Response.StatusCode = 500; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs index b464a3d14..7a967bfc5 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs @@ -1,584 +1,610 @@ -namespace Ocelot.AcceptanceTests -{ - using Configuration.File; - using Consul; - using Microsoft.AspNetCore.Http; - using Newtonsoft.Json; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net; - using TestStack.BDDfy; - using Xunit; - - public class ServiceDiscoveryTests : IDisposable - { - private readonly Steps _steps; - private readonly List _consulServices; - private int _counterOne; - private int _counterTwo; - private static readonly object SyncLock = new object(); - private string _downstreamPath; - private string _receivedToken; - private readonly ServiceHandler _serviceHandler; - - public ServiceDiscoveryTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - _consulServices = new List(); - } - - [Fact] - public void should_use_consul_service_discovery_and_load_balance_request() - { - var consulPort = 8502; - var serviceName = "product"; - var downstreamServiceOneUrl = "http://localhost:50881"; - var downstreamServiceTwoUrl = "http://localhost:50882"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry() - { - Service = new AgentService() - { - Service = serviceName, - Address = "localhost", - Port = 50881, - ID = Guid.NewGuid().ToString(), - Tags = new string[0] - }, - }; - var serviceEntryTwo = new ServiceEntry() - { - Service = new AgentService() - { - Service = serviceName, - Address = "localhost", - Port = 50882, - ID = Guid.NewGuid().ToString(), - Tags = new string[0] - }, - }; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = consulPort - } - } - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request() - { - const int consulPort = 8505; - const string serviceName = "web"; - const string downstreamServiceOneUrl = "http://localhost:8080"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry() - { - Service = new AgentService() - { - Service = serviceName, - Address = "localhost", - Port = 8080, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" } - }, - }; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = consulPort - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/home")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes() - { - const int consulPort = 8513; - const string serviceName = "web"; - const int downstreamServicePort = 8087; - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry() - { - Service = new AgentService() - { - Service = serviceName, - Address = "localhost", - Port = downstreamServicePort, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" } - }, - }; - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Port = consulPort - }, - DownstreamScheme = "http", - HttpHandlerOptions = new FileHttpHandlerOptions - { - AllowAutoRedirect = true, - UseCookieContainer = true, - UseTracing = false - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/web/something")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_use_consul_service_discovery_and_load_balance_request_no_re_routes() - { - var consulPort = 8510; - var serviceName = "product"; - var serviceOnePort = 50888; - var serviceTwoPort = 50889; - var downstreamServiceOneUrl = $"http://localhost:{serviceOnePort}"; - var downstreamServiceTwoUrl = $"http://localhost:{serviceTwoPort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry() - { - Service = new AgentService() - { - Service = serviceName, - Address = "localhost", - Port = serviceOnePort, - ID = Guid.NewGuid().ToString(), - Tags = new string[0] - }, - }; - var serviceEntryTwo = new ServiceEntry() - { - Service = new AgentService() - { - Service = serviceName, - Address = "localhost", - Port = serviceTwoPort, - ID = Guid.NewGuid().ToString(), - Tags = new string[0] - }, - }; - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration() - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = consulPort - }, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - DownstreamScheme = "http" - } - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) - .BDDfy(); - } - - [Fact] - public void should_use_token_to_make_request_to_consul() - { - var token = "abctoken"; - var consulPort = 8515; - var serviceName = "web"; - var downstreamServiceOneUrl = "http://localhost:8081"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry() - { - Service = new AgentService() - { - Service = serviceName, - Address = "localhost", - Port = 8081, - ID = "web_90_0_2_224_8080", - Tags = new[] { "version-v1" } - }, - }; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = consulPort, - Token = token - } - } - }; - - this.Given(_ => GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(_ => _steps.GivenThereIsAConfiguration(configuration)) - .And(_ => _steps.GivenOcelotIsRunningWithConsul()) - .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/home")) - .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(_ => _receivedToken.ShouldBe(token)) - .BDDfy(); - } - - [Fact] - public void should_send_request_to_service_after_it_becomes_available_in_consul() - { - var consulPort = 8501; - var serviceName = "product"; - var downstreamServiceOneUrl = "http://localhost:50879"; - var downstreamServiceTwoUrl = "http://localhost:50880"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry() - { - Service = new AgentService() - { - Service = serviceName, - Address = "localhost", - Port = 50879, - ID = Guid.NewGuid().ToString(), - Tags = new string[0] - }, - }; - var serviceEntryTwo = new ServiceEntry() - { - Service = new AgentService() - { - Service = serviceName, - Address = "localhost", - Port = 50880, - ID = Guid.NewGuid().ToString(), - Tags = new string[0] - }, - }; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = consulPort - } - } - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .And(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) - .And(x => WhenIRemoveAService(serviceEntryTwo)) - .And(x => GivenIResetCounters()) - .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .And(x => ThenOnlyOneServiceHasBeenCalled()) - .And(x => WhenIAddAServiceBackIn(serviceEntryTwo)) - .And(x => GivenIResetCounters()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) - .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) - .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) - .BDDfy(); - } - - [Fact] - public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request() - { - const int consulPort = 8518; - const string serviceName = "web"; - const int downstreamServicePort = 8082; - var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; - var serviceEntryOne = new ServiceEntry() - { - Service = new AgentService() - { - Service = serviceName, - Address = "localhost", - Port = downstreamServicePort, - ID = $"web_90_0_2_224_{downstreamServicePort}", - Tags = new[] { "version-v1" } - }, - }; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/home", - DownstreamScheme = "http", - UpstreamPathTemplate = "/home", - UpstreamHttpMethod = new List { "Get", "Options" }, - ServiceName = serviceName, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = consulPort, - Type = "PollConsul", - PollingInterval = 0, - Namespace = string.Empty - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithConsul()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; + +using Ocelot.Configuration.File; + +using Consul; + +using Microsoft.AspNetCore.Http; + +using Newtonsoft.Json; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class ServiceDiscoveryTests : IDisposable + { + private readonly Steps _steps; + private readonly List _consulServices; + private int _counterOne; + private int _counterTwo; + private static readonly object SyncLock = new(); + private string _downstreamPath; + private string _receivedToken; + private readonly ServiceHandler _serviceHandler; + + public ServiceDiscoveryTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + _consulServices = new List(); + } + + [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 serviceName = "product"; + var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; + var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort1, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + var serviceEntryTwo = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort2, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) + .BDDfy(); + } + + [Fact] + public void should_handle_request_to_consul_for_downstream_service_and_make_request() + { + var consulPort = RandomPortFinder.GetRandomPort(); + var servicePort = RandomPortFinder.GetRandomPort(); + const string serviceName = "web"; + var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort, + ID = "web_90_0_2_224_8080", + Tags = new[] { "version-v1" }, + }, + }; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/home", + DownstreamScheme = "http", + UpstreamPathTemplate = "/home", + UpstreamHttpMethod = new List { "Get", "Options" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/home")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes() + { + var consulPort = RandomPortFinder.GetRandomPort(); + const string serviceName = "web"; + var downstreamServicePort = RandomPortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = downstreamServicePort, + ID = "web_90_0_2_224_8080", + Tags = new[] { "version-v1" }, + }, + }; + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + DownstreamScheme = "http", + HttpHandlerOptions = new FileHttpHandlerOptions + { + AllowAutoRedirect = true, + UseCookieContainer = true, + UseTracing = false, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/something", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/web/something")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_use_consul_service_discovery_and_load_balance_request_no_re_routes() + { + var consulPort = RandomPortFinder.GetRandomPort(); + var serviceName = "product"; + var serviceOnePort = RandomPortFinder.GetRandomPort(); + var serviceTwoPort = RandomPortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{serviceOnePort}"; + var downstreamServiceTwoUrl = $"http://localhost:{serviceTwoPort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = serviceOnePort, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + var serviceEntryTwo = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = serviceTwoPort, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + DownstreamScheme = "http", + }, + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes($"/{serviceName}/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(24, 26)) + .BDDfy(); + } + + [Fact] + public void should_use_token_to_make_request_to_consul() + { + var token = "abctoken"; + var consulPort = RandomPortFinder.GetRandomPort(); + var serviceName = "web"; + var servicePort = RandomPortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort, + ID = "web_90_0_2_224_8080", + Tags = new[] { "version-v1" }, + }, + }; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/home", + DownstreamScheme = "http", + UpstreamPathTemplate = "/home", + UpstreamHttpMethod = new List { "Get", "Options" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + Token = token, + }, + }, + }; + + this.Given(_ => GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(_ => _steps.GivenThereIsAConfiguration(configuration)) + .And(_ => _steps.GivenOcelotIsRunningWithConsul()) + .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/home")) + .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(_ => ThenTheTokenIs(token)) + .BDDfy(); + } + + [Fact] + public void should_send_request_to_service_after_it_becomes_available_in_consul() + { + var consulPort = RandomPortFinder.GetRandomPort(); + var serviceName = "product"; + var servicePort1 = RandomPortFinder.GetRandomPort(); + var servicePort2 = RandomPortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; + var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort1, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + var serviceEntryTwo = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = servicePort2, + ID = Guid.NewGuid().ToString(), + Tags = Array.Empty(), + }, + }; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .And(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) + .And(x => WhenIRemoveAService(serviceEntryTwo)) + .And(x => GivenIResetCounters()) + .And(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .And(x => ThenOnlyOneServiceHasBeenCalled()) + .And(x => WhenIAddAServiceBackIn(serviceEntryTwo)) + .And(x => GivenIResetCounters()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(10)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes(4, 6)) + .BDDfy(); + } + + [Fact] + public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request() + { + var consulPort = RandomPortFinder.GetRandomPort(); + const string serviceName = "web"; + var downstreamServicePort = RandomPortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry + { + Service = new AgentService + { + Service = serviceName, + Address = "localhost", + Port = downstreamServicePort, + ID = $"web_90_0_2_224_{downstreamServicePort}", + Tags = new[] { "version-v1" }, + }, + }; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/home", + DownstreamScheme = "http", + UpstreamPathTemplate = "/home", + UpstreamHttpMethod = new List { "Get", "Options" }, + ServiceName = serviceName, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "LeastConnection" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "http", + Host = "localhost", + Port = consulPort, + Type = "PollConsul", + PollingInterval = 0, + Namespace = string.Empty, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithConsul()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); } - private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo) + private void ThenTheTokenIs(string token) { - _consulServices.Add(serviceEntryTwo); - } - - private void ThenOnlyOneServiceHasBeenCalled() - { - _counterOne.ShouldBe(10); - _counterTwo.ShouldBe(0); - } - - private void WhenIRemoveAService(ServiceEntry serviceEntryTwo) - { - _consulServices.Remove(serviceEntryTwo); - } - - private void GivenIResetCounters() - { - _counterOne = 0; - _counterTwo = 0; - } - - private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) - { - _counterOne.ShouldBeInRange(bottom, top); - _counterOne.ShouldBeInRange(bottom, top); - } - - private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) - { - var total = _counterOne + _counterTwo; - total.ShouldBe(expected); - } - - private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) - { - foreach (var serviceEntry in serviceEntries) - { - _consulServices.Add(serviceEntry); - } - } - - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") - { - if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) - { - _receivedToken = values.First(); - } - var json = JsonConvert.SerializeObject(_consulServices); - context.Response.Headers.Add("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - } - - private void GivenProductServiceOneIsRunning(string url, int statusCode) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - try - { - string response; - lock (SyncLock) - { - _counterOne++; - response = _counterOne.ToString(); - } - - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(response); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } - - private void GivenProductServiceTwoIsRunning(string url, int statusCode) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - try - { - string response; - lock (SyncLock) - { - _counterTwo++; - response = _counterTwo.ToString(); - } - - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(response); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPath != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } -} + _receivedToken.ShouldBe(token); + } + + private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo) + { + _consulServices.Add(serviceEntryTwo); + } + + private void ThenOnlyOneServiceHasBeenCalled() + { + _counterOne.ShouldBe(10); + _counterTwo.ShouldBe(0); + } + + private void WhenIRemoveAService(ServiceEntry serviceEntryTwo) + { + _consulServices.Remove(serviceEntryTwo); + } + + private void GivenIResetCounters() + { + _counterOne = 0; + _counterTwo = 0; + } + + private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) + { + _counterOne.ShouldBeInRange(bottom, top); + _counterOne.ShouldBeInRange(bottom, top); + } + + private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) + { + var total = _counterOne + _counterTwo; + total.ShouldBe(expected); + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach (var serviceEntry in serviceEntries) + { + _consulServices.Add(serviceEntry); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") + { + if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) + { + _receivedToken = values.First(); + } + + var json = JsonConvert.SerializeObject(_consulServices); + context.Response.Headers.Add("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + } + + private void GivenProductServiceOneIsRunning(string url, int statusCode) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + string response; + lock (SyncLock) + { + _counterOne++; + response = _counterOne.ToString(); + } + + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } + + private void GivenProductServiceTwoIsRunning(string url, int statusCode) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + string response; + lock (SyncLock) + { + _counterTwo++; + response = _counterTwo.ToString(); + } + + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs b/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs index 57dd38553..df1681817 100644 --- a/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs @@ -1,200 +1,212 @@ -namespace Ocelot.AcceptanceTests -{ - using Microsoft.AspNetCore.Http; - using Ocelot.Configuration.File; - using System; - using System.Collections.Generic; - using System.Net; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Net; - public class ServiceFabricTests : IDisposable - { - private readonly Steps _steps; - private string _downstreamPath; - private readonly ServiceHandler _serviceHandler; +using Ocelot.Configuration.File; - public ServiceFabricTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } +using Microsoft.AspNetCore.Http; - [Fact] - public void should_fix_issue_555() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/{everything}", - DownstreamScheme = "http", - UpstreamPathTemplate = "/{everything}", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "OcelotServiceApplication/OcelotApplicationService" - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = 19081, - Type = "ServiceFabric" - } - } - }; +using TestStack.BDDfy; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:19081", "/OcelotServiceApplication/OcelotApplicationService/a", 200, "Hello from Laura", "b=c")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/a?b=c")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_support_service_fabric_naming_and_dns_service_stateless_and_guest() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/values", - DownstreamScheme = "http", - UpstreamPathTemplate = "/EquipmentInterfaces", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "OcelotServiceApplication/OcelotApplicationService" - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = 19081, - Type = "ServiceFabric" - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:19081", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "test=best")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?test=best")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_support_service_fabric_naming_and_dns_service_statefull_and_actors() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/values", - DownstreamScheme = "http", - UpstreamPathTemplate = "/EquipmentInterfaces", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "OcelotServiceApplication/OcelotApplicationService" - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = 19081, - Type = "ServiceFabric" - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:19081", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "PartitionKind=test&PartitionKey=1")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?PartitionKind=test&PartitionKey=1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_support_placeholder_in_service_fabric_service_name() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/values", - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/{version}/values", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "Service_{version}/Api" - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = 19081, - Type = "ServiceFabric" - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:19081", "/Service_1.0/Api/values", 200, "Hello from Laura", "test=best")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/1.0/values?test=best")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody, string expectedQueryString) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPath != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - if (context.Request.QueryString.Value.Contains(expectedQueryString)) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - } - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } -} +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class ServiceFabricTests : IDisposable + { + private readonly Steps _steps; + private string _downstreamPath; + private readonly ServiceHandler _serviceHandler; + + public ServiceFabricTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_fix_issue_555() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/{everything}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/{everything}", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "OcelotServiceApplication/OcelotApplicationService", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = port, + Type = "ServiceFabric", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/a", 200, "Hello from Laura", "b=c")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/a?b=c")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_support_service_fabric_naming_and_dns_service_stateless_and_guest() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + UpstreamPathTemplate = "/EquipmentInterfaces", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "OcelotServiceApplication/OcelotApplicationService", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = port, + Type = "ServiceFabric", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "test=best")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?test=best")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_support_service_fabric_naming_and_dns_service_statefull_and_actors() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + UpstreamPathTemplate = "/EquipmentInterfaces", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "OcelotServiceApplication/OcelotApplicationService", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = port, + Type = "ServiceFabric", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "PartitionKind=test&PartitionKey=1")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?PartitionKind=test&PartitionKey=1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_support_placeholder_in_service_fabric_service_name() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/values", + DownstreamScheme = "http", + UpstreamPathTemplate = "/api/{version}/values", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "Service_{version}/Api", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = port, + Type = "ServiceFabric", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/Service_1.0/Api/values", 200, "Hello from Laura", "test=best")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/1.0/values?test=best")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody, string expectedQueryString) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + if (context.Request.QueryString.Value.Contains(expectedQueryString)) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + } + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ServiceHandler.cs b/test/Ocelot.AcceptanceTests/ServiceHandler.cs index c10c122b1..e86550350 100644 --- a/test/Ocelot.AcceptanceTests/ServiceHandler.cs +++ b/test/Ocelot.AcceptanceTests/ServiceHandler.cs @@ -1,15 +1,18 @@ -namespace Ocelot.AcceptanceTests -{ - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.Logging; - using System; - using System.IO; - using System.Net; - using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Net; +using System.Security.Authentication; +using System.Threading.Tasks; +namespace Ocelot.AcceptanceTests +{ public class ServiceHandler : IDisposable { private IWebHost _builder; @@ -47,6 +50,58 @@ public void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, Reque _builder.Start(); } + public void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, RequestDelegate del, int port, HttpProtocols protocols) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .ConfigureKestrel(serverOptions => + { + serverOptions.Listen(IPAddress.Loopback, port, listenOptions => + { + listenOptions.Protocols = protocols; + }); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(del); + }) + .Build(); + + _builder.Start(); + } + + public void GivenThereIsAServiceRunningOnUsingHttps(string baseUrl, string basePath, RequestDelegate del, int port, HttpProtocols protocols) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .ConfigureKestrel(serverOptions => + { + serverOptions.Listen(IPAddress.Loopback, port, listenOptions => + { + listenOptions.UseHttps("mycert.pfx", "password", options => + { + options.SslProtocols = SslProtocols.Tls12; + }); + listenOptions.Protocols = protocols; + }); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(del); + }) + .Build(); + + _builder.Start(); + } + public void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string fileName, string password, int port, RequestDelegate del) { _builder = new WebHostBuilder() @@ -69,7 +124,7 @@ public void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, strin _builder.Start(); } - public async Task StartFakeDownstreamService(string url, string path, Func, Task> middleware) + public async Task StartFakeDownstreamService(string url, Func, Task> middleware) { _builder = new WebHostBuilder() .ConfigureServices(s => { }).UseKestrel() @@ -102,6 +157,7 @@ public async Task StartFakeDownstreamService(string url, string path, Func + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "https", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", Port = port, - } + }, }, UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, - DangerousAcceptAnyServerCertificateValidator = true - } - } + DangerousAcceptAnyServerCertificateValidator = true, + }, + }, }; this.Given(x => x.GivenThereIsAServiceRunningOn($"https://localhost:{port}", "/", 200, "Hello from Laura", port)) @@ -60,42 +64,42 @@ public void should_dangerous_accept_any_server_certificate_validator() [Fact] public void should_not_dangerous_accept_any_server_certificate_validator() { - int port = 52129; + var port = RandomPortFinder.GetRandomPort(); var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "https", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", Port = port, - } + }, }, UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, - DangerousAcceptAnyServerCertificateValidator = false - } - } + DangerousAcceptAnyServerCertificateValidator = false, + }, + }, }; this.Given(x => x.GivenThereIsAServiceRunningOn($"https://localhost:{port}", "/", 200, "Hello from Laura", port)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) .BDDfy(); } private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody, int port) { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, "idsrv3test.pfx", "idsrv3test", port, async context => + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, "mycert.pfx", "password", port, async context => { _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; @@ -118,4 +122,4 @@ public void Dispose() _steps.Dispose(); } } -} +} diff --git a/test/Ocelot.AcceptanceTests/StartupTests.cs b/test/Ocelot.AcceptanceTests/StartupTests.cs index 3bab3f57c..dc8a2bf30 100644 --- a/test/Ocelot.AcceptanceTests/StartupTests.cs +++ b/test/Ocelot.AcceptanceTests/StartupTests.cs @@ -1,100 +1,106 @@ -namespace Ocelot.AcceptanceTests -{ - using Configuration.Repository; - using Microsoft.AspNetCore.Http; - using Ocelot.Configuration.File; - using Responses; - using System; - using System.Collections.Generic; - using System.IO; - using System.Net; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; - public class StartupTests : IDisposable - { - private readonly Steps _steps; - private readonly ServiceHandler _serviceHandler; - private string _downstreamPath; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Repository; - public StartupTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } +using Microsoft.AspNetCore.Http; - [Fact] - public void should_not_try_and_write_to_disk_on_startup_when_not_using_admin_api() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 52179, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; +using Ocelot.Responses; - var fakeRepo = new FakeFileConfigurationRepository(); +using TestStack.BDDfy; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:52179", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithBlowingUpDiskRepo(fakeRepo)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPath != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - - private class FakeFileConfigurationRepository : IFileConfigurationRepository - { - public Task> Get() - { - throw new NotImplementedException(); - } - - public Task Set(FileConfiguration fileConfiguration) - { - throw new NotImplementedException(); - } - } - } -} +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class StartupTests : IDisposable + { + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + private string _downstreamPath; + + public StartupTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_not_try_and_write_to_disk_on_startup_when_not_using_admin_api() + { + 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" }, + }, + }, + }; + + var fakeRepo = new FakeFileConfigurationRepository(); + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithBlowingUpDiskRepo(fakeRepo)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + + private class FakeFileConfigurationRepository : IFileConfigurationRepository + { + public Task> Get() + { + throw new NotImplementedException(); + } + + public Task Set(FileConfiguration fileConfiguration) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 9bfb20916..4f9a34d1f 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -1,47 +1,70 @@ -namespace Ocelot.AcceptanceTests -{ - using Caching; - using Configuration.Repository; - using global::CacheManager.Core; - using IdentityServer4.AccessTokenValidation; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.TestHost; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Logging; - using Moq; - using Newtonsoft.Json; - using Ocelot.Cache.CacheManager; - using Ocelot.Configuration.Creator; - using Ocelot.Configuration.File; - using Ocelot.DependencyInjection; - using Ocelot.Infrastructure; - using Ocelot.Logging; - using Ocelot.Middleware; - using Ocelot.Middleware.Multiplexer; - using Ocelot.Provider.Consul; - using Ocelot.Provider.Eureka; - using Ocelot.Provider.Polly; - using Ocelot.Tracing.Butterfly; - using Requester; - using Shouldly; - using System; - using System.Collections.Generic; - using System.IO; - using System.IO.Compression; - using System.Linq; - using System.Net; - using System.Net.Http; - using System.Net.Http.Headers; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests; - using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; - using CookieHeaderValue = Microsoft.Net.Http.Headers.CookieHeaderValue; - using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue; +using Ocelot.Configuration.ChangeTracking; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using CacheManager.Core; + +using Ocelot.AcceptanceTests.Caching; + +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Repository; + +using Ocelot.DependencyInjection; + +using IdentityServer4.AccessTokenValidation; + +using Ocelot.LoadBalancer.LoadBalancers; + +using Ocelot.Logging; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using Ocelot.Middleware; + +using Moq; + +using Ocelot.Multiplexer; +using Newtonsoft.Json; + +using Ocelot.Cache.CacheManager; +using Ocelot.Provider.Consul; +using Ocelot.Provider.Polly; +using Ocelot.Tracing.Butterfly; +using Ocelot.Tracing.OpenTracing; + +using Ocelot.Provider.Eureka; + +using Ocelot.Requester; + +using Ocelot.ServiceDiscovery.Providers; + +using Shouldly; + +using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests; + +using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; +using CookieHeaderValue = Microsoft.Net.Http.Headers.CookieHeaderValue; +using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue; + +namespace Ocelot.AcceptanceTests +{ public class Steps : IDisposable { private TestServer _ocelotServer; @@ -54,6 +77,7 @@ public class Steps : IDisposable private IWebHostBuilder _webHostBuilder; private WebHostBuilder _ocelotBuilder; private IWebHost _ocelotHost; + private IOcelotConfigurationChangeTokenSource _changeToken; public Steps() { @@ -216,6 +240,11 @@ public void GivenOcelotIsRunningReloadingConfig(bool shouldReload) _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. /// @@ -247,6 +276,41 @@ public void GivenOcelotIsRunning() _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. + /// + /// The type. + /// The delegate object to load balancer factory. + public void GivenOcelotIsRunningWithCustomLoadBalancer(Func loadBalancerFactoryFunc) + where T : ILoadBalancer + { + _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("ocelot.json", false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot() + .AddCustomLoadBalancer(loadBalancerFactoryFunc); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + public void GivenOcelotIsRunningWithConsul() { _webHostBuilder = new WebHostBuilder(); @@ -384,6 +448,7 @@ public void GivenOcelotIsRunningUsingConsulToStoreConfig() _ocelotServer = new TestServer(_webHostBuilder); _ocelotClient = _ocelotServer.CreateClient(); + Thread.Sleep(1000); } public void WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk(string url) @@ -458,7 +523,7 @@ public void GivenOcelotIsRunningWithFakeHttpClientCache(IHttpClientCache cache) }) .ConfigureServices(s => { - s.AddSingleton(cache); + s.AddSingleton(cache); s.AddOcelot(); }) .Configure(app => @@ -653,7 +718,7 @@ public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi(FakeDepen .ConfigureServices(s => { s.AddSingleton(_webHostBuilder); - s.AddSingleton(dependency); + s.AddSingleton(dependency); s.AddOcelot() .AddDelegatingHandler(true); }) @@ -763,12 +828,12 @@ public void GivenIHaveAToken(string url) var tokenUrl = $"{url}/connect/token"; var formData = new List> { - new KeyValuePair("client_id", "client"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "api"), - new KeyValuePair("username", "test"), - new KeyValuePair("password", "test"), - new KeyValuePair("grant_type", "password") + 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); @@ -786,12 +851,12 @@ public void GivenIHaveATokenForApiReadOnlyScope(string url) var tokenUrl = $"{url}/connect/token"; var formData = new List> { - new KeyValuePair("client_id", "client"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "api.readOnly"), - new KeyValuePair("username", "test"), - new KeyValuePair("password", "test"), - new KeyValuePair("grant_type", "password") + 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); @@ -809,12 +874,12 @@ public void GivenIHaveATokenForApi2(string url) var tokenUrl = $"{url}/connect/token"; var formData = new List> { - new KeyValuePair("client_id", "client"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "api2"), - new KeyValuePair("username", "test"), - new KeyValuePair("password", "test"), - new KeyValuePair("grant_type", "password") + 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); @@ -901,6 +966,18 @@ public void WhenIGetUrlOnTheApiGateway(string url) _response = _ocelotClient.GetAsync(url).Result; } + public void WhenIGetUrlOnTheApiGateway(string url, HttpContent content) + { + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url) { Content = content }; + _response = _ocelotClient.SendAsync(httpRequestMessage).Result; + } + + public void WhenIPostUrlOnTheApiGateway(string url, HttpContent content) + { + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, url) { Content = content }; + _response = _ocelotClient.SendAsync(httpRequestMessage).Result; + } + public void WhenIGetUrlOnTheApiGateway(string url, string cookie, string value) { var request = _ocelotServer.CreateRequest(url); @@ -918,7 +995,7 @@ public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) { var tasks = new Task[times]; - for (int i = 0; i < times; i++) + for (var i = 0; i < times; i++) { var urlCopy = url; tasks[i] = GetForServiceDiscoveryTest(urlCopy); @@ -932,7 +1009,7 @@ public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times, strin { var tasks = new Task[times]; - for (int i = 0; i < times; i++) + for (var i = 0; i < times; i++) { var urlCopy = url; tasks[i] = GetForServiceDiscoveryTest(urlCopy, cookie, value); @@ -948,7 +1025,7 @@ private async Task GetForServiceDiscoveryTest(string url, string cookie, string request.And(x => { x.Headers.Add("Cookie", new CookieHeaderValue(cookie, value).ToString()); }); var response = await request.GetAsync(); var content = await response.Content.ReadAsStringAsync(); - int count = int.Parse(content); + var count = int.Parse(content); count.ShouldBeGreaterThan(0); } @@ -956,13 +1033,13 @@ private async Task GetForServiceDiscoveryTest(string url) { var response = await _ocelotClient.GetAsync(url); var content = await response.Content.ReadAsStringAsync(); - int count = int.Parse(content); + var count = int.Parse(content); count.ShouldBeGreaterThan(0); } public void WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(string url, int times) { - for (int i = 0; i < times; i++) + for (var i = 0; i < times; i++) { var clientId = "ocelotclient1"; var request = new HttpRequestMessage(new HttpMethod("GET"), url); @@ -1050,7 +1127,7 @@ public void ThenTheRequestIdIsReturned(string expected) public void WhenIMakeLotsOfDifferentRequestsToTheApiGateway() { - int numberOfRequests = 100; + var numberOfRequests = 100; var aggregateUrl = "/"; var aggregateExpected = "{\"Laura\":{Hello from Laura},\"Tom\":{Hello from Tom}}"; var tomUrl = "/tom"; @@ -1061,21 +1138,21 @@ public void WhenIMakeLotsOfDifferentRequestsToTheApiGateway() var aggregateTasks = new Task[numberOfRequests]; - for (int i = 0; i < numberOfRequests; i++) + for (var i = 0; i < numberOfRequests; i++) { aggregateTasks[i] = Fire(aggregateUrl, aggregateExpected, random); } var tomTasks = new Task[numberOfRequests]; - for (int i = 0; i < numberOfRequests; i++) + for (var i = 0; i < numberOfRequests; i++) { tomTasks[i] = Fire(tomUrl, tomExpected, random); } var lauraTasks = new Task[numberOfRequests]; - for (int i = 0; i < numberOfRequests; i++) + for (var i = 0; i < numberOfRequests; i++) { lauraTasks[i] = Fire(lauraUrl, lauraExpected, random); } @@ -1110,7 +1187,7 @@ public void GivenOcelotIsRunningWithBlowingUpDiskRepo(IFileConfigurationReposito }) .ConfigureServices(s => { - s.AddSingleton(fake); + s.AddSingleton(fake); s.AddOcelot(); }) .Configure(app => @@ -1123,6 +1200,11 @@ public void GivenOcelotIsRunningWithBlowingUpDiskRepo(IFileConfigurationReposito _ocelotClient = _ocelotServer.CreateClient(); } + public void TheChangeTokenShouldBeActive(bool itShouldBeActive) + { + _changeToken.ChangeToken.HasChanged.ShouldBe(itShouldBeActive); + } + public void GivenOcelotIsRunningWithLogger() { _webHostBuilder = new WebHostBuilder(); @@ -1152,9 +1234,44 @@ public void GivenOcelotIsRunningWithLogger() _ocelotClient = _ocelotServer.CreateClient(); } + 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", optional: true, reloadOnChange: false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); + config.AddJsonFile("ocelot.json", 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(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + public void ThenWarningShouldBeLogged() { - MockLoggerFactory loggerFactory = (MockLoggerFactory)_ocelotServer.Host.Services.GetService(); + var loggerFactory = (MockLoggerFactory)_ocelotServer.Host.Services.GetService(); loggerFactory.Verify(); } @@ -1169,6 +1286,7 @@ public IOcelotLogger CreateLogger() _logger = new Mock(); _logger.Setup(x => x.LogWarning(It.IsAny())).Verifiable(); } + return _logger.Object; } diff --git a/test/Ocelot.AcceptanceTests/StickySessionsTests.cs b/test/Ocelot.AcceptanceTests/StickySessionsTests.cs index 77f8b176b..cefa17d76 100644 --- a/test/Ocelot.AcceptanceTests/StickySessionsTests.cs +++ b/test/Ocelot.AcceptanceTests/StickySessionsTests.cs @@ -1,293 +1,299 @@ -namespace Ocelot.AcceptanceTests -{ - using Microsoft.AspNetCore.Http; - using Ocelot.Configuration.File; - using Shouldly; - using System; - using System.Collections.Generic; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; - public class StickySessionsTests : IDisposable - { - private readonly Steps _steps; - private int _counterOne; - private int _counterTwo; - private static readonly object SyncLock = new object(); - private readonly ServiceHandler _serviceHandler; +using Ocelot.Configuration.File; - public StickySessionsTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } +using Microsoft.AspNetCore.Http; - [Fact] - public void should_use_same_downstream_host() - { - var downstreamPortOne = 51375; - var downstreamPortTwo = 51892; - var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; - var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; +using Shouldly; - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - LoadBalancerOptions = new FileLoadBalancerOptions - { - Type = "CookieStickySessions", - Key = "sessionid", - Expiry = 300000 - }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = downstreamPortOne - }, - new FileHostAndPort - { - Host = "localhost", - Port = downstreamPortTwo - } - } - } - } - }; +using TestStack.BDDfy; - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10, "sessionid", "123")) - .Then(x => x.ThenTheFirstServiceIsCalled(10)) - .Then(x => x.ThenTheSecondServiceIsCalled(0)) - .BDDfy(); - } - - [Fact] - public void should_use_different_downstream_host_for_different_re_route() - { - var downstreamPortOne = 52881; - var downstreamPortTwo = 52892; - var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; - var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - LoadBalancerOptions = new FileLoadBalancerOptions - { - Type = "CookieStickySessions", - Key = "sessionid", - Expiry = 300000 - }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = downstreamPortOne - }, - new FileHostAndPort - { - Host = "localhost", - Port = downstreamPortTwo - } - } - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/test", - UpstreamHttpMethod = new List { "Get" }, - LoadBalancerOptions = new FileLoadBalancerOptions - { - Type = "CookieStickySessions", - Key = "bestid", - Expiry = 300000 - }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = downstreamPortTwo - }, - new FileHostAndPort - { - Host = "localhost", - Port = downstreamPortOne - } - } - } - } - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/", "sessionid", "123")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/test", "bestid", "123")) - .Then(x => x.ThenTheFirstServiceIsCalled(1)) - .Then(x => x.ThenTheSecondServiceIsCalled(1)) - .BDDfy(); - } - - [Fact] - public void should_use_same_downstream_host_for_different_re_route() - { - var downstreamPortOne = 53881; - var downstreamPortTwo = 53892; - var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; - var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - LoadBalancerOptions = new FileLoadBalancerOptions - { - Type = "CookieStickySessions", - Key = "sessionid", - Expiry = 300000 - }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = downstreamPortOne - }, - new FileHostAndPort - { - Host = "localhost", - Port = downstreamPortTwo - } - } - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/test", - UpstreamHttpMethod = new List { "Get" }, - LoadBalancerOptions = new FileLoadBalancerOptions - { - Type = "CookieStickySessions", - Key = "sessionid", - Expiry = 300000 - }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = downstreamPortTwo - }, - new FileHostAndPort - { - Host = "localhost", - Port = downstreamPortOne - } - } - } - } - }; - - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/", "sessionid", "123")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/test", "sessionid", "123")) - .Then(x => x.ThenTheFirstServiceIsCalled(2)) - .Then(x => x.ThenTheSecondServiceIsCalled(0)) - .BDDfy(); - } - - private void ThenTheFirstServiceIsCalled(int expected) - { - _counterOne.ShouldBe(expected); - } - - private void ThenTheSecondServiceIsCalled(int expected) - { - _counterTwo.ShouldBe(expected); - } - - private void GivenProductServiceOneIsRunning(string url, int statusCode) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - try - { - var response = string.Empty; - lock (SyncLock) - { - _counterOne++; - response = _counterOne.ToString(); - } - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(response); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } - - private void GivenProductServiceTwoIsRunning(string url, int statusCode) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - try - { - var response = string.Empty; - lock (SyncLock) - { - _counterTwo++; - response = _counterTwo.ToString(); - } - - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(response); - } - catch (Exception exception) - { - await context.Response.WriteAsync(exception.StackTrace); - } - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } -} +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class StickySessionsTests : IDisposable + { + private readonly Steps _steps; + private int _counterOne; + private int _counterTwo; + private static readonly object SyncLock = new(); + private readonly ServiceHandler _serviceHandler; + + public StickySessionsTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_use_same_downstream_host() + { + var downstreamPortOne = RandomPortFinder.GetRandomPort(); + var downstreamPortTwo = RandomPortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; + var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + LoadBalancerOptions = new FileLoadBalancerOptions + { + Type = "CookieStickySessions", + Key = "sessionid", + Expiry = 300000, + }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = downstreamPortOne, + }, + new() + { + Host = "localhost", + Port = downstreamPortTwo, + }, + }, + }, + }, + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10, "sessionid", "123")) + .Then(x => x.ThenTheFirstServiceIsCalled(10)) + .Then(x => x.ThenTheSecondServiceIsCalled(0)) + .BDDfy(); + } + + [Fact] + public void should_use_different_downstream_host_for_different_re_route() + { + var downstreamPortOne = RandomPortFinder.GetRandomPort(); + var downstreamPortTwo = RandomPortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; + var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + LoadBalancerOptions = new FileLoadBalancerOptions + { + Type = "CookieStickySessions", + Key = "sessionid", + Expiry = 300000, + }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = downstreamPortOne, + }, + new() + { + Host = "localhost", + Port = downstreamPortTwo, + }, + }, + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/test", + UpstreamHttpMethod = new List { "Get" }, + LoadBalancerOptions = new FileLoadBalancerOptions + { + Type = "CookieStickySessions", + Key = "bestid", + Expiry = 300000, + }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = downstreamPortTwo, + }, + new() + { + Host = "localhost", + Port = downstreamPortOne, + }, + }, + }, + }, + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/", "sessionid", "123")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/test", "bestid", "123")) + .Then(x => x.ThenTheFirstServiceIsCalled(1)) + .Then(x => x.ThenTheSecondServiceIsCalled(1)) + .BDDfy(); + } + + [Fact] + public void should_use_same_downstream_host_for_different_re_route() + { + var downstreamPortOne = RandomPortFinder.GetRandomPort(); + var downstreamPortTwo = RandomPortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; + var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + LoadBalancerOptions = new FileLoadBalancerOptions + { + Type = "CookieStickySessions", + Key = "sessionid", + Expiry = 300000, + }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = downstreamPortOne, + }, + new() + { + Host = "localhost", + Port = downstreamPortTwo, + }, + }, + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/test", + UpstreamHttpMethod = new List { "Get" }, + LoadBalancerOptions = new FileLoadBalancerOptions + { + Type = "CookieStickySessions", + Key = "sessionid", + Expiry = 300000, + }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = downstreamPortTwo, + }, + new() + { + Host = "localhost", + Port = downstreamPortOne, + }, + }, + }, + }, + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/", "sessionid", "123")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/test", "sessionid", "123")) + .Then(x => x.ThenTheFirstServiceIsCalled(2)) + .Then(x => x.ThenTheSecondServiceIsCalled(0)) + .BDDfy(); + } + + private void ThenTheFirstServiceIsCalled(int expected) + { + _counterOne.ShouldBe(expected); + } + + private void ThenTheSecondServiceIsCalled(int expected) + { + _counterTwo.ShouldBe(expected); + } + + private void GivenProductServiceOneIsRunning(string url, int statusCode) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + string response; + lock (SyncLock) + { + _counterOne++; + response = _counterOne.ToString(); + } + + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } + + private void GivenProductServiceTwoIsRunning(string url, int statusCode) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + try + { + string response; + lock (SyncLock) + { + _counterTwo++; + response = _counterTwo.ToString(); + } + + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs b/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs index 284ebd6f2..24aef192a 100644 --- a/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs +++ b/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs @@ -1,154 +1,163 @@ -namespace Ocelot.AcceptanceTests -{ - using Configuration.File; - using Consul; - using Microsoft.AspNetCore.Http; - using Newtonsoft.Json; - using System; - using System.Collections.Generic; - using System.Net; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Net; - public class TwoDownstreamServicesTests : IDisposable - { - private readonly Steps _steps; - private readonly List _serviceEntries; - private string _downstreamPathOne; - private string _downstreamPathTwo; - private readonly ServiceHandler _serviceHandler; +using Ocelot.Configuration.File; - public TwoDownstreamServicesTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - _serviceEntries = new List(); - } +using Consul; - [Fact] - public void should_fix_issue_194() - { - var consulPort = 8503; - var downstreamServiceOneUrl = "http://localhost:8362"; - var downstreamServiceTwoUrl = "http://localhost:8330"; - var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; +using Microsoft.AspNetCore.Http; - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/user/{user}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 8362, - } - }, - UpstreamPathTemplate = "/api/user/{user}", - UpstreamHttpMethod = new List { "Get" }, - }, - new FileReRoute - { - DownstreamPathTemplate = "/api/product/{product}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 8330, - } - }, - UpstreamPathTemplate = "/api/product/{product}", - UpstreamHttpMethod = new List { "Get" }, - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() - { - Host = "localhost", - Port = consulPort - } - } - }; +using Newtonsoft.Json; - this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, "/api/user/info", 200, "user")) - .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, "/api/product/info", 200, "product")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/user/info?id=1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("user")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/product/info?id=1")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("product")) - .BDDfy(); - } +using TestStack.BDDfy; - private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - if (context.Request.Path.Value == "/v1/health/service/product") - { - var json = JsonConvert.SerializeObject(_serviceEntries); - context.Response.Headers.Add("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - } - - private void GivenProductServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPathOne = !string.IsNullOrEmpty(context.Request.PathBase.Value) - ? context.Request.PathBase.Value - : context.Request.Path.Value; - - if (_downstreamPathOne != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); - } - - private void GivenProductServiceTwoIsRunning(string baseUrl, string basePath, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPathTwo = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPathTwo != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } -} +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class TwoDownstreamServicesTests : IDisposable + { + private readonly Steps _steps; + private readonly List _serviceEntries; + private string _downstreamPathOne; + private string _downstreamPathTwo; + private readonly ServiceHandler _serviceHandler; + + public TwoDownstreamServicesTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + _serviceEntries = new List(); + } + + [Fact] + public void should_fix_issue_194() + { + var consulPort = RandomPortFinder.GetRandomPort(); + var servicePort1 = RandomPortFinder.GetRandomPort(); + var servicePort2 = RandomPortFinder.GetRandomPort(); + var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; + var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/user/{user}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort1, + }, + }, + UpstreamPathTemplate = "/api/user/{user}", + UpstreamHttpMethod = new List { "Get" }, + }, + new() + { + DownstreamPathTemplate = "/api/product/{product}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = servicePort2, + }, + }, + UpstreamPathTemplate = "/api/product/{product}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "https", + Host = "localhost", + Port = consulPort, + }, + }, + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, "/api/user/info", 200, "user")) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, "/api/product/info", 200, "product")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/user/info?id=1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("user")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/product/info?id=1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("product")) + .BDDfy(); + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + if (context.Request.Path.Value == "/v1/health/service/product") + { + var json = JsonConvert.SerializeObject(_serviceEntries); + context.Response.Headers.Add("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + } + + private void GivenProductServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPathOne = !string.IsNullOrEmpty(context.Request.PathBase.Value) + ? context.Request.PathBase.Value + : context.Request.Path.Value; + + if (_downstreamPathOne != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + } + + private void GivenProductServiceTwoIsRunning(string baseUrl, string basePath, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPathTwo = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPathTwo != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/UpstreamHostTests.cs b/test/Ocelot.AcceptanceTests/UpstreamHostTests.cs index 4af5a9740..7d3a00c48 100644 --- a/test/Ocelot.AcceptanceTests/UpstreamHostTests.cs +++ b/test/Ocelot.AcceptanceTests/UpstreamHostTests.cs @@ -1,279 +1,283 @@ -namespace Ocelot.AcceptanceTests -{ - using Microsoft.AspNetCore.Http; - using Ocelot.Configuration.File; - using System; - using System.Collections.Generic; - using System.Net; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Net; - public class UpstreamHostTests : IDisposable - { - private readonly Steps _steps; - private string _downstreamPath; - private readonly ServiceHandler _serviceHandler; +using Ocelot.Configuration.File; - public UpstreamHostTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } +using Microsoft.AspNetCore.Http; - [Fact] - public void should_return_response_200_with_simple_url_and_hosts_match() - { - int port = 64905; +using TestStack.BDDfy; - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = port, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - UpstreamHost = "localhost" - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes() - { - int port = 64904; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = port, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - UpstreamHost = "localhost" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 50000, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - UpstreamHost = "DONTMATCH" - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes_reversed() - { - int port = 64903; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 50000, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - UpstreamHost = "DONTMATCH" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = port, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - UpstreamHost = "localhost" - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes_reversed_with_no_host_first() - { - int port = 64902; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 50000, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = port, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - UpstreamHost = "localhost" - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_return_response_404_with_simple_url_and_hosts_dont_match() - { - int port = 64901; - - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = port, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - UpstreamHost = "127.0.0.20:5000" - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; - - if (_downstreamPath != basePath) - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync("downstream path didnt match base path"); - } - else - { - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - } - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } -} +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class UpstreamHostTests : IDisposable + { + private readonly Steps _steps; + private string _downstreamPath; + private readonly ServiceHandler _serviceHandler; + + public UpstreamHostTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_return_response_200_with_simple_url_and_hosts_match() + { + 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" }, + UpstreamHost = "localhost", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes() + { + 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" }, + UpstreamHost = "localhost", + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 50000, + }, + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + UpstreamHost = "DONTMATCH", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes_reversed() + { + var port = RandomPortFinder.GetRandomPort(); + + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 50000, + }, + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + UpstreamHost = "DONTMATCH", + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + UpstreamHost = "localhost", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [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 configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 50000, + }, + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + UpstreamHost = "localhost", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_404_with_simple_url_and_hosts_dont_match() + { + 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" }, + UpstreamHost = "127.0.0.20:5000", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPath != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/WebSocketTests.cs b/test/Ocelot.AcceptanceTests/WebSocketTests.cs index 425416e2c..1e63ba6e6 100644 --- a/test/Ocelot.AcceptanceTests/WebSocketTests.cs +++ b/test/Ocelot.AcceptanceTests/WebSocketTests.cs @@ -1,334 +1,340 @@ -namespace Ocelot.AcceptanceTests -{ - using Ocelot.Configuration.File; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Net.WebSockets; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - - public class WebSocketTests : IDisposable - { - private readonly List _secondRecieved; - private readonly List _firstRecieved; - private readonly Steps _steps; - private readonly ServiceHandler _serviceHandler; - - public WebSocketTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - _firstRecieved = new List(); - _secondRecieved = new List(); - } - - [Fact] - public void should_proxy_websocket_input_to_downstream_service() - { - var downstreamPort = 5001; - var downstreamHost = "localhost"; - - var config = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - UpstreamPathTemplate = "/", - DownstreamPathTemplate = "/ws", - DownstreamScheme = "ws", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = downstreamHost, - Port = downstreamPort - } - } - } - } - }; - - this.Given(_ => _steps.GivenThereIsAConfiguration(config)) - .And(_ => _steps.StartFakeOcelotWithWebSockets()) - .And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws")) - .When(_ => StartClient("ws://localhost:5000/")) - .Then(_ => _firstRecieved.Count.ShouldBe(10)) - .BDDfy(); - } - - [Fact] - public void should_proxy_websocket_input_to_downstream_service_and_use_load_balancer() - { - var downstreamPort = 5005; - var downstreamHost = "localhost"; - var secondDownstreamPort = 5006; - var secondDownstreamHost = "localhost"; - - var config = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - UpstreamPathTemplate = "/", - DownstreamPathTemplate = "/ws", - DownstreamScheme = "ws", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = downstreamHost, - Port = downstreamPort - }, - new FileHostAndPort - { - Host = secondDownstreamHost, - Port = secondDownstreamPort - } - }, - LoadBalancerOptions = new FileLoadBalancerOptions { Type = "RoundRobin" } - } - } - }; - - this.Given(_ => _steps.GivenThereIsAConfiguration(config)) - .And(_ => _steps.StartFakeOcelotWithWebSockets()) - .And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws")) - .And(_ => StartSecondFakeDownstreamService($"http://{secondDownstreamHost}:{secondDownstreamPort}", "/ws")) - .When(_ => WhenIStartTheClients()) - .Then(_ => ThenBothDownstreamServicesAreCalled()) - .BDDfy(); - } - - private void ThenBothDownstreamServicesAreCalled() - { - _firstRecieved.Count.ShouldBe(10); - _firstRecieved.ForEach(x => - { - x.ShouldBe("test"); - }); - - _secondRecieved.Count.ShouldBe(10); - _secondRecieved.ForEach(x => - { - x.ShouldBe("chocolate"); - }); - } - - private async Task WhenIStartTheClients() - { - var firstClient = StartClient("ws://localhost:5000/"); - - var secondClient = StartSecondClient("ws://localhost:5000/"); - - await Task.WhenAll(firstClient, secondClient); - } - - private async Task StartClient(string url) - { - var client = new ClientWebSocket(); - - await client.ConnectAsync(new Uri(url), CancellationToken.None); - - var sending = Task.Run(async () => - { - string line = "test"; - for (int i = 0; i < 10; i++) - { - var bytes = Encoding.UTF8.GetBytes(line); - - await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, - CancellationToken.None); - await Task.Delay(10); - } - - await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); - }); - - var receiving = Task.Run(async () => - { - var buffer = new byte[1024 * 4]; - - while (true) - { - var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - if (result.MessageType == WebSocketMessageType.Text) - { - _firstRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); - } - else if (result.MessageType == WebSocketMessageType.Close) - { - if (client.State != WebSocketState.Closed) - { - // Last version, the client state is CloseReceived - // Valid states are: Open, CloseReceived, CloseSent - await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); - } - - break; - } - } - }); - - await Task.WhenAll(sending, receiving); - } - - private async Task StartSecondClient(string url) - { - await Task.Delay(500); - - var client = new ClientWebSocket(); - - await client.ConnectAsync(new Uri(url), CancellationToken.None); - - var sending = Task.Run(async () => - { - string line = "test"; - for (int i = 0; i < 10; i++) - { - var bytes = Encoding.UTF8.GetBytes(line); - - await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, - CancellationToken.None); - await Task.Delay(10); - } - - await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); - }); - - var receiving = Task.Run(async () => - { - var buffer = new byte[1024 * 4]; - - while (true) - { - var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - if (result.MessageType == WebSocketMessageType.Text) - { - _secondRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); - } - else if (result.MessageType == WebSocketMessageType.Close) - { - if (client.State != WebSocketState.Closed) - { - // Last version, the client state is CloseReceived - // Valid states are: Open, CloseReceived, CloseSent - await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); - } - - break; - } - } - }); - - await Task.WhenAll(sending, receiving); - } - - private async Task StartFakeDownstreamService(string url, string path) - { - await _serviceHandler.StartFakeDownstreamService(url, path, async (context, next) => - { - if (context.Request.Path == path) - { - if (context.WebSockets.IsWebSocketRequest) - { - var webSocket = await context.WebSockets.AcceptWebSocketAsync(); - await Echo(webSocket); - } - else - { - context.Response.StatusCode = 400; - } - } - else - { - await next(); - } - }); - } - - private async Task StartSecondFakeDownstreamService(string url, string path) - { - await _serviceHandler.StartFakeDownstreamService(url, path, async (context, next) => - { - if (context.Request.Path == path) - { - if (context.WebSockets.IsWebSocketRequest) - { - WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync(); - await Message(webSocket); - } - else - { - context.Response.StatusCode = 400; - } - } - else - { - await next(); - } - }); - } - - private async Task Echo(WebSocket webSocket) - { - try - { - var buffer = new byte[1024 * 4]; - - var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - - while (!result.CloseStatus.HasValue) - { - await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); - - result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - } - - await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - - private async Task Message(WebSocket webSocket) - { - try - { - var buffer = new byte[1024 * 4]; - - var bytes = Encoding.UTF8.GetBytes("chocolate"); - - var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - +using Ocelot.Configuration.File; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class WebSocketTests : IDisposable + { + private readonly List _secondRecieved; + private readonly List _firstRecieved; + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public WebSocketTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + _firstRecieved = new List(); + _secondRecieved = new List(); + } + + [Fact] + public void ShouldProxyWebsocketInputToDownstreamService() + { + var downstreamPort = RandomPortFinder.GetRandomPort(); + var downstreamHost = "localhost"; + + var config = new FileConfiguration + { + Routes = new List + { + new() + { + UpstreamPathTemplate = "/", + DownstreamPathTemplate = "/ws", + DownstreamScheme = "ws", + DownstreamHostAndPorts = new List + { + new() + { + Host = downstreamHost, + Port = downstreamPort, + }, + }, + }, + }, + }; + + this.Given(_ => _steps.GivenThereIsAConfiguration(config)) + .And(_ => _steps.StartFakeOcelotWithWebSockets()) + .And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws")) + .When(_ => StartClient("ws://localhost:5000/")) + .Then(_ => ThenTheReceivedCountIs(10)) + .BDDfy(); + } + + [Fact] + public void ShouldProxyWebsocketInputToDownstreamServiceAndUseLoadBalancer() + { + var downstreamPort = RandomPortFinder.GetRandomPort(); + var downstreamHost = "localhost"; + var secondDownstreamPort = RandomPortFinder.GetRandomPort(); + var secondDownstreamHost = "localhost"; + + var config = new FileConfiguration + { + Routes = new List + { + new() + { + UpstreamPathTemplate = "/", + DownstreamPathTemplate = "/ws", + DownstreamScheme = "ws", + DownstreamHostAndPorts = new List + { + new() + { + Host = downstreamHost, + Port = downstreamPort, + }, + new() + { + Host = secondDownstreamHost, + Port = secondDownstreamPort, + }, + }, + LoadBalancerOptions = new FileLoadBalancerOptions { Type = "RoundRobin" }, + }, + }, + }; + + this.Given(_ => _steps.GivenThereIsAConfiguration(config)) + .And(_ => _steps.StartFakeOcelotWithWebSockets()) + .And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws")) + .And(_ => StartSecondFakeDownstreamService($"http://{secondDownstreamHost}:{secondDownstreamPort}", "/ws")) + .When(_ => WhenIStartTheClients()) + .Then(_ => ThenBothDownstreamServicesAreCalled()) + .BDDfy(); + } + + private void ThenBothDownstreamServicesAreCalled() + { + _firstRecieved.Count.ShouldBe(10); + _firstRecieved.ForEach(x => + { + x.ShouldBe("test"); + }); + + _secondRecieved.Count.ShouldBe(10); + _secondRecieved.ForEach(x => + { + x.ShouldBe("chocolate"); + }); + } + + private async Task WhenIStartTheClients() + { + var firstClient = StartClient("ws://localhost:5000/"); + + var secondClient = StartSecondClient("ws://localhost:5000/"); + + await Task.WhenAll(firstClient, secondClient); + } + + private async Task StartClient(string url) + { + var client = new ClientWebSocket(); + + await client.ConnectAsync(new Uri(url), CancellationToken.None); + + var sending = Task.Run(async () => + { + var line = "test"; + for (var i = 0; i < 10; i++) + { + var bytes = Encoding.UTF8.GetBytes(line); + + await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, + CancellationToken.None); + await Task.Delay(10); + } + + await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + }); + + var receiving = Task.Run(async () => + { + var buffer = new byte[1024 * 4]; + + while (true) + { + var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + if (result.MessageType == WebSocketMessageType.Text) + { + _firstRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); + } + else if (result.MessageType == WebSocketMessageType.Close) + { + if (client.State != WebSocketState.Closed) + { + // Last version, the client state is CloseReceived + // Valid states are: Open, CloseReceived, CloseSent + await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + } + + break; + } + } + }); + + await Task.WhenAll(sending, receiving); + } + + private async Task StartSecondClient(string url) + { + await Task.Delay(500); + + var client = new ClientWebSocket(); + + await client.ConnectAsync(new Uri(url), CancellationToken.None); + + var sending = Task.Run(async () => + { + var line = "test"; + for (var i = 0; i < 10; i++) + { + var bytes = Encoding.UTF8.GetBytes(line); + + await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, + CancellationToken.None); + await Task.Delay(10); + } + + await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + }); + + var receiving = Task.Run(async () => + { + var buffer = new byte[1024 * 4]; + + while (true) + { + var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + if (result.MessageType == WebSocketMessageType.Text) + { + _secondRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); + } + else if (result.MessageType == WebSocketMessageType.Close) + { + if (client.State != WebSocketState.Closed) + { + // Last version, the client state is CloseReceived + // Valid states are: Open, CloseReceived, CloseSent + await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + } + + break; + } + } + }); + + await Task.WhenAll(sending, receiving); + } + + private async Task StartFakeDownstreamService(string url, string path) + { + await _serviceHandler.StartFakeDownstreamService(url, async (context, next) => + { + if (context.Request.Path == path) + { + if (context.WebSockets.IsWebSocketRequest) + { + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await Echo(webSocket); + } + else + { + context.Response.StatusCode = 400; + } + } + else + { + await next(); + } + }); + } + + private async Task StartSecondFakeDownstreamService(string url, string path) + { + await _serviceHandler.StartFakeDownstreamService(url, async (context, next) => + { + if (context.Request.Path == path) + { + if (context.WebSockets.IsWebSocketRequest) + { + var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await Message(webSocket); + } + else + { + context.Response.StatusCode = 400; + } + } + else + { + await next(); + } + }); + } + + private static async Task Echo(WebSocket webSocket) + { + try + { + var buffer = new byte[1024 * 4]; + + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + while (!result.CloseStatus.HasValue) - { - await webSocket.SendAsync(new ArraySegment(bytes), result.MessageType, result.EndOfMessage, CancellationToken.None); - - result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); } - await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); - } - catch (Exception e) - { - Console.WriteLine(e); - } + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + private static async Task Message(WebSocket webSocket) + { + try + { + var buffer = new byte[1024 * 4]; + + var bytes = Encoding.UTF8.GetBytes("chocolate"); + + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(bytes), result.MessageType, result.EndOfMessage, CancellationToken.None); + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + catch (Exception e) + { + Console.WriteLine(e); + } } - public void Dispose() + private void ThenTheReceivedCountIs(int count) { - _serviceHandler?.Dispose(); + _firstRecieved.Count.ShouldBe(count); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); _steps.Dispose(); - } - } -} + GC.SuppressFinalize(this); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/appsettings.json b/test/Ocelot.AcceptanceTests/appsettings.json index fd57aaac3..2d6acc604 100644 --- a/test/Ocelot.AcceptanceTests/appsettings.json +++ b/test/Ocelot.AcceptanceTests/appsettings.json @@ -14,7 +14,7 @@ }, "eureka": { "client": { - "serviceUrl": "http://localhost:8761/eureka/", + "serviceUrl": "http://127.0.0.1:8761/eureka/", "shouldRegisterWithEureka": true, "shouldFetchRegistry": true, "port": 5000, diff --git a/test/Ocelot.AcceptanceTests/mycert.pfx b/test/Ocelot.AcceptanceTests/mycert.pfx new file mode 100644 index 000000000..ead06a05d Binary files /dev/null and b/test/Ocelot.AcceptanceTests/mycert.pfx differ diff --git a/test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs b/test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs index 38cb3a063..a7203ca38 100644 --- a/test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs +++ b/test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs @@ -1,156 +1,160 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Columns; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Diagnosers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Validators; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Ocelot.Configuration.File; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; - -namespace Ocelot.Benchmarks -{ - [Config(typeof(AllTheThingsBenchmarks))] - public class AllTheThingsBenchmarks : ManualConfig - { - private IWebHost _service; - private IWebHost _ocelot; - private HttpClient _httpClient; - - public AllTheThingsBenchmarks() - { - Add(StatisticColumn.AllStatistics); - Add(MemoryDiagnoser.Default); - Add(BaselineValidator.FailOnError); - } - - [GlobalSetup] - public void SetUp() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51879, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201, string.Empty); - GivenThereIsAConfiguration(configuration); - GivenOcelotIsRunning("http://localhost:5000"); - - _httpClient = new HttpClient(); - } - - [Benchmark(Baseline = true)] - public async Task Baseline() - { - var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:5000/"); - var response = await _httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); - } - - /* * Summary* - BenchmarkDotNet = v0.10.13, OS = macOS 10.12.6 (16G1212) [Darwin 16.7.0] - Intel Core i5-4278U CPU 2.60GHz(Haswell), 1 CPU, 4 logical cores and 2 physical cores - .NET Core SDK = 2.1.4 - [Host] : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT - DefaultJob : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT - Method | Mean | Error | StdDev | StdErr | Min | Q1 | Median | Q3 | Max | Op/s | Scaled | Gen 0 | Gen 1 | Allocated | - --------- |---------:|----------:|----------:|----------:|---------:|---------:|---------:|---------:|---------:|------:|-------:|--------:|-------:|----------:| - Baseline | 2.102 ms | 0.0292 ms | 0.0273 ms | 0.0070 ms | 2.063 ms | 2.080 ms | 2.093 ms | 2.122 ms | 2.152 ms | 475.8 | 1.00 | 31.2500 | 3.9063 | 1.63 KB |*/ - - private void GivenOcelotIsRunning(string url) - { - _ocelot = new WebHostBuilder() - .UseKestrel() - .UseUrls(url) - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("ocelot.json", false, false) - .AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - }) - .ConfigureLogging((hostingContext, logging) => - { - logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); - }) - .UseIISIntegration() - .Configure(app => - { - app.UseOcelot().Wait(); - }) - .Build(); - - _ocelot.Start(); - } - - public 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); - } +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; - 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(); +using Newtonsoft.Json; - _service.Start(); - } - } -} +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; + +namespace Ocelot.Benchmarks +{ + [Config(typeof(AllTheThingsBenchmarks))] + public class AllTheThingsBenchmarks : ManualConfig + { + private IWebHost _service; + private IWebHost _ocelot; + private HttpClient _httpClient; + + public AllTheThingsBenchmarks() + { + AddColumn(StatisticColumn.AllStatistics); + AddDiagnoser(MemoryDiagnoser.Default); + AddValidator(BaselineValidator.FailOnError); + } + + [GlobalSetup] + public void SetUp() + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51879, + }, + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201, string.Empty); + GivenThereIsAConfiguration(configuration); + GivenOcelotIsRunning("http://localhost:5000"); + + _httpClient = new HttpClient(); + } + + [Benchmark(Baseline = true)] + public async Task Baseline() + { + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:5000/"); + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + } + + /* * Summary* + BenchmarkDotNet = v0.10.13, OS = macOS 10.12.6 (16G1212) [Darwin 16.7.0] + Intel Core i5-4278U CPU 2.60GHz(Haswell), 1 CPU, 4 logical cores and 2 physical cores + .NET Core SDK = 2.1.4 + + [Host] : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT + DefaultJob : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT + Method | Mean | Error | StdDev | StdErr | Min | Q1 | Median | Q3 | Max | Op/s | Scaled | Gen 0 | Gen 1 | Allocated | + --------- |---------:|----------:|----------:|----------:|---------:|---------:|---------:|---------:|---------:|------:|-------:|--------:|-------:|----------:| + Baseline | 2.102 ms | 0.0292 ms | 0.0273 ms | 0.0070 ms | 2.063 ms | 2.080 ms | 2.093 ms | 2.122 ms | 2.152 ms | 475.8 | 1.00 | 31.2500 | 3.9063 | 1.63 KB |*/ + + private void GivenOcelotIsRunning(string url) + { + _ocelot = new WebHostBuilder() + .UseKestrel() + .UseUrls(url) + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json", false, false) + .AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot(); + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + }) + .UseIISIntegration() + .Configure(app => + { + app.UseOcelot().Wait(); + }) + .Build(); + + _ocelot.Start(); + } + + 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/DictionaryBenchmarks.cs b/test/Ocelot.Benchmarks/DictionaryBenchmarks.cs index 5799ca420..73dfd3835 100644 --- a/test/Ocelot.Benchmarks/DictionaryBenchmarks.cs +++ b/test/Ocelot.Benchmarks/DictionaryBenchmarks.cs @@ -1,77 +1,79 @@ +using System.Collections.Concurrent; +using System.Net.Http; + using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Validators; -using System.Net.Http; - -namespace Ocelot.Benchmarks -{ - using Configuration; - using Configuration.Builder; - using Requester; - using System.Collections.Concurrent; - - [Config(typeof(DictionaryBenchmarks))] - public class DictionaryBenchmarks : ManualConfig - { - private ConcurrentDictionary _downstreamReRouteDictionary; - private ConcurrentDictionary _stringReRouteDictionary; - private HttpClientWrapper _client; - private string _stringKey; - private DownstreamReRoute _downstreamReRouteKey; - - public DictionaryBenchmarks() - { - Add(StatisticColumn.AllStatistics); - Add(MemoryDiagnoser.Default); - Add(BaselineValidator.FailOnError); - } - - [GlobalSetup] - public void SetUp() - { - _downstreamReRouteKey = new DownstreamReRouteBuilder().Build(); - _stringKey = "test"; - _client = new HttpClientWrapper(new HttpClient()); - _downstreamReRouteDictionary = new ConcurrentDictionary(); - - _downstreamReRouteDictionary.TryAdd(new DownstreamReRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); - _downstreamReRouteDictionary.TryAdd(new DownstreamReRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); - _downstreamReRouteDictionary.TryAdd(new DownstreamReRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); - _downstreamReRouteDictionary.TryAdd(new DownstreamReRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); - _downstreamReRouteDictionary.TryAdd(new DownstreamReRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); - _downstreamReRouteDictionary.TryAdd(new DownstreamReRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); - _downstreamReRouteDictionary.TryAdd(new DownstreamReRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); - _downstreamReRouteDictionary.TryAdd(new DownstreamReRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); - _downstreamReRouteDictionary.TryAdd(new DownstreamReRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); - _downstreamReRouteDictionary.TryAdd(new DownstreamReRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); - - _stringReRouteDictionary = new ConcurrentDictionary(); - _stringReRouteDictionary.TryAdd("1", new HttpClientWrapper(new HttpClient())); - _stringReRouteDictionary.TryAdd("2", new HttpClientWrapper(new HttpClient())); - _stringReRouteDictionary.TryAdd("3", new HttpClientWrapper(new HttpClient())); - _stringReRouteDictionary.TryAdd("4", new HttpClientWrapper(new HttpClient())); - _stringReRouteDictionary.TryAdd("5", new HttpClientWrapper(new HttpClient())); - _stringReRouteDictionary.TryAdd("6", new HttpClientWrapper(new HttpClient())); - _stringReRouteDictionary.TryAdd("7", new HttpClientWrapper(new HttpClient())); - _stringReRouteDictionary.TryAdd("8", new HttpClientWrapper(new HttpClient())); - _stringReRouteDictionary.TryAdd("9", new HttpClientWrapper(new HttpClient())); - _stringReRouteDictionary.TryAdd("10", new HttpClientWrapper(new HttpClient())); - } - [Benchmark(Baseline = true)] - public IHttpClient StringKey() - { - _stringReRouteDictionary.AddOrUpdate(_stringKey, _client, (k, oldValue) => _client); - return _stringReRouteDictionary.TryGetValue(_stringKey, out var client) ? client : null; - } +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; - [Benchmark] - public IHttpClient DownstreamReRouteKey() - { - _downstreamReRouteDictionary.AddOrUpdate(_downstreamReRouteKey, _client, (k, oldValue) => _client); - return _downstreamReRouteDictionary.TryGetValue(_downstreamReRouteKey, out var client) ? client : null; - } - } -} +using Ocelot.Requester; + +namespace Ocelot.Benchmarks +{ + [Config(typeof(DictionaryBenchmarks))] + public class DictionaryBenchmarks : ManualConfig + { + private ConcurrentDictionary _downstreamRouteDictionary; + private ConcurrentDictionary _stringRouteDictionary; + private HttpClientWrapper _client; + private string _stringKey; + private DownstreamRoute _downstreamRouteKey; + + public DictionaryBenchmarks() + { + AddColumn(StatisticColumn.AllStatistics); + AddDiagnoser(MemoryDiagnoser.Default); + AddValidator(BaselineValidator.FailOnError); + } + + [GlobalSetup] + public void SetUp() + { + _downstreamRouteKey = new DownstreamRouteBuilder().Build(); + _stringKey = "test"; + _client = new HttpClientWrapper(new HttpClient()); + _downstreamRouteDictionary = new ConcurrentDictionary(); + + _downstreamRouteDictionary.TryAdd(new DownstreamRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); + _downstreamRouteDictionary.TryAdd(new DownstreamRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); + _downstreamRouteDictionary.TryAdd(new DownstreamRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); + _downstreamRouteDictionary.TryAdd(new DownstreamRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); + _downstreamRouteDictionary.TryAdd(new DownstreamRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); + _downstreamRouteDictionary.TryAdd(new DownstreamRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); + _downstreamRouteDictionary.TryAdd(new DownstreamRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); + _downstreamRouteDictionary.TryAdd(new DownstreamRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); + _downstreamRouteDictionary.TryAdd(new DownstreamRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); + _downstreamRouteDictionary.TryAdd(new DownstreamRouteBuilder().Build(), new HttpClientWrapper(new HttpClient())); + + _stringRouteDictionary = new ConcurrentDictionary(); + _stringRouteDictionary.TryAdd("1", new HttpClientWrapper(new HttpClient())); + _stringRouteDictionary.TryAdd("2", new HttpClientWrapper(new HttpClient())); + _stringRouteDictionary.TryAdd("3", new HttpClientWrapper(new HttpClient())); + _stringRouteDictionary.TryAdd("4", new HttpClientWrapper(new HttpClient())); + _stringRouteDictionary.TryAdd("5", new HttpClientWrapper(new HttpClient())); + _stringRouteDictionary.TryAdd("6", new HttpClientWrapper(new HttpClient())); + _stringRouteDictionary.TryAdd("7", new HttpClientWrapper(new HttpClient())); + _stringRouteDictionary.TryAdd("8", new HttpClientWrapper(new HttpClient())); + _stringRouteDictionary.TryAdd("9", new HttpClientWrapper(new HttpClient())); + _stringRouteDictionary.TryAdd("10", new HttpClientWrapper(new HttpClient())); + } + + [Benchmark(Baseline = true)] + public IHttpClient StringKey() + { + _stringRouteDictionary.AddOrUpdate(_stringKey, _client, (k, oldValue) => _client); + return _stringRouteDictionary.TryGetValue(_stringKey, out var client) ? client : null; + } + + [Benchmark] + public IHttpClient DownstreamRouteKey() + { + _downstreamRouteDictionary.AddOrUpdate(_downstreamRouteKey, _client, (k, oldValue) => _client); + return _downstreamRouteDictionary.TryGetValue(_downstreamRouteKey, out var client) ? client : null; + } + } +} diff --git a/test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs b/test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs index 57f7386a7..9a43642c2 100644 --- a/test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs +++ b/test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs @@ -1,21 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Validators; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; + using Ocelot.Configuration; + using Ocelot.DependencyInjection; + using Ocelot.DownstreamRouteFinder.Finder; -using Ocelot.DownstreamRouteFinder.Middleware; + using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + using Ocelot.Middleware; -using Ocelot.Middleware.Multiplexer; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; + +using Ocelot.DownstreamRouteFinder.Middleware; namespace Ocelot.Benchmarks { @@ -24,14 +31,14 @@ namespace Ocelot.Benchmarks public class DownstreamRouteFinderMiddlewareBenchmarks : ManualConfig { private DownstreamRouteFinderMiddleware _middleware; - private DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; + private RequestDelegate _next; + private HttpContext _httpContext; public DownstreamRouteFinderMiddlewareBenchmarks() { - Add(StatisticColumn.AllStatistics); - Add(MemoryDiagnoser.Default); - Add(BaselineValidator.FailOnError); + AddColumn(StatisticColumn.AllStatistics); + AddDiagnoser(MemoryDiagnoser.Default); + AddValidator(BaselineValidator.FailOnError); } [GlobalSetup] @@ -43,7 +50,6 @@ public void SetUp() var services = serviceCollection.BuildServiceProvider(); var loggerFactory = services.GetService(); var drpf = services.GetService(); - var multiplexer = services.GetService(); _next = async context => { @@ -51,22 +57,26 @@ public void SetUp() throw new Exception("BOOM"); }; - _middleware = new DownstreamRouteFinderMiddleware(_next, loggerFactory, drpf, multiplexer); - var httpContext = new DefaultHttpContext(); - httpContext.Request.Path = new PathString("/test"); - httpContext.Request.QueryString = new QueryString("?a=b"); - httpContext.Request.Headers.Add("Host", "most"); + _middleware = new DownstreamRouteFinderMiddleware(_next, loggerFactory, drpf); - _downstreamContext = new DownstreamContext(httpContext) + var httpContext = new DefaultHttpContext { - Configuration = new InternalConfiguration(new List(), null, null, null, null, null, null, null) + Request = + { + Path = new PathString("/test"), + QueryString = new QueryString("?a=b"), + }, }; + httpContext.Request.Headers.Add("Host", "most"); + httpContext.Items.SetIInternalConfiguration(new InternalConfiguration(new List(), null, null, null, null, null, null, null, null)); + + _httpContext = httpContext; } [Benchmark(Baseline = true)] public async Task Baseline() { - await _middleware.Invoke(_downstreamContext); + await _middleware.Invoke(_httpContext); } } } diff --git a/test/Ocelot.Benchmarks/ExceptionHandlerMiddlewareBenchmarks.cs b/test/Ocelot.Benchmarks/ExceptionHandlerMiddlewareBenchmarks.cs index 995efea5c..f3f3b27c2 100644 --- a/test/Ocelot.Benchmarks/ExceptionHandlerMiddlewareBenchmarks.cs +++ b/test/Ocelot.Benchmarks/ExceptionHandlerMiddlewareBenchmarks.cs @@ -1,61 +1,66 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Columns; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Diagnosers; -using BenchmarkDotNet.Validators; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Ocelot.Configuration.Repository; -using Ocelot.DependencyInjection; -using Ocelot.Errors.Middleware; -using Ocelot.Infrastructure.RequestData; -using Ocelot.Logging; -using Ocelot.Middleware; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Ocelot.Benchmarks -{ - [SimpleJob(launchCount: 1, warmupCount: 2, targetCount: 5)] - [Config(typeof(ExceptionHandlerMiddlewareBenchmarks))] - public class ExceptionHandlerMiddlewareBenchmarks : ManualConfig - { - private ExceptionHandlerMiddleware _middleware; - private DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; - - public ExceptionHandlerMiddlewareBenchmarks() - { - Add(StatisticColumn.AllStatistics); - Add(MemoryDiagnoser.Default); - Add(BaselineValidator.FailOnError); - } - - [GlobalSetup] - public void SetUp() - { - var serviceCollection = new ServiceCollection(); - var config = new ConfigurationRoot(new List()); - var builder = new OcelotBuilder(serviceCollection, config); - var services = serviceCollection.BuildServiceProvider(); - var loggerFactory = services.GetService(); - var configRepo = services.GetService(); - var repo = services.GetService(); +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Validators; + +using Ocelot.DependencyInjection; + +using Ocelot.Infrastructure.RequestData; + +using Ocelot.Logging; + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +using Ocelot.Errors.Middleware; + +namespace Ocelot.Benchmarks +{ + [SimpleJob(launchCount: 1, warmupCount: 2, targetCount: 5)] + [Config(typeof(ExceptionHandlerMiddlewareBenchmarks))] + public class ExceptionHandlerMiddlewareBenchmarks : ManualConfig + { + private ExceptionHandlerMiddleware _middleware; + private RequestDelegate _next; + private HttpContext _httpContext; + + public ExceptionHandlerMiddlewareBenchmarks() + { + AddColumn(StatisticColumn.AllStatistics); + AddDiagnoser(MemoryDiagnoser.Default); + AddValidator(BaselineValidator.FailOnError); + } + + [GlobalSetup] + public void SetUp() + { + var serviceCollection = new ServiceCollection(); + var config = new ConfigurationRoot(new List()); + var builder = new OcelotBuilder(serviceCollection, config); + var services = serviceCollection.BuildServiceProvider(); + var loggerFactory = services.GetService(); + var repo = services.GetService(); + _next = async context => - { - await Task.CompletedTask; - throw new Exception("BOOM"); - }; - _middleware = new ExceptionHandlerMiddleware(_next, loggerFactory, configRepo, repo); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - } - - [Benchmark(Baseline = true)] - public async Task Baseline() - { - await _middleware.Invoke(_downstreamContext); - } - } + { + await Task.CompletedTask; + throw new Exception("BOOM"); + }; + + _middleware = new ExceptionHandlerMiddleware(_next, loggerFactory, repo); + _httpContext = new DefaultHttpContext(); + } + + [Benchmark(Baseline = true)] + public async Task Baseline() + { + await _middleware.Invoke(_httpContext); + } + } } diff --git a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj index 0ebd49e7c..84c40564f 100644 --- a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj +++ b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj @@ -1,30 +1,32 @@ - - - - 0.0.0-dev - netcoreapp3.1 - Ocelot.Benchmarks - Exe - Ocelot.Benchmarks - osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64 - false - false - false - ..\..\codeanalysis.ruleset - - - - - - - - - - all - - - - - - - \ No newline at end of file + + + + 0.0.0-dev + net7.0 + Ocelot.Benchmarks + Exe + Ocelot.Benchmarks + osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64 + false + false + false + ..\..\codeanalysis.ruleset + True + 1591 + + + + + + + + + + all + + + + + + + diff --git a/test/Ocelot.Benchmarks/Program.cs b/test/Ocelot.Benchmarks/Program.cs index d642032da..a42519606 100644 --- a/test/Ocelot.Benchmarks/Program.cs +++ b/test/Ocelot.Benchmarks/Program.cs @@ -11,7 +11,7 @@ public static void Main(string[] args) typeof(UrlPathToUrlPathTemplateMatcherBenchmarks), typeof(AllTheThingsBenchmarks), typeof(ExceptionHandlerMiddlewareBenchmarks), - typeof(DownstreamRouteFinderMiddlewareBenchmarks) + typeof(DownstreamRouteFinderMiddlewareBenchmarks), }); switcher.Run(args); diff --git a/test/Ocelot.Benchmarks/UrlPathToUrlPathTemplateMatcherBenchmarks.cs b/test/Ocelot.Benchmarks/UrlPathToUrlPathTemplateMatcherBenchmarks.cs index 4bd20b1ee..72752ab40 100644 --- a/test/Ocelot.Benchmarks/UrlPathToUrlPathTemplateMatcherBenchmarks.cs +++ b/test/Ocelot.Benchmarks/UrlPathToUrlPathTemplateMatcherBenchmarks.cs @@ -3,6 +3,7 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Validators; + using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Values; @@ -14,13 +15,12 @@ public class UrlPathToUrlPathTemplateMatcherBenchmarks : ManualConfig private RegExUrlMatcher _urlPathMatcher; private UpstreamPathTemplate _pathTemplate; private string _downstreamUrlPath; - private string _upstreamQuery; public UrlPathToUrlPathTemplateMatcherBenchmarks() { - Add(StatisticColumn.AllStatistics); - Add(MemoryDiagnoser.Default); - Add(BaselineValidator.FailOnError); + AddColumn(StatisticColumn.AllStatistics); + AddDiagnoser(MemoryDiagnoser.Default); + AddValidator(BaselineValidator.FailOnError); } [GlobalSetup] @@ -34,7 +34,7 @@ public void SetUp() [Benchmark(Baseline = true)] public void Baseline() { - _urlPathMatcher.Match(_downstreamUrlPath, _upstreamQuery, _pathTemplate); + _urlPathMatcher.Match(_downstreamUrlPath, null, _pathTemplate); } // * Summary * diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs index fe18965d5..14667c7dc 100644 --- a/test/Ocelot.IntegrationTests/AdministrationTests.cs +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -1,864 +1,915 @@ -using IdentityServer4.AccessTokenValidation; -using IdentityServer4.Models; -using IdentityServer4.Test; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Newtonsoft.Json; -using Ocelot.Administration; -using Ocelot.Cache; -using Ocelot.Configuration.File; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; -using Shouldly; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.IntegrationTests -{ - public class AdministrationTests : IDisposable - { - private HttpClient _httpClient; - private readonly HttpClient _httpClientTwo; - private HttpResponseMessage _response; - private IHost _builder; - private IHostBuilder _webHostBuilder; - private string _ocelotBaseUrl; - private BearerToken _token; - private IHostBuilder _webHostBuilderTwo; - private IHost _builderTwo; - private IHost _identityServerBuilder; - private IHost _fooServiceBuilder; - private IHost _barServiceBuilder; - - public AdministrationTests() - { - _httpClient = new HttpClient(); - _httpClientTwo = new HttpClient(); - _ocelotBaseUrl = "http://localhost:5000"; - _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); - } - - [Fact] - public void should_return_response_401_with_call_re_routes_controller() - { - var configuration = new FileConfiguration(); - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunning()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_call_re_routes_controller() - { - var configuration = new FileConfiguration(); - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunning()) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - 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"; - _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - BaseUrl = _ocelotBaseUrl - } - }; - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunningWithNoWebHostBuilder(_ocelotBaseUrl)) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void should_be_able_to_use_token_from_ocelot_a_on_ocelot_b() - { - var configuration = new FileConfiguration(); - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenIdentityServerSigningEnvironmentalVariablesAreSet()) - .And(x => GivenOcelotIsRunning()) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenAnotherOcelotIsRunning("http://localhost:5017")) - .When(x => WhenIGetUrlOnTheSecondOcelot("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void should_return_file_configuration() - { - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - RequestIdKey = "RequestId", - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "127.0.0.1", - } - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/", - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 10, - Region = "Geoff" - } - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/test", - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 10, - Region = "Dave" - } - } - } - }; - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunning()) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseShouldBe(configuration)) - .BDDfy(); - } - - [Fact] - public void should_get_file_configuration_edit_and_post_updated_version() - { - var initialConfiguration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/" - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/test" - } - }, - }; - - var updatedConfiguration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "http", - DownstreamPathTemplate = "/geoffrey", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/" - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "123.123.123", - Port = 443, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/blooper/{productId}", - UpstreamHttpMethod = new List { "post" }, - UpstreamPathTemplate = "/test" - } - } - }; - - this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) - .And(x => GivenOcelotIsRunning()) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .When(x => WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration)) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseShouldBe(updatedConfiguration)) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .And(x => ThenTheResponseShouldBe(updatedConfiguration)) - .And(_ => ThenTheConfigurationIsSavedCorrectly(updatedConfiguration)) - .BDDfy(); - } - - private void ThenTheConfigurationIsSavedCorrectly(FileConfiguration expected) - { - var ocelotJsonPath = $"{AppContext.BaseDirectory}ocelot.json"; - var resultText = File.ReadAllText(ocelotJsonPath); - var expectedText = JsonConvert.SerializeObject(expected, Formatting.Indented); - resultText.ShouldBe(expectedText); - - var environmentSpecificPath = $"{AppContext.BaseDirectory}/ocelot.Production.json"; - resultText = File.ReadAllText(environmentSpecificPath); - expectedText = JsonConvert.SerializeObject(expected, Formatting.Indented); - resultText.ShouldBe(expectedText); - } - - [Fact] - public void should_get_file_configuration_edit_and_post_updated_version_redirecting_reroute() - { - var fooPort = 47689; - var barPort = 47690; - - var initialConfiguration = new FileConfiguration - { - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = fooPort, - } - }, - DownstreamScheme = "http", - DownstreamPathTemplate = "/foo", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/foo" - } - } - }; - - var updatedConfiguration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = barPort, - } - }, - DownstreamScheme = "http", - DownstreamPathTemplate = "/bar", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/foo" - } - } - }; - - this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) - .And(x => GivenThereIsAFooServiceRunningOn($"http://localhost:{fooPort}")) - .And(x => GivenThereIsABarServiceRunningOn($"http://localhost:{barPort}")) - .And(x => GivenOcelotIsRunning()) - .And(x => WhenIGetUrlOnTheApiGateway("/foo")) - .Then(x => ThenTheResponseBodyShouldBe("foo")) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration)) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseShouldBe(updatedConfiguration)) - .And(x => WhenIGetUrlOnTheApiGateway("/foo")) - .Then(x => ThenTheResponseBodyShouldBe("bar")) - .When(x => WhenIPostOnTheApiGateway("/administration/configuration", initialConfiguration)) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseShouldBe(initialConfiguration)) - .And(x => WhenIGetUrlOnTheApiGateway("/foo")) - .Then(x => ThenTheResponseBodyShouldBe("foo")) - .BDDfy(); - } - - [Fact] - public void should_clear_region() - { - var initialConfiguration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/", - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 10 - } - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/test", - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 10 - } - } - } - }; - - var regionToClear = "gettest"; - - this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) - .And(x => GivenOcelotIsRunning()) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIDeleteOnTheApiGateway($"/administration/outputcache/{regionToClear}")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NoContent)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_call_re_routes_controller_when_using_own_identity_server_to_secure_admin_area() - { - var configuration = new FileConfiguration(); - - var identityServerRootUrl = "http://localhost:5123"; - - Action options = o => - { - o.Authority = identityServerRootUrl; - o.ApiName = "api"; - o.RequireHttpsMetadata = false; - o.SupportedTokens = SupportedTokens.Both; - o.ApiSecret = "secret"; - }; - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenThereIsAnIdentityServerOn(identityServerRootUrl, "api")) - .And(x => GivenOcelotIsRunningWithIdentityServerSettings(options)) - .And(x => GivenIHaveAToken(identityServerRootUrl)) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - private void GivenIHaveAToken(string url) - { - var formData = new List> - { - new KeyValuePair("client_id", "api"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "api"), - new KeyValuePair("username", "test"), - new KeyValuePair("password", "test"), - new KeyValuePair("grant_type", "password") - }; - var content = new FormUrlEncodedContent(formData); - - using (var httpClient = new HttpClient()) - { - var response = httpClient.PostAsync($"{url}/connect/token", content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } - } - - private void GivenThereIsAnIdentityServerOn(string url, string apiName) - { - _identityServerBuilder = Host.CreateDefaultBuilder() - .ConfigureWebHost(webBuilder => - { - webBuilder.UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureServices(services => - { - services.AddLogging(); - services.AddIdentityServer() - .AddDeveloperSigningCredential() - .AddInMemoryApiResources(new List - { - new ApiResource - { - Name = apiName, - Description = apiName, - Enabled = true, - DisplayName = apiName, - Scopes = new List() - { - new Scope(apiName), - }, - }, - }) - .AddInMemoryClients(new List - { - new Client - { - ClientId = apiName, - AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, - ClientSecrets = new List { new Secret("secret".Sha256()) }, - AllowedScopes = new List { apiName }, - AccessTokenType = AccessTokenType.Jwt, - Enabled = true - }, - }) - .AddTestUsers(new List - { - new TestUser - { - Username = "test", - Password = "test", - SubjectId = "1231231" - }, - }); - }) - .Configure(app => - { - app.UseIdentityServer(); - } - ); - }).Build(); - - _identityServerBuilder.Start(); - - using (var httpClient = new HttpClient()) - { - var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result; - response.EnsureSuccessStatusCode(); - } - } - - private void GivenAnotherOcelotIsRunning(string baseUrl) - { - _httpClientTwo.BaseAddress = new Uri(baseUrl); - - _webHostBuilderTwo = Host.CreateDefaultBuilder() - .ConfigureWebHost(webBuilder => - { - webBuilder.UseUrls(baseUrl) - .UseKestrel() - .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("ocelot.json", false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(x => - { - x.AddMvc(option => option.EnableEndpointRouting = false); - x.AddOcelot() - .AddAdministration("/administration", "secret"); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - }); - - _builderTwo = _webHostBuilderTwo.Build(); - - _builderTwo.Start(); - } - - private void GivenIdentityServerSigningEnvironmentalVariablesAreSet() - { - Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", "idsrv3test.pfx"); - Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", "idsrv3test"); - } - - private void WhenIGetUrlOnTheSecondOcelot(string url) - { - _httpClientTwo.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - _response = _httpClientTwo.GetAsync(url).Result; - } - - private void WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration) - { - var json = JsonConvert.SerializeObject(updatedConfiguration); - var content = new StringContent(json); - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - _response = _httpClient.PostAsync(url, content).Result; - } - - private void ThenTheResponseShouldBe(List expected) - { - var content = _response.Content.ReadAsStringAsync().Result; - var result = JsonConvert.DeserializeObject(content); - result.Value.ShouldBe(expected); - } - - private void ThenTheResponseBodyShouldBe(string expected) - { - var content = _response.Content.ReadAsStringAsync().Result; - content.ShouldBe(expected); - } - - private void ThenTheResponseShouldBe(FileConfiguration expecteds) - { - var response = JsonConvert.DeserializeObject(_response.Content.ReadAsStringAsync().Result); - - response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey); - response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host); - response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port); - - for (var i = 0; i < response.ReRoutes.Count; i++) - { - for (var j = 0; j < response.ReRoutes[i].DownstreamHostAndPorts.Count; j++) - { - var result = response.ReRoutes[i].DownstreamHostAndPorts[j]; - var expected = expecteds.ReRoutes[i].DownstreamHostAndPorts[j]; - result.Host.ShouldBe(expected.Host); - result.Port.ShouldBe(expected.Port); - } - - response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].DownstreamPathTemplate); - response.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.ReRoutes[i].DownstreamScheme); - response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].UpstreamPathTemplate); - response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expecteds.ReRoutes[i].UpstreamHttpMethod); - } - } - - private void GivenIHaveAddedATokenToMyRequest() - { - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - } - - private void GivenIHaveAnOcelotToken(string adminPath) - { - var tokenUrl = $"{adminPath}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", "admin"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "admin"), - new KeyValuePair("grant_type", "client_credentials") - }; - var content = new FormUrlEncodedContent(formData); - - var response = _httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - var configPath = $"{adminPath}/.well-known/openid-configuration"; - response = _httpClient.GetAsync(configPath).Result; - response.EnsureSuccessStatusCode(); - } - - private void GivenOcelotIsRunningWithIdentityServerSettings(Action configOptions) - { - _webHostBuilder = Host.CreateDefaultBuilder() - .ConfigureWebHost(webBuilder => - { - webBuilder.UseUrls(_ocelotBaseUrl) - .UseKestrel() - .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("ocelot.json", false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(x => - { - x.AddMvc(option => option.EnableEndpointRouting = false); - x.AddSingleton(_webHostBuilder); - x.AddOcelot() - .AddAdministration("/administration", configOptions); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - }); - - _builder = _webHostBuilder.Build(); - - _builder.Start(); - } - - private void GivenOcelotIsRunning() - { - _webHostBuilder = Host.CreateDefaultBuilder() - .ConfigureWebHost(webBuilder => - { - webBuilder.UseUrls(_ocelotBaseUrl) - .UseKestrel() - .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("ocelot.json", false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(x => - { - x.AddMvc(s => s.EnableEndpointRouting = false); - x.AddOcelot() - .AddAdministration("/administration", "secret"); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - }); - - _builder = _webHostBuilder.Build(); - - _builder.Start(); - } - - private void GivenOcelotIsRunningWithNoWebHostBuilder(string baseUrl) - { - _webHostBuilder = Host.CreateDefaultBuilder() - .ConfigureWebHost(webBuilder => - { - webBuilder.UseUrls(_ocelotBaseUrl) - .UseKestrel() - .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("ocelot.json", false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(x => - { - x.AddMvc(option => option.EnableEndpointRouting = false); - x.AddSingleton(_webHostBuilder); - x.AddOcelot() - .AddAdministration("/administration", "secret"); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - }); - - _builder = _webHostBuilder.Build(); - - _builder.Start(); - } - - private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) - { - var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; - - var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - - var text = File.ReadAllText(configurationPath); - - configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - - text = File.ReadAllText(configurationPath); - } - - private void WhenIGetUrlOnTheApiGateway(string url) - { - _response = _httpClient.GetAsync(url).Result; - } - - private void WhenIDeleteOnTheApiGateway(string url) - { - _response = _httpClient.DeleteAsync(url).Result; - } - - private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) - { - _response.StatusCode.ShouldBe(expectedHttpStatusCode); - } - - public void Dispose() - { - Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", ""); - Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", ""); - _builder?.Dispose(); - _httpClient?.Dispose(); - _identityServerBuilder?.Dispose(); - } - - private void GivenThereIsAFooServiceRunningOn(string baseUrl) - { - _fooServiceBuilder = Host.CreateDefaultBuilder() - .ConfigureWebHost(webBuilder => - { - webBuilder.UseUrls(baseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .Configure(app => - { - app.UsePathBase("/foo"); - app.Run(async context => - { - context.Response.StatusCode = 200; - await context.Response.WriteAsync("foo"); - }); - }); - }).Build(); - - _fooServiceBuilder.Start(); - } - - private void GivenThereIsABarServiceRunningOn(string baseUrl) - { - _barServiceBuilder = Host.CreateDefaultBuilder() - .ConfigureWebHost(webBuilder => - { - webBuilder.UseUrls(baseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .Configure(app => - { - app.UsePathBase("/bar"); - app.Run(async context => - { - context.Response.StatusCode = 200; - await context.Response.WriteAsync("bar"); - }); - }); - }).Build(); - - _barServiceBuilder.Start(); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; + +using IdentityServer4.Models; +using IdentityServer4.Test; + +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.Tokens; + +using Newtonsoft.Json; + +using Ocelot.Administration; +using Ocelot.Cache; +using Ocelot.Configuration.ChangeTracking; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.IntegrationTests +{ + public class AdministrationTests : IDisposable + { + private HttpClient _httpClient; + private readonly HttpClient _httpClientTwo; + private HttpResponseMessage _response; + private IHost _builder; + private IHostBuilder _webHostBuilder; + private string _ocelotBaseUrl; + private BearerToken _token; + private IHostBuilder _webHostBuilderTwo; + private IHost _builderTwo; + private IHost _identityServerBuilder; + private IHost _fooServiceBuilder; + private IHost _barServiceBuilder; + + public AdministrationTests() + { + _httpClient = new HttpClient(); + _httpClientTwo = new HttpClient(); + _ocelotBaseUrl = "http://localhost:5000"; + _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); + } + + [Fact] + public void should_return_response_401_with_call_re_routes_controller() + { + var configuration = new FileConfiguration(); + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)) + .BDDfy(); + } + + //this seems to be be answer https://github.com/IdentityServer/IdentityServer4/issues/4914 + [Fact] + public void should_return_response_200_with_call_re_routes_controller() + { + var configuration = new FileConfiguration(); + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + 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"; + _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + BaseUrl = _ocelotBaseUrl, + }, + }; + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithNoWebHostBuilder(_ocelotBaseUrl)) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_be_able_to_use_token_from_ocelot_a_on_ocelot_b() + { + var configuration = new FileConfiguration(); + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenIdentityServerSigningEnvironmentalVariablesAreSet()) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenAnotherOcelotIsRunning("http://localhost:5017")) + .When(x => WhenIGetUrlOnTheSecondOcelot("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_return_file_configuration() + { + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + RequestIdKey = "RequestId", + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "https", + Host = "127.0.0.1", + }, + }, + Routes = new List + { + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 80, + }, + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10, + Region = "Geoff", + }, + }, + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 80, + }, + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/test", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10, + Region = "Dave", + }, + }, + }, + }; + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseShouldBe(configuration)) + .BDDfy(); + } + + [Fact] + public void should_get_file_configuration_edit_and_post_updated_version() + { + var initialConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration(), + Routes = new List + { + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 80, + }, + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/", + }, + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 80, + }, + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/test", + }, + }, + }; + + var updatedConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration(), + Routes = new List + { + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 80, + }, + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/geoffrey", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/", + }, + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "123.123.123", + Port = 443, + }, + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/blooper/{productId}", + UpstreamHttpMethod = new List { "post" }, + UpstreamPathTemplate = "/test", + }, + }, + }; + + this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .When(x => WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseShouldBe(updatedConfiguration)) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .And(x => ThenTheResponseShouldBe(updatedConfiguration)) + .And(_ => ThenTheConfigurationIsSavedCorrectly(updatedConfiguration)) + .BDDfy(); + } + + [Fact] + public void should_activate_change_token_when_configuration_is_updated() + { + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration(), + Routes = new List + { + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 80, + }, + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/", + }, + }, + }; + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIPostOnTheApiGateway("/administration/configuration", configuration)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => TheChangeTokenShouldBeActive()) + .And(x => ThenTheResponseShouldBe(configuration)) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .And(x => ThenTheResponseShouldBe(configuration)) + .And(_ => ThenTheConfigurationIsSavedCorrectly(configuration)) + .BDDfy(); + } + + private void TheChangeTokenShouldBeActive() + { + _builder.Services.GetRequiredService().ChangeToken.HasChanged.ShouldBeTrue(); + } + + private static void ThenTheConfigurationIsSavedCorrectly(FileConfiguration expected) + { + var ocelotJsonPath = $"{AppContext.BaseDirectory}ocelot.json"; + var resultText = File.ReadAllText(ocelotJsonPath); + var expectedText = JsonConvert.SerializeObject(expected, Formatting.Indented); + resultText.ShouldBe(expectedText); + + var environmentSpecificPath = $"{AppContext.BaseDirectory}/ocelot.Production.json"; + resultText = File.ReadAllText(environmentSpecificPath); + expectedText = JsonConvert.SerializeObject(expected, Formatting.Indented); + resultText.ShouldBe(expectedText); + } + + [Fact] + public void should_get_file_configuration_edit_and_post_updated_version_redirecting_route() + { + var fooPort = 47689; + var barPort = 27654; + + var initialConfiguration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = fooPort, + }, + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/foo", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/foo", + }, + }, + }; + + var updatedConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration(), + Routes = new List + { + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = barPort, + }, + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/bar", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/foo", + }, + }, + }; + + this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) + .And(x => GivenThereIsAFooServiceRunningOn($"http://localhost:{fooPort}")) + .And(x => GivenThereIsABarServiceRunningOn($"http://localhost:{barPort}")) + .And(x => GivenOcelotIsRunning()) + .And(x => WhenIGetUrlOnTheApiGateway("/foo")) + .Then(x => ThenTheResponseBodyShouldBe("foo")) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseShouldBe(updatedConfiguration)) + .And(x => WhenIGetUrlOnTheApiGateway("/foo")) + .Then(x => ThenTheResponseBodyShouldBe("bar")) + .When(x => WhenIPostOnTheApiGateway("/administration/configuration", initialConfiguration)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseShouldBe(initialConfiguration)) + .And(x => WhenIGetUrlOnTheApiGateway("/foo")) + .Then(x => ThenTheResponseBodyShouldBe("foo")) + .BDDfy(); + } + + [Fact] + public void should_clear_region() + { + var initialConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration(), + Routes = new List + { + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 80, + }, + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10, + }, + }, + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 80, + }, + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/test", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10, + }, + }, + }, + }; + + var regionToClear = "gettest"; + + this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIDeleteOnTheApiGateway($"/administration/outputcache/{regionToClear}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NoContent)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_call_re_routes_controller_when_using_own_identity_server_to_secure_admin_area() + { + var configuration = new FileConfiguration(); + + var identityServerRootUrl = "http://localhost:5123"; + + Action options = o => + { + o.Authority = identityServerRootUrl; + o.RequireHttpsMetadata = false; + o.TokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = false, + }; + }; + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenThereIsAnIdentityServerOn(identityServerRootUrl, "api")) + .And(x => GivenOcelotIsRunningWithIdentityServerSettings(options)) + .And(x => GivenIHaveAToken(identityServerRootUrl)) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + private void GivenIHaveAToken(string url) + { + var formData = new List> + { + new("client_id", "api"), + 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($"{url}/connect/token", content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + } + } + + private void GivenThereIsAnIdentityServerOn(string url, string apiName) + { + _identityServerBuilder = Host.CreateDefaultBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder.UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureServices(services => + { + services.AddLogging(); + services.AddIdentityServer() + .AddDeveloperSigningCredential() + .AddInMemoryApiScopes(new List { new(apiName) }) + .AddInMemoryApiResources(new List + { + new() + { + Name = apiName, + Description = apiName, + Enabled = true, + DisplayName = apiName, + Scopes = new List + { + apiName, + }, + }, + }) + .AddInMemoryClients(new List + { + new() + { + ClientId = apiName, + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List { new("secret".Sha256()) }, + AllowedScopes = new List { apiName }, + AccessTokenType = AccessTokenType.Jwt, + Enabled = true, + }, + }) + .AddTestUsers(new List + { + new() + { + Username = "test", + Password = "test", + SubjectId = "1231231", + }, + }); + }) + .Configure(app => + { + app.UseIdentityServer(); + } + ); + }).Build(); + + _identityServerBuilder.Start(); + + using (var httpClient = new HttpClient()) + { + var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result; + response.EnsureSuccessStatusCode(); + } + } + + private void GivenAnotherOcelotIsRunning(string baseUrl) + { + _httpClientTwo.BaseAddress = new Uri(baseUrl); + + _webHostBuilderTwo = Host.CreateDefaultBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder.UseUrls(baseUrl) + .UseKestrel() + .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("ocelot.json", false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(x => + { + x.AddMvc(option => option.EnableEndpointRouting = false); + x.AddOcelot() + .AddAdministration("/administration", "secret"); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + }); + + _builderTwo = _webHostBuilderTwo.Build(); + + _builderTwo.Start(); + } + + private static void GivenIdentityServerSigningEnvironmentalVariablesAreSet() + { + Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", "mycert.pfx"); + Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", "password"); + } + + private void WhenIGetUrlOnTheSecondOcelot(string url) + { + _httpClientTwo.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + _response = _httpClientTwo.GetAsync(url).Result; + } + + private void WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration) + { + var json = JsonConvert.SerializeObject(updatedConfiguration); + var content = new StringContent(json); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + _response = _httpClient.PostAsync(url, content).Result; + } + + private void ThenTheResponseShouldBe(List expected) + { + var content = _response.Content.ReadAsStringAsync().Result; + var result = JsonConvert.DeserializeObject(content); + result.Value.ShouldBe(expected); + } + + private void ThenTheResponseBodyShouldBe(string expected) + { + var content = _response.Content.ReadAsStringAsync().Result; + content.ShouldBe(expected); + } + + private void ThenTheResponseShouldBe(FileConfiguration expecteds) + { + var response = JsonConvert.DeserializeObject(_response.Content.ReadAsStringAsync().Result); + + response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey); + response.GlobalConfiguration.ServiceDiscoveryProvider.Scheme.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Scheme); + response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host); + response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port); + + for (var i = 0; i < response.Routes.Count; i++) + { + for (var j = 0; j < response.Routes[i].DownstreamHostAndPorts.Count; j++) + { + var result = response.Routes[i].DownstreamHostAndPorts[j]; + var expected = expecteds.Routes[i].DownstreamHostAndPorts[j]; + result.Host.ShouldBe(expected.Host); + result.Port.ShouldBe(expected.Port); + } + + response.Routes[i].DownstreamPathTemplate.ShouldBe(expecteds.Routes[i].DownstreamPathTemplate); + response.Routes[i].DownstreamScheme.ShouldBe(expecteds.Routes[i].DownstreamScheme); + response.Routes[i].UpstreamPathTemplate.ShouldBe(expecteds.Routes[i].UpstreamPathTemplate); + response.Routes[i].UpstreamHttpMethod.ShouldBe(expecteds.Routes[i].UpstreamHttpMethod); + } + } + + private void GivenIHaveAddedATokenToMyRequest() + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + } + + private void GivenIHaveAnOcelotToken(string adminPath) + { + var tokenUrl = $"{adminPath}/connect/token"; + var formData = new List> + { + new("client_id", "admin"), + new("client_secret", "secret"), + new("scope", "admin"), + new("grant_type", "client_credentials"), + }; + var content = new FormUrlEncodedContent(formData); + + var response = _httpClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + var configPath = $"{adminPath}/.well-known/openid-configuration"; + response = _httpClient.GetAsync(configPath).Result; + response.EnsureSuccessStatusCode(); + } + + private void GivenOcelotIsRunningWithIdentityServerSettings(Action configOptions) + { + _webHostBuilder = Host.CreateDefaultBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder.UseUrls(_ocelotBaseUrl) + .UseKestrel() + .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("ocelot.json", false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(x => + { + x.AddMvc(option => option.EnableEndpointRouting = false); + x.AddSingleton(_webHostBuilder); + x.AddOcelot() + .AddAdministration("/administration", configOptions); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + }); + + _builder = _webHostBuilder.Build(); + + _builder.Start(); + } + + private void GivenOcelotIsRunning() + { + _webHostBuilder = Host.CreateDefaultBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder.UseUrls(_ocelotBaseUrl) + .UseKestrel() + .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("ocelot.json", false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(x => + { + x.AddMvc(s => s.EnableEndpointRouting = false); + x.AddOcelot() + .AddAdministration("/administration", "secret"); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + }); + + _builder = _webHostBuilder.Build(); + + _builder.Start(); + } + + private void GivenOcelotIsRunningWithNoWebHostBuilder(string baseUrl) + { + _webHostBuilder = Host.CreateDefaultBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder.UseUrls(_ocelotBaseUrl) + .UseKestrel() + .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("ocelot.json", false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(x => + { + x.AddMvc(option => option.EnableEndpointRouting = false); + x.AddSingleton(_webHostBuilder); + x.AddOcelot() + .AddAdministration("/administration", "secret"); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + }); + + _builder = _webHostBuilder.Build(); + + _builder.Start(); + } + + private static void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + var text = File.ReadAllText(configurationPath); + + configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + text = File.ReadAllText(configurationPath); + } + + private void WhenIGetUrlOnTheApiGateway(string url) + { + _response = _httpClient.GetAsync(url).Result; + } + + private void WhenIDeleteOnTheApiGateway(string url) + { + _response = _httpClient.DeleteAsync(url).Result; + } + + private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) + { + _response.StatusCode.ShouldBe(expectedHttpStatusCode); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", string.Empty); + Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", string.Empty); + _builder?.Dispose(); + _httpClient?.Dispose(); + _identityServerBuilder?.Dispose(); + } + + private void GivenThereIsAFooServiceRunningOn(string baseUrl) + { + _fooServiceBuilder = Host.CreateDefaultBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder.UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase("/foo"); + app.Run(async context => + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync("foo"); + }); + }); + }).Build(); + + _fooServiceBuilder.Start(); + } + + private void GivenThereIsABarServiceRunningOn(string baseUrl) + { + _barServiceBuilder = Host.CreateDefaultBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder.UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase("/bar"); + app.Run(async context => + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync("bar"); + }); + }); + }).Build(); + + _barServiceBuilder.Start(); + } + } +} diff --git a/test/Ocelot.IntegrationTests/CacheManagerTests.cs b/test/Ocelot.IntegrationTests/CacheManagerTests.cs index 6be90f01f..85f51848d 100644 --- a/test/Ocelot.IntegrationTests/CacheManagerTests.cs +++ b/test/Ocelot.IntegrationTests/CacheManagerTests.cs @@ -1,220 +1,227 @@ -namespace Ocelot.IntegrationTests -{ - using Configuration.File; - using DependencyInjection; - using global::CacheManager.Core; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; - using Microsoft.Extensions.Logging; - using Newtonsoft.Json; - using Ocelot.Administration; - using Ocelot.Cache.CacheManager; - using Ocelot.Middleware; - using Shouldly; - using System; - using System.Collections.Generic; - using System.IO; - using System.Net; - using System.Net.Http; - using System.Net.Http.Headers; - using TestStack.BDDfy; - using Xunit; - - public class CacheManagerTests : IDisposable - { - private HttpClient _httpClient; - private readonly HttpClient _httpClientTwo; - private HttpResponseMessage _response; - private IHost _builder; - private IHostBuilder _webHostBuilder; - private string _ocelotBaseUrl; - private BearerToken _token; - - public CacheManagerTests() - { - _httpClient = new HttpClient(); - _httpClientTwo = new HttpClient(); - _ocelotBaseUrl = "http://localhost:5000"; - _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); - } - - [Fact] - public void should_clear_region() - { - var initialConfiguration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - }, - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/", - FileCacheOptions = new FileCacheOptions +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; + +using Ocelot.Administration; + +using CacheManager.Core; + +using Ocelot.Configuration.File; + +using Ocelot.DependencyInjection; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +using Ocelot.Middleware; + +using Newtonsoft.Json; + +using Ocelot.Cache.CacheManager; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.IntegrationTests +{ + public class CacheManagerTests : IDisposable + { + private readonly HttpClient _httpClient; + private readonly HttpClient _httpClientTwo; + private HttpResponseMessage _response; + private IHost _builder; + private IHostBuilder _webHostBuilder; + private readonly string _ocelotBaseUrl; + private BearerToken _token; + + public CacheManagerTests() + { + _httpClient = new HttpClient(); + _httpClientTwo = new HttpClient(); + _ocelotBaseUrl = "http://localhost:5000"; + _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); + } + + [Fact] + public void should_clear_region() + { + var initialConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration(), + Routes = new List + { + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 80, + }, + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10, + }, + }, + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 80, + }, + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/test", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10, + }, + }, + }, + }; + + var regionToClear = "gettest"; + + this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIDeleteOnTheApiGateway($"/administration/outputcache/{regionToClear}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NoContent)) + .BDDfy(); + } + + private void GivenIHaveAddedATokenToMyRequest() + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + } + + private void GivenIHaveAnOcelotToken(string adminPath) + { + var tokenUrl = $"{adminPath}/connect/token"; + var formData = new List> + { + new("client_id", "admin"), + new("client_secret", "secret"), + new("scope", "admin"), + new("grant_type", "client_credentials"), + }; + var content = new FormUrlEncodedContent(formData); + + var response = _httpClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + var configPath = $"{adminPath}/.well-known/openid-configuration"; + response = _httpClient.GetAsync(configPath).Result; + response.EnsureSuccessStatusCode(); + } + + private void GivenOcelotIsRunning() + { + _webHostBuilder = Host.CreateDefaultBuilder() + .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("ocelot.json", false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(x => + { + Action settings = (s) => + { + s.WithMicrosoftLogging(log => { - TtlSeconds = 10, - }, - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - }, - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/test", - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 10, - }, - }, - }, - }; - - var regionToClear = "gettest"; - - this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) - .And(x => GivenOcelotIsRunning()) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIDeleteOnTheApiGateway($"/administration/outputcache/{regionToClear}")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NoContent)) - .BDDfy(); - } - - private void GivenIHaveAddedATokenToMyRequest() - { - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - } - - private void GivenIHaveAnOcelotToken(string adminPath) - { - var tokenUrl = $"{adminPath}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", "admin"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "admin"), - new KeyValuePair("grant_type", "client_credentials"), - }; - var content = new FormUrlEncodedContent(formData); - - var response = _httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - var configPath = $"{adminPath}/.well-known/openid-configuration"; - response = _httpClient.GetAsync(configPath).Result; - response.EnsureSuccessStatusCode(); - } - - private void GivenOcelotIsRunning() - { - _webHostBuilder = Host.CreateDefaultBuilder() - .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("ocelot.json", false, false); - config.AddEnvironmentVariables(); + //log.AddConsole(LogLevel.Debug); + }) + .WithDictionaryHandle(); + }; + x.AddMvc(option => option.EnableEndpointRouting = false); + x.AddOcelot() + .AddCacheManager(settings) + .AddAdministration("/administration", "secret"); }) - .ConfigureServices(x => - { - Action settings = (s) => - { - s.WithMicrosoftLogging(log => - { - //log.AddConsole(LogLevel.Debug); - }) - .WithDictionaryHandle(); - }; - x.AddMvc(option => option.EnableEndpointRouting = false); - x.AddOcelot() - .AddCacheManager(settings) - .AddAdministration("/administration", "secret"); - }) - .ConfigureWebHost(webBuilder => - { - webBuilder.UseUrls(_ocelotBaseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - }); - - _builder = _webHostBuilder.Build(); - - _builder.Start(); - } - - private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) - { - var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; - - var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - - var text = File.ReadAllText(configurationPath); - - configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - - text = File.ReadAllText(configurationPath); - } - - private void WhenIDeleteOnTheApiGateway(string url) - { - _response = _httpClient.DeleteAsync(url).Result; - } - - private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) - { - _response.StatusCode.ShouldBe(expectedHttpStatusCode); - } - - public void Dispose() - { - Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", ""); - Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", ""); - _builder?.Dispose(); - _httpClient?.Dispose(); - //_identityServerBuilder?.Dispose(); - } - } -} + .ConfigureWebHost(webBuilder => + { + webBuilder.UseUrls(_ocelotBaseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + }); + + _builder = _webHostBuilder.Build(); + + _builder.Start(); + } + + private static void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + var text = File.ReadAllText(configurationPath); + + configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + text = File.ReadAllText(configurationPath); + } + + private void WhenIDeleteOnTheApiGateway(string url) + { + _response = _httpClient.DeleteAsync(url).Result; + } + + private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) + { + _response.StatusCode.ShouldBe(expectedHttpStatusCode); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", string.Empty); + Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", string.Empty); + _builder?.Dispose(); + _httpClient?.Dispose(); + } + } +} diff --git a/test/Ocelot.IntegrationTests/HeaderTests.cs b/test/Ocelot.IntegrationTests/HeaderTests.cs index a57644f81..179bd6e27 100644 --- a/test/Ocelot.IntegrationTests/HeaderTests.cs +++ b/test/Ocelot.IntegrationTests/HeaderTests.cs @@ -1,27 +1,33 @@ -using Xunit; +using Xunit; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +using Ocelot.Configuration.File; + +using Ocelot.DependencyInjection; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; + +using Ocelot.Middleware; + +using Newtonsoft.Json; + +using Shouldly; + +using TestStack.BDDfy; [assembly: CollectionBehavior(DisableTestParallelization = true)] namespace Ocelot.IntegrationTests { - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.Configuration; - using Newtonsoft.Json; - using Ocelot.Configuration.File; - using Ocelot.DependencyInjection; - using Ocelot.Middleware; - using Shouldly; - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Net; - using System.Net.Http; - using System.Threading.Tasks; - using TestStack.BDDfy; - public class HeaderTests : IDisposable { private readonly HttpClient _httpClient; @@ -43,32 +49,32 @@ public void should_pass_remote_ip_address_if_as_x_forwarded_for_header() { var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", Port = 6773, - } + }, }, UpstreamPathTemplate = "/", UpstreamHttpMethod = new List { "Get" }, UpstreamHeaderTransform = new Dictionary { - {"X-Forwarded-For", "{RemoteIpAddress}"} + {"X-Forwarded-For", "{RemoteIpAddress}"}, }, HttpHandlerOptions = new FileHttpHandlerOptions { - AllowAutoRedirect = false - } - } - } + AllowAutoRedirect = false, + }, + }, + }, }; this.Given(x => GivenThereIsAServiceRunningOn("http://localhost:6773", 200, "X-Forwarded-For")) @@ -134,7 +140,7 @@ private void GivenOcelotIsRunning() _builder.Start(); } - private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + private static void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) { var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; @@ -161,7 +167,7 @@ private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) text = File.ReadAllText(configurationPath); } - public async Task WhenIGetUrlOnTheApiGateway(string url) + private async Task WhenIGetUrlOnTheApiGateway(string url) { var request = new HttpRequestMessage(HttpMethod.Get, url); _response = await _httpClient.SendAsync(request); @@ -179,12 +185,7 @@ private void ThenXForwardedForIsSet() var header = _response.Content.ReadAsStringAsync().Result; - bool passed = false; - - if (header == windowsOrMac || header == linux) - { - passed = true; - } + var passed = header == windowsOrMac || header == linux; passed.ShouldBeTrue(); } diff --git a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj index 96286ca43..3dfcaf800 100644 --- a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj +++ b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj @@ -1,61 +1,60 @@ - - - 0.0.0-dev - netcoreapp3.1 - Ocelot.IntegrationTests - Exe - Ocelot.IntegrationTests - true - win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 - false - false - false - ..\..\codeanalysis.ruleset - - - - PreserveNewest - - - - - - - - - - - - - - - - - - - - all - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + 0.0.0-dev + net7.0 + Ocelot.IntegrationTests + Exe + Ocelot.IntegrationTests + true + win10-x64;osx.10.11-x64;osx.10.12-x64;win7-x64 + false + false + false + ..\..\codeanalysis.ruleset + True + 1591 + + + + PreserveNewest + + + + + + + + + + + + + + + + + + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + diff --git a/test/Ocelot.IntegrationTests/RaftTests.cs b/test/Ocelot.IntegrationTests/RaftTests.cs deleted file mode 100644 index 4fbbb3dbb..000000000 --- a/test/Ocelot.IntegrationTests/RaftTests.cs +++ /dev/null @@ -1,513 +0,0 @@ -namespace Ocelot.IntegrationTests -{ - using Administration; - using Configuration.File; - using DependencyInjection; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Data.Sqlite; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Middleware; - using Newtonsoft.Json; - using Ocelot.Provider.Rafty; - using Rafty.Infrastructure; - using Shouldly; - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Net.Http; - using System.Net.Http.Headers; - using System.Threading; - using System.Threading.Tasks; - using Xunit; - using Xunit.Abstractions; - using Wait = Rafty.Infrastructure.Wait; - - public class RaftTests : IDisposable - { - private readonly List _builders; - private readonly List _webHostBuilders; - private readonly List _threads; - private FilePeers _peers; - private HttpClient _httpClient; - private readonly HttpClient _httpClientForAssertions; - private BearerToken _token; - private HttpResponseMessage _response; - private static readonly object _lock = new object(); - private ITestOutputHelper _output; - - public RaftTests(ITestOutputHelper output) - { - _output = output; - _httpClientForAssertions = new HttpClient(); - _webHostBuilders = new List(); - _builders = new List(); - _threads = new List(); - } - - [Fact(Skip = "Still not stable, more work required in rafty..")] - public async Task should_persist_command_to_five_servers() - { - var peers = new List - { - new FilePeer {HostAndPort = "http://localhost:5000"}, - - new FilePeer {HostAndPort = "http://localhost:5001"}, - - new FilePeer {HostAndPort = "http://localhost:5002"}, - - new FilePeer {HostAndPort = "http://localhost:5003"}, - - new FilePeer {HostAndPort = "http://localhost:5004"} - }; - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - } - }; - - var updatedConfiguration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "127.0.0.1", - Port = 80, - } - }, - DownstreamScheme = "http", - DownstreamPathTemplate = "/geoffrey", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/" - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "123.123.123", - Port = 443, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/blooper/{productId}", - UpstreamHttpMethod = new List { "post" }, - UpstreamPathTemplate = "/test" - } - } - }; - - var command = new UpdateFileConfiguration(updatedConfiguration); - GivenThePeersAre(peers); - GivenThereIsAConfiguration(configuration); - GivenFiveServersAreRunning(); - await GivenIHaveAnOcelotToken("/administration"); - await WhenISendACommandIntoTheCluster(command); - Thread.Sleep(5000); - await ThenTheCommandIsReplicatedToAllStateMachines(command); - } - - [Fact(Skip = "Still not stable, more work required in rafty..")] - public async Task should_persist_command_to_five_servers_when_using_administration_api() - { - var peers = new List - { - new FilePeer {HostAndPort = "http://localhost:5005"}, - - new FilePeer {HostAndPort = "http://localhost:5006"}, - - new FilePeer {HostAndPort = "http://localhost:5007"}, - - new FilePeer {HostAndPort = "http://localhost:5008"}, - - new FilePeer {HostAndPort = "http://localhost:5009"} - }; - - var configuration = new FileConfiguration - { - }; - - var updatedConfiguration = new FileConfiguration - { - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "127.0.0.1", - Port = 80, - } - }, - DownstreamScheme = "http", - DownstreamPathTemplate = "/geoffrey", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/" - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "123.123.123", - Port = 443, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/blooper/{productId}", - UpstreamHttpMethod = new List { "post" }, - UpstreamPathTemplate = "/test" - } - } - }; - - var command = new UpdateFileConfiguration(updatedConfiguration); - GivenThePeersAre(peers); - GivenThereIsAConfiguration(configuration); - GivenFiveServersAreRunning(); - await GivenIHaveAnOcelotToken("/administration"); - GivenIHaveAddedATokenToMyRequest(); - await WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration); - await ThenTheCommandIsReplicatedToAllStateMachines(command); - } - - private void GivenThePeersAre(List peers) - { - FilePeers filePeers = new FilePeers(); - filePeers.Peers.AddRange(peers); - var json = JsonConvert.SerializeObject(filePeers); - File.WriteAllText("peers.json", json); - _httpClient = new HttpClient(); - var ocelotBaseUrl = peers[0].HostAndPort; - _httpClient.BaseAddress = new Uri(ocelotBaseUrl); - } - - private async Task WhenISendACommandIntoTheCluster(UpdateFileConfiguration command) - { - async Task SendCommand() - { - try - { - var p = _peers.Peers.First(); - var json = JsonConvert.SerializeObject(command, new JsonSerializerSettings() - { - TypeNameHandling = TypeNameHandling.All - }); - var httpContent = new StringContent(json); - httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - using (var httpClient = new HttpClient()) - { - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - var response = await httpClient.PostAsync($"{p.HostAndPort}/administration/raft/command", httpContent); - response.EnsureSuccessStatusCode(); - var content = await response.Content.ReadAsStringAsync(); - - var errorResult = JsonConvert.DeserializeObject>(content); - - if (!string.IsNullOrEmpty(errorResult.Error)) - { - return false; - } - - var okResult = JsonConvert.DeserializeObject>(content); - - if (okResult.Command.Configuration.ReRoutes.Count == 2) - { - return true; - } - } - - return false; - } - catch (Exception e) - { - Console.WriteLine(e); - return false; - } - } - - var commandSent = await Wait.WaitFor(40000).Until(async () => - { - var result = await SendCommand(); - Thread.Sleep(1000); - return result; - }); - - commandSent.ShouldBeTrue(); - } - - private async Task ThenTheCommandIsReplicatedToAllStateMachines(UpdateFileConfiguration expecteds) - { - async Task CommandCalledOnAllStateMachines() - { - try - { - var passed = 0; - foreach (var peer in _peers.Peers) - { - var path = $"{peer.HostAndPort.Replace("/", "").Replace(":", "")}.db"; - using (var connection = new SqliteConnection($"Data Source={path};")) - { - connection.Open(); - var sql = @"select count(id) from logs"; - using (var command = new SqliteCommand(sql, connection)) - { - var index = Convert.ToInt32(command.ExecuteScalar()); - index.ShouldBe(1); - } - } - - _httpClientForAssertions.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - var result = await _httpClientForAssertions.GetAsync($"{peer.HostAndPort}/administration/configuration"); - var json = await result.Content.ReadAsStringAsync(); - var response = JsonConvert.DeserializeObject(json, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }); - response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.Configuration.GlobalConfiguration.RequestIdKey); - response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Host); - response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.Configuration.GlobalConfiguration.ServiceDiscoveryProvider.Port); - - for (var i = 0; i < response.ReRoutes.Count; i++) - { - for (var j = 0; j < response.ReRoutes[i].DownstreamHostAndPorts.Count; j++) - { - var res = response.ReRoutes[i].DownstreamHostAndPorts[j]; - var expected = expecteds.Configuration.ReRoutes[i].DownstreamHostAndPorts[j]; - res.Host.ShouldBe(expected.Host); - res.Port.ShouldBe(expected.Port); - } - - response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.Configuration.ReRoutes[i].DownstreamPathTemplate); - response.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.Configuration.ReRoutes[i].DownstreamScheme); - response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expecteds.Configuration.ReRoutes[i].UpstreamPathTemplate); - response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expecteds.Configuration.ReRoutes[i].UpstreamHttpMethod); - } - - passed++; - } - - return passed == 5; - } - catch (Exception e) - { - //_output.WriteLine($"{e.Message}, {e.StackTrace}"); - Console.WriteLine(e); - return false; - } - } - - var commandOnAllStateMachines = await Wait.WaitFor(40000).Until(async () => - { - var result = await CommandCalledOnAllStateMachines(); - Thread.Sleep(1000); - return result; - }); - - commandOnAllStateMachines.ShouldBeTrue(); - } - - private async Task WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration) - { - async Task SendCommand() - { - var json = JsonConvert.SerializeObject(updatedConfiguration); - - var content = new StringContent(json); - - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - - _response = await _httpClient.PostAsync(url, content); - - var responseContent = await _response.Content.ReadAsStringAsync(); - - if (responseContent == "There was a problem. This error message sucks raise an issue in GitHub.") - { - return false; - } - - if (string.IsNullOrEmpty(responseContent)) - { - return false; - } - - return _response.IsSuccessStatusCode; - } - - var commandSent = await Wait.WaitFor(40000).Until(async () => - { - var result = await SendCommand(); - Thread.Sleep(1000); - return result; - }); - - commandSent.ShouldBeTrue(); - } - - private void GivenIHaveAddedATokenToMyRequest() - { - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - } - - private async Task GivenIHaveAnOcelotToken(string adminPath) - { - async Task AddToken() - { - try - { - var tokenUrl = $"{adminPath}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", "admin"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "admin"), - new KeyValuePair("grant_type", "client_credentials") - }; - var content = new FormUrlEncodedContent(formData); - - var response = await _httpClient.PostAsync(tokenUrl, content); - var responseContent = await response.Content.ReadAsStringAsync(); - if (!response.IsSuccessStatusCode) - { - return false; - } - - _token = JsonConvert.DeserializeObject(responseContent); - var configPath = $"{adminPath}/.well-known/openid-configuration"; - response = await _httpClient.GetAsync(configPath); - return response.IsSuccessStatusCode; - } - catch (Exception) - { - return false; - } - } - - var addToken = await Wait.WaitFor(40000).Until(async () => - { - var result = await AddToken(); - Thread.Sleep(1000); - return result; - }); - - addToken.ShouldBeTrue(); - } - - private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) - { - var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; - - var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - - var text = File.ReadAllText(configurationPath); - - configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - - text = File.ReadAllText(configurationPath); - } - - private void GivenAServerIsRunning(string url) - { - lock (_lock) - { - IWebHostBuilder webHostBuilder = new WebHostBuilder(); - webHostBuilder.UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile("ocelot.json", false, false); - config.AddJsonFile("peers.json", optional: true, reloadOnChange: false); -#pragma warning disable CS0618 - config.AddOcelotBaseUrl(url); -#pragma warning restore CS0618 - config.AddEnvironmentVariables(); - }) - .ConfigureServices(x => - { - x.AddSingleton(new NodeId(url)); - x - .AddOcelot() - .AddAdministration("/administration", "secret") - .AddRafty(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - var builder = webHostBuilder.Build(); - builder.Start(); - - _webHostBuilders.Add(webHostBuilder); - _builders.Add(builder); - } - } - - private void GivenFiveServersAreRunning() - { - var bytes = File.ReadAllText("peers.json"); - _peers = JsonConvert.DeserializeObject(bytes); - - foreach (var peer in _peers.Peers) - { - File.Delete(peer.HostAndPort.Replace("/", "").Replace(":", "")); - File.Delete($"{peer.HostAndPort.Replace("/", "").Replace(":", "")}.db"); - var thread = new Thread(() => GivenAServerIsRunning(peer.HostAndPort)); - thread.Start(); - _threads.Add(thread); - } - } - - public void Dispose() - { - foreach (var builder in _builders) - { - builder?.Dispose(); - } - - foreach (var peer in _peers.Peers) - { - try - { - File.Delete(peer.HostAndPort.Replace("/", "").Replace(":", "")); - File.Delete($"{peer.HostAndPort.Replace("/", "").Replace(":", "")}.db"); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - } - } -} diff --git a/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs b/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs index d99eb2e08..75ded8237 100644 --- a/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs +++ b/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs @@ -1,19 +1,25 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Newtonsoft.Json; -using Ocelot.Configuration.File; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; -using Shouldly; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Net.Http; -using System.Threading.Tasks; -using TestStack.BDDfy; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; + +using Newtonsoft.Json; + +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; + +using Shouldly; + +using TestStack.BDDfy; + using Xunit; namespace Ocelot.IntegrationTests @@ -42,15 +48,15 @@ public void should_return_same_response_for_each_different_header_under_load_to_ { var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new() { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort + new() { Host = "localhost", Port = 51611, @@ -122,7 +128,7 @@ private void GivenOcelotIsRunning() _builder.Start(); } - private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + private static void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) { var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; @@ -153,7 +159,7 @@ private void WhenIGetUrlOnTheApiGatewayMultipleTimesWithDifferentHeaderValues(st { var tasks = new Task[times]; - for (int i = 0; i < times; i++) + for (var i = 0; i < times; i++) { var urlCopy = url; var random = _random.Next(0, 50); @@ -169,7 +175,7 @@ private async Task GetForThreadSafeHeadersTest(string url, int random) request.Headers.Add("ThreadSafeHeadersTest", new List { random.ToString() }); var response = await _httpClient.SendAsync(request); var content = await response.Content.ReadAsStringAsync(); - int result = int.Parse(content); + var result = int.Parse(content); var tshtr = new ThreadSafeHeadersTestResult(result, random); _results.Add(tshtr); } @@ -197,8 +203,8 @@ public ThreadSafeHeadersTestResult(int result, int random) Random = random; } - public int Result { get; private set; } - public int Random { get; private set; } + public int Result { get; } + public int Random { get; } } } } diff --git a/test/Ocelot.IntegrationTests/idsrv3test.pfx b/test/Ocelot.IntegrationTests/idsrv3test.pfx deleted file mode 100644 index 0247dea03..000000000 Binary files a/test/Ocelot.IntegrationTests/idsrv3test.pfx and /dev/null differ diff --git a/test/Ocelot.IntegrationTests/mycert.pfx b/test/Ocelot.IntegrationTests/mycert.pfx new file mode 100644 index 000000000..ead06a05d Binary files /dev/null and b/test/Ocelot.IntegrationTests/mycert.pfx differ diff --git a/test/Ocelot.IntegrationTests/ocelot.json b/test/Ocelot.IntegrationTests/ocelot.json index a4ca4ce5c..39b71c1bb 100644 --- a/test/Ocelot.IntegrationTests/ocelot.json +++ b/test/Ocelot.IntegrationTests/ocelot.json @@ -1,57 +1,57 @@ -{ - "ReRoutes": [ - { - "DownstreamPathTemplate": "/", - "UpstreamPathTemplate": "/", - "UpstreamHttpMethod": "Get", - "AuthenticationOptions": { - "Provider": null, - "ProviderRootUrl": null, - "ApiName": null, - "RequireHttps": false, - "AllowedScopes": [], - "ApiSecret": null - }, - "AddHeadersToRequest": {}, - "AddClaimsToRequest": {}, - "RouteClaimsRequirement": {}, - "AddQueriesToRequest": {}, - "RequestIdKey": null, - "FileCacheOptions": { "TtlSeconds": 0 }, - "ReRouteIsCaseSensitive": false, - "ServiceName": null, - "DownstreamScheme": "http", - "DownstreamHost": "localhost", - "DownstreamPort": 51879, - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 0, - "DurationOfBreak": 0, - "TimeoutValue": 0 - }, - "LoadBalancer": null, - "RateLimitOptions": { - "ClientWhitelist": [], - "EnableRateLimiting": false, - "Period": null, - "PeriodTimespan": 0.0, - "Limit": 0 - } - } - ], - "GlobalConfiguration": { - "RequestIdKey": null, - "ServiceDiscoveryProvider": { - "Provider": null, - "Host": null, - "Port": 0 - }, - "AdministrationPath": null, - "RateLimitOptions": { - "ClientIdHeader": "ClientId", - "QuotaExceededMessage": null, - "RateLimitCounterPrefix": "ocelot", - "DisableRateLimitHeaders": false, - "HttpStatusCode": 429 - } - } -} +{ + "Routes": [ + { + "DownstreamPathTemplate": "/", + "UpstreamPathTemplate": "/", + "UpstreamHttpMethod": "Get", + "AuthenticationOptions": { + "Provider": null, + "ProviderRootUrl": null, + "ApiName": null, + "RequireHttps": false, + "AllowedScopes": [], + "ApiSecret": null + }, + "AddHeadersToRequest": {}, + "AddClaimsToRequest": {}, + "RouteClaimsRequirement": {}, + "AddQueriesToRequest": {}, + "RequestIdKey": null, + "FileCacheOptions": { "TtlSeconds": 0 }, + "RouteIsCaseSensitive": false, + "ServiceName": null, + "DownstreamScheme": "http", + "DownstreamHost": "localhost", + "DownstreamPort": 51879, + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 0, + "DurationOfBreak": 0, + "TimeoutValue": 0 + }, + "LoadBalancer": null, + "RateLimitOptions": { + "ClientWhitelist": [], + "EnableRateLimiting": false, + "Period": null, + "PeriodTimespan": 0.0, + "Limit": 0 + } + } + ], + "GlobalConfiguration": { + "RequestIdKey": null, + "ServiceDiscoveryProvider": { + "Provider": null, + "Host": null, + "Port": 0 + }, + "AdministrationPath": null, + "RateLimitOptions": { + "ClientIdHeader": "ClientId", + "QuotaExceededMessage": null, + "RateLimitCounterPrefix": "ocelot", + "DisableRateLimitHeaders": false, + "HttpStatusCode": 429 + } + } +} diff --git a/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj b/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj index 2d31ecc36..675af8993 100644 --- a/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj +++ b/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj @@ -1,45 +1,47 @@ - - - 0.0.0-dev - netcoreapp3.1 - true - Ocelot.ManualTest - Exe - Ocelot.ManualTest - osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64 - ..\..\codeanalysis.ruleset - - - - PreserveNewest - - - - - PreserveNewest - - - - - PreserveNewest - - - - - - - - - - - - - - - all - - - - - - \ No newline at end of file + + + 0.0.0-dev + net7.0 + true + Ocelot.ManualTest + Exe + Ocelot.ManualTest + osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64 + ..\..\codeanalysis.ruleset + True + 1591 + + + + PreserveNewest + + + + + PreserveNewest + + + + + PreserveNewest + + + + + + + + + + + + + + + all + + + + + + diff --git a/test/Ocelot.ManualTest/Ocelot.postman_collection.json b/test/Ocelot.ManualTest/Ocelot.postman_collection.json index 76428fb96..a3affbf35 100644 --- a/test/Ocelot.ManualTest/Ocelot.postman_collection.json +++ b/test/Ocelot.ManualTest/Ocelot.postman_collection.json @@ -368,4 +368,4 @@ "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" } ] -} +} diff --git a/test/Ocelot.ManualTest/Program.cs b/test/Ocelot.ManualTest/Program.cs index 7d3fc969d..b7a0dded8 100644 --- a/test/Ocelot.ManualTest/Program.cs +++ b/test/Ocelot.ManualTest/Program.cs @@ -1,19 +1,21 @@ using Ocelot.Requester; +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Ocelot.DependencyInjection; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using Ocelot.Middleware; namespace Ocelot.ManualTest { - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Logging; - using Ocelot.DependencyInjection; - using Ocelot.Middleware; - using System; - using System.IO; - using System.Net.Http; - using System.Threading; - using System.Threading.Tasks; - public class Program { public static void Main(string[] args) @@ -33,25 +35,25 @@ public static void Main(string[] args) .ConfigureServices(s => { s.AddAuthentication(); - //.AddJwtBearer("TestKey", x => - //{ - // x.Authority = "test"; - // x.Audience = "test"; - //}); + /*.AddJwtBearer("TestKey", x => + { + x.Authority = "test"; + x.Audience = "test"; + });*/ s.AddSingleton((x, t) => new FakeHandler()); s.AddOcelot() .AddDelegatingHandler(true); - // .AddCacheManager(x => - // { - // x.WithDictionaryHandle(); - // }) - // .AddOpenTracing(option => - // { - // option.CollectorUrl = "http://localhost:9618"; - // option.Service = "Ocelot.ManualTest"; - // }) - // .AddAdministration("/administration", "secret"); + /*.AddCacheManager(x => + { + x.WithDictionaryHandle(); + }) + .AddOpenTracing(option => + { + option.CollectorUrl = "http://localhost:9618"; + option.Service = "Ocelot.ManualTest"; + }) + .AddAdministration("/administration", "secret");*/ }) .ConfigureLogging((hostingContext, logging) => { diff --git a/test/Ocelot.ManualTest/mycert.pfx b/test/Ocelot.ManualTest/mycert.pfx new file mode 100644 index 000000000..ead06a05d Binary files /dev/null and b/test/Ocelot.ManualTest/mycert.pfx differ diff --git a/test/Ocelot.ManualTest/ocelot.json b/test/Ocelot.ManualTest/ocelot.json index 7bb8978bd..df8b1f976 100644 --- a/test/Ocelot.ManualTest/ocelot.json +++ b/test/Ocelot.ManualTest/ocelot.json @@ -1,345 +1,345 @@ -{ - "ReRoutes": [ - { - "DownstreamPathTemplate": "/profile", - "DownstreamScheme": "http", - "UpstreamPathTemplate": "/profile", - "UpstreamHttpMethod": [ "Get" ], - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 5001 - } - ], - "QoSOptions": { - "TimeoutValue": 360000 - } - }, - { - "DownstreamPathTemplate": "/api/v1/todo/", - "DownstreamScheme": "http", - "UpstreamPathTemplate": "/api/v1/todo/", - "UpstreamHttpMethod": [ "Get", "Post" ], - "DownstreamHostAndPorts": [ - { - "Host": "lxtodo.azurewebsites.net", - "Port": 80 - } - - ], - "DownstreamHeaderTransform": { - "Location": "{DownstreamBaseUrl}, {BaseUrl}" - } - }, - { - "DownstreamPathTemplate": "/api/values", - "DownstreamScheme": "https", - "UpstreamPathTemplate": "/api/values", - "UpstreamHttpMethod": [ "Get" ], - "DownstreamHostAndPorts": [ - { - "Host": "testapivalues.azurewebsites.net", - "Port": 443 - } - ] - }, - { - "DownstreamPathTemplate": "/", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 52876 - } - ], - "UpstreamPathTemplate": "/identityserverexample", - "UpstreamHttpMethod": [ "Get" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - }, - "AuthenticationOptions": { - "AuthenticationProviderKey": "TestKey", - "AllowedScopes": [ - "openid", - "offline_access" - ] - }, - "AddHeadersToRequest": { - "CustomerId": "Claims[CustomerId] > value", - "LocationId": "Claims[LocationId] > value", - "UserType": "Claims[sub] > value[0] > |", - "UserId": "Claims[sub] > value[1] > |" - }, - "AddClaimsToRequest": { - "CustomerId": "Claims[CustomerId] > value", - "LocationId": "Claims[LocationId] > value", - "UserType": "Claims[sub] > value[0] > |", - "UserId": "Claims[sub] > value[1] > |" - }, - "AddQueriesToRequest": { - "CustomerId": "Claims[CustomerId] > value", - "LocationId": "Claims[LocationId] > value", - "UserType": "Claims[sub] > value[0] > |", - "UserId": "Claims[sub] > value[1] > |" - }, - "RouteClaimsRequirement": { - "UserType": "registered" - }, - "RequestIdKey": "OcRequestId" - }, - { - "DownstreamPathTemplate": "/posts", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 443 - } - ], - "UpstreamPathTemplate": "/posts", - "UpstreamHttpMethod": [ "Get" ], - "HttpHandlerOptions": { - "AllowAutoRedirect": true, - "UseCookieContainer": true - }, - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/posts/{postId}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/posts/{postId}", - "UpstreamHttpMethod": [ "Get" ], - "RequestIdKey": "ReRouteRequestId", - "HttpHandlerOptions": { - "AllowAutoRedirect": true, - "UseCookieContainer": true, - "UseTracing": true, - "UseProxy": true - }, - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/posts/{postId}/comments", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/posts/{postId}/comments", - "UpstreamHttpMethod": [ "Get" ], - "HttpHandlerOptions": { - "AllowAutoRedirect": true, - "UseCookieContainer": true, - "UseTracing": false - }, - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/comments", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/comments", - "UpstreamHttpMethod": [ "Get" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/posts", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/posts", - "UpstreamHttpMethod": [ "Post" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/posts/{postId}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/posts/{postId}", - "UpstreamHttpMethod": [ "Put" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/posts/{postId}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/posts/{postId}", - "UpstreamHttpMethod": [ "Patch" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/posts/{postId}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/posts/{postId}", - "UpstreamHttpMethod": [ "Delete" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/api/products", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/products", - "UpstreamHttpMethod": [ "Get" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - }, - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamPathTemplate": "/api/products/{productId}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/products/{productId}", - "UpstreamHttpMethod": [ "Get" ], - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamPathTemplate": "/api/products", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/products", - "UpstreamHttpMethod": [ "Post" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - } - }, - { - "DownstreamPathTemplate": "/api/products/{productId}", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/products/{productId}", - "UpstreamHttpMethod": [ "Put" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - }, - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamPathTemplate": "/posts", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "jsonplaceholder.typicode.com", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/posts/", - "UpstreamHttpMethod": [ "Get" ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 3, - "DurationOfBreak": 10, - "TimeoutValue": 5000 - }, - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamPathTemplate": "/", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "www.bbc.co.uk", - "Port": 80 - } - ], - "UpstreamPathTemplate": "/bbc/", - "UpstreamHttpMethod": [ "Get" ] - } - ], - - "GlobalConfiguration": { - "RequestIdKey": "ot-traceid" - } -} +{ + "Routes": [ + { + "DownstreamPathTemplate": "/profile", + "DownstreamScheme": "http", + "UpstreamPathTemplate": "/profile", + "UpstreamHttpMethod": [ "Get" ], + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 5001 + } + ], + "QoSOptions": { + "TimeoutValue": 360000 + } + }, + { + "DownstreamPathTemplate": "/api/v1/todo/", + "DownstreamScheme": "http", + "UpstreamPathTemplate": "/api/v1/todo/", + "UpstreamHttpMethod": [ "Get", "Post" ], + "DownstreamHostAndPorts": [ + { + "Host": "lxtodo.azurewebsites.net", + "Port": 80 + } + + ], + "DownstreamHeaderTransform": { + "Location": "{DownstreamBaseUrl}, {BaseUrl}" + } + }, + { + "DownstreamPathTemplate": "/api/values", + "DownstreamScheme": "https", + "UpstreamPathTemplate": "/api/values", + "UpstreamHttpMethod": [ "Get" ], + "DownstreamHostAndPorts": [ + { + "Host": "testapivalues.azurewebsites.net", + "Port": 443 + } + ] + }, + { + "DownstreamPathTemplate": "/", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 52876 + } + ], + "UpstreamPathTemplate": "/identityserverexample", + "UpstreamHttpMethod": [ "Get" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "AuthenticationOptions": { + "AuthenticationProviderKey": "TestKey", + "AllowedScopes": [ + "openid", + "offline_access" + ] + }, + "AddHeadersToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + "AddClaimsToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + "AddQueriesToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + "RouteClaimsRequirement": { + "UserType": "registered" + }, + "RequestIdKey": "OcRequestId" + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 443 + } + ], + "UpstreamPathTemplate": "/posts", + "UpstreamHttpMethod": [ "Get" ], + "HttpHandlerOptions": { + "AllowAutoRedirect": true, + "UseCookieContainer": true + }, + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": [ "Get" ], + "RequestIdKey": "RouteRequestId", + "HttpHandlerOptions": { + "AllowAutoRedirect": true, + "UseCookieContainer": true, + "UseTracing": true, + "UseProxy": true + }, + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}/comments", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/posts/{postId}/comments", + "UpstreamHttpMethod": [ "Get" ], + "HttpHandlerOptions": { + "AllowAutoRedirect": true, + "UseCookieContainer": true, + "UseTracing": false + }, + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/comments", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/comments", + "UpstreamHttpMethod": [ "Get" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/posts", + "UpstreamHttpMethod": [ "Post" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": [ "Put" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": [ "Patch" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": [ "Delete" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/api/products", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/products", + "UpstreamHttpMethod": [ "Get" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/products/{productId}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/products/{productId}", + "UpstreamHttpMethod": [ "Get" ], + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/products", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/products", + "UpstreamHttpMethod": [ "Post" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/api/products/{productId}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/products/{productId}", + "UpstreamHttpMethod": [ "Put" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/posts/", + "UpstreamHttpMethod": [ "Get" ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "www.bbc.co.uk", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/bbc/", + "UpstreamHttpMethod": [ "Get" ] + } + ], + + "GlobalConfiguration": { + "RequestIdKey": "ot-traceid" + } +} diff --git a/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs b/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs index e19025d1d..68f203648 100644 --- a/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs +++ b/test/Ocelot.UnitTests/Administration/OcelotAdministrationBuilderTests.cs @@ -1,101 +1,107 @@ -namespace Ocelot.UnitTests.Administration -{ - using IdentityServer4.AccessTokenValidation; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Moq; - using Ocelot.Administration; - using Ocelot.DependencyInjection; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Reflection; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Reflection; - public class OcelotAdministrationBuilderTests - { - private readonly IServiceCollection _services; - private IServiceProvider _serviceProvider; - private readonly IConfiguration _configRoot; - private IOcelotBuilder _ocelotBuilder; - private Exception _ex; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; - public OcelotAdministrationBuilderTests() - { - _configRoot = new ConfigurationRoot(new List()); - _services = new ServiceCollection(); - _services.AddSingleton(GetHostingEnvironment()); - _services.AddSingleton(_configRoot); - } - - private IWebHostEnvironment GetHostingEnvironment() - { - var environment = new Mock(); - environment - .Setup(e => e.ApplicationName) - .Returns(typeof(OcelotAdministrationBuilderTests).GetTypeInfo().Assembly.GetName().Name); - - return environment.Object; - } - - //keep - [Fact] - public void should_set_up_administration_with_identity_server_options() - { - Action options = o => { }; - - this.Given(x => WhenISetUpOcelotServices()) - .When(x => WhenISetUpAdministration(options)) - .Then(x => ThenAnExceptionIsntThrown()) - .Then(x => ThenTheCorrectAdminPathIsRegitered()) - .BDDfy(); - } +using Moq; - //keep - [Fact] - public void should_set_up_administration() - { - this.Given(x => WhenISetUpOcelotServices()) - .When(x => WhenISetUpAdministration()) - .Then(x => ThenAnExceptionIsntThrown()) - .Then(x => ThenTheCorrectAdminPathIsRegitered()) - .BDDfy(); - } - - private void WhenISetUpAdministration() - { - _ocelotBuilder.AddAdministration("/administration", "secret"); - } +using Ocelot.Administration; +using Ocelot.DependencyInjection; - private void WhenISetUpAdministration(Action options) - { - _ocelotBuilder.AddAdministration("/administration", options); - } +using Shouldly; - private void ThenTheCorrectAdminPathIsRegitered() - { - _serviceProvider = _services.BuildServiceProvider(); - var path = _serviceProvider.GetService(); - path.Path.ShouldBe("/administration"); - } +using TestStack.BDDfy; - private void WhenISetUpOcelotServices() - { - try - { - _ocelotBuilder = _services.AddOcelot(_configRoot); - } - catch (Exception e) - { - _ex = e; - } +using Xunit; + +namespace Ocelot.UnitTests.Administration +{ + public class OcelotAdministrationBuilderTests + { + private readonly IServiceCollection _services; + private IServiceProvider _serviceProvider; + private readonly IConfiguration _configRoot; + private IOcelotBuilder _ocelotBuilder; + private Exception _ex; + + public OcelotAdministrationBuilderTests() + { + _configRoot = new ConfigurationRoot(new List()); + _services = new ServiceCollection(); + _services.AddSingleton(GetHostingEnvironment()); + _services.AddSingleton(_configRoot); } - private void ThenAnExceptionIsntThrown() - { - _ex.ShouldBeNull(); - } - } -} + private static IWebHostEnvironment GetHostingEnvironment() + { + var environment = new Mock(); + environment + .Setup(e => e.ApplicationName) + .Returns(typeof(OcelotAdministrationBuilderTests).GetTypeInfo().Assembly.GetName().Name); + + return environment.Object; + } + + //keep + [Fact] + public void should_set_up_administration_with_identity_server_options() + { + Action options = o => { }; + + this.Given(x => WhenISetUpOcelotServices()) + .When(x => WhenISetUpAdministration(options)) + .Then(x => ThenAnExceptionIsntThrown()) + .Then(x => ThenTheCorrectAdminPathIsRegitered()) + .BDDfy(); + } + + //keep + [Fact] + public void should_set_up_administration() + { + this.Given(x => WhenISetUpOcelotServices()) + .When(x => WhenISetUpAdministration()) + .Then(x => ThenAnExceptionIsntThrown()) + .Then(x => ThenTheCorrectAdminPathIsRegitered()) + .BDDfy(); + } + + private void WhenISetUpAdministration() + { + _ocelotBuilder.AddAdministration("/administration", "secret"); + } + + private void WhenISetUpAdministration(Action options) + { + _ocelotBuilder.AddAdministration("/administration", options); + } + + private void ThenTheCorrectAdminPathIsRegitered() + { + _serviceProvider = _services.BuildServiceProvider(); + var path = _serviceProvider.GetService(); + path.Path.ShouldBe("/administration"); + } + + private void WhenISetUpOcelotServices() + { + try + { + _ocelotBuilder = _services.AddOcelot(_configRoot); + } + catch (Exception e) + { + _ex = e; + } + } + + private void ThenAnExceptionIsntThrown() + { + _ex.ShouldBeNull(); + } + } +} diff --git a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs index 467966f98..016a6b4c3 100644 --- a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs @@ -1,45 +1,51 @@ using Xunit; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.Authentication.Middleware; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; + +using Shouldly; + +using TestStack.BDDfy; [assembly: CollectionBehavior(DisableTestParallelization = true)] namespace Ocelot.UnitTests.Authentication { - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Authentication.Middleware; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Logging; - using Ocelot.Middleware; - using Shouldly; - using System.Collections.Generic; - using System.IO; - using System.Text; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - public class AuthenticationMiddlewareTests { private AuthenticationMiddleware _middleware; private readonly Mock _factory; - private Mock _logger; - private OcelotRequestDelegate _next; - private readonly DownstreamContext _downstreamContext; + private readonly Mock _logger; + private RequestDelegate _next; + private readonly HttpContext _httpContext; + private Mock _repo; public AuthenticationMiddlewareTests() { + _repo = new Mock(); + _httpContext = new DefaultHttpContext(); _factory = new Mock(); _logger = new Mock(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); } [Fact] public void should_call_next_middleware_if_route_is_not_authenticated() { this.Given(x => GivenTheDownStreamRouteIs( - new DownstreamReRouteBuilder().WithUpstreamHttpMethod(new List { "Get" }).Build())) + new DownstreamRouteBuilder().WithUpstreamHttpMethod(new List { "Get" }).Build())) .And(x => GivenTheTestServerPipelineIsConfigured()) .When(x => WhenICallTheMiddleware()) .Then(x => ThenTheUserIsAuthenticated()) @@ -50,7 +56,7 @@ public void should_call_next_middleware_if_route_is_not_authenticated() public void should_call_next_middleware_if_route_is_using_options_method() { this.Given(x => GivenTheDownStreamRouteIs( - new DownstreamReRouteBuilder() + new DownstreamRouteBuilder() .WithUpstreamHttpMethod(new List { "Options" }) .WithIsAuthenticated(true) .Build())) @@ -64,40 +70,40 @@ private void WhenICallTheMiddleware() { _next = (context) => { - byte[] byteArray = Encoding.ASCII.GetBytes("The user is authenticated"); + var byteArray = Encoding.ASCII.GetBytes("The user is authenticated"); var stream = new MemoryStream(byteArray); - context.HttpContext.Response.Body = stream; + _httpContext.Response.Body = stream; return Task.CompletedTask; }; _middleware = new AuthenticationMiddleware(_next, _factory.Object); - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); } private void GivenTheTestServerPipelineIsConfigured() { _next = (context) => { - byte[] byteArray = Encoding.ASCII.GetBytes("The user is authenticated"); + var byteArray = Encoding.ASCII.GetBytes("The user is authenticated"); var stream = new MemoryStream(byteArray); - context.HttpContext.Response.Body = stream; + _httpContext.Response.Body = stream; return Task.CompletedTask; }; } private void GivenTheRequestIsUsingOptionsMethod() { - _downstreamContext.HttpContext.Request.Method = "OPTIONS"; + _httpContext.Request.Method = "OPTIONS"; } private void ThenTheUserIsAuthenticated() { - var content = _downstreamContext.HttpContext.Response.Body.AsString(); + var content = _httpContext.Response.Body.AsString(); content.ShouldBe("The user is authenticated"); } - private void GivenTheDownStreamRouteIs(DownstreamReRoute downstreamRoute) + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) { - _downstreamContext.DownstreamReRoute = downstreamRoute; + _httpContext.Items.UpsertDownstreamRoute(downstreamRoute); } } @@ -107,7 +113,7 @@ public static string AsString(this Stream stream) { using (var reader = new StreamReader(stream)) { - string text = reader.ReadToEnd(); + var text = reader.ReadToEnd(); return text; } } diff --git a/test/Ocelot.UnitTests/Authorization/AuthorisationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authorization/AuthorisationMiddlewareTests.cs deleted file mode 100644 index eeb990499..000000000 --- a/test/Ocelot.UnitTests/Authorization/AuthorisationMiddlewareTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Ocelot.Middleware; - -namespace Ocelot.UnitTests.Authorization -{ - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Authorisation; - using Ocelot.Authorisation.Middleware; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.DownstreamRouteFinder.UrlMatcher; - using Ocelot.Logging; - using Ocelot.Responses; - using System.Collections.Generic; - using System.Security.Claims; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - - public class AuthorisationMiddlewareTests - { - private readonly Mock _authService; - private readonly Mock _authScopesService; - private Mock _loggerFactory; - private Mock _logger; - private readonly AuthorisationMiddleware _middleware; - private readonly DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; - - public AuthorisationMiddlewareTests() - { - _authService = new Mock(); - _authScopesService = new Mock(); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _next = context => Task.CompletedTask; - _middleware = new AuthorisationMiddleware(_next, _authService.Object, _authScopesService.Object, _loggerFactory.Object); - } - - [Fact] - public void should_call_authorisation_service() - { - this.Given(x => x.GivenTheDownStreamRouteIs(new List(), - new DownstreamReRouteBuilder() - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().Build()) - .WithIsAuthorised(true) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build())) - .And(x => x.GivenTheAuthServiceReturns(new OkResponse(true))) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheAuthServiceIsCalledCorrectly()) - .BDDfy(); - } - - private void WhenICallTheMiddleware() - { - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); - } - - private void GivenTheDownStreamRouteIs(List templatePlaceholderNameAndValues, DownstreamReRoute downstreamReRoute) - { - _downstreamContext.TemplatePlaceholderNameAndValues = templatePlaceholderNameAndValues; - _downstreamContext.DownstreamReRoute = downstreamReRoute; - } - - private void GivenTheAuthServiceReturns(Response expected) - { - _authService - .Setup(x => x.Authorise( - It.IsAny(), - It.IsAny>(), - It.IsAny>())) - .Returns(expected); - } - - private void ThenTheAuthServiceIsCalledCorrectly() - { - _authService - .Verify(x => x.Authorise( - It.IsAny(), - It.IsAny>(), - It.IsAny>()) - , Times.Once); - } - } -} diff --git a/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs new file mode 100644 index 000000000..cdff15da4 --- /dev/null +++ b/test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.Authorization; +using Ocelot.Authorization.Middleware; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Logging; +using Ocelot.Middleware; + +using Ocelot.Responses; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Authorization +{ + public class AuthorizationMiddlewareTests + { + private readonly Mock _authService; + private readonly Mock _authScopesService; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private readonly AuthorizationMiddleware _middleware; + private readonly RequestDelegate _next; + private readonly HttpContext _httpContext; + + public AuthorizationMiddlewareTests() + { + _httpContext = new DefaultHttpContext(); + _authService = new Mock(); + _authScopesService = new Mock(); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _next = context => Task.CompletedTask; + _middleware = new AuthorizationMiddleware(_next, _authService.Object, _authScopesService.Object, _loggerFactory.Object); + } + + [Fact] + public void should_call_authorization_service() + { + this.Given(x => x.GivenTheDownStreamRouteIs(new List(), + new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().Build()) + .WithIsAuthorized(true) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build())) + .And(x => x.GivenTheAuthServiceReturns(new OkResponse(true))) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheAuthServiceIsCalledCorrectly()) + .BDDfy(); + } + + private void WhenICallTheMiddleware() + { + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); + } + + private void GivenTheDownStreamRouteIs(List templatePlaceholderNameAndValues, DownstreamRoute downstreamRoute) + { + _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(templatePlaceholderNameAndValues); + _httpContext.Items.UpsertDownstreamRoute(downstreamRoute); + } + + private void GivenTheAuthServiceReturns(Response expected) + { + _authService + .Setup(x => x.Authorize( + It.IsAny(), + It.IsAny>(), + It.IsAny>())) + .Returns(expected); + } + + private void ThenTheAuthServiceIsCalledCorrectly() + { + _authService.Verify( + x => x.Authorize(It.IsAny(), It.IsAny>(), It.IsAny>()), + Times.Once); + } + } +} diff --git a/test/Ocelot.UnitTests/Authorization/ClaimsAuthoriserTests.cs b/test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs similarity index 61% rename from test/Ocelot.UnitTests/Authorization/ClaimsAuthoriserTests.cs rename to test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs index b22e7ffce..358231897 100644 --- a/test/Ocelot.UnitTests/Authorization/ClaimsAuthoriserTests.cs +++ b/test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs @@ -1,42 +1,45 @@ -using Ocelot.Authorisation; +using System.Collections.Generic; +using System.Security.Claims; + +using Ocelot.Authorization; using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.Responses; -using Shouldly; -using System.Collections.Generic; -using System.Security.Claims; -using TestStack.BDDfy; -using Xunit; +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; +using Ocelot.Infrastructure.Claims.Parser; namespace Ocelot.UnitTests.Authorization { - using Ocelot.Infrastructure.Claims.Parser; - - public class ClaimsAuthoriserTests + public class ClaimsAuthorizerTests { - private readonly ClaimsAuthoriser _claimsAuthoriser; + private readonly ClaimsAuthorizer _claimsAuthorizer; private ClaimsPrincipal _claimsPrincipal; private Dictionary _requirement; private List _urlPathPlaceholderNameAndValues; private Response _result; - public ClaimsAuthoriserTests() + public ClaimsAuthorizerTests() { - _claimsAuthoriser = new ClaimsAuthoriser(new ClaimsParser()); + _claimsAuthorizer = new ClaimsAuthorizer(new ClaimsParser()); } [Fact] - public void should_authorise_user() + public void should_authorize_user() { this.Given(x => x.GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List { - new Claim("UserType", "registered"), + new("UserType", "registered"), })))) .And(x => x.GivenARouteClaimsRequirement(new Dictionary { - {"UserType", "registered"} + {"UserType", "registered"}, })) - .When(x => x.WhenICallTheAuthoriser()) - .Then(x => x.ThenTheUserIsAuthorised()) + .When(x => x.WhenICallTheAuthorizer()) + .Then(x => x.ThenTheUserIsAuthorized()) .BDDfy(); } @@ -45,18 +48,18 @@ public void should_authorize_dynamic_user() { this.Given(x => x.GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List { - new Claim("userid", "14"), + new("userid", "14"), })))) .And(x => x.GivenARouteClaimsRequirement(new Dictionary { - {"userid", "{userId}"} + {"userid", "{userId}"}, })) .And(x => x.GivenAPlaceHolderNameAndValueList(new List { - new PlaceholderNameAndValue("{userId}", "14") + new("{userId}", "14"), })) - .When(x => x.WhenICallTheAuthoriser()) - .Then(x => x.ThenTheUserIsAuthorised()) + .When(x => x.WhenICallTheAuthorizer()) + .Then(x => x.ThenTheUserIsAuthorized()) .BDDfy(); } @@ -65,48 +68,48 @@ public void should_not_authorize_dynamic_user() { this.Given(x => x.GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List { - new Claim("userid", "15"), + new("userid", "15"), })))) .And(x => x.GivenARouteClaimsRequirement(new Dictionary { - {"userid", "{userId}"} + {"userid", "{userId}"}, })) .And(x => x.GivenAPlaceHolderNameAndValueList(new List { - new PlaceholderNameAndValue("{userId}", "14") + new("{userId}", "14"), })) - .When(x => x.WhenICallTheAuthoriser()) - .Then(x => x.ThenTheUserIsntAuthorised()) + .When(x => x.WhenICallTheAuthorizer()) + .Then(x => x.ThenTheUserIsntAuthorized()) .BDDfy(); - } + } [Fact] - public void should_authorise_user_multiple_claims_of_same_type() + public void should_authorize_user_multiple_claims_of_same_type() { this.Given(x => x.GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List { - new Claim("UserType", "guest"), - new Claim("UserType", "registered"), + new("UserType", "guest"), + new("UserType", "registered"), })))) .And(x => x.GivenARouteClaimsRequirement(new Dictionary { - {"UserType", "registered"} + {"UserType", "registered"}, })) - .When(x => x.WhenICallTheAuthoriser()) - .Then(x => x.ThenTheUserIsAuthorised()) + .When(x => x.WhenICallTheAuthorizer()) + .Then(x => x.ThenTheUserIsAuthorized()) .BDDfy(); } [Fact] - public void should_not_authorise_user() + public void should_not_authorize_user() { this.Given(x => x.GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List())))) .And(x => x.GivenARouteClaimsRequirement(new Dictionary { - { "UserType", "registered" } + { "UserType", "registered" }, })) - .When(x => x.WhenICallTheAuthoriser()) - .Then(x => x.ThenTheUserIsntAuthorised()) + .When(x => x.WhenICallTheAuthorizer()) + .Then(x => x.ThenTheUserIsntAuthorized()) .BDDfy(); } @@ -125,19 +128,19 @@ private void GivenAPlaceHolderNameAndValueList(List url _urlPathPlaceholderNameAndValues = urlPathPlaceholderNameAndValues; } - private void WhenICallTheAuthoriser() + private void WhenICallTheAuthorizer() { - _result = _claimsAuthoriser.Authorise(_claimsPrincipal, _requirement, _urlPathPlaceholderNameAndValues); + _result = _claimsAuthorizer.Authorize(_claimsPrincipal, _requirement, _urlPathPlaceholderNameAndValues); } - private void ThenTheUserIsAuthorised() + private void ThenTheUserIsAuthorized() { _result.Data.ShouldBe(true); } - private void ThenTheUserIsntAuthorised() + private void ThenTheUserIsntAuthorized() { _result.Data.ShouldBe(false); } } -} +} diff --git a/test/Ocelot.UnitTests/Cache/InMemoryCacheTests.cs b/test/Ocelot.UnitTests/Cache/AspMemoryCacheTests.cs similarity index 68% rename from test/Ocelot.UnitTests/Cache/InMemoryCacheTests.cs rename to test/Ocelot.UnitTests/Cache/AspMemoryCacheTests.cs index b9f5b7c07..91728e67d 100644 --- a/test/Ocelot.UnitTests/Cache/InMemoryCacheTests.cs +++ b/test/Ocelot.UnitTests/Cache/AspMemoryCacheTests.cs @@ -1,18 +1,23 @@ -namespace Ocelot.UnitTests.Cache -{ - using Ocelot.Cache; - using Shouldly; - using System; - using System.Threading; - using Xunit; +using System; +using System.Threading; + +using Microsoft.Extensions.Caching.Memory; + +using Ocelot.Cache; + +using Shouldly; - public class InMemoryCacheTests +using Xunit; + +namespace Ocelot.UnitTests.Cache +{ + public class AspMemoryCacheTests { - private readonly InMemoryCache _cache; + private readonly AspMemoryCache _cache; - public InMemoryCacheTests() + public AspMemoryCacheTests() { - _cache = new InMemoryCache(); + _cache = new AspMemoryCache(new MemoryCache(new MemoryCacheOptions())); } [Fact] @@ -25,6 +30,13 @@ public void should_cache() fake.Value.ShouldBe(1); } + [Fact] + public void doesnt_exist() + { + var result = _cache.Get("1", "region"); + result.ShouldBeNull(); + } + [Fact] public void should_add_and_delete() { @@ -40,11 +52,15 @@ public void should_add_and_delete() [Fact] public void should_clear_region() { - var fake = new Fake(1); - _cache.Add("1", fake, TimeSpan.FromSeconds(100), "region"); + var fake1 = new Fake(1); + var fake2 = new Fake(2); + _cache.Add("1", fake1, TimeSpan.FromSeconds(100), "region"); + _cache.Add("2", fake2, TimeSpan.FromSeconds(100), "region"); _cache.ClearRegion("region"); - var result = _cache.Get("1", "region"); - result.ShouldBeNull(); + var result1 = _cache.Get("1", "region"); + result1.ShouldBeNull(); + var result2 = _cache.Get("2", "region"); + result2.ShouldBeNull(); } [Fact] diff --git a/test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs b/test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs index 6dc51319e..18c4c5cb1 100644 --- a/test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs +++ b/test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs @@ -1,9 +1,12 @@ -using Microsoft.AspNetCore.Http; +using System.Net.Http; + using Ocelot.Cache; -using Ocelot.Middleware; +using Ocelot.Request.Middleware; + using Shouldly; -using System.Net.Http; + using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Cache @@ -11,29 +14,26 @@ namespace Ocelot.UnitTests.Cache public class CacheKeyGeneratorTests { private readonly ICacheKeyGenerator _cacheKeyGenerator; - private readonly DownstreamContext _downstreamContext; + private readonly DownstreamRequest _downstreamRequest; public CacheKeyGeneratorTests() { _cacheKeyGenerator = new CacheKeyGenerator(); _cacheKeyGenerator = new CacheKeyGenerator(); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()) - { - DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123")) - }; + _downstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123")); } [Fact] public void should_generate_cache_key_from_context() { - this.Given(x => x.GivenCacheKeyFromContext(_downstreamContext)) + this.Given(x => x.GivenCacheKeyFromContext(_downstreamRequest)) .BDDfy(); } - private void GivenCacheKeyFromContext(DownstreamContext context) + private void GivenCacheKeyFromContext(DownstreamRequest downstreamRequest) { - string generatedCacheKey = _cacheKeyGenerator.GenerateRequestCacheKey(context); - string cachekey = MD5Helper.GenerateMd5("GET-https://some.url/blah?abcd=123"); + var generatedCacheKey = _cacheKeyGenerator.GenerateRequestCacheKey(downstreamRequest); + var cachekey = MD5Helper.GenerateMd5("GET-https://some.url/blah?abcd=123"); generatedCacheKey.ShouldBe(cachekey); } } diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs index bb936f50a..d6e39e1e5 100644 --- a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs @@ -1,45 +1,51 @@ -namespace Ocelot.UnitTests.Cache -{ - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Cache; - using Ocelot.Cache.Middleware; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.DownstreamRouteFinder; - using Ocelot.DownstreamRouteFinder.UrlMatcher; - using Ocelot.Logging; - using Ocelot.Middleware; - using System; - using System.Collections.Generic; - using System.Net; - using System.Net.Http; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.Cache; +using Ocelot.Cache.Middleware; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; + +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Cache +{ public class OutputCacheMiddlewareTests { private readonly Mock> _cache; private readonly Mock _loggerFactory; - private Mock _logger; + private readonly Mock _logger; private OutputCacheMiddleware _middleware; - private readonly DownstreamContext _downstreamContext; - private readonly OcelotRequestDelegate _next; + private readonly RequestDelegate _next; private readonly ICacheKeyGenerator _cacheKeyGenerator; private CachedResponse _response; - + private readonly HttpContext _httpContext; + private Mock _repo; public OutputCacheMiddlewareTests() { + _repo = new Mock(); + _httpContext = new DefaultHttpContext(); _cache = new Mock>(); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); _loggerFactory = new Mock(); _logger = new Mock(); _cacheKeyGenerator = new CacheKeyGenerator(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; - _downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123")); + _httpContext.Items.UpsertDownstreamRequest(new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123"))); } [Fact] @@ -47,15 +53,15 @@ public void should_returned_cached_item_when_it_is_in_cache() { var headers = new Dictionary> { - { "test", new List { "test" } } + { "test", new List { "test" } }, }; var contentHeaders = new Dictionary> { - { "content-type", new List { "application/json" } } + { "content-type", new List { "application/json" } }, }; - var cachedResponse = new CachedResponse(HttpStatusCode.OK, headers, "", contentHeaders, "some reason"); + var cachedResponse = new CachedResponse(HttpStatusCode.OK, headers, string.Empty, contentHeaders, "some reason"); this.Given(x => x.GivenThereIsACachedResponse(cachedResponse)) .And(x => x.GivenTheDownstreamRouteIs()) .When(x => x.WhenICallTheMiddleware()) @@ -68,10 +74,10 @@ public void should_returned_cached_item_when_it_is_in_cache_expires_header() { var contentHeaders = new Dictionary> { - { "Expires", new List { "-1" } } + { "Expires", new List { "-1" } }, }; - var cachedResponse = new CachedResponse(HttpStatusCode.OK, new Dictionary>(), "", contentHeaders, "some reason"); + var cachedResponse = new CachedResponse(HttpStatusCode.OK, new Dictionary>(), string.Empty, contentHeaders, "some reason"); this.Given(x => x.GivenThereIsACachedResponse(cachedResponse)) .And(x => x.GivenTheDownstreamRouteIs()) .When(x => x.WhenICallTheMiddleware()) @@ -92,7 +98,7 @@ public void should_continue_with_pipeline_and_cache_response() private void WhenICallTheMiddleware() { _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cache.Object, _cacheKeyGenerator); - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); } private void GivenThereIsACachedResponse(CachedResponse response) @@ -105,13 +111,13 @@ private void GivenThereIsACachedResponse(CachedResponse response) private void GivenResponseIsNotCached(HttpResponseMessage responseMessage) { - _downstreamContext.DownstreamResponse = new DownstreamResponse(responseMessage); + _httpContext.Items.UpsertDownstreamResponse(new DownstreamResponse(responseMessage)); } private void GivenTheDownstreamRouteIs() { - var reRoute = new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() + var route = new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() .WithIsCached(true) .WithCacheOptions(new CacheOptions(100, "kanken")) .WithUpstreamHttpMethod(new List { "Get" }) @@ -119,10 +125,11 @@ private void GivenTheDownstreamRouteIs() .WithUpstreamHttpMethod(new List { "Get" }) .Build(); - var downstreamRoute = new DownstreamRoute(new List(), reRoute); + var downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), route); + + _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); - _downstreamContext.TemplatePlaceholderNameAndValues = downstreamRoute.TemplatePlaceholderNameAndValues; - _downstreamContext.DownstreamReRoute = downstreamRoute.ReRoute.DownstreamReRoute[0]; + _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); } private void ThenTheCacheGetIsCalledCorrectly() diff --git a/test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs b/test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs index ebcd6e25e..701c62de0 100644 --- a/test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs +++ b/test/Ocelot.UnitTests/Cache/RegionCreatorTests.cs @@ -1,8 +1,12 @@ +using System.Collections.Generic; + using Ocelot.Cache; -using Ocelot.Configuration.File; -using Shouldly; -using System.Collections.Generic; -using TestStack.BDDfy; +using Ocelot.Configuration.File; + +using Shouldly; + +using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Cache @@ -10,18 +14,18 @@ namespace Ocelot.UnitTests.Cache public class RegionCreatorTests { private string _result; - private FileReRoute _reRoute; + private FileRoute _route; [Fact] public void should_create_region() { - var reRoute = new FileReRoute + var route = new FileRoute { UpstreamHttpMethod = new List { "Get" }, - UpstreamPathTemplate = "/testdummy" + UpstreamPathTemplate = "/testdummy", }; - this.Given(_ => GivenTheReRoute(reRoute)) + this.Given(_ => GivenTheRoute(route)) .When(_ => WhenICreateTheRegion()) .Then(_ => ThenTheRegionIs("Gettestdummy")) .BDDfy(); @@ -30,29 +34,29 @@ public void should_create_region() [Fact] public void should_use_region() { - var reRoute = new FileReRoute + var route = new FileRoute { FileCacheOptions = new FileCacheOptions { - Region = "region" - } + Region = "region", + }, }; - this.Given(_ => GivenTheReRoute(reRoute)) + this.Given(_ => GivenTheRoute(route)) .When(_ => WhenICreateTheRegion()) .Then(_ => ThenTheRegionIs("region")) .BDDfy(); - } - - private void GivenTheReRoute(FileReRoute reRoute) + } + + private void GivenTheRoute(FileRoute route) { - _reRoute = reRoute; + _route = route; } private void WhenICreateTheRegion() - { - RegionCreator regionCreator = new RegionCreator(); - _result = regionCreator.Create(_reRoute); + { + var regionCreator = new RegionCreator(); + _result = regionCreator.Create(_route); } private void ThenTheRegionIs(string expected) @@ -60,4 +64,4 @@ private void ThenTheRegionIs(string expected) _result.ShouldBe(expected); } } -} +} diff --git a/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs index b85ee1f48..438a41cb9 100644 --- a/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/CacheManager/OcelotBuilderExtensionsTests.cs @@ -1,28 +1,26 @@ -namespace Ocelot.UnitTests.CacheManager -{ - using global::CacheManager.Core; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting.Internal; - using Moq; - using Ocelot.Cache; - using Ocelot.Cache.CacheManager; - using Ocelot.Configuration; - using Ocelot.Configuration.File; - using Ocelot.DependencyInjection; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - using TestStack.BDDfy; - using Xunit; +using global::CacheManager.Core; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Ocelot.Cache; +using Ocelot.Cache.CacheManager; +using Ocelot.Configuration; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using TestStack.BDDfy; +using Xunit; +namespace Ocelot.UnitTests.CacheManager +{ public class OcelotBuilderExtensionsTests { private readonly IServiceCollection _services; - private IServiceProvider _serviceProvider; private readonly IConfiguration _configRoot; private IOcelotBuilder _ocelotBuilder; private readonly int _maxRetries; @@ -32,12 +30,12 @@ public OcelotBuilderExtensionsTests() { _configRoot = new ConfigurationRoot(new List()); _services = new ServiceCollection(); - _services.AddSingleton(GetHostingEnvironment()); + _services.AddSingleton(GetHostingEnvironment()); _services.AddSingleton(_configRoot); _maxRetries = 100; } - private IWebHostEnvironment GetHostingEnvironment() + private static IWebHostEnvironment GetHostingEnvironment() { var environment = new Mock(); environment diff --git a/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs b/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs index 26432b326..14582040c 100644 --- a/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs +++ b/test/Ocelot.UnitTests/CacheManager/OcelotCacheManagerCache.cs @@ -1,17 +1,23 @@ -namespace Ocelot.UnitTests.CacheManager -{ - using global::CacheManager.Core; - using Moq; - using Ocelot.Cache.CacheManager; - using Shouldly; - using System; - using TestStack.BDDfy; - using Xunit; +using System; + +using global::CacheManager.Core; + +using Moq; + +using Ocelot.Cache.CacheManager; +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.CacheManager +{ public class OcelotCacheManagerCache { - private OcelotCacheManagerCache _ocelotOcelotCacheManager; - private Mock> _mockCacheManager; + private readonly OcelotCacheManagerCache _ocelotOcelotCacheManager; + private readonly Mock> _mockCacheManager; private string _key; private string _value; private string _resultGet; diff --git a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs index 0a8c23b6f..3f2fd9c71 100644 --- a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs +++ b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs @@ -1,96 +1,103 @@ -namespace Ocelot.UnitTests.CacheManager -{ - using global::CacheManager.Core; - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Cache; - using Ocelot.Cache.CacheManager; - using Ocelot.Cache.Middleware; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Logging; - using Ocelot.Middleware; - using Shouldly; - using System.Collections.Generic; - using System.Linq; - using System.Net; - using System.Net.Http; - using System.Net.Http.Headers; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; - public class OutputCacheMiddlewareRealCacheTests - { - private readonly IOcelotCache _cacheManager; - private readonly ICacheKeyGenerator _cacheKeyGenerator; - private readonly OutputCacheMiddleware _middleware; - private readonly DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; - private Mock _loggerFactory; - private Mock _logger; +using global::CacheManager.Core; - public OutputCacheMiddlewareRealCacheTests() - { - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - var cacheManagerOutputCache = CacheFactory.Build("OcelotOutputCache", x => - { - x.WithDictionaryHandle(); - }); - _cacheManager = new OcelotCacheManagerCache(cacheManagerOutputCache); - _cacheKeyGenerator = new CacheKeyGenerator(); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - _downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123")); - _next = context => Task.CompletedTask; - _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cacheManager, _cacheKeyGenerator); - } +using Microsoft.AspNetCore.Http; - [Fact] - public void should_cache_content_headers() - { - var content = new StringContent("{\"Test\": 1}") - { - Headers = { ContentType = new MediaTypeHeaderValue("application/json") } - }; +using Moq; - var response = new DownstreamResponse(content, HttpStatusCode.OK, new List>>(), "fooreason"); +using Ocelot.Cache; +using Ocelot.Cache.CacheManager; +using Ocelot.Cache.Middleware; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Middleware; - this.Given(x => x.GivenResponseIsNotCached(response)) - .And(x => x.GivenTheDownstreamRouteIs()) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheContentTypeHeaderIsCached()) - .BDDfy(); - } +using Shouldly; - private void WhenICallTheMiddleware() - { - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); - } +using TestStack.BDDfy; - private void ThenTheContentTypeHeaderIsCached() - { - string cacheKey = MD5Helper.GenerateMd5("GET-https://some.url/blah?abcd=123"); - var result = _cacheManager.Get(cacheKey, "kanken"); - var header = result.ContentHeaders["Content-Type"]; - header.First().ShouldBe("application/json"); - } - - private void GivenResponseIsNotCached(DownstreamResponse response) - { - _downstreamContext.DownstreamResponse = response; - } - - private void GivenTheDownstreamRouteIs() - { - var reRoute = new DownstreamReRouteBuilder() - .WithIsCached(true) - .WithCacheOptions(new CacheOptions(100, "kanken")) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build(); - - _downstreamContext.DownstreamReRoute = reRoute; - } - } -} +using Xunit; + +namespace Ocelot.UnitTests.CacheManager +{ + public class OutputCacheMiddlewareRealCacheTests + { + private readonly IOcelotCache _cacheManager; + private readonly ICacheKeyGenerator _cacheKeyGenerator; + private readonly OutputCacheMiddleware _middleware; + private readonly RequestDelegate _next; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private readonly HttpContext _httpContext; + + public OutputCacheMiddlewareRealCacheTests() + { + _httpContext = new DefaultHttpContext(); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + var cacheManagerOutputCache = CacheFactory.Build("OcelotOutputCache", x => + { + x.WithDictionaryHandle(); + }); + _cacheManager = new OcelotCacheManagerCache(cacheManagerOutputCache); + _cacheKeyGenerator = new CacheKeyGenerator(); + _httpContext.Items.UpsertDownstreamRequest(new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123"))); + _next = context => Task.CompletedTask; + _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cacheManager, _cacheKeyGenerator); + } + + [Fact] + public void should_cache_content_headers() + { + var content = new StringContent("{\"Test\": 1}") + { + Headers = { ContentType = new MediaTypeHeaderValue("application/json") }, + }; + + var response = new DownstreamResponse(content, HttpStatusCode.OK, new List>>(), "fooreason"); + + this.Given(x => x.GivenResponseIsNotCached(response)) + .And(x => x.GivenTheDownstreamRouteIs()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheContentTypeHeaderIsCached()) + .BDDfy(); + } + + private void WhenICallTheMiddleware() + { + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); + } + + private void ThenTheContentTypeHeaderIsCached() + { + var cacheKey = MD5Helper.GenerateMd5("GET-https://some.url/blah?abcd=123"); + var result = _cacheManager.Get(cacheKey, "kanken"); + var header = result.ContentHeaders["Content-Type"]; + header.First().ShouldBe("application/json"); + } + + private void GivenResponseIsNotCached(DownstreamResponse response) + { + _httpContext.Items.UpsertDownstreamResponse(response); + } + + private void GivenTheDownstreamRouteIs() + { + var route = new DownstreamRouteBuilder() + .WithIsCached(true) + .WithCacheOptions(new CacheOptions(100, "kanken")) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build(); + + _httpContext.Items.UpsertDownstreamRoute(route); + } + } +} diff --git a/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs b/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs index cc6428f4e..e1c4e1c8a 100644 --- a/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs +++ b/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs @@ -1,142 +1,148 @@ -using Microsoft.AspNetCore.Http; -using Moq; -using Ocelot.Claims; -using Ocelot.Configuration; -using Ocelot.Errors; -using Ocelot.Infrastructure.Claims.Parser; -using Ocelot.Responses; -using Shouldly; -using System.Collections.Generic; -using System.Security.Claims; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Claims -{ - public class AddClaimsToRequestTests - { - private readonly AddClaimsToRequest _addClaimsToRequest; - private readonly Mock _parser; - private List _claimsToThings; - private HttpContext _context; - private Response _result; - private Response _claimValue; - - public AddClaimsToRequestTests() - { - _parser = new Mock(); - _addClaimsToRequest = new AddClaimsToRequest(_parser.Object); - } - - [Fact] - public void should_add_claims_to_context() - { - var context = new DefaultHttpContext - { - User = new ClaimsPrincipal(new ClaimsIdentity(new List - { - new Claim("test", "data") - })) - }; - - this.Given( - x => x.GivenClaimsToThings(new List - { - new ClaimToThing("claim-key", "", "", 0) - })) - .Given(x => x.GivenHttpContext(context)) - .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) - .When(x => x.WhenIAddClaimsToTheRequest()) - .Then(x => x.ThenTheResultIsSuccess()) - .BDDfy(); - } - - [Fact] - public void if_claims_exists_should_replace_it() - { - var context = new DefaultHttpContext - { - User = new ClaimsPrincipal(new ClaimsIdentity(new List - { - new Claim("existing-key", "data"), - new Claim("new-key", "data") - })), - }; - - this.Given( - x => x.GivenClaimsToThings(new List - { - new ClaimToThing("existing-key", "new-key", "", 0) - })) - .Given(x => x.GivenHttpContext(context)) - .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) - .When(x => x.WhenIAddClaimsToTheRequest()) - .Then(x => x.ThenTheResultIsSuccess()) - .BDDfy(); - } - - [Fact] - public void should_return_error() - { - this.Given( - x => x.GivenClaimsToThings(new List - { - new ClaimToThing("", "", "", 0) - })) - .Given(x => x.GivenHttpContext(new DefaultHttpContext())) - .And(x => x.GivenTheClaimParserReturns(new ErrorResponse(new List - { - new AnyError() - }))) - .When(x => x.WhenIAddClaimsToTheRequest()) - .Then(x => x.ThenTheResultIsError()) - .BDDfy(); - } - - private void GivenClaimsToThings(List configuration) - { - _claimsToThings = configuration; - } - - private void GivenHttpContext(HttpContext context) - { - _context = context; - } - - private void GivenTheClaimParserReturns(Response claimValue) - { - _claimValue = claimValue; - _parser - .Setup( - x => - x.GetValue(It.IsAny>(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(_claimValue); - } - - private void WhenIAddClaimsToTheRequest() - { - _result = _addClaimsToRequest.SetClaimsOnContext(_claimsToThings, _context); - } - - private void ThenTheResultIsSuccess() - { - _result.IsError.ShouldBe(false); - } - - private void ThenTheResultIsError() - { - _result.IsError.ShouldBe(true); - } - - private class AnyError : Error - { - public AnyError() - : base("blahh", OcelotErrorCode.UnknownError) - { - } - } - } +using System.Collections.Generic; +using System.Security.Claims; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.Claims; +using Ocelot.Configuration; +using Ocelot.Errors; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Claims +{ + public class AddClaimsToRequestTests + { + private readonly AddClaimsToRequest _addClaimsToRequest; + private readonly Mock _parser; + private List _claimsToThings; + private HttpContext _context; + private Response _result; + private Response _claimValue; + + public AddClaimsToRequestTests() + { + _parser = new Mock(); + _addClaimsToRequest = new AddClaimsToRequest(_parser.Object); + } + + [Fact] + public void should_add_claims_to_context() + { + var context = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new List + { + new("test", "data"), + })), + }; + + this.Given( + x => x.GivenClaimsToThings(new List + { + new("claim-key", string.Empty, string.Empty, 0), + })) + .Given(x => x.GivenHttpContext(context)) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .When(x => x.WhenIAddClaimsToTheRequest()) + .Then(x => x.ThenTheResultIsSuccess()) + .BDDfy(); + } + + [Fact] + public void if_claims_exists_should_replace_it() + { + var context = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new List + { + new("existing-key", "data"), + new("new-key", "data"), + })), + }; + + this.Given( + x => x.GivenClaimsToThings(new List + { + new("existing-key", "new-key", string.Empty, 0), + })) + .Given(x => x.GivenHttpContext(context)) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .When(x => x.WhenIAddClaimsToTheRequest()) + .Then(x => x.ThenTheResultIsSuccess()) + .BDDfy(); + } + + [Fact] + public void should_return_error() + { + this.Given( + x => x.GivenClaimsToThings(new List + { + new(string.Empty, string.Empty, string.Empty, 0), + })) + .Given(x => x.GivenHttpContext(new DefaultHttpContext())) + .And(x => x.GivenTheClaimParserReturns(new ErrorResponse(new List + { + new AnyError(), + }))) + .When(x => x.WhenIAddClaimsToTheRequest()) + .Then(x => x.ThenTheResultIsError()) + .BDDfy(); + } + + private void GivenClaimsToThings(List configuration) + { + _claimsToThings = configuration; + } + + private void GivenHttpContext(HttpContext context) + { + _context = context; + } + + private void GivenTheClaimParserReturns(Response claimValue) + { + _claimValue = claimValue; + _parser + .Setup( + x => + x.GetValue(It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(_claimValue); + } + + private void WhenIAddClaimsToTheRequest() + { + _result = _addClaimsToRequest.SetClaimsOnContext(_claimsToThings, _context); + } + + private void ThenTheResultIsSuccess() + { + _result.IsError.ShouldBe(false); + } + + private void ThenTheResultIsError() + { + _result.IsError.ShouldBe(true); + } + + private class AnyError : Error + { + public AnyError() + : base("blahh", OcelotErrorCode.UnknownError, 404) + { + } + } + } } diff --git a/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs b/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs index 7652aeee1..ac616f09d 100644 --- a/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs @@ -1,35 +1,39 @@ using Ocelot.Middleware; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.Claims; +using Ocelot.Claims.Middleware; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Logging; + +using Ocelot.Responses; + +using TestStack.BDDfy; + +using Xunit; namespace Ocelot.UnitTests.Claims { - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Claims; - using Ocelot.Claims.Middleware; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.DownstreamRouteFinder; - using Ocelot.DownstreamRouteFinder.UrlMatcher; - using Ocelot.Logging; - using Ocelot.Responses; - using System.Collections.Generic; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - public class ClaimsToClaimsMiddlewareTests { private readonly Mock _addHeaders; - private Mock _loggerFactory; - private Mock _logger; + private readonly Mock _loggerFactory; + private readonly Mock _logger; private readonly ClaimsToClaimsMiddleware _middleware; - private readonly DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; + private readonly RequestDelegate _next; + private readonly HttpContext _httpContext; public ClaimsToClaimsMiddlewareTests() { + _httpContext = new DefaultHttpContext(); _addHeaders = new Mock(); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); @@ -40,13 +44,13 @@ public ClaimsToClaimsMiddlewareTests() [Fact] public void should_call_claims_to_request_correctly() { - var downstreamRoute = new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() + var downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithClaimsToClaims(new List { - new ClaimToThing("sub", "UserType", "|", 0) + new("sub", "UserType", "|", 0), }) .WithUpstreamHttpMethod(new List { "Get" }) .Build()) @@ -62,13 +66,14 @@ public void should_call_claims_to_request_correctly() private void WhenICallTheMiddleware() { - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); } - private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + private void GivenTheDownStreamRouteIs(Ocelot.DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute) { - _downstreamContext.TemplatePlaceholderNameAndValues = downstreamRoute.TemplatePlaceholderNameAndValues; - _downstreamContext.DownstreamReRoute = downstreamRoute.ReRoute.DownstreamReRoute[0]; + _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + + _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); } private void GivenTheAddClaimsToRequestReturns() diff --git a/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs index 7395e1b3e..872b5b7d0 100644 --- a/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs @@ -1,164 +1,170 @@ -namespace Ocelot.UnitTests.Configuration -{ - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Configuration.Creator; - using Ocelot.Configuration.File; - using Shouldly; - using System.Collections.Generic; - using System.Net.Http; - using TestStack.BDDfy; - using Values; - using Xunit; - - public class AggregatesCreatorTests - { - private readonly AggregatesCreator _creator; - private readonly Mock _utpCreator; - private FileConfiguration _fileConfiguration; - private List _reRoutes; - private List _result; - private UpstreamPathTemplate _aggregate1Utp; - private UpstreamPathTemplate _aggregate2Utp; - - public AggregatesCreatorTests() - { - _utpCreator = new Mock(); - _creator = new AggregatesCreator(_utpCreator.Object); - } - - [Fact] - public void should_return_no_aggregates() - { - var fileConfig = new FileConfiguration - { - Aggregates = new List - { - new FileAggregateReRoute - { - ReRouteKeys = new List{"key1"} - } - } - }; - var reRoutes = new List(); - - this.Given(_ => GivenThe(fileConfig)) - .And(_ => GivenThe(reRoutes)) - .When(_ => WhenICreate()) - .Then(_ => TheUtpCreatorIsNotCalled()) - .And(_ => ThenTheResultIsNotNull()) - .And(_ => ThenTheResultIsEmpty()) - .BDDfy(); - } - - [Fact] - public void should_create_aggregates() - { - var fileConfig = new FileConfiguration - { - Aggregates = new List - { - new FileAggregateReRoute - { - ReRouteKeys = new List{"key1", "key2"}, - UpstreamHost = "hosty", - UpstreamPathTemplate = "templatey", - Aggregator = "aggregatory", - ReRouteIsCaseSensitive = true - }, - new FileAggregateReRoute - { - ReRouteKeys = new List{"key3", "key4"}, - UpstreamHost = "hosty", - UpstreamPathTemplate = "templatey", - Aggregator = "aggregatory", - ReRouteIsCaseSensitive = true - } - } - }; - - var reRoutes = new List - { - new ReRouteBuilder().WithDownstreamReRoute(new DownstreamReRouteBuilder().WithKey("key1").Build()).Build(), - new ReRouteBuilder().WithDownstreamReRoute(new DownstreamReRouteBuilder().WithKey("key2").Build()).Build(), - new ReRouteBuilder().WithDownstreamReRoute(new DownstreamReRouteBuilder().WithKey("key3").Build()).Build(), - new ReRouteBuilder().WithDownstreamReRoute(new DownstreamReRouteBuilder().WithKey("key4").Build()).Build() - }; - - this.Given(_ => GivenThe(fileConfig)) - .And(_ => GivenThe(reRoutes)) - .And(_ => GivenTheUtpCreatorReturns()) - .When(_ => WhenICreate()) - .Then(_ => ThenTheUtpCreatorIsCalledCorrectly()) - .And(_ => ThenTheAggregatesAreCreated()) - .BDDfy(); - } - - private void ThenTheAggregatesAreCreated() - { - _result.ShouldNotBeNull(); - _result.Count.ShouldBe(2); - - _result[0].UpstreamHttpMethod.ShouldContain(x => x == HttpMethod.Get); - _result[0].UpstreamHost.ShouldBe(_fileConfiguration.Aggregates[0].UpstreamHost); - _result[0].UpstreamTemplatePattern.ShouldBe(_aggregate1Utp); - _result[0].Aggregator.ShouldBe(_fileConfiguration.Aggregates[0].Aggregator); - _result[0].DownstreamReRoute.ShouldContain(x => x == _reRoutes[0].DownstreamReRoute[0]); - _result[0].DownstreamReRoute.ShouldContain(x => x == _reRoutes[1].DownstreamReRoute[0]); - - _result[1].UpstreamHttpMethod.ShouldContain(x => x == HttpMethod.Get); - _result[1].UpstreamHost.ShouldBe(_fileConfiguration.Aggregates[1].UpstreamHost); - _result[1].UpstreamTemplatePattern.ShouldBe(_aggregate2Utp); - _result[1].Aggregator.ShouldBe(_fileConfiguration.Aggregates[1].Aggregator); - _result[1].DownstreamReRoute.ShouldContain(x => x == _reRoutes[2].DownstreamReRoute[0]); - _result[1].DownstreamReRoute.ShouldContain(x => x == _reRoutes[3].DownstreamReRoute[0]); - } - - private void ThenTheUtpCreatorIsCalledCorrectly() - { - _utpCreator.Verify(x => x.Create(_fileConfiguration.Aggregates[0]), Times.Once); - _utpCreator.Verify(x => x.Create(_fileConfiguration.Aggregates[1]), Times.Once); - } - - private void GivenTheUtpCreatorReturns() - { - _aggregate1Utp = new UpstreamPathTemplateBuilder().Build(); - _aggregate2Utp = new UpstreamPathTemplateBuilder().Build(); - - _utpCreator.SetupSequence(x => x.Create(It.IsAny())) - .Returns(_aggregate1Utp) - .Returns(_aggregate2Utp); - } - - private void ThenTheResultIsEmpty() - { - _result.Count.ShouldBe(0); - } - - private void ThenTheResultIsNotNull() - { - _result.ShouldNotBeNull(); - } - - private void TheUtpCreatorIsNotCalled() - { - _utpCreator.Verify(x => x.Create(It.IsAny()), Times.Never); - } - - private void GivenThe(FileConfiguration fileConfiguration) - { - _fileConfiguration = fileConfiguration; - } - - private void GivenThe(List reRoutes) - { - _reRoutes = reRoutes; - } - - private void WhenICreate() - { - _result = _creator.Create(_fileConfiguration, _reRoutes); - } - } -} +using System.Collections.Generic; +using System.Net.Http; + +using Moq; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; + +using Shouldly; + +using TestStack.BDDfy; + +using Ocelot.Values; + +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class AggregatesCreatorTests + { + private readonly AggregatesCreator _creator; + private readonly Mock _utpCreator; + private FileConfiguration _fileConfiguration; + private List _routes; + private List _result; + private UpstreamPathTemplate _aggregate1Utp; + private UpstreamPathTemplate _aggregate2Utp; + + public AggregatesCreatorTests() + { + _utpCreator = new Mock(); + _creator = new AggregatesCreator(_utpCreator.Object); + } + + [Fact] + public void should_return_no_aggregates() + { + var fileConfig = new FileConfiguration + { + Aggregates = new List + { + new() + { + RouteKeys = new List{"key1"}, + }, + }, + }; + var routes = new List(); + + this.Given(_ => GivenThe(fileConfig)) + .And(_ => GivenThe(routes)) + .When(_ => WhenICreate()) + .Then(_ => TheUtpCreatorIsNotCalled()) + .And(_ => ThenTheResultIsNotNull()) + .And(_ => ThenTheResultIsEmpty()) + .BDDfy(); + } + + [Fact] + public void should_create_aggregates() + { + var fileConfig = new FileConfiguration + { + Aggregates = new List + { + new() + { + RouteKeys = new List{"key1", "key2"}, + UpstreamHost = "hosty", + UpstreamPathTemplate = "templatey", + Aggregator = "aggregatory", + RouteIsCaseSensitive = true, + }, + new() + { + RouteKeys = new List{"key3", "key4"}, + UpstreamHost = "hosty", + UpstreamPathTemplate = "templatey", + Aggregator = "aggregatory", + RouteIsCaseSensitive = true, + }, + }, + }; + + var routes = new List + { + new RouteBuilder().WithDownstreamRoute(new DownstreamRouteBuilder().WithKey("key1").Build()).Build(), + new RouteBuilder().WithDownstreamRoute(new DownstreamRouteBuilder().WithKey("key2").Build()).Build(), + new RouteBuilder().WithDownstreamRoute(new DownstreamRouteBuilder().WithKey("key3").Build()).Build(), + new RouteBuilder().WithDownstreamRoute(new DownstreamRouteBuilder().WithKey("key4").Build()).Build(), + }; + + this.Given(_ => GivenThe(fileConfig)) + .And(_ => GivenThe(routes)) + .And(_ => GivenTheUtpCreatorReturns()) + .When(_ => WhenICreate()) + .Then(_ => ThenTheUtpCreatorIsCalledCorrectly()) + .And(_ => ThenTheAggregatesAreCreated()) + .BDDfy(); + } + + private void ThenTheAggregatesAreCreated() + { + _result.ShouldNotBeNull(); + _result.Count.ShouldBe(2); + + _result[0].UpstreamHttpMethod.ShouldContain(x => x == HttpMethod.Get); + _result[0].UpstreamHost.ShouldBe(_fileConfiguration.Aggregates[0].UpstreamHost); + _result[0].UpstreamTemplatePattern.ShouldBe(_aggregate1Utp); + _result[0].Aggregator.ShouldBe(_fileConfiguration.Aggregates[0].Aggregator); + _result[0].DownstreamRoute.ShouldContain(x => x == _routes[0].DownstreamRoute[0]); + _result[0].DownstreamRoute.ShouldContain(x => x == _routes[1].DownstreamRoute[0]); + + _result[1].UpstreamHttpMethod.ShouldContain(x => x == HttpMethod.Get); + _result[1].UpstreamHost.ShouldBe(_fileConfiguration.Aggregates[1].UpstreamHost); + _result[1].UpstreamTemplatePattern.ShouldBe(_aggregate2Utp); + _result[1].Aggregator.ShouldBe(_fileConfiguration.Aggregates[1].Aggregator); + _result[1].DownstreamRoute.ShouldContain(x => x == _routes[2].DownstreamRoute[0]); + _result[1].DownstreamRoute.ShouldContain(x => x == _routes[3].DownstreamRoute[0]); + } + + private void ThenTheUtpCreatorIsCalledCorrectly() + { + _utpCreator.Verify(x => x.Create(_fileConfiguration.Aggregates[0]), Times.Once); + _utpCreator.Verify(x => x.Create(_fileConfiguration.Aggregates[1]), Times.Once); + } + + private void GivenTheUtpCreatorReturns() + { + _aggregate1Utp = new UpstreamPathTemplateBuilder().Build(); + _aggregate2Utp = new UpstreamPathTemplateBuilder().Build(); + + _utpCreator.SetupSequence(x => x.Create(It.IsAny())) + .Returns(_aggregate1Utp) + .Returns(_aggregate2Utp); + } + + private void ThenTheResultIsEmpty() + { + _result.Count.ShouldBe(0); + } + + private void ThenTheResultIsNotNull() + { + _result.ShouldNotBeNull(); + } + + private void TheUtpCreatorIsNotCalled() + { + _utpCreator.Verify(x => x.Create(It.IsAny()), Times.Never); + } + + private void GivenThe(FileConfiguration fileConfiguration) + { + _fileConfiguration = fileConfiguration; + } + + private void GivenThe(List routes) + { + _routes = routes; + } + + private void WhenICreate() + { + _result = _creator.Create(_fileConfiguration, _routes); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs index 1056dca59..9869b7bf7 100644 --- a/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/AuthenticationOptionsCreatorTests.cs @@ -1,62 +1,66 @@ -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; -using Shouldly; -using System.Collections.Generic; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Configuration -{ - public class AuthenticationOptionsCreatorTests - { - private readonly AuthenticationOptionsCreator _authOptionsCreator; - private FileReRoute _fileReRoute; - private AuthenticationOptions _result; - - public AuthenticationOptionsCreatorTests() - { - _authOptionsCreator = new AuthenticationOptionsCreator(); - } - - [Fact] - public void should_return_auth_options() - { - var fileReRoute = new FileReRoute() - { - AuthenticationOptions = new FileAuthenticationOptions - { - AuthenticationProviderKey = "Test", - AllowedScopes = new List { "cheese" }, - } - }; - - var expected = new AuthenticationOptionsBuilder() - .WithAllowedScopes(fileReRoute.AuthenticationOptions?.AllowedScopes) - .WithAuthenticationProviderKey("Test") - .Build(); - - this.Given(x => x.GivenTheFollowing(fileReRoute)) - .When(x => x.WhenICreateTheAuthenticationOptions()) - .Then(x => x.ThenTheFollowingConfigIsReturned(expected)) - .BDDfy(); - } - - private void GivenTheFollowing(FileReRoute fileReRoute) - { - _fileReRoute = fileReRoute; - } - - private void WhenICreateTheAuthenticationOptions() - { - _result = _authOptionsCreator.Create(_fileReRoute); - } - - private void ThenTheFollowingConfigIsReturned(AuthenticationOptions expected) +using System.Collections.Generic; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class AuthenticationOptionsCreatorTests + { + private readonly AuthenticationOptionsCreator _authOptionsCreator; + private FileRoute _fileRoute; + private AuthenticationOptions _result; + + public AuthenticationOptionsCreatorTests() { - _result.AllowedScopes.ShouldBe(expected.AllowedScopes); - _result.AuthenticationProviderKey.ShouldBe(expected.AuthenticationProviderKey); - } - } + _authOptionsCreator = new AuthenticationOptionsCreator(); + } + + [Fact] + public void should_return_auth_options() + { + var fileRoute = new FileRoute + { + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + AllowedScopes = new List { "cheese" }, + }, + }; + + var expected = new AuthenticationOptionsBuilder() + .WithAllowedScopes(fileRoute.AuthenticationOptions?.AllowedScopes) + .WithAuthenticationProviderKey("Test") + .Build(); + + this.Given(x => x.GivenTheFollowing(fileRoute)) + .When(x => x.WhenICreateTheAuthenticationOptions()) + .Then(x => x.ThenTheFollowingConfigIsReturned(expected)) + .BDDfy(); + } + + private void GivenTheFollowing(FileRoute fileRoute) + { + _fileRoute = fileRoute; + } + + private void WhenICreateTheAuthenticationOptions() + { + _result = _authOptionsCreator.Create(_fileRoute); + } + + private void ThenTheFollowingConfigIsReturned(AuthenticationOptions expected) + { + _result.AllowedScopes.ShouldBe(expected.AllowedScopes); + _result.AuthenticationProviderKey.ShouldBe(expected.AuthenticationProviderKey); + } + } } diff --git a/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs b/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs new file mode 100644 index 000000000..081ec33e0 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs @@ -0,0 +1,38 @@ +using Ocelot.Configuration.ChangeTracking; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Configuration.ChangeTracking +{ + public class OcelotConfigurationChangeTokenSourceTests + { + private readonly IOcelotConfigurationChangeTokenSource _source; + + public OcelotConfigurationChangeTokenSourceTests() + { + _source = new OcelotConfigurationChangeTokenSource(); + } + + [Fact] + public void should_activate_change_token() + { + this.Given(_ => GivenIActivateTheChangeTokenSource()) + .Then(_ => ThenTheChangeTokenShouldBeActivated()) + .BDDfy(); + } + + private void GivenIActivateTheChangeTokenSource() + { + _source.Activate(); + } + + private void ThenTheChangeTokenShouldBeActivated() + { + _source.ChangeToken.HasChanged.ShouldBeTrue(); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs b/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs new file mode 100644 index 000000000..a3489f452 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs @@ -0,0 +1,93 @@ +using Xunit; +using System; + +using Ocelot.Configuration.ChangeTracking; + +using Shouldly; + +using TestStack.BDDfy; + +namespace Ocelot.UnitTests.Configuration.ChangeTracking +{ + public class OcelotConfigurationChangeTokenTests + { + [Fact] + public void should_call_callback_with_state() + { + this.Given(_ => GivenIHaveAChangeToken()) + .And(_ => AndIRegisterACallback()) + .Then(_ => ThenIShouldGetADisposableWrapper()) + .Given(_ => GivenIActivateTheToken()) + .Then(_ => ThenTheCallbackShouldBeCalled()) + .BDDfy(); + } + + [Fact] + public void should_not_call_callback_if_it_is_disposed() + { + this.Given(_ => GivenIHaveAChangeToken()) + .And(_ => AndIRegisterACallback()) + .Then(_ => ThenIShouldGetADisposableWrapper()) + .And(_ => GivenIActivateTheToken()) + .And(_ => AndIDisposeTheCallbackWrapper()) + .And(_ => GivenIActivateTheToken()) + .Then(_ => ThenTheCallbackShouldNotBeCalled()) + .BDDfy(); + } + + private OcelotConfigurationChangeToken _changeToken; + private IDisposable _callbackWrapper; + private int _callbackCounter; + private readonly object _callbackInitialState = new(); + private object _callbackState; + + private void Callback(object state) + { + _callbackCounter++; + _callbackState = state; + _changeToken.HasChanged.ShouldBeTrue(); + } + + private void GivenIHaveAChangeToken() + { + _changeToken = new OcelotConfigurationChangeToken(); + } + + private void AndIRegisterACallback() + { + _callbackWrapper = _changeToken.RegisterChangeCallback(Callback, _callbackInitialState); + } + + private void ThenIShouldGetADisposableWrapper() + { + _callbackWrapper.ShouldNotBeNull(); + } + + private void GivenIActivateTheToken() + { + _callbackCounter = 0; + _callbackState = null; + _changeToken.Activate(); + } + + private void ThenTheCallbackShouldBeCalled() + { + _callbackCounter.ShouldBe(1); + _callbackState.ShouldNotBeNull(); + _callbackState.ShouldBeSameAs(_callbackInitialState); + } + + private void ThenTheCallbackShouldNotBeCalled() + { + _callbackCounter.ShouldBe(0); + _callbackState.ShouldBeNull(); + } + + private void AndIDisposeTheCallbackWrapper() + { + _callbackState = null; + _callbackCounter = 0; + _callbackWrapper.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs b/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs index db1f881ae..902f74772 100644 --- a/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs @@ -1,11 +1,15 @@ -using Ocelot.Configuration; +using System.Collections.Generic; +using System.Linq; + +using Ocelot.Configuration; using Ocelot.Configuration.Parser; using Ocelot.Errors; -using Ocelot.Responses; -using Shouldly; -using System.Collections.Generic; -using System.Linq; -using TestStack.BDDfy; +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Configuration @@ -24,9 +28,9 @@ public ClaimToThingConfigurationParserTests() [Fact] public void returns_no_instructions_error() { - this.Given(x => x.GivenTheDictionaryIs(new Dictionary() + this.Given(x => x.GivenTheDictionaryIs(new Dictionary { - {"CustomerId", ""}, + {"CustomerId", string.Empty}, })) .When(x => x.WhenICallTheExtractor()) .Then( @@ -34,7 +38,7 @@ public void returns_no_instructions_error() x.ThenAnErrorIsReturned(new ErrorResponse( new List { - new NoInstructionsError(">") + new NoInstructionsError(">"), }))) .BDDfy(); } @@ -42,7 +46,7 @@ public void returns_no_instructions_error() [Fact] public void returns_no_instructions_not_for_claims_error() { - this.Given(x => x.GivenTheDictionaryIs(new Dictionary() + this.Given(x => x.GivenTheDictionaryIs(new Dictionary { {"CustomerId", "Cheese[CustomerId] > value"}, })) @@ -52,7 +56,7 @@ public void returns_no_instructions_not_for_claims_error() x.ThenAnErrorIsReturned(new ErrorResponse( new List { - new InstructionNotForClaimsError() + new InstructionNotForClaimsError(), }))) .BDDfy(); } @@ -60,7 +64,7 @@ public void returns_no_instructions_not_for_claims_error() [Fact] public void can_parse_entry_to_work_out_properties_with_key() { - this.Given(x => x.GivenTheDictionaryIs(new Dictionary() + this.Given(x => x.GivenTheDictionaryIs(new Dictionary { {"CustomerId", "Claims[CustomerId] > value"}, })) @@ -69,14 +73,14 @@ public void can_parse_entry_to_work_out_properties_with_key() x => x.ThenTheClaimParserPropertiesAreReturned( new OkResponse( - new ClaimToThing("CustomerId", "CustomerId", "", 0)))) + new ClaimToThing("CustomerId", "CustomerId", string.Empty, 0)))) .BDDfy(); } [Fact] public void can_parse_entry_to_work_out_properties_with_key_delimiter_and_index() { - this.Given(x => x.GivenTheDictionaryIs(new Dictionary() + this.Given(x => x.GivenTheDictionaryIs(new Dictionary { {"UserId", "Claims[Subject] > value[0] > |"}, })) diff --git a/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs index 8f2a64f2e..235d3f34b 100644 --- a/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs @@ -1,14 +1,19 @@ -using Moq; +using System.Collections.Generic; +using System.Linq; + +using Moq; + using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.Parser; using Ocelot.Errors; using Ocelot.Logging; -using Ocelot.Responses; -using Shouldly; -using System.Collections.Generic; -using System.Linq; -using TestStack.BDDfy; +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Configuration @@ -17,10 +22,10 @@ public class ClaimsToThingCreatorTests { private readonly Mock _configParser; private Dictionary _claimsToThings; - private ClaimsToThingCreator _claimsToThingsCreator; - private Mock _loggerFactory; + private readonly ClaimsToThingCreator _claimsToThingsCreator; + private readonly Mock _loggerFactory; private List _result; - private Mock _logger; + private readonly Mock _logger; public ClaimsToThingCreatorTests() { @@ -36,12 +41,12 @@ public ClaimsToThingCreatorTests() [Fact] public void should_return_claims_to_things() { - var userInput = new Dictionary() + var userInput = new Dictionary { - {"CustomerId", "Claims[CustomerId] > value"} + {"CustomerId", "Claims[CustomerId] > value"}, }; - var claimsToThing = new OkResponse(new ClaimToThing("CustomerId", "CustomerId", "", 0)); + var claimsToThing = new OkResponse(new ClaimToThing("CustomerId", "CustomerId", string.Empty, 0)); this.Given(x => x.GivenTheFollowingDictionary(userInput)) .And(x => x.GivenTheConfigHeaderExtractorReturns(claimsToThing)) @@ -54,9 +59,9 @@ public void should_return_claims_to_things() [Fact] public void should_log_error_if_cannot_parse_claim_to_thing() { - var userInput = new Dictionary() + var userInput = new Dictionary { - {"CustomerId", "Claims[CustomerId] > value"} + {"CustomerId", "Claims[CustomerId] > value"}, }; var claimsToThing = new ErrorResponse(It.IsAny()); diff --git a/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs index a51ec161e..6df7e70df 100644 --- a/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs @@ -1,124 +1,132 @@ -namespace Ocelot.UnitTests.Configuration -{ - using Microsoft.Extensions.DependencyInjection; - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Creator; - using Ocelot.Configuration.File; - using Ocelot.DependencyInjection; - using Shouldly; - using System.Collections.Generic; - using TestStack.BDDfy; - using Xunit; +using System.Collections.Generic; - public class ConfigurationCreatorTests - { - private ConfigurationCreator _creator; - private InternalConfiguration _result; - private readonly Mock _spcCreator; - private readonly Mock _qosCreator; - private readonly Mock _hhoCreator; - private readonly Mock _lboCreator; - private FileConfiguration _fileConfig; - private List _reRoutes; - private ServiceProviderConfiguration _spc; - private LoadBalancerOptions _lbo; - private QoSOptions _qoso; - private HttpHandlerOptions _hho; - private AdministrationPath _adminPath; - private readonly ServiceCollection _serviceCollection; +using Microsoft.Extensions.DependencyInjection; - public ConfigurationCreatorTests() - { - _lboCreator = new Mock(); - _hhoCreator = new Mock(); - _qosCreator = new Mock(); - _spcCreator = new Mock(); - _serviceCollection = new ServiceCollection(); - } +using Moq; - [Fact] - public void should_build_configuration_with_no_admin_path() - { - this.Given(_ => GivenTheDependenciesAreSetUp()) - .When(_ => WhenICreate()) - .Then(_ => ThenTheDepdenciesAreCalledCorrectly()) - .And(_ => ThenThePropertiesAreSetCorrectly()) - .And(_ => ThenTheAdminPathIsNull()) - .BDDfy(); - } +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; - [Fact] - public void should_build_configuration_with_admin_path() - { - this.Given(_ => GivenTheDependenciesAreSetUp()) - .And(_ => GivenTheAdminPath()) - .When(_ => WhenICreate()) - .Then(_ => ThenTheDepdenciesAreCalledCorrectly()) - .And(_ => ThenThePropertiesAreSetCorrectly()) - .And(_ => ThenTheAdminPathIsSet()) - .BDDfy(); - } +using Shouldly; - private void ThenTheAdminPathIsNull() - { - _result.AdministrationPath.ShouldBeNull(); - } +using TestStack.BDDfy; - private void ThenThePropertiesAreSetCorrectly() - { - _result.ShouldNotBeNull(); - _result.ServiceProviderConfiguration.ShouldBe(_spc); - _result.LoadBalancerOptions.ShouldBe(_lbo); - _result.QoSOptions.ShouldBe(_qoso); - _result.HttpHandlerOptions.ShouldBe(_hho); - _result.ReRoutes.ShouldBe(_reRoutes); - _result.RequestId.ShouldBe(_fileConfig.GlobalConfiguration.RequestIdKey); - _result.DownstreamScheme.ShouldBe(_fileConfig.GlobalConfiguration.DownstreamScheme); - } - - private void ThenTheAdminPathIsSet() - { - _result.AdministrationPath.ShouldBe("wooty"); - } - - private void ThenTheDepdenciesAreCalledCorrectly() - { - _spcCreator.Verify(x => x.Create(_fileConfig.GlobalConfiguration), Times.Once); - _lboCreator.Verify(x => x.Create(_fileConfig.GlobalConfiguration.LoadBalancerOptions), Times.Once); - _qosCreator.Verify(x => x.Create(_fileConfig.GlobalConfiguration.QoSOptions), Times.Once); - _hhoCreator.Verify(x => x.Create(_fileConfig.GlobalConfiguration.HttpHandlerOptions), Times.Once); - } - - private void GivenTheAdminPath() - { - _adminPath = new AdministrationPath("wooty"); - _serviceCollection.AddSingleton(_adminPath); - } - - private void GivenTheDependenciesAreSetUp() - { - _fileConfig = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration() - }; - _reRoutes = new List(); - _spc = new ServiceProviderConfiguration("", "", 1, "", "", 1); - _lbo = new LoadBalancerOptionsBuilder().Build(); - _qoso = new QoSOptions(1, 1, 1, ""); - _hho = new HttpHandlerOptionsBuilder().Build(); - - _spcCreator.Setup(x => x.Create(It.IsAny())).Returns(_spc); - _lboCreator.Setup(x => x.Create(It.IsAny())).Returns(_lbo); - _qosCreator.Setup(x => x.Create(It.IsAny())).Returns(_qoso); - _hhoCreator.Setup(x => x.Create(It.IsAny())).Returns(_hho); - } - - private void WhenICreate() - { - var serviceProvider = _serviceCollection.BuildServiceProvider(); - _creator = new ConfigurationCreator(_spcCreator.Object, _qosCreator.Object, _hhoCreator.Object, serviceProvider, _lboCreator.Object); - _result = _creator.Create(_fileConfig, _reRoutes); - } - } -} +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class ConfigurationCreatorTests + { + private ConfigurationCreator _creator; + private InternalConfiguration _result; + private readonly Mock _spcCreator; + private readonly Mock _qosCreator; + private readonly Mock _hhoCreator; + private readonly Mock _lboCreator; + private readonly Mock _vCreator; + private FileConfiguration _fileConfig; + private List _routes; + private ServiceProviderConfiguration _spc; + private LoadBalancerOptions _lbo; + private QoSOptions _qoso; + private HttpHandlerOptions _hho; + private AdministrationPath _adminPath; + private readonly ServiceCollection _serviceCollection; + + public ConfigurationCreatorTests() + { + _vCreator = new Mock(); + _lboCreator = new Mock(); + _hhoCreator = new Mock(); + _qosCreator = new Mock(); + _spcCreator = new Mock(); + _serviceCollection = new ServiceCollection(); + } + + [Fact] + public void should_build_configuration_with_no_admin_path() + { + this.Given(_ => GivenTheDependenciesAreSetUp()) + .When(_ => WhenICreate()) + .Then(_ => ThenTheDepdenciesAreCalledCorrectly()) + .And(_ => ThenThePropertiesAreSetCorrectly()) + .And(_ => ThenTheAdminPathIsNull()) + .BDDfy(); + } + + [Fact] + public void should_build_configuration_with_admin_path() + { + this.Given(_ => GivenTheDependenciesAreSetUp()) + .And(_ => GivenTheAdminPath()) + .When(_ => WhenICreate()) + .Then(_ => ThenTheDepdenciesAreCalledCorrectly()) + .And(_ => ThenThePropertiesAreSetCorrectly()) + .And(_ => ThenTheAdminPathIsSet()) + .BDDfy(); + } + + private void ThenTheAdminPathIsNull() + { + _result.AdministrationPath.ShouldBeNull(); + } + + private void ThenThePropertiesAreSetCorrectly() + { + _result.ShouldNotBeNull(); + _result.ServiceProviderConfiguration.ShouldBe(_spc); + _result.LoadBalancerOptions.ShouldBe(_lbo); + _result.QoSOptions.ShouldBe(_qoso); + _result.HttpHandlerOptions.ShouldBe(_hho); + _result.Routes.ShouldBe(_routes); + _result.RequestId.ShouldBe(_fileConfig.GlobalConfiguration.RequestIdKey); + _result.DownstreamScheme.ShouldBe(_fileConfig.GlobalConfiguration.DownstreamScheme); + } + + private void ThenTheAdminPathIsSet() + { + _result.AdministrationPath.ShouldBe("wooty"); + } + + private void ThenTheDepdenciesAreCalledCorrectly() + { + _spcCreator.Verify(x => x.Create(_fileConfig.GlobalConfiguration), Times.Once); + _lboCreator.Verify(x => x.Create(_fileConfig.GlobalConfiguration.LoadBalancerOptions), Times.Once); + _qosCreator.Verify(x => x.Create(_fileConfig.GlobalConfiguration.QoSOptions), Times.Once); + _hhoCreator.Verify(x => x.Create(_fileConfig.GlobalConfiguration.HttpHandlerOptions), Times.Once); + } + + private void GivenTheAdminPath() + { + _adminPath = new AdministrationPath("wooty"); + _serviceCollection.AddSingleton(_adminPath); + } + + private void GivenTheDependenciesAreSetUp() + { + _fileConfig = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration(), + }; + _routes = new List(); + _spc = new ServiceProviderConfiguration(string.Empty, string.Empty, string.Empty, 1, string.Empty, string.Empty, 1); + _lbo = new LoadBalancerOptionsBuilder().Build(); + _qoso = new QoSOptions(1, 1, 1, string.Empty); + _hho = new HttpHandlerOptionsBuilder().Build(); + + _spcCreator.Setup(x => x.Create(It.IsAny())).Returns(_spc); + _lboCreator.Setup(x => x.Create(It.IsAny())).Returns(_lbo); + _qosCreator.Setup(x => x.Create(It.IsAny())).Returns(_qoso); + _hhoCreator.Setup(x => x.Create(It.IsAny())).Returns(_hho); + } + + private void WhenICreate() + { + var serviceProvider = _serviceCollection.BuildServiceProvider(); + _creator = new ConfigurationCreator(_spcCreator.Object, _qosCreator.Object, _hhoCreator.Object, serviceProvider, _lboCreator.Object, _vCreator.Object); + _result = _creator.Create(_fileConfig, _routes); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs index b2eb06b51..c273173a7 100644 --- a/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs @@ -1,288 +1,309 @@ -namespace Ocelot.UnitTests.Configuration -{ - using Microsoft.AspNetCore.Hosting; - using Moq; - using Newtonsoft.Json; - using Ocelot.Configuration.File; - using Ocelot.Configuration.Repository; - using Shouldly; - using System; - using System.Collections.Generic; - using System.IO; - using System.Threading; - using TestStack.BDDfy; - using Xunit; - - public class DiskFileConfigurationRepositoryTests : IDisposable - { - private readonly Mock _hostingEnvironment; - private IFileConfigurationRepository _repo; - private string _environmentSpecificPath; - private string _ocelotJsonPath; - private FileConfiguration _result; - private FileConfiguration _fileConfiguration; - - // This is a bit dirty and it is dev.dev so that the ConfigurationBuilderExtensionsTests - // cant pick it up if they run in parralel..and the semaphore stops them running at the same time...sigh - // these are not really unit tests but whatever... - private string _environmentName = "DEV.DEV"; - - private static SemaphoreSlim _semaphore; - - public DiskFileConfigurationRepositoryTests() - { - _semaphore = new SemaphoreSlim(1, 1); - _semaphore.Wait(); - _hostingEnvironment = new Mock(); - _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName); - _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object); - } - - [Fact] - public void should_return_file_configuration() - { - var config = FakeFileConfigurationForGet(); - - this.Given(_ => GivenTheConfigurationIs(config)) - .When(_ => WhenIGetTheReRoutes()) - .Then(_ => ThenTheFollowingIsReturned(config)) - .BDDfy(); - } - - [Fact] - public void should_return_file_configuration_if_environment_name_is_unavailable() - { - var config = FakeFileConfigurationForGet(); - - this.Given(_ => GivenTheEnvironmentNameIsUnavailable()) - .And(_ => GivenTheConfigurationIs(config)) - .When(_ => WhenIGetTheReRoutes()) - .Then(_ => ThenTheFollowingIsReturned(config)) - .BDDfy(); - } - - [Fact] - public void should_set_file_configuration() - { - var config = FakeFileConfigurationForSet(); - - this.Given(_ => GivenIHaveAConfiguration(config)) - .When(_ => WhenISetTheConfiguration()) - .Then(_ => ThenTheConfigurationIsStoredAs(config)) - .And(_ => ThenTheConfigurationJsonIsIndented(config)) - .BDDfy(); - } - - [Fact] - public void should_set_file_configuration_if_environment_name_is_unavailable() - { - var config = FakeFileConfigurationForSet(); - - this.Given(_ => GivenIHaveAConfiguration(config)) - .And(_ => GivenTheEnvironmentNameIsUnavailable()) - .When(_ => WhenISetTheConfiguration()) - .Then(_ => ThenTheConfigurationIsStoredAs(config)) - .And(_ => ThenTheConfigurationJsonIsIndented(config)) - .BDDfy(); - } - - [Fact] - public void should_set_environment_file_configuration_and_ocelot_file_configuration() - { - var config = FakeFileConfigurationForSet(); - - this.Given(_ => GivenIHaveAConfiguration(config)) - .And(_ => GivenTheConfigurationIs(config)) - .And(_ => GivenTheUserAddedOcelotJson()) - .When(_ => WhenISetTheConfiguration()) - .Then(_ => ThenTheConfigurationIsStoredAs(config)) - .And(_ => ThenTheConfigurationJsonIsIndented(config)) - .Then(_ => ThenTheOcelotJsonIsStoredAs(config)) - .BDDfy(); - } - - private void GivenTheUserAddedOcelotJson() - { - _ocelotJsonPath = $"{AppContext.BaseDirectory}/ocelot.json"; - - if (File.Exists(_ocelotJsonPath)) - { - File.Delete(_ocelotJsonPath); - } - - File.WriteAllText(_ocelotJsonPath, "Doesnt matter"); - } - - private void GivenTheEnvironmentNameIsUnavailable() - { - _environmentName = null; - _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName); - _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object); - } - - private void GivenIHaveAConfiguration(FileConfiguration fileConfiguration) - { - _fileConfiguration = fileConfiguration; - } - - private void WhenISetTheConfiguration() - { - _repo.Set(_fileConfiguration); - _result = _repo.Get().Result.Data; - } - - private void ThenTheConfigurationIsStoredAs(FileConfiguration expecteds) - { - _result.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey); - _result.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host); - _result.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port); - - for (var i = 0; i < _result.ReRoutes.Count; i++) - { - for (int j = 0; j < _result.ReRoutes[i].DownstreamHostAndPorts.Count; j++) - { - var result = _result.ReRoutes[i].DownstreamHostAndPorts[j]; - var expected = expecteds.ReRoutes[i].DownstreamHostAndPorts[j]; - - result.Host.ShouldBe(expected.Host); - result.Port.ShouldBe(expected.Port); - } - - _result.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].DownstreamPathTemplate); - _result.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.ReRoutes[i].DownstreamScheme); - } - } - - private void ThenTheOcelotJsonIsStoredAs(FileConfiguration expecteds) - { - var resultText = File.ReadAllText(_ocelotJsonPath); - var expectedText = JsonConvert.SerializeObject(expecteds, Formatting.Indented); - resultText.ShouldBe(expectedText); - } - - private void GivenTheConfigurationIs(FileConfiguration fileConfiguration) - { - _environmentSpecificPath = $"{AppContext.BaseDirectory}/ocelot{(string.IsNullOrEmpty(_environmentName) ? string.Empty : ".")}{_environmentName}.json"; - - var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); - - if (File.Exists(_environmentSpecificPath)) - { - File.Delete(_environmentSpecificPath); - } - - File.WriteAllText(_environmentSpecificPath, jsonConfiguration); - } - - private void ThenTheConfigurationJsonIsIndented(FileConfiguration expecteds) - { - var path = !string.IsNullOrEmpty(_environmentSpecificPath) ? _environmentSpecificPath : _environmentSpecificPath = $"{AppContext.BaseDirectory}/ocelot{(string.IsNullOrEmpty(_environmentName) ? string.Empty : ".")}{_environmentName}.json"; - - var resultText = File.ReadAllText(path); - var expectedText = JsonConvert.SerializeObject(expecteds, Formatting.Indented); - resultText.ShouldBe(expectedText); - } - - private void WhenIGetTheReRoutes() - { - _result = _repo.Get().Result.Data; - } - - private void ThenTheFollowingIsReturned(FileConfiguration expecteds) - { - _result.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey); - _result.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host); - _result.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port); - - for (var i = 0; i < _result.ReRoutes.Count; i++) - { - for (int j = 0; j < _result.ReRoutes[i].DownstreamHostAndPorts.Count; j++) - { - var result = _result.ReRoutes[i].DownstreamHostAndPorts[j]; - var expected = expecteds.ReRoutes[i].DownstreamHostAndPorts[j]; - - result.Host.ShouldBe(expected.Host); - result.Port.ShouldBe(expected.Port); - } - - _result.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].DownstreamPathTemplate); - _result.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.ReRoutes[i].DownstreamScheme); - } - } - - private FileConfiguration FakeFileConfigurationForSet() - { - var reRoutes = new List - { - new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "123.12.12.12", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/asdfs/test/{test}" - } - }; - - var globalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Port = 198, - Host = "blah" - } - }; - - return new FileConfiguration - { - GlobalConfiguration = globalConfiguration, - ReRoutes = reRoutes - }; - } - - private FileConfiguration FakeFileConfigurationForGet() - { - var reRoutes = new List - { - new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/test/test/{test}" - } - }; - - var globalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Port = 198, - Host = "blah" - } - }; - - return new FileConfiguration - { - GlobalConfiguration = globalConfiguration, - ReRoutes = reRoutes - }; - } - - public void Dispose() - { - _semaphore.Release(); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +using Microsoft.AspNetCore.Hosting; + +using Moq; + +using Newtonsoft.Json; + +using Ocelot.Configuration.ChangeTracking; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Repository; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class DiskFileConfigurationRepositoryTests : IDisposable + { + private readonly Mock _hostingEnvironment; + private readonly Mock _changeTokenSource; + private IFileConfigurationRepository _repo; + private string _environmentSpecificPath; + private string _ocelotJsonPath; + private FileConfiguration _result; + private FileConfiguration _fileConfiguration; + + // This is a bit dirty and it is dev.dev so that the ConfigurationBuilderExtensionsTests + // cant pick it up if they run in parralel..and the semaphore stops them running at the same time...sigh + // these are not really unit tests but whatever... + private string _environmentName = "DEV.DEV"; + + private static SemaphoreSlim _semaphore; + + public DiskFileConfigurationRepositoryTests() + { + _semaphore = new SemaphoreSlim(1, 1); + _semaphore.Wait(); + _hostingEnvironment = new Mock(); + _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName); + _changeTokenSource = new Mock(MockBehavior.Strict); + _changeTokenSource.Setup(m => m.Activate()); + _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object); + } + + [Fact] + public void should_return_file_configuration() + { + var config = FakeFileConfigurationForGet(); + + this.Given(_ => GivenTheConfigurationIs(config)) + .When(_ => WhenIGetTheRoutes()) + .Then(_ => ThenTheFollowingIsReturned(config)) + .BDDfy(); + } + + [Fact] + public void should_return_file_configuration_if_environment_name_is_unavailable() + { + var config = FakeFileConfigurationForGet(); + + this.Given(_ => GivenTheEnvironmentNameIsUnavailable()) + .And(_ => GivenTheConfigurationIs(config)) + .When(_ => WhenIGetTheRoutes()) + .Then(_ => ThenTheFollowingIsReturned(config)) + .BDDfy(); + } + + [Fact] + public void should_set_file_configuration() + { + var config = FakeFileConfigurationForSet(); + + this.Given(_ => GivenIHaveAConfiguration(config)) + .When(_ => WhenISetTheConfiguration()) + .Then(_ => ThenTheConfigurationIsStoredAs(config)) + .And(_ => ThenTheConfigurationJsonIsIndented(config)) + .And(x => AndTheChangeTokenIsActivated()) + .BDDfy(); + } + + [Fact] + public void should_set_file_configuration_if_environment_name_is_unavailable() + { + var config = FakeFileConfigurationForSet(); + + this.Given(_ => GivenIHaveAConfiguration(config)) + .And(_ => GivenTheEnvironmentNameIsUnavailable()) + .When(_ => WhenISetTheConfiguration()) + .Then(_ => ThenTheConfigurationIsStoredAs(config)) + .And(_ => ThenTheConfigurationJsonIsIndented(config)) + .BDDfy(); + } + + [Fact] + public void should_set_environment_file_configuration_and_ocelot_file_configuration() + { + var config = FakeFileConfigurationForSet(); + + this.Given(_ => GivenIHaveAConfiguration(config)) + .And(_ => GivenTheConfigurationIs(config)) + .And(_ => GivenTheUserAddedOcelotJson()) + .When(_ => WhenISetTheConfiguration()) + .Then(_ => ThenTheConfigurationIsStoredAs(config)) + .And(_ => ThenTheConfigurationJsonIsIndented(config)) + .Then(_ => ThenTheOcelotJsonIsStoredAs(config)) + .BDDfy(); + } + + private void GivenTheUserAddedOcelotJson() + { + _ocelotJsonPath = $"{AppContext.BaseDirectory}/ocelot.json"; + + if (File.Exists(_ocelotJsonPath)) + { + File.Delete(_ocelotJsonPath); + } + + File.WriteAllText(_ocelotJsonPath, "Doesnt matter"); + } + + private void GivenTheEnvironmentNameIsUnavailable() + { + _environmentName = null; + _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName); + _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object); + } + + private void GivenIHaveAConfiguration(FileConfiguration fileConfiguration) + { + _fileConfiguration = fileConfiguration; + } + + private void WhenISetTheConfiguration() + { + _repo.Set(_fileConfiguration); + _result = _repo.Get().Result.Data; + } + + private void ThenTheConfigurationIsStoredAs(FileConfiguration expecteds) + { + _result.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey); + _result.GlobalConfiguration.ServiceDiscoveryProvider.Scheme.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Scheme); + _result.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host); + _result.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port); + + for (var i = 0; i < _result.Routes.Count; i++) + { + for (var j = 0; j < _result.Routes[i].DownstreamHostAndPorts.Count; j++) + { + var result = _result.Routes[i].DownstreamHostAndPorts[j]; + var expected = expecteds.Routes[i].DownstreamHostAndPorts[j]; + + result.Host.ShouldBe(expected.Host); + result.Port.ShouldBe(expected.Port); + } + + _result.Routes[i].DownstreamPathTemplate.ShouldBe(expecteds.Routes[i].DownstreamPathTemplate); + _result.Routes[i].DownstreamScheme.ShouldBe(expecteds.Routes[i].DownstreamScheme); + } + } + + private void ThenTheOcelotJsonIsStoredAs(FileConfiguration expecteds) + { + var resultText = File.ReadAllText(_ocelotJsonPath); + var expectedText = JsonConvert.SerializeObject(expecteds, Formatting.Indented); + resultText.ShouldBe(expectedText); + } + + private void GivenTheConfigurationIs(FileConfiguration fileConfiguration) + { + _environmentSpecificPath = $"{AppContext.BaseDirectory}/ocelot{(string.IsNullOrEmpty(_environmentName) ? string.Empty : ".")}{_environmentName}.json"; + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); + + if (File.Exists(_environmentSpecificPath)) + { + File.Delete(_environmentSpecificPath); + } + + File.WriteAllText(_environmentSpecificPath, jsonConfiguration); + } + + private void ThenTheConfigurationJsonIsIndented(FileConfiguration expecteds) + { + var path = !string.IsNullOrEmpty(_environmentSpecificPath) ? _environmentSpecificPath : _environmentSpecificPath = $"{AppContext.BaseDirectory}/ocelot{(string.IsNullOrEmpty(_environmentName) ? string.Empty : ".")}{_environmentName}.json"; + + var resultText = File.ReadAllText(path); + var expectedText = JsonConvert.SerializeObject(expecteds, Formatting.Indented); + resultText.ShouldBe(expectedText); + } + + private void WhenIGetTheRoutes() + { + _result = _repo.Get().Result.Data; + } + + private void ThenTheFollowingIsReturned(FileConfiguration expecteds) + { + _result.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey); + _result.GlobalConfiguration.ServiceDiscoveryProvider.Scheme.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Scheme); + _result.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host); + _result.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port); + + for (var i = 0; i < _result.Routes.Count; i++) + { + for (var j = 0; j < _result.Routes[i].DownstreamHostAndPorts.Count; j++) + { + var result = _result.Routes[i].DownstreamHostAndPorts[j]; + var expected = expecteds.Routes[i].DownstreamHostAndPorts[j]; + + result.Host.ShouldBe(expected.Host); + result.Port.ShouldBe(expected.Port); + } + + _result.Routes[i].DownstreamPathTemplate.ShouldBe(expecteds.Routes[i].DownstreamPathTemplate); + _result.Routes[i].DownstreamScheme.ShouldBe(expecteds.Routes[i].DownstreamScheme); + } + } + + private void AndTheChangeTokenIsActivated() + { + _changeTokenSource.Verify(m => m.Activate(), Times.Once); + } + + private static FileConfiguration FakeFileConfigurationForSet() + { + var routes = new List + { + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "123.12.12.12", + Port = 80, + }, + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/asdfs/test/{test}", + }, + }; + + var globalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "https", + Port = 198, + Host = "blah", + }, + }; + + return new FileConfiguration + { + GlobalConfiguration = globalConfiguration, + Routes = routes, + }; + } + + private static FileConfiguration FakeFileConfigurationForGet() + { + var routes = new List + { + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 80, + }, + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/test/test/{test}", + }, + }; + + var globalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "https", + Port = 198, + Host = "blah", + }, + }; + + return new FileConfiguration + { + GlobalConfiguration = globalConfiguration, + Routes = routes, + }; + } + + public void Dispose() + { + _semaphore.Release(); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs index ef6bfede5..4f9615f3e 100644 --- a/test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs @@ -1,121 +1,121 @@ -using Ocelot.Configuration; -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; -using Shouldly; -using System.Collections.Generic; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Configuration -{ - public class DownstreamAddressesCreatorTests - { - public DownstreamAddressesCreator _creator; - private FileReRoute _reRoute; - private List _result; - - public DownstreamAddressesCreatorTests() - { - _creator = new DownstreamAddressesCreator(); - } - - [Fact] - public void should_do_nothing() - { - var reRoute = new FileReRoute - { - }; - - var expected = new List - { - }; - - this.Given(x => GivenTheFollowingReRoute(reRoute)) - .When(x => WhenICreate()) - .Then(x => TheThenFollowingIsReturned(expected)) - .BDDfy(); - } - - [Fact] - public void should_create_downstream_addresses_from_old_downstream_path_and_port() - { - var reRoute = new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "test", - Port = 80 - } - }, - }; - - var expected = new List - { - new DownstreamHostAndPort("test", 80), - }; - - this.Given(x => GivenTheFollowingReRoute(reRoute)) - .When(x => WhenICreate()) - .Then(x => TheThenFollowingIsReturned(expected)) - .BDDfy(); - } - - [Fact] - public void should_create_downstream_addresses_from_downstream_host_and_ports() - { - var reRoute = new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "test", - Port = 80 - }, - new FileHostAndPort - { - Host = "west", - Port = 443 - } - } - }; - - var expected = new List - { - new DownstreamHostAndPort("test", 80), - new DownstreamHostAndPort("west", 443) +using System.Collections.Generic; + +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class DownstreamAddressesCreatorTests + { + public DownstreamAddressesCreator _creator; + private FileRoute _route; + private List _result; + + public DownstreamAddressesCreatorTests() + { + _creator = new DownstreamAddressesCreator(); + } + + [Fact] + public void should_do_nothing() + { + var route = new FileRoute(); + + var expected = new List(); + + this.Given(x => GivenTheFollowingRoute(route)) + .When(x => WhenICreate()) + .Then(x => TheThenFollowingIsReturned(expected)) + .BDDfy(); + } + + [Fact] + public void should_create_downstream_addresses_from_old_downstream_path_and_port() + { + var route = new FileRoute + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "test", + Port = 80, + }, + }, + }; + + var expected = new List + { + new("test", 80), + }; + + this.Given(x => GivenTheFollowingRoute(route)) + .When(x => WhenICreate()) + .Then(x => TheThenFollowingIsReturned(expected)) + .BDDfy(); + } + + [Fact] + public void should_create_downstream_addresses_from_downstream_host_and_ports() + { + var route = new FileRoute + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "test", + Port = 80, + }, + new() + { + Host = "west", + Port = 443, + }, + }, + }; + + var expected = new List + { + new("test", 80), + new("west", 443), }; - this.Given(x => GivenTheFollowingReRoute(reRoute)) - .When(x => WhenICreate()) - .Then(x => TheThenFollowingIsReturned(expected)) - .BDDfy(); - } - - private void GivenTheFollowingReRoute(FileReRoute reRoute) - { - _reRoute = reRoute; - } - - private void WhenICreate() - { - _result = _creator.Create(_reRoute); - } - - private void TheThenFollowingIsReturned(List expecteds) - { - _result.Count.ShouldBe(expecteds.Count); - - for (int i = 0; i < _result.Count; i++) - { - var result = _result[i]; - var expected = expecteds[i]; - - result.Host.ShouldBe(expected.Host); - result.Port.ShouldBe(expected.Port); - } - } - } + this.Given(x => GivenTheFollowingRoute(route)) + .When(x => WhenICreate()) + .Then(x => TheThenFollowingIsReturned(expected)) + .BDDfy(); + } + + private void GivenTheFollowingRoute(FileRoute route) + { + _route = route; + } + + private void WhenICreate() + { + _result = _creator.Create(_route); + } + + private void TheThenFollowingIsReturned(List expecteds) + { + _result.Count.ShouldBe(expecteds.Count); + + for (var i = 0; i < _result.Count; i++) + { + var result = _result[i]; + var expected = expecteds[i]; + + result.Host.ShouldBe(expected.Host); + result.Port.ShouldBe(expected.Port); + } + } + } } diff --git a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs index 8a0601d30..27b0a85e3 100644 --- a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs @@ -1,126 +1,153 @@ -namespace Ocelot.UnitTests.Configuration -{ - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Configuration.Creator; - using Ocelot.Configuration.File; - using Shouldly; - using System.Collections.Generic; - using TestStack.BDDfy; - using Xunit; - - public class DynamicsCreatorTests - { - private readonly DynamicsCreator _creator; - private readonly Mock _rloCreator; - private List _result; - private FileConfiguration _fileConfig; - private RateLimitOptions _rlo1; - private RateLimitOptions _rlo2; - - public DynamicsCreatorTests() - { - _rloCreator = new Mock(); - _creator = new DynamicsCreator(_rloCreator.Object); - } - - [Fact] - public void should_return_nothing() - { - var fileConfig = new FileConfiguration(); - - this.Given(_ => GivenThe(fileConfig)) - .When(_ => WhenICreate()) - .Then(_ => ThenNothingIsReturned()) - .And(_ => ThenTheRloCreatorIsNotCalled()) - .BDDfy(); - } - - [Fact] - public void should_return_re_routes() - { - var fileConfig = new FileConfiguration - { - DynamicReRoutes = new List - { - new FileDynamicReRoute - { - ServiceName = "1", - RateLimitRule = new FileRateLimitRule - { - EnableRateLimiting = false - } - }, - new FileDynamicReRoute - { - ServiceName = "2", - RateLimitRule = new FileRateLimitRule - { - EnableRateLimiting = true - } - } - } - }; - - this.Given(_ => GivenThe(fileConfig)) - .And(_ => GivenTheRloCreatorReturns()) - .When(_ => WhenICreate()) - .Then(_ => ThenTheReRoutesAreReturned()) - .And(_ => ThenTheRloCreatorIsCalledCorrectly()) - .BDDfy(); - } - - private void ThenTheRloCreatorIsCalledCorrectly() - { - _rloCreator.Verify(x => x.Create(_fileConfig.DynamicReRoutes[0].RateLimitRule, - _fileConfig.GlobalConfiguration), Times.Once); - - _rloCreator.Verify(x => x.Create(_fileConfig.DynamicReRoutes[1].RateLimitRule, - _fileConfig.GlobalConfiguration), Times.Once); - } - - private void ThenTheReRoutesAreReturned() - { - _result.Count.ShouldBe(2); - _result[0].DownstreamReRoute[0].EnableEndpointEndpointRateLimiting.ShouldBeFalse(); - _result[0].DownstreamReRoute[0].RateLimitOptions.ShouldBe(_rlo1); - _result[0].DownstreamReRoute[0].ServiceName.ShouldBe(_fileConfig.DynamicReRoutes[0].ServiceName); - - _result[1].DownstreamReRoute[0].EnableEndpointEndpointRateLimiting.ShouldBeTrue(); - _result[1].DownstreamReRoute[0].RateLimitOptions.ShouldBe(_rlo2); - _result[1].DownstreamReRoute[0].ServiceName.ShouldBe(_fileConfig.DynamicReRoutes[1].ServiceName); - } - - private void GivenTheRloCreatorReturns() - { - _rlo1 = new RateLimitOptionsBuilder().Build(); - _rlo2 = new RateLimitOptionsBuilder().WithEnableRateLimiting(true).Build(); - - _rloCreator - .SetupSequence(x => x.Create(It.IsAny(), It.IsAny())) - .Returns(_rlo1) - .Returns(_rlo2); - } - - private void ThenTheRloCreatorIsNotCalled() - { - _rloCreator.Verify(x => x.Create(It.IsAny(), It.IsAny()), Times.Never); - } - - private void ThenNothingIsReturned() - { - _result.Count.ShouldBe(0); - } - - private void WhenICreate() - { - _result = _creator.Create(_fileConfig); - } - - private void GivenThe(FileConfiguration fileConfig) - { - _fileConfig = fileConfig; - } - } -} +using System; +using System.Collections.Generic; + +using Moq; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class DynamicsCreatorTests + { + private readonly DynamicsCreator _creator; + private readonly Mock _rloCreator; + private readonly Mock _versionCreator; + private List _result; + private FileConfiguration _fileConfig; + private RateLimitOptions _rlo1; + private RateLimitOptions _rlo2; + private Version _version; + + public DynamicsCreatorTests() + { + _versionCreator = new Mock(); + _rloCreator = new Mock(); + _creator = new DynamicsCreator(_rloCreator.Object, _versionCreator.Object); + } + + [Fact] + public void should_return_nothing() + { + var fileConfig = new FileConfiguration(); + + this.Given(_ => GivenThe(fileConfig)) + .When(_ => WhenICreate()) + .Then(_ => ThenNothingIsReturned()) + .And(_ => ThenTheRloCreatorIsNotCalled()) + .BDDfy(); + } + + [Fact] + public void should_return_re_routes() + { + var fileConfig = new FileConfiguration + { + DynamicRoutes = new List + { + new() + { + ServiceName = "1", + RateLimitRule = new FileRateLimitRule + { + EnableRateLimiting = false, + }, + DownstreamHttpVersion = "1.1", + }, + new() + { + ServiceName = "2", + RateLimitRule = new FileRateLimitRule + { + EnableRateLimiting = true, + }, + DownstreamHttpVersion = "2.0", + }, + }, + }; + + this.Given(_ => GivenThe(fileConfig)) + .And(_ => GivenTheRloCreatorReturns()) + .And(_ => GivenTheVersionCreatorReturns()) + .When(_ => WhenICreate()) + .Then(_ => ThenTheRoutesAreReturned()) + .And(_ => ThenTheRloCreatorIsCalledCorrectly()) + .And(_ => ThenTheVersionCreatorIsCalledCorrectly()) + .BDDfy(); + } + + private void ThenTheRloCreatorIsCalledCorrectly() + { + _rloCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].RateLimitRule, + _fileConfig.GlobalConfiguration), Times.Once); + + _rloCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].RateLimitRule, + _fileConfig.GlobalConfiguration), Times.Once); + } + + private void ThenTheVersionCreatorIsCalledCorrectly() + { + _versionCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].DownstreamHttpVersion), Times.Once); + _versionCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].DownstreamHttpVersion), Times.Once); + } + + private void ThenTheRoutesAreReturned() + { + _result.Count.ShouldBe(2); + _result[0].DownstreamRoute[0].EnableEndpointEndpointRateLimiting.ShouldBeFalse(); + _result[0].DownstreamRoute[0].RateLimitOptions.ShouldBe(_rlo1); + _result[0].DownstreamRoute[0].DownstreamHttpVersion.ShouldBe(_version); + _result[0].DownstreamRoute[0].ServiceName.ShouldBe(_fileConfig.DynamicRoutes[0].ServiceName); + + _result[1].DownstreamRoute[0].EnableEndpointEndpointRateLimiting.ShouldBeTrue(); + _result[1].DownstreamRoute[0].RateLimitOptions.ShouldBe(_rlo2); + _result[1].DownstreamRoute[0].DownstreamHttpVersion.ShouldBe(_version); + _result[1].DownstreamRoute[0].ServiceName.ShouldBe(_fileConfig.DynamicRoutes[1].ServiceName); + } + + private void GivenTheVersionCreatorReturns() + { + _version = new Version("1.1"); + _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_version); + } + + private void GivenTheRloCreatorReturns() + { + _rlo1 = new RateLimitOptionsBuilder().Build(); + _rlo2 = new RateLimitOptionsBuilder().WithEnableRateLimiting(true).Build(); + + _rloCreator + .SetupSequence(x => x.Create(It.IsAny(), It.IsAny())) + .Returns(_rlo1) + .Returns(_rlo2); + } + + private void ThenTheRloCreatorIsNotCalled() + { + _rloCreator.Verify(x => x.Create(It.IsAny(), It.IsAny()), Times.Never); + } + + private void ThenNothingIsReturned() + { + _result.Count.ShouldBe(0); + } + + private void WhenICreate() + { + _result = _creator.Create(_fileConfig); + } + + private void GivenThe(FileConfiguration fileConfig) + { + _fileConfig = fileConfig; + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationPollerTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationPollerTests.cs index bfc0c98c6..2b8a4ae01 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationPollerTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationPollerTests.cs @@ -1,205 +1,206 @@ using Moq; -using Ocelot.Configuration; -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; -using Ocelot.Configuration.Repository; -using Ocelot.Logging; -using Ocelot.Responses; +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Repository; +using Ocelot.Logging; +using Ocelot.Responses; using Ocelot.UnitTests.Responder; using Shouldly; -using System; -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading; using TestStack.BDDfy; using Xunit; -using static Ocelot.Infrastructure.Wait; - -namespace Ocelot.UnitTests.Configuration -{ - public class FileConfigurationPollerTests : IDisposable - { - private readonly FileConfigurationPoller _poller; - private Mock _factory; - private readonly Mock _repo; - private readonly FileConfiguration _fileConfig; - private Mock _config; - private readonly Mock _internalConfigRepo; - private readonly Mock _internalConfigCreator; - private IInternalConfiguration _internalConfig; - - public FileConfigurationPollerTests() - { - var logger = new Mock(); - _factory = new Mock(); - _factory.Setup(x => x.CreateLogger()).Returns(logger.Object); - _repo = new Mock(); - _fileConfig = new FileConfiguration(); - _config = new Mock(); - _repo.Setup(x => x.Get()).ReturnsAsync(new OkResponse(_fileConfig)); +using static Ocelot.Infrastructure.Wait; + +namespace Ocelot.UnitTests.Configuration +{ + public class FileConfigurationPollerTests : IDisposable + { + private readonly FileConfigurationPoller _poller; + private readonly Mock _factory; + private readonly Mock _repo; + private readonly FileConfiguration _fileConfig; + private readonly Mock _config; + private readonly Mock _internalConfigRepo; + private readonly Mock _internalConfigCreator; + private readonly Mock _internalConfig; + + public FileConfigurationPollerTests() + { + var logger = new Mock(); + _factory = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(logger.Object); + _repo = new Mock(); + _fileConfig = new FileConfiguration(); + _config = new Mock(); + _repo.Setup(x => x.Get()).ReturnsAsync(new OkResponse(_fileConfig)); _config.Setup(x => x.Delay).Returns(100); - _internalConfigRepo = new Mock(); - _internalConfigCreator = new Mock(); - _internalConfigCreator.Setup(x => x.Create(It.IsAny())).ReturnsAsync(new OkResponse(_internalConfig)); - _poller = new FileConfigurationPoller(_factory.Object, _repo.Object, _config.Object, _internalConfigRepo.Object, _internalConfigCreator.Object); - } - - [Fact] - public void should_start() - { - this.Given(x => GivenPollerHasStarted()) - .Given(x => ThenTheSetterIsCalled(_fileConfig, 1)) - .BDDfy(); - } - - [Fact] - public void should_call_setter_when_gets_new_config() - { - var newConfig = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "test" - } - }, - } - } - }; - - this.Given(x => GivenPollerHasStarted()) - .Given(x => WhenTheConfigIsChanged(newConfig, 0)) - .Then(x => ThenTheSetterIsCalledAtLeast(newConfig, 1)) - .BDDfy(); - } - - [Fact] - public void should_not_poll_if_already_polling() - { - var newConfig = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "test" - } - }, - } - } - }; - - this.Given(x => GivenPollerHasStarted()) - .Given(x => WhenTheConfigIsChanged(newConfig, 10)) - .Then(x => ThenTheSetterIsCalled(newConfig, 1)) - .BDDfy(); - } - - [Fact] - public void should_do_nothing_if_call_to_provider_fails() - { - var newConfig = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "test" - } - }, - } - } - }; - - this.Given(x => GivenPollerHasStarted()) - .Given(x => WhenProviderErrors()) - .Then(x => ThenTheSetterIsCalled(newConfig, 0)) - .BDDfy(); - } - - [Fact] - public void should_dispose_cleanly_without_starting() - { - this.When(x => WhenPollerIsDisposed()) - .BDDfy(); - } - - private void GivenPollerHasStarted() - { - _poller.StartAsync(CancellationToken.None); - } - - private void WhenProviderErrors() - { - _repo - .Setup(x => x.Get()) - .ReturnsAsync(new ErrorResponse(new AnyError())); - } - - private void WhenTheConfigIsChanged(FileConfiguration newConfig, int delay) - { - _repo - .Setup(x => x.Get()) - .Callback(() => Thread.Sleep(delay)) - .ReturnsAsync(new OkResponse(newConfig)); - } - - private void WhenPollerIsDisposed() - { - _poller.Dispose(); - } - - private void ThenTheSetterIsCalled(FileConfiguration fileConfig, int times) - { - var result = WaitFor(4000).Until(() => - { - try - { - _internalConfigRepo.Verify(x => x.AddOrReplace(_internalConfig), Times.Exactly(times)); - _internalConfigCreator.Verify(x => x.Create(fileConfig), Times.Exactly(times)); - return true; - } - catch (Exception) - { - return false; - } - }); - result.ShouldBeTrue(); - } - - private void ThenTheSetterIsCalledAtLeast(FileConfiguration fileConfig, int times) - { - var result = WaitFor(4000).Until(() => - { - try - { - _internalConfigRepo.Verify(x => x.AddOrReplace(_internalConfig), Times.AtLeast(times)); - _internalConfigCreator.Verify(x => x.Create(fileConfig), Times.AtLeast(times)); - return true; - } - catch (Exception) - { - return false; - } - }); - result.ShouldBeTrue(); - } - - public void Dispose() - { - _poller.Dispose(); - } - } -} + _internalConfig = new Mock(); + _internalConfigRepo = new Mock(); + _internalConfigCreator = new Mock(); + _internalConfigCreator.Setup(x => x.Create(It.IsAny())).ReturnsAsync(new OkResponse(_internalConfig.Object)); + _poller = new FileConfigurationPoller(_factory.Object, _repo.Object, _config.Object, _internalConfigRepo.Object, _internalConfigCreator.Object); + } + + [Fact] + public void should_start() + { + this.Given(x => GivenPollerHasStarted()) + .Given(x => ThenTheSetterIsCalled(_fileConfig, 1)) + .BDDfy(); + } + + [Fact] + public void should_call_setter_when_gets_new_config() + { + var newConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "test", + }, + }, + }, + }, + }; + + this.Given(x => GivenPollerHasStarted()) + .Given(x => WhenTheConfigIsChanged(newConfig, 0)) + .Then(x => ThenTheSetterIsCalledAtLeast(newConfig, 1)) + .BDDfy(); + } + + [Fact] + public void should_not_poll_if_already_polling() + { + var newConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "test", + }, + }, + }, + }, + }; + + this.Given(x => GivenPollerHasStarted()) + .Given(x => WhenTheConfigIsChanged(newConfig, 10)) + .Then(x => ThenTheSetterIsCalled(newConfig, 1)) + .BDDfy(); + } + + [Fact] + public void should_do_nothing_if_call_to_provider_fails() + { + var newConfig = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "test", + }, + }, + }, + }, + }; + + this.Given(x => GivenPollerHasStarted()) + .Given(x => WhenProviderErrors()) + .Then(x => ThenTheSetterIsCalled(newConfig, 0)) + .BDDfy(); + } + + [Fact] + public void should_dispose_cleanly_without_starting() + { + this.When(x => WhenPollerIsDisposed()) + .BDDfy(); + } + + private void GivenPollerHasStarted() + { + _poller.StartAsync(CancellationToken.None); + } + + private void WhenProviderErrors() + { + _repo + .Setup(x => x.Get()) + .ReturnsAsync(new ErrorResponse(new AnyError())); + } + + private void WhenTheConfigIsChanged(FileConfiguration newConfig, int delay) + { + _repo + .Setup(x => x.Get()) + .Callback(() => Thread.Sleep(delay)) + .ReturnsAsync(new OkResponse(newConfig)); + } + + private void WhenPollerIsDisposed() + { + _poller.Dispose(); + } + + private void ThenTheSetterIsCalled(FileConfiguration fileConfig, int times) + { + var result = WaitFor(4000).Until(() => + { + try + { + _internalConfigRepo.Verify(x => x.AddOrReplace(_internalConfig.Object), Times.Exactly(times)); + _internalConfigCreator.Verify(x => x.Create(fileConfig), Times.Exactly(times)); + return true; + } + catch (Exception) + { + return false; + } + }); + result.ShouldBeTrue(); + } + + private void ThenTheSetterIsCalledAtLeast(FileConfiguration fileConfig, int times) + { + var result = WaitFor(4000).Until(() => + { + try + { + _internalConfigRepo.Verify(x => x.AddOrReplace(_internalConfig.Object), Times.AtLeast(times)); + _internalConfigCreator.Verify(x => x.Create(fileConfig), Times.AtLeast(times)); + return true; + } + catch (Exception) + { + return false; + } + }); + result.ShouldBeTrue(); + } + + public void Dispose() + { + _poller.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs index 53289434b..30b1477a8 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs @@ -1,4 +1,7 @@ -using Moq; +using System.Collections.Generic; + +using Moq; + using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; @@ -6,23 +9,26 @@ using Ocelot.Configuration.Repository; using Ocelot.Configuration.Setter; using Ocelot.Errors; -using Ocelot.Responses; -using Shouldly; -using System.Collections.Generic; -using TestStack.BDDfy; -using Xunit; +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; +using System; namespace Ocelot.UnitTests.Configuration { public class FileConfigurationSetterTests { private FileConfiguration _fileConfiguration; - private FileAndInternalConfigurationSetter _configSetter; - private Mock _configRepo; - private Mock _configCreator; + private readonly FileAndInternalConfigurationSetter _configSetter; + private readonly Mock _configRepo; + private readonly Mock _configCreator; private Response _configuration; - private object _result; - private Mock _repo; + private object _result; + private readonly Mock _repo; public FileConfigurationSetterTests() { @@ -37,7 +43,7 @@ public void should_set_configuration() { var fileConfig = new FileConfiguration(); var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - var config = new InternalConfiguration(new List(), string.Empty, serviceProviderConfig, "asdf", new LoadBalancerOptionsBuilder().Build(), "", new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build()); + var config = new InternalConfiguration(new List(), string.Empty, serviceProviderConfig, "asdf", new LoadBalancerOptionsBuilder().Build(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1")); this.Given(x => GivenTheFollowingConfiguration(fileConfig)) .And(x => GivenTheRepoReturns(new OkResponse())) @@ -104,8 +110,7 @@ private void WhenISetTheConfiguration() private void ThenTheConfigurationRepositoryIsCalledCorrectly() { - _configRepo - .Verify(x => x.AddOrReplace(_configuration.Data), Times.Once); + _configRepo.Verify(x => x.AddOrReplace(_configuration.Data), Times.Once); } } -} +} diff --git a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs index 6ccc44481..9a97779d2 100644 --- a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs @@ -1,127 +1,126 @@ -namespace Ocelot.UnitTests.Configuration -{ - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Configuration.Creator; - using Ocelot.Configuration.File; - using Ocelot.Configuration.Validator; - using Ocelot.Errors; - using Ocelot.Responses; - using Ocelot.UnitTests.Responder; - using Shouldly; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - - public class FileInternalConfigurationCreatorTests - { - private readonly Mock _validator; - private readonly Mock _reRoutesCreator; - private readonly Mock _aggregatesCreator; - private readonly Mock _dynamicsCreator; - private readonly Mock _configCreator; - private Response _config; - private FileConfiguration _fileConfiguration; - private readonly FileInternalConfigurationCreator _creator; - private Response _result; - private List _reRoutes; - private List _aggregates; - private List _dynamics; - private InternalConfiguration _internalConfig; - - public FileInternalConfigurationCreatorTests() - { - _validator = new Mock(); - _reRoutesCreator = new Mock(); - _aggregatesCreator = new Mock(); - _dynamicsCreator = new Mock(); - _configCreator = new Mock(); - - _creator = new FileInternalConfigurationCreator(_validator.Object, _reRoutesCreator.Object, _aggregatesCreator.Object, _dynamicsCreator.Object, _configCreator.Object); - } - - [Fact] - public void should_return_validation_error() - { - var fileConfiguration = new FileConfiguration(); - - this.Given(_ => GivenThe(fileConfiguration)) - .And(_ => GivenTheValidationFails()) - .When(_ => WhenICreate()) - .Then(_ => ThenAnErrorIsReturned()) - .BDDfy(); - } - - [Fact] - public void should_return_internal_configuration() - { - var fileConfiguration = new FileConfiguration(); - - this.Given(_ => GivenThe(fileConfiguration)) - .And(_ => GivenTheValidationSucceeds()) - .And(_ => GivenTheDependenciesAreSetUp()) - .When(_ => WhenICreate()) - .Then(_ => ThenTheDependenciesAreCalledCorrectly()) - .BDDfy(); - } - - private void ThenTheDependenciesAreCalledCorrectly() - { - _reRoutesCreator.Verify(x => x.Create(_fileConfiguration), Times.Once); - _aggregatesCreator.Verify(x => x.Create(_fileConfiguration, _reRoutes), Times.Once); - _dynamicsCreator.Verify(x => x.Create(_fileConfiguration), Times.Once); - - var mergedReRoutes = _reRoutes - .Union(_aggregates) - .Union(_dynamics) - .ToList(); - - _configCreator.Verify(x => x.Create(_fileConfiguration, It.Is>(y => y.Count == mergedReRoutes.Count)), Times.Once); - } - - private void GivenTheDependenciesAreSetUp() - { - _reRoutes = new List { new ReRouteBuilder().Build() }; - _aggregates = new List { new ReRouteBuilder().Build() }; - _dynamics = new List { new ReRouteBuilder().Build() }; - _internalConfig = new InternalConfiguration(null, "", null, "", null, "", null, null); - - _reRoutesCreator.Setup(x => x.Create(It.IsAny())).Returns(_reRoutes); - _aggregatesCreator.Setup(x => x.Create(It.IsAny(), It.IsAny>())).Returns(_aggregates); - _dynamicsCreator.Setup(x => x.Create(It.IsAny())).Returns(_dynamics); - _configCreator.Setup(x => x.Create(It.IsAny(), It.IsAny>())).Returns(_internalConfig); - } - - private void GivenTheValidationSucceeds() - { - var ok = new ConfigurationValidationResult(false); - var response = new OkResponse(ok); - _validator.Setup(x => x.IsValid(It.IsAny())).ReturnsAsync(response); - } - - private void ThenAnErrorIsReturned() - { - _result.IsError.ShouldBeTrue(); - } - - private async Task WhenICreate() - { - _result = await _creator.Create(_fileConfiguration); - } - - private void GivenTheValidationFails() - { - var error = new ConfigurationValidationResult(true, new List { new AnyError() }); - var response = new OkResponse(error); - _validator.Setup(x => x.IsValid(It.IsAny())).ReturnsAsync(response); - } - - private void GivenThe(FileConfiguration fileConfiguration) - { - _fileConfiguration = fileConfiguration; - } - } -} +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Validator; +using Ocelot.Errors; +using Ocelot.Responses; +using Ocelot.UnitTests.Responder; +using Shouldly; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class FileInternalConfigurationCreatorTests + { + private readonly Mock _validator; + private readonly Mock _routesCreator; + private readonly Mock _aggregatesCreator; + private readonly Mock _dynamicsCreator; + private readonly Mock _configCreator; + private FileConfiguration _fileConfiguration; + private readonly FileInternalConfigurationCreator _creator; + private Response _result; + private List _routes; + private List _aggregates; + private List _dynamics; + private InternalConfiguration _internalConfig; + + public FileInternalConfigurationCreatorTests() + { + _validator = new Mock(); + _routesCreator = new Mock(); + _aggregatesCreator = new Mock(); + _dynamicsCreator = new Mock(); + _configCreator = new Mock(); + + _creator = new FileInternalConfigurationCreator(_validator.Object, _routesCreator.Object, _aggregatesCreator.Object, _dynamicsCreator.Object, _configCreator.Object); + } + + [Fact] + public void should_return_validation_error() + { + var fileConfiguration = new FileConfiguration(); + + this.Given(_ => GivenThe(fileConfiguration)) + .And(_ => GivenTheValidationFails()) + .When(_ => WhenICreate()) + .Then(_ => ThenAnErrorIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_internal_configuration() + { + var fileConfiguration = new FileConfiguration(); + + this.Given(_ => GivenThe(fileConfiguration)) + .And(_ => GivenTheValidationSucceeds()) + .And(_ => GivenTheDependenciesAreSetUp()) + .When(_ => WhenICreate()) + .Then(_ => ThenTheDependenciesAreCalledCorrectly()) + .BDDfy(); + } + + private void ThenTheDependenciesAreCalledCorrectly() + { + _routesCreator.Verify(x => x.Create(_fileConfiguration), Times.Once); + _aggregatesCreator.Verify(x => x.Create(_fileConfiguration, _routes), Times.Once); + _dynamicsCreator.Verify(x => x.Create(_fileConfiguration), Times.Once); + + var mergedRoutes = _routes + .Union(_aggregates) + .Union(_dynamics) + .ToList(); + + _configCreator.Verify(x => x.Create(_fileConfiguration, It.Is>(y => y.Count == mergedRoutes.Count)), Times.Once); + } + + private void GivenTheDependenciesAreSetUp() + { + _routes = new List { new RouteBuilder().Build() }; + _aggregates = new List { new RouteBuilder().Build() }; + _dynamics = new List { new RouteBuilder().Build() }; + _internalConfig = new InternalConfiguration(null, string.Empty, null, string.Empty, null, string.Empty, null, null, null); + + _routesCreator.Setup(x => x.Create(It.IsAny())).Returns(_routes); + _aggregatesCreator.Setup(x => x.Create(It.IsAny(), It.IsAny>())).Returns(_aggregates); + _dynamicsCreator.Setup(x => x.Create(It.IsAny())).Returns(_dynamics); + _configCreator.Setup(x => x.Create(It.IsAny(), It.IsAny>())).Returns(_internalConfig); + } + + private void GivenTheValidationSucceeds() + { + var ok = new ConfigurationValidationResult(false); + var response = new OkResponse(ok); + _validator.Setup(x => x.IsValid(It.IsAny())).ReturnsAsync(response); + } + + private void ThenAnErrorIsReturned() + { + _result.IsError.ShouldBeTrue(); + } + + private async Task WhenICreate() + { + _result = await _creator.Create(_fileConfiguration); + } + + private void GivenTheValidationFails() + { + var error = new ConfigurationValidationResult(true, new List { new AnyError() }); + var response = new OkResponse(error); + _validator.Setup(x => x.IsValid(It.IsAny())).ReturnsAsync(response); + } + + private void GivenThe(FileConfiguration fileConfiguration) + { + _fileConfiguration = fileConfiguration; + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/HashCreationTests.cs b/test/Ocelot.UnitTests/Configuration/HashCreationTests.cs index e243dce4e..0337ae37f 100644 --- a/test/Ocelot.UnitTests/Configuration/HashCreationTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HashCreationTests.cs @@ -1,6 +1,8 @@ -using Microsoft.AspNetCore.Cryptography.KeyDerivation; using System; -using System.Security.Cryptography; +using System.Security.Cryptography; + +using Microsoft.AspNetCore.Cryptography.KeyDerivation; + using Xunit; namespace Ocelot.UnitTests.Configuration diff --git a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs index 78896bfb9..b5ea3f0c5 100644 --- a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs @@ -1,26 +1,31 @@ -using Moq; +using System.Collections.Generic; + +using Moq; + using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Infrastructure; using Ocelot.Logging; using Ocelot.Responses; -using Ocelot.UnitTests.Responder; -using Shouldly; -using System.Collections.Generic; -using TestStack.BDDfy; +using Ocelot.UnitTests.Responder; + +using Shouldly; + +using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Configuration { public class HeaderFindAndReplaceCreatorTests { - private HeaderFindAndReplaceCreator _creator; - private FileReRoute _reRoute; + private readonly HeaderFindAndReplaceCreator _creator; + private FileRoute _route; private HeaderTransformations _result; - private Mock _placeholders; - private Mock _factory; - private Mock _logger; + private readonly Mock _placeholders; + private readonly Mock _factory; + private readonly Mock _logger; public HeaderFindAndReplaceCreatorTests() { @@ -34,33 +39,33 @@ public HeaderFindAndReplaceCreatorTests() [Fact] public void should_create() { - var reRoute = new FileReRoute + var route = new FileRoute { UpstreamHeaderTransform = new Dictionary { {"Test", "Test, Chicken"}, - {"Moop", "o, a"} - }, + {"Moop", "o, a"}, + }, DownstreamHeaderTransform = new Dictionary { {"Pop", "West, East"}, - {"Bop", "e, r"} - } + {"Bop", "e, r"}, + }, }; var upstream = new List { - new HeaderFindAndReplace("Test", "Test", "Chicken", 0), - new HeaderFindAndReplace("Moop", "o", "a", 0) + new("Test", "Test", "Chicken", 0), + new("Moop", "o", "a", 0), }; var downstream = new List { - new HeaderFindAndReplace("Pop", "West", "East", 0), - new HeaderFindAndReplace("Bop", "e", "r", 0) + new("Pop", "West", "East", 0), + new("Bop", "e", "r", 0), }; - this.Given(x => GivenTheReRoute(reRoute)) + this.Given(x => GivenTheRoute(route)) .When(x => WhenICreate()) .Then(x => ThenTheFollowingUpstreamIsReturned(upstream)) .Then(x => ThenTheFollowingDownstreamIsReturned(downstream)) @@ -71,19 +76,19 @@ public void should_create() public void should_create_with_add_headers_to_request() { const string key = "X-Forwarded-For"; - const string value = "{RemoteIpAddress}"; + const string value = "{RemoteIpAddress}"; - var reRoute = new FileReRoute + var route = new FileRoute { UpstreamHeaderTransform = new Dictionary { {key, value}, - } - }; + }, + }; var expected = new AddHeader(key, value); - this.Given(x => GivenTheReRoute(reRoute)) + this.Given(x => GivenTheRoute(route)) .When(x => WhenICreate()) .Then(x => ThenTheFollowingAddHeaderToUpstreamIsReturned(expected)) .BDDfy(); @@ -92,20 +97,20 @@ public void should_create_with_add_headers_to_request() [Fact] public void should_use_base_url_placeholder() { - var reRoute = new FileReRoute - { + var route = new FileRoute + { DownstreamHeaderTransform = new Dictionary { {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, - } + }, }; var downstream = new List { - new HeaderFindAndReplace("Location", "http://www.bbc.co.uk/", "http://ocelot.com/", 0), + new("Location", "http://www.bbc.co.uk/", "http://ocelot.com/", 0), }; - this.Given(x => GivenTheReRoute(reRoute)) + this.Given(x => GivenTheRoute(route)) .And(x => GivenTheBaseUrlIs("http://ocelot.com/")) .When(x => WhenICreate()) .Then(x => ThenTheFollowingDownstreamIsReturned(downstream)) @@ -115,7 +120,7 @@ public void should_use_base_url_placeholder() [Fact] public void should_log_errors_and_not_add_headers() { - var reRoute = new FileReRoute + var route = new FileRoute { DownstreamHeaderTransform = new Dictionary { @@ -124,14 +129,12 @@ public void should_log_errors_and_not_add_headers() UpstreamHeaderTransform = new Dictionary { {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, - } + }, }; - var expected = new List - { - }; + var expected = new List(); - this.Given(x => GivenTheReRoute(reRoute)) + this.Given(x => GivenTheRoute(route)) .And(x => GivenTheBaseUrlErrors()) .When(x => WhenICreate()) .Then(x => ThenTheFollowingDownstreamIsReturned(expected)) @@ -149,20 +152,20 @@ private void ThenTheLoggerIsCalledCorrectly(string message) [Fact] public void should_use_base_url_partial_placeholder() { - var reRoute = new FileReRoute - { + var route = new FileRoute + { DownstreamHeaderTransform = new Dictionary { {"Location", "http://www.bbc.co.uk/pay, {BaseUrl}pay"}, - } + }, }; var downstream = new List { - new HeaderFindAndReplace("Location", "http://www.bbc.co.uk/pay", "http://ocelot.com/pay", 0), + new("Location", "http://www.bbc.co.uk/pay", "http://ocelot.com/pay", 0), }; - this.Given(x => GivenTheReRoute(reRoute)) + this.Given(x => GivenTheRoute(route)) .And(x => GivenTheBaseUrlIs("http://ocelot.com/")) .When(x => WhenICreate()) .Then(x => ThenTheFollowingDownstreamIsReturned(downstream)) @@ -172,17 +175,17 @@ public void should_use_base_url_partial_placeholder() [Fact] public void should_add_trace_id_header() { - var reRoute = new FileReRoute - { + var route = new FileRoute + { DownstreamHeaderTransform = new Dictionary { {"Trace-Id", "{TraceId}"}, - } + }, }; var expected = new AddHeader("Trace-Id", "{TraceId}"); - this.Given(x => GivenTheReRoute(reRoute)) + this.Given(x => GivenTheRoute(route)) .And(x => GivenTheBaseUrlIs("http://ocelot.com/")) .When(x => WhenICreate()) .Then(x => ThenTheFollowingAddHeaderToDownstreamIsReturned(expected)) @@ -192,17 +195,17 @@ public void should_add_trace_id_header() [Fact] public void should_add_downstream_header_as_is_when_no_replacement_is_given() { - var reRoute = new FileReRoute + var route = new FileRoute { DownstreamHeaderTransform = new Dictionary { {"X-Custom-Header", "Value"}, - } + }, }; var expected = new AddHeader("X-Custom-Header", "Value"); - this.Given(x => GivenTheReRoute(reRoute)) + this.Given(x => GivenTheRoute(route)) .And(x => WhenICreate()) .Then(x => x.ThenTheFollowingAddHeaderToDownstreamIsReturned(expected)) .BDDfy(); @@ -211,17 +214,17 @@ public void should_add_downstream_header_as_is_when_no_replacement_is_given() [Fact] public void should_add_upstream_header_as_is_when_no_replacement_is_given() { - var reRoute = new FileReRoute + var route = new FileRoute { UpstreamHeaderTransform = new Dictionary { {"X-Custom-Header", "Value"}, - } + }, }; var expected = new AddHeader("X-Custom-Header", "Value"); - this.Given(x => GivenTheReRoute(reRoute)) + this.Given(x => GivenTheRoute(route)) .And(x => WhenICreate()) .Then(x => x.ThenTheFollowingAddHeaderToUpstreamIsReturned(expected)) .BDDfy(); @@ -241,8 +244,8 @@ private void ThenTheFollowingAddHeaderToDownstreamIsReturned(AddHeader addHeader { _result.AddHeadersToDownstream[0].Key.ShouldBe(addHeader.Key); _result.AddHeadersToDownstream[0].Value.ShouldBe(addHeader.Value); - } - + } + private void ThenTheFollowingAddHeaderToUpstreamIsReturned(AddHeader addHeader) { _result.AddHeadersToUpstream[0].Key.ShouldBe(addHeader.Key); @@ -251,9 +254,9 @@ private void ThenTheFollowingAddHeaderToUpstreamIsReturned(AddHeader addHeader) private void ThenTheFollowingDownstreamIsReturned(List downstream) { - _result.Downstream.Count.ShouldBe(downstream.Count); - - for (int i = 0; i < _result.Downstream.Count; i++) + _result.Downstream.Count.ShouldBe(downstream.Count); + + for (var i = 0; i < _result.Downstream.Count; i++) { var result = _result.Downstream[i]; var expected = downstream[i]; @@ -261,24 +264,24 @@ private void ThenTheFollowingDownstreamIsReturned(List dow result.Index.ShouldBe(expected.Index); result.Key.ShouldBe(expected.Key); result.Replace.ShouldBe(expected.Replace); - } + } } - private void GivenTheReRoute(FileReRoute reRoute) + private void GivenTheRoute(FileRoute route) { - _reRoute = reRoute; + _route = route; } private void WhenICreate() { - _result = _creator.Create(_reRoute); + _result = _creator.Create(_route); } private void ThenTheFollowingUpstreamIsReturned(List expecteds) { - _result.Upstream.Count.ShouldBe(expecteds.Count); - - for (int i = 0; i < _result.Upstream.Count; i++) + _result.Upstream.Count.ShouldBe(expecteds.Count); + + for (var i = 0; i < _result.Upstream.Count; i++) { var result = _result.Upstream[i]; var expected = expecteds[i]; @@ -289,4 +292,4 @@ private void ThenTheFollowingUpstreamIsReturned(List expec } } } -} +} diff --git a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs index c4f6eb843..378285015 100644 --- a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs @@ -1,239 +1,245 @@ -using Microsoft.Extensions.DependencyInjection; -using Ocelot.Configuration; -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; -using Shouldly; -using System; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Configuration -{ - using Microsoft.AspNetCore.Http; - using Ocelot.Logging; - using System.Net.Http; - using System.Threading; - using System.Threading.Tasks; - - public class HttpHandlerOptionsCreatorTests - { - private IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; - private FileReRoute _fileReRoute; - private HttpHandlerOptions _httpHandlerOptions; - private IServiceProvider _serviceProvider; - private IServiceCollection _serviceCollection; - - public HttpHandlerOptionsCreatorTests() - { - _serviceCollection = new ServiceCollection(); - _serviceProvider = _serviceCollection.BuildServiceProvider(); - _httpHandlerOptionsCreator = new HttpHandlerOptionsCreator(_serviceProvider); - } - - [Fact] - public void should_not_use_tracing_if_fake_tracer_registered() - { - var fileReRoute = new FileReRoute - { - HttpHandlerOptions = new FileHttpHandlerOptions - { - UseTracing = true - } - }; - - var expectedOptions = new HttpHandlerOptions(false, false, false, true, int.MaxValue); - - this.Given(x => GivenTheFollowing(fileReRoute)) - .When(x => WhenICreateHttpHandlerOptions()) - .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) - .BDDfy(); - } - - [Fact] - public void should_use_tracing_if_real_tracer_registered() - { - var fileReRoute = new FileReRoute - { - HttpHandlerOptions = new FileHttpHandlerOptions - { - UseTracing = true - } - }; - - var expectedOptions = new HttpHandlerOptions(false, false, true, true, int.MaxValue); - - this.Given(x => GivenTheFollowing(fileReRoute)) - .And(x => GivenARealTracer()) - .When(x => WhenICreateHttpHandlerOptions()) - .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) - .BDDfy(); - } - - [Fact] - public void should_create_options_with_useCookie_false_and_allowAutoRedirect_true_as_default() - { - var fileReRoute = new FileReRoute(); - var expectedOptions = new HttpHandlerOptions(false, false, false, true, int.MaxValue); - - this.Given(x => GivenTheFollowing(fileReRoute)) - .When(x => WhenICreateHttpHandlerOptions()) - .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) - .BDDfy(); - } - - [Fact] - public void should_create_options_with_specified_useCookie_and_allowAutoRedirect() - { - var fileReRoute = new FileReRoute - { - HttpHandlerOptions = new FileHttpHandlerOptions - { - AllowAutoRedirect = false, - UseCookieContainer = false, - UseTracing = false - } - }; - - var expectedOptions = new HttpHandlerOptions(false, false, false, true, int.MaxValue); - - this.Given(x => GivenTheFollowing(fileReRoute)) - .When(x => WhenICreateHttpHandlerOptions()) - .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) - .BDDfy(); - } - - [Fact] - public void should_create_options_with_useproxy_true_as_default() - { - var fileReRoute = new FileReRoute - { - HttpHandlerOptions = new FileHttpHandlerOptions() - }; +using System; - var expectedOptions = new HttpHandlerOptions(false, false, false, true, int.MaxValue); +using Microsoft.Extensions.DependencyInjection; - this.Given(x => GivenTheFollowing(fileReRoute)) - .When(x => WhenICreateHttpHandlerOptions()) - .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) - .BDDfy(); - } - - [Fact] - public void should_create_options_with_specified_useproxy() - { - var fileReRoute = new FileReRoute - { - HttpHandlerOptions = new FileHttpHandlerOptions - { - UseProxy = false - } - }; - - var expectedOptions = new HttpHandlerOptions(false, false, false, false, int.MaxValue); - - this.Given(x => GivenTheFollowing(fileReRoute)) - .When(x => WhenICreateHttpHandlerOptions()) - .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) - .BDDfy(); - } - - [Fact] - public void should_create_options_with_specified_MaxConnectionsPerServer() - { - var fileReRoute = new FileReRoute - { - HttpHandlerOptions = new FileHttpHandlerOptions - { - MaxConnectionsPerServer = 10 - } - }; - - var expectedOptions = new HttpHandlerOptions(false, false, false, true, 10); - - this.Given(x => GivenTheFollowing(fileReRoute)) - .When(x => WhenICreateHttpHandlerOptions()) - .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) - .BDDfy(); - } - - [Fact] - public void should_create_options_fixing_specified_MaxConnectionsPerServer_range() - { - var fileReRoute = new FileReRoute - { - HttpHandlerOptions = new FileHttpHandlerOptions - { - MaxConnectionsPerServer = -1 - } - }; - - var expectedOptions = new HttpHandlerOptions(false, false, false, true, int.MaxValue); - - this.Given(x => GivenTheFollowing(fileReRoute)) - .When(x => WhenICreateHttpHandlerOptions()) - .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) - .BDDfy(); - } - - [Fact] - public void should_create_options_fixing_specified_MaxConnectionsPerServer_range_when_zero() - { - var fileReRoute = new FileReRoute - { - HttpHandlerOptions = new FileHttpHandlerOptions - { - MaxConnectionsPerServer = 0 - } - }; - - var expectedOptions = new HttpHandlerOptions(false, false, false, true, int.MaxValue); - - this.Given(x => GivenTheFollowing(fileReRoute)) - .When(x => WhenICreateHttpHandlerOptions()) - .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) - .BDDfy(); - } - - private void GivenTheFollowing(FileReRoute fileReRoute) - { - _fileReRoute = fileReRoute; - } - - private void WhenICreateHttpHandlerOptions() - { - _httpHandlerOptions = _httpHandlerOptionsCreator.Create(_fileReRoute.HttpHandlerOptions); - } - - private void ThenTheFollowingOptionsReturned(HttpHandlerOptions expected) - { - _httpHandlerOptions.ShouldNotBeNull(); - _httpHandlerOptions.AllowAutoRedirect.ShouldBe(expected.AllowAutoRedirect); - _httpHandlerOptions.UseCookieContainer.ShouldBe(expected.UseCookieContainer); - _httpHandlerOptions.UseTracing.ShouldBe(expected.UseTracing); - _httpHandlerOptions.UseProxy.ShouldBe(expected.UseProxy); - _httpHandlerOptions.MaxConnectionsPerServer.ShouldBe(expected.MaxConnectionsPerServer); - } +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; - private void GivenARealTracer() - { - var tracer = new FakeTracer(); - _serviceCollection.AddSingleton(); - _serviceProvider = _serviceCollection.BuildServiceProvider(); - _httpHandlerOptionsCreator = new HttpHandlerOptionsCreator(_serviceProvider); - } +using Shouldly; - private class FakeTracer : ITracer - { - public void Event(HttpContext httpContext, string @event) - { - throw new NotImplementedException(); - } +using TestStack.BDDfy; - public Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken, Action addTraceIdToRepo, - Func> baseSendAsync) - { - throw new NotImplementedException(); - } - } - } -} +using Xunit; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Logging; + +namespace Ocelot.UnitTests.Configuration +{ + public class HttpHandlerOptionsCreatorTests + { + private IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; + private FileRoute _fileRoute; + private HttpHandlerOptions _httpHandlerOptions; + private IServiceProvider _serviceProvider; + private readonly IServiceCollection _serviceCollection; + + public HttpHandlerOptionsCreatorTests() + { + _serviceCollection = new ServiceCollection(); + _serviceProvider = _serviceCollection.BuildServiceProvider(); + _httpHandlerOptionsCreator = new HttpHandlerOptionsCreator(_serviceProvider); + } + + [Fact] + public void should_not_use_tracing_if_fake_tracer_registered() + { + var fileRoute = new FileRoute + { + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true, + }, + }; + + var expectedOptions = new HttpHandlerOptions(false, false, false, true, int.MaxValue); + + this.Given(x => GivenTheFollowing(fileRoute)) + .When(x => WhenICreateHttpHandlerOptions()) + .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) + .BDDfy(); + } + + [Fact] + public void should_use_tracing_if_real_tracer_registered() + { + var fileRoute = new FileRoute + { + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true, + }, + }; + + var expectedOptions = new HttpHandlerOptions(false, false, true, true, int.MaxValue); + + this.Given(x => GivenTheFollowing(fileRoute)) + .And(x => GivenARealTracer()) + .When(x => WhenICreateHttpHandlerOptions()) + .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) + .BDDfy(); + } + + [Fact] + public void should_create_options_with_useCookie_false_and_allowAutoRedirect_true_as_default() + { + var fileRoute = new FileRoute(); + var expectedOptions = new HttpHandlerOptions(false, false, false, true, int.MaxValue); + + this.Given(x => GivenTheFollowing(fileRoute)) + .When(x => WhenICreateHttpHandlerOptions()) + .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) + .BDDfy(); + } + + [Fact] + public void should_create_options_with_specified_useCookie_and_allowAutoRedirect() + { + var fileRoute = new FileRoute + { + HttpHandlerOptions = new FileHttpHandlerOptions + { + AllowAutoRedirect = false, + UseCookieContainer = false, + UseTracing = false, + }, + }; + + var expectedOptions = new HttpHandlerOptions(false, false, false, true, int.MaxValue); + + this.Given(x => GivenTheFollowing(fileRoute)) + .When(x => WhenICreateHttpHandlerOptions()) + .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) + .BDDfy(); + } + + [Fact] + public void should_create_options_with_useproxy_true_as_default() + { + var fileRoute = new FileRoute + { + HttpHandlerOptions = new FileHttpHandlerOptions(), + }; + + var expectedOptions = new HttpHandlerOptions(false, false, false, true, int.MaxValue); + + this.Given(x => GivenTheFollowing(fileRoute)) + .When(x => WhenICreateHttpHandlerOptions()) + .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) + .BDDfy(); + } + + [Fact] + public void should_create_options_with_specified_useproxy() + { + var fileRoute = new FileRoute + { + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseProxy = false, + }, + }; + + var expectedOptions = new HttpHandlerOptions(false, false, false, false, int.MaxValue); + + this.Given(x => GivenTheFollowing(fileRoute)) + .When(x => WhenICreateHttpHandlerOptions()) + .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) + .BDDfy(); + } + + [Fact] + public void should_create_options_with_specified_MaxConnectionsPerServer() + { + var fileRoute = new FileRoute + { + HttpHandlerOptions = new FileHttpHandlerOptions + { + MaxConnectionsPerServer = 10, + }, + }; + + var expectedOptions = new HttpHandlerOptions(false, false, false, true, 10); + + this.Given(x => GivenTheFollowing(fileRoute)) + .When(x => WhenICreateHttpHandlerOptions()) + .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) + .BDDfy(); + } + + [Fact] + public void should_create_options_fixing_specified_MaxConnectionsPerServer_range() + { + var fileRoute = new FileRoute + { + HttpHandlerOptions = new FileHttpHandlerOptions + { + MaxConnectionsPerServer = -1, + }, + }; + + var expectedOptions = new HttpHandlerOptions(false, false, false, true, int.MaxValue); + + this.Given(x => GivenTheFollowing(fileRoute)) + .When(x => WhenICreateHttpHandlerOptions()) + .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) + .BDDfy(); + } + + [Fact] + public void should_create_options_fixing_specified_MaxConnectionsPerServer_range_when_zero() + { + var fileRoute = new FileRoute + { + HttpHandlerOptions = new FileHttpHandlerOptions + { + MaxConnectionsPerServer = 0, + }, + }; + + var expectedOptions = new HttpHandlerOptions(false, false, false, true, int.MaxValue); + + this.Given(x => GivenTheFollowing(fileRoute)) + .When(x => WhenICreateHttpHandlerOptions()) + .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) + .BDDfy(); + } + + private void GivenTheFollowing(FileRoute fileRoute) + { + _fileRoute = fileRoute; + } + + private void WhenICreateHttpHandlerOptions() + { + _httpHandlerOptions = _httpHandlerOptionsCreator.Create(_fileRoute.HttpHandlerOptions); + } + + private void ThenTheFollowingOptionsReturned(HttpHandlerOptions expected) + { + _httpHandlerOptions.ShouldNotBeNull(); + _httpHandlerOptions.AllowAutoRedirect.ShouldBe(expected.AllowAutoRedirect); + _httpHandlerOptions.UseCookieContainer.ShouldBe(expected.UseCookieContainer); + _httpHandlerOptions.UseTracing.ShouldBe(expected.UseTracing); + _httpHandlerOptions.UseProxy.ShouldBe(expected.UseProxy); + _httpHandlerOptions.MaxConnectionsPerServer.ShouldBe(expected.MaxConnectionsPerServer); + } + + private void GivenARealTracer() + { + var tracer = new FakeTracer(); + _serviceCollection.AddSingleton(); + _serviceProvider = _serviceCollection.BuildServiceProvider(); + _httpHandlerOptionsCreator = new HttpHandlerOptionsCreator(_serviceProvider); + } + + private class FakeTracer : ITracer + { + public void Event(HttpContext httpContext, string @event) + { + throw new NotImplementedException(); + } + + public Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken, Action addTraceIdToRepo, + Func> baseSendAsync) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs index 30f899166..661f6465d 100644 --- a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs @@ -1,11 +1,18 @@ -using Ocelot.Configuration; +using System; +using System.Collections.Generic; + +using Moq; + +using Ocelot.Configuration; using Ocelot.Configuration.Builder; +using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.Repository; using Ocelot.Responses; + using Shouldly; -using System; -using System.Collections.Generic; + using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Configuration @@ -16,10 +23,13 @@ public class InMemoryConfigurationRepositoryTests private IInternalConfiguration _config; private Response _result; private Response _getResult; + private readonly Mock _changeTokenSource; public InMemoryConfigurationRepositoryTests() { - _repo = new InMemoryInternalConfigurationRepository(); + _changeTokenSource = new Mock(MockBehavior.Strict); + _changeTokenSource.Setup(m => m.Activate()); + _repo = new InMemoryInternalConfigurationRepository(_changeTokenSource.Object); } [Fact] @@ -28,6 +38,7 @@ public void can_add_config() this.Given(x => x.GivenTheConfigurationIs(new FakeConfig("initial", "adminath"))) .When(x => x.WhenIAddOrReplaceTheConfig()) .Then(x => x.ThenNoErrorsAreReturned()) + .And(x => AndTheChangeTokenIsActivated()) .BDDfy(); } @@ -42,7 +53,7 @@ public void can_get_config() private void ThenTheConfigurationIsReturned() { - _getResult.Data.ReRoutes[0].DownstreamReRoute[0].DownstreamPathTemplate.Value.ShouldBe("initial"); + _getResult.Data.Routes[0].DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe("initial"); } private void WhenIGetTheConfiguration() @@ -71,6 +82,11 @@ private void ThenNoErrorsAreReturned() _result.IsError.ShouldBeFalse(); } + private void AndTheChangeTokenIsActivated() + { + _changeTokenSource.Verify(m => m.Activate(), Times.Once); + } + private class FakeConfig : IInternalConfiguration { private readonly string _downstreamTemplatePath; @@ -81,21 +97,21 @@ public FakeConfig(string downstreamTemplatePath, string administrationPath) AdministrationPath = administrationPath; } - public List ReRoutes + public List Routes { get { - var downstreamReRoute = new DownstreamReRouteBuilder() + var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate(_downstreamTemplatePath) .WithUpstreamHttpMethod(new List { "Get" }) .Build(); - return new List + return new List { - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List {"Get"}) - .Build() + .Build(), }; } } @@ -109,6 +125,7 @@ public List ReRoutes public string DownstreamScheme { get; } public QoSOptions QoSOptions { get; } public HttpHandlerOptions HttpHandlerOptions { get; } + public Version DownstreamHttpVersion { get; } } } -} +} diff --git a/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs index 8429bde4c..24ce683dc 100644 --- a/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/LoadBalancerOptionsCreatorTests.cs @@ -1,12 +1,15 @@ +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + namespace Ocelot.UnitTests.Configuration { - using Ocelot.Configuration; - using Ocelot.Configuration.Creator; - using Ocelot.Configuration.File; - using Shouldly; - using TestStack.BDDfy; - using Xunit; - public class LoadBalancerOptionsCreatorTests { private readonly ILoadBalancerOptionsCreator _creator; @@ -25,7 +28,7 @@ public void should_create() { Type = "test", Key = "west", - Expiry = 1 + Expiry = 1, }; this.Given(_ => GivenThe(fileLoadBalancerOptions)) diff --git a/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs index 911f30e35..c51f90ce3 100644 --- a/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/QoSOptionsCreatorTests.cs @@ -1,35 +1,38 @@ using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; -using Shouldly; -using TestStack.BDDfy; +using Ocelot.Configuration.File; + +using Shouldly; + +using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Configuration { public class QoSOptionsCreatorTests { - private QoSOptionsCreator _creator; - private FileReRoute _fileReRoute; + private readonly QoSOptionsCreator _creator; + private FileRoute _fileRoute; private QoSOptions _result; public QoSOptionsCreatorTests() { _creator = new QoSOptionsCreator(); - } - + } + [Fact] public void should_create_qos_options() { - var reRoute = new FileReRoute + var route = new FileRoute { QoSOptions = new FileQoSOptions { - ExceptionsAllowedBeforeBreaking = 1, + ExceptionsAllowedBeforeBreaking = 1, DurationOfBreak = 1, - TimeoutValue = 1 - } + TimeoutValue = 1, + }, }; var expected = new QoSOptionsBuilder() .WithDurationOfBreak(1) @@ -37,20 +40,20 @@ public void should_create_qos_options() .WithTimeoutValue(1) .Build(); - this.Given(x => x.GivenTheFollowingReRoute(reRoute)) + this.Given(x => x.GivenTheFollowingRoute(route)) .When(x => x.WhenICreate()) .Then(x => x.ThenTheFollowingIsReturned(expected)) .BDDfy(); } - private void GivenTheFollowingReRoute(FileReRoute fileReRoute) + private void GivenTheFollowingRoute(FileRoute fileRoute) { - _fileReRoute = fileReRoute; + _fileRoute = fileRoute; } private void WhenICreate() { - _result = _creator.Create(_fileReRoute.QoSOptions); + _result = _creator.Create(_fileRoute.QoSOptions); } private void ThenTheFollowingIsReturned(QoSOptions expected) @@ -60,4 +63,4 @@ private void ThenTheFollowingIsReturned(QoSOptions expected) _result.TimeoutValue.ShouldBe(expected.TimeoutValue); } } -} +} diff --git a/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs index a2bc19570..62a9db261 100644 --- a/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs @@ -1,106 +1,113 @@ -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; -using Shouldly; -using System; +using System; using System.Collections.Generic; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Configuration -{ - public class RateLimitOptionsCreatorTests - { - private FileReRoute _fileReRoute; - private FileGlobalConfiguration _fileGlobalConfig; - private bool _enabled; - private RateLimitOptionsCreator _creator; - private RateLimitOptions _result; - - public RateLimitOptionsCreatorTests() - { - _creator = new RateLimitOptionsCreator(); - } - - [Fact] - public void should_create_rate_limit_options() - { - var fileReRoute = new FileReRoute - { - RateLimitOptions = new FileRateLimitRule - { - ClientWhitelist = new List(), - Period = "Period", - Limit = 1, - PeriodTimespan = 1, - EnableRateLimiting = true - } - }; - var fileGlobalConfig = new FileGlobalConfiguration - { - RateLimitOptions = new FileRateLimitOptions - { - ClientIdHeader = "ClientIdHeader", - DisableRateLimitHeaders = true, - QuotaExceededMessage = "QuotaExceededMessage", - RateLimitCounterPrefix = "RateLimitCounterPrefix", - HttpStatusCode = 200 - } - }; - var expected = new RateLimitOptionsBuilder() - .WithClientIdHeader("ClientIdHeader") - .WithClientWhiteList(() => fileReRoute.RateLimitOptions.ClientWhitelist) - .WithDisableRateLimitHeaders(true) - .WithEnableRateLimiting(true) - .WithHttpStatusCode(200) - .WithQuotaExceededMessage("QuotaExceededMessage") - .WithRateLimitCounterPrefix("RateLimitCounterPrefix") - .WithRateLimitRule(new RateLimitRule(fileReRoute.RateLimitOptions.Period, - fileReRoute.RateLimitOptions.PeriodTimespan, - fileReRoute.RateLimitOptions.Limit)) - .Build(); - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .And(x => x.GivenTheFollowingFileGlobalConfig(fileGlobalConfig)) - .And(x => x.GivenRateLimitingIsEnabled()) - .When(x => x.WhenICreate()) - .Then(x => x.ThenTheFollowingIsReturned(expected)) - .BDDfy(); - } - - private void GivenTheFollowingFileReRoute(FileReRoute fileReRoute) - { - _fileReRoute = fileReRoute; - } +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; - private void GivenTheFollowingFileGlobalConfig(FileGlobalConfiguration fileGlobalConfig) - { - _fileGlobalConfig = fileGlobalConfig; - } +using Shouldly; - private void GivenRateLimitingIsEnabled() - { - _enabled = true; - } +using TestStack.BDDfy; - private void WhenICreate() - { - _result = _creator.Create(_fileReRoute.RateLimitOptions, _fileGlobalConfig); - } +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class RateLimitOptionsCreatorTests + { + private FileRoute _fileRoute; + private FileGlobalConfiguration _fileGlobalConfig; + private bool _enabled; + private readonly RateLimitOptionsCreator _creator; + private RateLimitOptions _result; + + public RateLimitOptionsCreatorTests() + { + _creator = new RateLimitOptionsCreator(); + } + + [Fact] + public void should_create_rate_limit_options() + { + var fileRoute = new FileRoute + { + RateLimitOptions = new FileRateLimitRule + { + ClientWhitelist = new List(), + Period = "Period", + Limit = 1, + PeriodTimespan = 1, + EnableRateLimiting = true, + }, + }; + var fileGlobalConfig = new FileGlobalConfiguration + { + RateLimitOptions = new FileRateLimitOptions + { + ClientIdHeader = "ClientIdHeader", + DisableRateLimitHeaders = true, + QuotaExceededMessage = "QuotaExceededMessage", + RateLimitCounterPrefix = "RateLimitCounterPrefix", + HttpStatusCode = 200, + }, + }; + var expected = new RateLimitOptionsBuilder() + .WithClientIdHeader("ClientIdHeader") + .WithClientWhiteList(() => fileRoute.RateLimitOptions.ClientWhitelist) + .WithDisableRateLimitHeaders(true) + .WithEnableRateLimiting(true) + .WithHttpStatusCode(200) + .WithQuotaExceededMessage("QuotaExceededMessage") + .WithRateLimitCounterPrefix("RateLimitCounterPrefix") + .WithRateLimitRule(new RateLimitRule(fileRoute.RateLimitOptions.Period, + fileRoute.RateLimitOptions.PeriodTimespan, + fileRoute.RateLimitOptions.Limit)) + .Build(); - private void ThenTheFollowingIsReturned(RateLimitOptions expected) + _enabled = false; + + this.Given(x => x.GivenTheFollowingFileRoute(fileRoute)) + .And(x => x.GivenTheFollowingFileGlobalConfig(fileGlobalConfig)) + .And(x => x.GivenRateLimitingIsEnabled()) + .When(x => x.WhenICreate()) + .Then(x => x.ThenTheFollowingIsReturned(expected)) + .BDDfy(); + } + + private void GivenTheFollowingFileRoute(FileRoute fileRoute) + { + _fileRoute = fileRoute; + } + + private void GivenTheFollowingFileGlobalConfig(FileGlobalConfiguration fileGlobalConfig) + { + _fileGlobalConfig = fileGlobalConfig; + } + + private void GivenRateLimitingIsEnabled() + { + _enabled = true; + } + + private void WhenICreate() + { + _result = _creator.Create(_fileRoute.RateLimitOptions, _fileGlobalConfig); + } + + private void ThenTheFollowingIsReturned(RateLimitOptions expected) { - _result.ClientIdHeader.ShouldBe(expected.ClientIdHeader); - _result.ClientWhitelist.ShouldBe(expected.ClientWhitelist); - _result.DisableRateLimitHeaders.ShouldBe(expected.DisableRateLimitHeaders); - _result.EnableRateLimiting.ShouldBe(expected.EnableRateLimiting); - _result.HttpStatusCode.ShouldBe(expected.HttpStatusCode); - _result.QuotaExceededMessage.ShouldBe(expected.QuotaExceededMessage); - _result.RateLimitCounterPrefix.ShouldBe(expected.RateLimitCounterPrefix); - _result.RateLimitRule.Limit.ShouldBe(expected.RateLimitRule.Limit); - _result.RateLimitRule.Period.ShouldBe(expected.RateLimitRule.Period); - TimeSpan.FromSeconds(_result.RateLimitRule.PeriodTimespan).Ticks.ShouldBe(TimeSpan.FromSeconds(expected.RateLimitRule.PeriodTimespan).Ticks); - } - } -} + _enabled.ShouldBeTrue(); + _result.ClientIdHeader.ShouldBe(expected.ClientIdHeader); + _result.ClientWhitelist.ShouldBe(expected.ClientWhitelist); + _result.DisableRateLimitHeaders.ShouldBe(expected.DisableRateLimitHeaders); + _result.EnableRateLimiting.ShouldBe(expected.EnableRateLimiting); + _result.HttpStatusCode.ShouldBe(expected.HttpStatusCode); + _result.QuotaExceededMessage.ShouldBe(expected.QuotaExceededMessage); + _result.RateLimitCounterPrefix.ShouldBe(expected.RateLimitCounterPrefix); + _result.RateLimitRule.Limit.ShouldBe(expected.RateLimitRule.Limit); + _result.RateLimitRule.Period.ShouldBe(expected.RateLimitRule.Period); + TimeSpan.FromSeconds(_result.RateLimitRule.PeriodTimespan).Ticks.ShouldBe(TimeSpan.FromSeconds(expected.RateLimitRule.PeriodTimespan).Ticks); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/ReRoutesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ReRoutesCreatorTests.cs deleted file mode 100644 index 32c235aa5..000000000 --- a/test/Ocelot.UnitTests/Configuration/ReRoutesCreatorTests.cs +++ /dev/null @@ -1,275 +0,0 @@ -namespace Ocelot.UnitTests.Configuration -{ - using Moq; - using Ocelot.Cache; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Configuration.Creator; - using Ocelot.Configuration.File; - using Ocelot.Values; - using Shouldly; - using System.Collections.Generic; - using System.Linq; - using TestStack.BDDfy; - using Xunit; - - public class ReRoutesCreatorTests - { - private ReRoutesCreator _creator; - private Mock _cthCreator; - private Mock _aoCreator; - private Mock _utpCreator; - private Mock _ridkCreator; - private Mock _qosoCreator; - private Mock _rroCreator; - private Mock _rloCreator; - private Mock _rCreator; - private Mock _hhoCreator; - private Mock _hfarCreator; - private Mock _daCreator; - private Mock _lboCreator; - private Mock _rrkCreator; - private Mock _soCreator; - private FileConfiguration _fileConfig; - private ReRouteOptions _rro; - private string _requestId; - private string _rrk; - private UpstreamPathTemplate _upt; - private AuthenticationOptions _ao; - private List _ctt; - private QoSOptions _qoso; - private RateLimitOptions _rlo; - private string _region; - private HttpHandlerOptions _hho; - private HeaderTransformations _ht; - private List _dhp; - private LoadBalancerOptions _lbo; - private List _result; - private SecurityOptions _securityOptions; - - public ReRoutesCreatorTests() - { - _cthCreator = new Mock(); - _aoCreator = new Mock(); - _utpCreator = new Mock(); - _ridkCreator = new Mock(); - _qosoCreator = new Mock(); - _rroCreator = new Mock(); - _rloCreator = new Mock(); - _rCreator = new Mock(); - _hhoCreator = new Mock(); - _hfarCreator = new Mock(); - _daCreator = new Mock(); - _lboCreator = new Mock(); - _rrkCreator = new Mock(); - _soCreator = new Mock(); - - _creator = new ReRoutesCreator( - _cthCreator.Object, - _aoCreator.Object, - _utpCreator.Object, - _ridkCreator.Object, - _qosoCreator.Object, - _rroCreator.Object, - _rloCreator.Object, - _rCreator.Object, - _hhoCreator.Object, - _hfarCreator.Object, - _daCreator.Object, - _lboCreator.Object, - _rrkCreator.Object, - _soCreator.Object - ); - } - - [Fact] - public void should_return_nothing() - { - var fileConfig = new FileConfiguration(); - - this.Given(_ => GivenThe(fileConfig)) - .When(_ => WhenICreate()) - .Then(_ => ThenNoReRoutesAreReturned()) - .BDDfy(); - } - - [Fact] - public void should_return_re_routes() - { - var fileConfig = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - ServiceName = "dave", - DangerousAcceptAnyServerCertificateValidator = true, - AddClaimsToRequest = new Dictionary - { - { "a","b" } - }, - AddHeadersToRequest = new Dictionary - { - { "c","d" } - }, - AddQueriesToRequest = new Dictionary - { - { "e","f" } - }, - UpstreamHttpMethod = new List { "GET", "POST" } - }, - new FileReRoute - { - ServiceName = "wave", - DangerousAcceptAnyServerCertificateValidator = false, - AddClaimsToRequest = new Dictionary - { - { "g","h" } - }, - AddHeadersToRequest = new Dictionary - { - { "i","j" } - }, - AddQueriesToRequest = new Dictionary - { - { "k","l" } - }, - UpstreamHttpMethod = new List { "PUT", "DELETE" } - } - } - }; - - this.Given(_ => GivenThe(fileConfig)) - .And(_ => GivenTheDependenciesAreSetUpCorrectly()) - .When(_ => WhenICreate()) - .Then(_ => ThenTheDependenciesAreCalledCorrectly()) - .And(_ => ThenTheReRoutesAreCreated()) - .BDDfy(); - } - - private void ThenTheDependenciesAreCalledCorrectly() - { - ThenTheDepsAreCalledFor(_fileConfig.ReRoutes[0], _fileConfig.GlobalConfiguration); - ThenTheDepsAreCalledFor(_fileConfig.ReRoutes[1], _fileConfig.GlobalConfiguration); - } - - private void GivenTheDependenciesAreSetUpCorrectly() - { - _rro = new ReRouteOptions(false, false, false, false, false); - _requestId = "testy"; - _rrk = "besty"; - _upt = new UpstreamPathTemplateBuilder().Build(); - _ao = new AuthenticationOptionsBuilder().Build(); - _ctt = new List(); - _qoso = new QoSOptionsBuilder().Build(); - _rlo = new RateLimitOptionsBuilder().Build(); - _region = "vesty"; - _hho = new HttpHandlerOptionsBuilder().Build(); - _ht = new HeaderTransformations(new List(), new List(), new List(), new List()); - _dhp = new List(); - _lbo = new LoadBalancerOptionsBuilder().Build(); - - _rroCreator.Setup(x => x.Create(It.IsAny())).Returns(_rro); - _ridkCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_requestId); - _rrkCreator.Setup(x => x.Create(It.IsAny())).Returns(_rrk); - _utpCreator.Setup(x => x.Create(It.IsAny())).Returns(_upt); - _aoCreator.Setup(x => x.Create(It.IsAny())).Returns(_ao); - _cthCreator.Setup(x => x.Create(It.IsAny>())).Returns(_ctt); - _qosoCreator.Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny>())).Returns(_qoso); - _rloCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_rlo); - _rCreator.Setup(x => x.Create(It.IsAny())).Returns(_region); - _hhoCreator.Setup(x => x.Create(It.IsAny())).Returns(_hho); - _hfarCreator.Setup(x => x.Create(It.IsAny())).Returns(_ht); - _daCreator.Setup(x => x.Create(It.IsAny())).Returns(_dhp); - _lboCreator.Setup(x => x.Create(It.IsAny())).Returns(_lbo); - } - - private void ThenTheReRoutesAreCreated() - { - _result.Count.ShouldBe(2); - - ThenTheReRouteIsSet(_fileConfig.ReRoutes[0], 0); - ThenTheReRouteIsSet(_fileConfig.ReRoutes[1], 1); - } - - private void ThenNoReRoutesAreReturned() - { - _result.ShouldBeEmpty(); - } - - private void GivenThe(FileConfiguration fileConfig) - { - _fileConfig = fileConfig; - } - - private void WhenICreate() - { - _result = _creator.Create(_fileConfig); - } - - private void ThenTheReRouteIsSet(FileReRoute expected, int reRouteIndex) - { - _result[reRouteIndex].DownstreamReRoute[0].IsAuthenticated.ShouldBe(_rro.IsAuthenticated); - _result[reRouteIndex].DownstreamReRoute[0].IsAuthorised.ShouldBe(_rro.IsAuthorised); - _result[reRouteIndex].DownstreamReRoute[0].IsCached.ShouldBe(_rro.IsCached); - _result[reRouteIndex].DownstreamReRoute[0].EnableEndpointEndpointRateLimiting.ShouldBe(_rro.EnableRateLimiting); - _result[reRouteIndex].DownstreamReRoute[0].RequestIdKey.ShouldBe(_requestId); - _result[reRouteIndex].DownstreamReRoute[0].LoadBalancerKey.ShouldBe(_rrk); - _result[reRouteIndex].DownstreamReRoute[0].UpstreamPathTemplate.ShouldBe(_upt); - _result[reRouteIndex].DownstreamReRoute[0].AuthenticationOptions.ShouldBe(_ao); - _result[reRouteIndex].DownstreamReRoute[0].ClaimsToHeaders.ShouldBe(_ctt); - _result[reRouteIndex].DownstreamReRoute[0].ClaimsToQueries.ShouldBe(_ctt); - _result[reRouteIndex].DownstreamReRoute[0].ClaimsToClaims.ShouldBe(_ctt); - _result[reRouteIndex].DownstreamReRoute[0].QosOptions.ShouldBe(_qoso); - _result[reRouteIndex].DownstreamReRoute[0].RateLimitOptions.ShouldBe(_rlo); - _result[reRouteIndex].DownstreamReRoute[0].CacheOptions.Region.ShouldBe(_region); - _result[reRouteIndex].DownstreamReRoute[0].CacheOptions.TtlSeconds.ShouldBe(expected.FileCacheOptions.TtlSeconds); - _result[reRouteIndex].DownstreamReRoute[0].HttpHandlerOptions.ShouldBe(_hho); - _result[reRouteIndex].DownstreamReRoute[0].UpstreamHeadersFindAndReplace.ShouldBe(_ht.Upstream); - _result[reRouteIndex].DownstreamReRoute[0].DownstreamHeadersFindAndReplace.ShouldBe(_ht.Downstream); - _result[reRouteIndex].DownstreamReRoute[0].AddHeadersToUpstream.ShouldBe(_ht.AddHeadersToUpstream); - _result[reRouteIndex].DownstreamReRoute[0].AddHeadersToDownstream.ShouldBe(_ht.AddHeadersToDownstream); - _result[reRouteIndex].DownstreamReRoute[0].DownstreamAddresses.ShouldBe(_dhp); - _result[reRouteIndex].DownstreamReRoute[0].LoadBalancerOptions.ShouldBe(_lbo); - _result[reRouteIndex].DownstreamReRoute[0].UseServiceDiscovery.ShouldBe(_rro.UseServiceDiscovery); - _result[reRouteIndex].DownstreamReRoute[0].DangerousAcceptAnyServerCertificateValidator.ShouldBe(expected.DangerousAcceptAnyServerCertificateValidator); - _result[reRouteIndex].DownstreamReRoute[0].DelegatingHandlers.ShouldBe(expected.DelegatingHandlers); - _result[reRouteIndex].DownstreamReRoute[0].ServiceName.ShouldBe(expected.ServiceName); - _result[reRouteIndex].DownstreamReRoute[0].DownstreamScheme.ShouldBe(expected.DownstreamScheme); - _result[reRouteIndex].DownstreamReRoute[0].RouteClaimsRequirement.ShouldBe(expected.RouteClaimsRequirement); - _result[reRouteIndex].DownstreamReRoute[0].DownstreamPathTemplate.Value.ShouldBe(expected.DownstreamPathTemplate); - _result[reRouteIndex].DownstreamReRoute[0].Key.ShouldBe(expected.Key); - _result[reRouteIndex].UpstreamHttpMethod - .Select(x => x.Method) - .ToList() - .ShouldContain(x => x == expected.UpstreamHttpMethod[0]); - _result[reRouteIndex].UpstreamHttpMethod - .Select(x => x.Method) - .ToList() - .ShouldContain(x => x == expected.UpstreamHttpMethod[1]); - _result[reRouteIndex].UpstreamHost.ShouldBe(expected.UpstreamHost); - _result[reRouteIndex].DownstreamReRoute.Count.ShouldBe(1); - _result[reRouteIndex].UpstreamTemplatePattern.ShouldBe(_upt); - } - - private void ThenTheDepsAreCalledFor(FileReRoute fileReRoute, FileGlobalConfiguration globalConfig) - { - _rroCreator.Verify(x => x.Create(fileReRoute), Times.Once); - _ridkCreator.Verify(x => x.Create(fileReRoute, globalConfig), Times.Once); - _rrkCreator.Verify(x => x.Create(fileReRoute), Times.Once); - _utpCreator.Verify(x => x.Create(fileReRoute), Times.Exactly(2)); - _aoCreator.Verify(x => x.Create(fileReRoute), Times.Once); - _cthCreator.Verify(x => x.Create(fileReRoute.AddHeadersToRequest), Times.Once); - _cthCreator.Verify(x => x.Create(fileReRoute.AddClaimsToRequest), Times.Once); - _cthCreator.Verify(x => x.Create(fileReRoute.AddQueriesToRequest), Times.Once); - _qosoCreator.Verify(x => x.Create(fileReRoute.QoSOptions, fileReRoute.UpstreamPathTemplate, fileReRoute.UpstreamHttpMethod)); - _rloCreator.Verify(x => x.Create(fileReRoute.RateLimitOptions, globalConfig), Times.Once); - _rCreator.Verify(x => x.Create(fileReRoute), Times.Once); - _hhoCreator.Verify(x => x.Create(fileReRoute.HttpHandlerOptions), Times.Once); - _hfarCreator.Verify(x => x.Create(fileReRoute), Times.Once); - _daCreator.Verify(x => x.Create(fileReRoute), Times.Once); - _lboCreator.Verify(x => x.Create(fileReRoute.LoadBalancerOptions), Times.Once); - _soCreator.Verify(x => x.Create(fileReRoute.SecurityOptions), Times.Once); - } - } -} diff --git a/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs index bf6ee6dc9..596979336 100644 --- a/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs @@ -1,17 +1,20 @@ using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; -using Shouldly; -using TestStack.BDDfy; +using Ocelot.Configuration.File; + +using Shouldly; + +using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Configuration { public class RequestIdKeyCreatorTests { - private FileReRoute _fileReRoute; + private FileRoute _fileRoute; private FileGlobalConfiguration _fileGlobalConfig; private string _result; - private RequestIdKeyCreator _creator; + private readonly RequestIdKeyCreator _creator; public RequestIdKeyCreatorTests() { @@ -21,13 +24,13 @@ public RequestIdKeyCreatorTests() [Fact] public void should_use_global_configuration() { - var reRoute = new FileReRoute(); + var route = new FileRoute(); var globalConfig = new FileGlobalConfiguration { - RequestIdKey = "cheese" + RequestIdKey = "cheese", }; - this.Given(x => x.GivenTheFollowingReRoute(reRoute)) + this.Given(x => x.GivenTheFollowingRoute(route)) .And(x => x.GivenTheFollowingGlobalConfig(globalConfig)) .When(x => x.WhenICreate()) .Then(x => x.ThenTheFollowingIsReturned("cheese")) @@ -37,13 +40,13 @@ public void should_use_global_configuration() [Fact] public void should_use_re_route_specific() { - var reRoute = new FileReRoute + var route = new FileRoute { - RequestIdKey = "cheese" + RequestIdKey = "cheese", }; var globalConfig = new FileGlobalConfiguration(); - this.Given(x => x.GivenTheFollowingReRoute(reRoute)) + this.Given(x => x.GivenTheFollowingRoute(route)) .And(x => x.GivenTheFollowingGlobalConfig(globalConfig)) .When(x => x.WhenICreate()) .Then(x => x.ThenTheFollowingIsReturned("cheese")) @@ -53,25 +56,25 @@ public void should_use_re_route_specific() [Fact] public void should_use_re_route_over_global_specific() { - var reRoute = new FileReRoute + var route = new FileRoute { - RequestIdKey = "cheese" - }; + RequestIdKey = "cheese", + }; var globalConfig = new FileGlobalConfiguration { - RequestIdKey = "test" + RequestIdKey = "test", }; - this.Given(x => x.GivenTheFollowingReRoute(reRoute)) + this.Given(x => x.GivenTheFollowingRoute(route)) .And(x => x.GivenTheFollowingGlobalConfig(globalConfig)) .When(x => x.WhenICreate()) .Then(x => x.ThenTheFollowingIsReturned("cheese")) .BDDfy(); } - private void GivenTheFollowingReRoute(FileReRoute fileReRoute) + private void GivenTheFollowingRoute(FileRoute fileRoute) { - _fileReRoute = fileReRoute; + _fileRoute = fileRoute; } private void GivenTheFollowingGlobalConfig(FileGlobalConfiguration globalConfig) @@ -81,7 +84,7 @@ private void GivenTheFollowingGlobalConfig(FileGlobalConfiguration globalConfig) private void WhenICreate() { - _result = _creator.Create(_fileReRoute, _fileGlobalConfig); + _result = _creator.Create(_fileRoute, _fileGlobalConfig); } private void ThenTheFollowingIsReturned(string expected) @@ -89,4 +92,4 @@ private void ThenTheFollowingIsReturned(string expected) _result.ShouldBe(expected); } } -} +} diff --git a/test/Ocelot.UnitTests/Configuration/ReRouteKeyCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs similarity index 57% rename from test/Ocelot.UnitTests/Configuration/ReRouteKeyCreatorTests.cs rename to test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs index 662f5adfb..c37b62142 100644 --- a/test/Ocelot.UnitTests/Configuration/ReRouteKeyCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs @@ -1,84 +1,88 @@ -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; -using Ocelot.LoadBalancer.LoadBalancers; -using Shouldly; using System.Collections.Generic; using System.Linq; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Configuration -{ - public class ReRouteKeyCreatorTests - { - private ReRouteKeyCreator _creator; - private FileReRoute _reRoute; - private string _result; - - public ReRouteKeyCreatorTests() - { - _creator = new ReRouteKeyCreator(); - } - - [Fact] - public void should_return_sticky_session_key() - { - var reRoute = new FileReRoute - { - LoadBalancerOptions = new FileLoadBalancerOptions - { - Key = "testy", - Type = nameof(CookieStickySessions) - } - }; - this.Given(_ => GivenThe(reRoute)) - .When(_ => WhenICreate()) - .Then(_ => ThenTheResultIs($"{nameof(CookieStickySessions)}:{reRoute.LoadBalancerOptions.Key}")) - .BDDfy(); - } - - [Fact] - public void should_return_re_route_key() - { - var reRoute = new FileReRoute - { - UpstreamPathTemplate = "/api/product", - UpstreamHttpMethod = new List { "GET", "POST", "PUT" }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 123 - }, - new FileHostAndPort - { - Host = "localhost", - Port = 123 - } - } - }; - - this.Given(_ => GivenThe(reRoute)) - .When(_ => WhenICreate()) - .Then(_ => ThenTheResultIs($"{reRoute.UpstreamPathTemplate}|{string.Join(",", reRoute.UpstreamHttpMethod)}|{string.Join(",", reRoute.DownstreamHostAndPorts.Select(x => $"{x.Host}:{x.Port}"))}")) - .BDDfy(); - } +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.LoadBalancer.LoadBalancers; - private void GivenThe(FileReRoute reRoute) - { - _reRoute = reRoute; - } +using Shouldly; - private void WhenICreate() - { - _result = _creator.Create(_reRoute); - } +using TestStack.BDDfy; - private void ThenTheResultIs(string expected) - { - _result.ShouldBe(expected); - } - } -} +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class RouteKeyCreatorTests + { + private readonly RouteKeyCreator _creator; + private FileRoute _route; + private string _result; + + public RouteKeyCreatorTests() + { + _creator = new RouteKeyCreator(); + } + + [Fact] + public void should_return_sticky_session_key() + { + var route = new FileRoute + { + LoadBalancerOptions = new FileLoadBalancerOptions + { + Key = "testy", + Type = nameof(CookieStickySessions), + }, + }; + + this.Given(_ => GivenThe(route)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheResultIs($"{nameof(CookieStickySessions)}:{route.LoadBalancerOptions.Key}")) + .BDDfy(); + } + + [Fact] + public void should_return_re_route_key() + { + var route = new FileRoute + { + UpstreamPathTemplate = "/api/product", + UpstreamHttpMethod = new List { "GET", "POST", "PUT" }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 123, + }, + new() + { + Host = "localhost", + Port = 123, + }, + }, + }; + + this.Given(_ => GivenThe(route)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheResultIs($"{route.UpstreamPathTemplate}|{string.Join(',', route.UpstreamHttpMethod)}|{string.Join(',', route.DownstreamHostAndPorts.Select(x => $"{x.Host}:{x.Port}"))}")) + .BDDfy(); + } + + private void GivenThe(FileRoute route) + { + _route = route; + } + + private void WhenICreate() + { + _result = _creator.Create(_route); + } + + private void ThenTheResultIs(string expected) + { + _result.ShouldBe(expected); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/ReRouteOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RouteOptionsCreatorTests.cs similarity index 51% rename from test/Ocelot.UnitTests/Configuration/ReRouteOptionsCreatorTests.cs rename to test/Ocelot.UnitTests/Configuration/RouteOptionsCreatorTests.cs index f5c97f713..dff25340e 100644 --- a/test/Ocelot.UnitTests/Configuration/ReRouteOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RouteOptionsCreatorTests.cs @@ -1,80 +1,84 @@ -namespace Ocelot.UnitTests.Configuration -{ - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Configuration.Creator; - using Ocelot.Configuration.File; - using Shouldly; - using System.Collections.Generic; - using TestStack.BDDfy; - using Xunit; +using System.Collections.Generic; - public class ReRouteOptionsCreatorTests - { - private readonly ReRouteOptionsCreator _creator; - private FileReRoute _reRoute; - private ReRouteOptions _result; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; - public ReRouteOptionsCreatorTests() - { - _creator = new ReRouteOptionsCreator(); - } +using Shouldly; - [Fact] - public void should_create_re_route_options() - { - var reRoute = new FileReRoute - { - RateLimitOptions = new FileRateLimitRule - { - EnableRateLimiting = true - }, - AuthenticationOptions = new FileAuthenticationOptions() - { - AuthenticationProviderKey = "Test" - }, - RouteClaimsRequirement = new Dictionary() - { - {"",""} - }, - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 1 - }, - ServiceName = "west" - }; +using TestStack.BDDfy; - var expected = new ReRouteOptionsBuilder() - .WithIsAuthenticated(true) - .WithIsAuthorised(true) - .WithIsCached(true) - .WithRateLimiting(true) - .WithUseServiceDiscovery(true) - .Build(); - - this.Given(x => x.GivenTheFollowing(reRoute)) - .When(x => x.WhenICreate()) - .Then(x => x.ThenTheFollowingIsReturned(expected)) - .BDDfy(); - } - - private void GivenTheFollowing(FileReRoute reRoute) - { - _reRoute = reRoute; - } - - private void WhenICreate() - { - _result = _creator.Create(_reRoute); - } - - private void ThenTheFollowingIsReturned(ReRouteOptions expected) - { - _result.IsAuthenticated.ShouldBe(expected.IsAuthenticated); - _result.IsAuthorised.ShouldBe(expected.IsAuthorised); - _result.IsCached.ShouldBe(expected.IsCached); - _result.EnableRateLimiting.ShouldBe(expected.EnableRateLimiting); - _result.UseServiceDiscovery.ShouldBe(expected.UseServiceDiscovery); - } - } -} +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class RouteOptionsCreatorTests + { + private readonly RouteOptionsCreator _creator; + private FileRoute _route; + private RouteOptions _result; + + public RouteOptionsCreatorTests() + { + _creator = new RouteOptionsCreator(); + } + + [Fact] + public void should_create_re_route_options() + { + var route = new FileRoute + { + RateLimitOptions = new FileRateLimitRule + { + EnableRateLimiting = true, + }, + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + }, + RouteClaimsRequirement = new Dictionary + { + {string.Empty,string.Empty}, + }, + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 1, + }, + ServiceName = "west", + }; + + var expected = new RouteOptionsBuilder() + .WithIsAuthenticated(true) + .WithIsAuthorized(true) + .WithIsCached(true) + .WithRateLimiting(true) + .WithUseServiceDiscovery(true) + .Build(); + + this.Given(x => x.GivenTheFollowing(route)) + .When(x => x.WhenICreate()) + .Then(x => x.ThenTheFollowingIsReturned(expected)) + .BDDfy(); + } + + private void GivenTheFollowing(FileRoute route) + { + _route = route; + } + + private void WhenICreate() + { + _result = _creator.Create(_route); + } + + private void ThenTheFollowingIsReturned(RouteOptions expected) + { + _result.IsAuthenticated.ShouldBe(expected.IsAuthenticated); + _result.IsAuthorized.ShouldBe(expected.IsAuthorized); + _result.IsCached.ShouldBe(expected.IsCached); + _result.EnableRateLimiting.ShouldBe(expected.EnableRateLimiting); + _result.UseServiceDiscovery.ShouldBe(expected.UseServiceDiscovery); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs new file mode 100644 index 000000000..4b07033ed --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs @@ -0,0 +1,282 @@ +using Moq; +using Ocelot.Cache; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Values; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class RoutesCreatorTests + { + private readonly RoutesCreator _creator; + private readonly Mock _cthCreator; + private readonly Mock _aoCreator; + private readonly Mock _utpCreator; + private readonly Mock _ridkCreator; + private readonly Mock _qosoCreator; + private readonly Mock _rroCreator; + private readonly Mock _rloCreator; + private readonly Mock _rCreator; + private readonly Mock _hhoCreator; + private readonly Mock _hfarCreator; + private readonly Mock _daCreator; + private readonly Mock _lboCreator; + private readonly Mock _rrkCreator; + private readonly Mock _soCreator; + private readonly Mock _versionCreator; + private FileConfiguration _fileConfig; + private RouteOptions _rro; + private string _requestId; + private string _rrk; + private UpstreamPathTemplate _upt; + private AuthenticationOptions _ao; + private List _ctt; + private QoSOptions _qoso; + private RateLimitOptions _rlo; + private string _region; + private HttpHandlerOptions _hho; + private HeaderTransformations _ht; + private List _dhp; + private LoadBalancerOptions _lbo; + private List _result; + private Version _expectedVersion; + + public RoutesCreatorTests() + { + _cthCreator = new Mock(); + _aoCreator = new Mock(); + _utpCreator = new Mock(); + _ridkCreator = new Mock(); + _qosoCreator = new Mock(); + _rroCreator = new Mock(); + _rloCreator = new Mock(); + _rCreator = new Mock(); + _hhoCreator = new Mock(); + _hfarCreator = new Mock(); + _daCreator = new Mock(); + _lboCreator = new Mock(); + _rrkCreator = new Mock(); + _soCreator = new Mock(); + _versionCreator = new Mock(); + + _creator = new RoutesCreator( + _cthCreator.Object, + _aoCreator.Object, + _utpCreator.Object, + _ridkCreator.Object, + _qosoCreator.Object, + _rroCreator.Object, + _rloCreator.Object, + _rCreator.Object, + _hhoCreator.Object, + _hfarCreator.Object, + _daCreator.Object, + _lboCreator.Object, + _rrkCreator.Object, + _soCreator.Object, + _versionCreator.Object + ); + } + + [Fact] + public void should_return_nothing() + { + var fileConfig = new FileConfiguration(); + + this.Given(_ => GivenThe(fileConfig)) + .When(_ => WhenICreate()) + .Then(_ => ThenNoRoutesAreReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_re_routes() + { + var fileConfig = new FileConfiguration + { + Routes = new List + { + new() + { + ServiceName = "dave", + DangerousAcceptAnyServerCertificateValidator = true, + AddClaimsToRequest = new Dictionary + { + { "a","b" }, + }, + AddHeadersToRequest = new Dictionary + { + { "c","d" }, + }, + AddQueriesToRequest = new Dictionary + { + { "e","f" }, + }, + UpstreamHttpMethod = new List { "GET", "POST" }, + }, + new() + { + ServiceName = "wave", + DangerousAcceptAnyServerCertificateValidator = false, + AddClaimsToRequest = new Dictionary + { + { "g","h" }, + }, + AddHeadersToRequest = new Dictionary + { + { "i","j" }, + }, + AddQueriesToRequest = new Dictionary + { + { "k","l" }, + }, + UpstreamHttpMethod = new List { "PUT", "DELETE" }, + }, + }, + }; + + this.Given(_ => GivenThe(fileConfig)) + .And(_ => GivenTheDependenciesAreSetUpCorrectly()) + .When(_ => WhenICreate()) + .Then(_ => ThenTheDependenciesAreCalledCorrectly()) + .And(_ => ThenTheRoutesAreCreated()) + .BDDfy(); + } + + private void ThenTheDependenciesAreCalledCorrectly() + { + ThenTheDepsAreCalledFor(_fileConfig.Routes[0], _fileConfig.GlobalConfiguration); + ThenTheDepsAreCalledFor(_fileConfig.Routes[1], _fileConfig.GlobalConfiguration); + } + + private void GivenTheDependenciesAreSetUpCorrectly() + { + _expectedVersion = new Version("1.1"); + _rro = new RouteOptions(false, false, false, false, false); + _requestId = "testy"; + _rrk = "besty"; + _upt = new UpstreamPathTemplateBuilder().Build(); + _ao = new AuthenticationOptionsBuilder().Build(); + _ctt = new List(); + _qoso = new QoSOptionsBuilder().Build(); + _rlo = new RateLimitOptionsBuilder().Build(); + _region = "vesty"; + _hho = new HttpHandlerOptionsBuilder().Build(); + _ht = new HeaderTransformations(new List(), new List(), new List(), new List()); + _dhp = new List(); + _lbo = new LoadBalancerOptionsBuilder().Build(); + + _rroCreator.Setup(x => x.Create(It.IsAny())).Returns(_rro); + _ridkCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_requestId); + _rrkCreator.Setup(x => x.Create(It.IsAny())).Returns(_rrk); + _utpCreator.Setup(x => x.Create(It.IsAny())).Returns(_upt); + _aoCreator.Setup(x => x.Create(It.IsAny())).Returns(_ao); + _cthCreator.Setup(x => x.Create(It.IsAny>())).Returns(_ctt); + _qosoCreator.Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny>())).Returns(_qoso); + _rloCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_rlo); + _rCreator.Setup(x => x.Create(It.IsAny())).Returns(_region); + _hhoCreator.Setup(x => x.Create(It.IsAny())).Returns(_hho); + _hfarCreator.Setup(x => x.Create(It.IsAny())).Returns(_ht); + _daCreator.Setup(x => x.Create(It.IsAny())).Returns(_dhp); + _lboCreator.Setup(x => x.Create(It.IsAny())).Returns(_lbo); + _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersion); + } + + private void ThenTheRoutesAreCreated() + { + _result.Count.ShouldBe(2); + + ThenTheRouteIsSet(_fileConfig.Routes[0], 0); + ThenTheRouteIsSet(_fileConfig.Routes[1], 1); + } + + private void ThenNoRoutesAreReturned() + { + _result.ShouldBeEmpty(); + } + + private void GivenThe(FileConfiguration fileConfig) + { + _fileConfig = fileConfig; + } + + private void WhenICreate() + { + _result = _creator.Create(_fileConfig); + } + + private void ThenTheRouteIsSet(FileRoute expected, int routeIndex) + { + _result[routeIndex].DownstreamRoute[0].DownstreamHttpVersion.ShouldBe(_expectedVersion); + _result[routeIndex].DownstreamRoute[0].IsAuthenticated.ShouldBe(_rro.IsAuthenticated); + _result[routeIndex].DownstreamRoute[0].IsAuthorized.ShouldBe(_rro.IsAuthorized); + _result[routeIndex].DownstreamRoute[0].IsCached.ShouldBe(_rro.IsCached); + _result[routeIndex].DownstreamRoute[0].EnableEndpointEndpointRateLimiting.ShouldBe(_rro.EnableRateLimiting); + _result[routeIndex].DownstreamRoute[0].RequestIdKey.ShouldBe(_requestId); + _result[routeIndex].DownstreamRoute[0].LoadBalancerKey.ShouldBe(_rrk); + _result[routeIndex].DownstreamRoute[0].UpstreamPathTemplate.ShouldBe(_upt); + _result[routeIndex].DownstreamRoute[0].AuthenticationOptions.ShouldBe(_ao); + _result[routeIndex].DownstreamRoute[0].ClaimsToHeaders.ShouldBe(_ctt); + _result[routeIndex].DownstreamRoute[0].ClaimsToQueries.ShouldBe(_ctt); + _result[routeIndex].DownstreamRoute[0].ClaimsToClaims.ShouldBe(_ctt); + _result[routeIndex].DownstreamRoute[0].QosOptions.ShouldBe(_qoso); + _result[routeIndex].DownstreamRoute[0].RateLimitOptions.ShouldBe(_rlo); + _result[routeIndex].DownstreamRoute[0].CacheOptions.Region.ShouldBe(_region); + _result[routeIndex].DownstreamRoute[0].CacheOptions.TtlSeconds.ShouldBe(expected.FileCacheOptions.TtlSeconds); + _result[routeIndex].DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_hho); + _result[routeIndex].DownstreamRoute[0].UpstreamHeadersFindAndReplace.ShouldBe(_ht.Upstream); + _result[routeIndex].DownstreamRoute[0].DownstreamHeadersFindAndReplace.ShouldBe(_ht.Downstream); + _result[routeIndex].DownstreamRoute[0].AddHeadersToUpstream.ShouldBe(_ht.AddHeadersToUpstream); + _result[routeIndex].DownstreamRoute[0].AddHeadersToDownstream.ShouldBe(_ht.AddHeadersToDownstream); + _result[routeIndex].DownstreamRoute[0].DownstreamAddresses.ShouldBe(_dhp); + _result[routeIndex].DownstreamRoute[0].LoadBalancerOptions.ShouldBe(_lbo); + _result[routeIndex].DownstreamRoute[0].UseServiceDiscovery.ShouldBe(_rro.UseServiceDiscovery); + _result[routeIndex].DownstreamRoute[0].DangerousAcceptAnyServerCertificateValidator.ShouldBe(expected.DangerousAcceptAnyServerCertificateValidator); + _result[routeIndex].DownstreamRoute[0].DelegatingHandlers.ShouldBe(expected.DelegatingHandlers); + _result[routeIndex].DownstreamRoute[0].ServiceName.ShouldBe(expected.ServiceName); + _result[routeIndex].DownstreamRoute[0].DownstreamScheme.ShouldBe(expected.DownstreamScheme); + _result[routeIndex].DownstreamRoute[0].RouteClaimsRequirement.ShouldBe(expected.RouteClaimsRequirement); + _result[routeIndex].DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe(expected.DownstreamPathTemplate); + _result[routeIndex].DownstreamRoute[0].Key.ShouldBe(expected.Key); + _result[routeIndex].UpstreamHttpMethod + .Select(x => x.Method) + .ToList() + .ShouldContain(x => x == expected.UpstreamHttpMethod[0]); + _result[routeIndex].UpstreamHttpMethod + .Select(x => x.Method) + .ToList() + .ShouldContain(x => x == expected.UpstreamHttpMethod[1]); + _result[routeIndex].UpstreamHost.ShouldBe(expected.UpstreamHost); + _result[routeIndex].DownstreamRoute.Count.ShouldBe(1); + _result[routeIndex].UpstreamTemplatePattern.ShouldBe(_upt); + } + + private void ThenTheDepsAreCalledFor(FileRoute fileRoute, FileGlobalConfiguration globalConfig) + { + _rroCreator.Verify(x => x.Create(fileRoute), Times.Once); + _ridkCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); + _rrkCreator.Verify(x => x.Create(fileRoute), Times.Once); + _utpCreator.Verify(x => x.Create(fileRoute), Times.Exactly(2)); + _aoCreator.Verify(x => x.Create(fileRoute), Times.Once); + _cthCreator.Verify(x => x.Create(fileRoute.AddHeadersToRequest), Times.Once); + _cthCreator.Verify(x => x.Create(fileRoute.AddClaimsToRequest), Times.Once); + _cthCreator.Verify(x => x.Create(fileRoute.AddQueriesToRequest), Times.Once); + _qosoCreator.Verify(x => x.Create(fileRoute.QoSOptions, fileRoute.UpstreamPathTemplate, fileRoute.UpstreamHttpMethod)); + _rloCreator.Verify(x => x.Create(fileRoute.RateLimitOptions, globalConfig), Times.Once); + _rCreator.Verify(x => x.Create(fileRoute), Times.Once); + _hhoCreator.Verify(x => x.Create(fileRoute.HttpHandlerOptions), Times.Once); + _hfarCreator.Verify(x => x.Create(fileRoute), Times.Once); + _daCreator.Verify(x => x.Create(fileRoute), Times.Once); + _lboCreator.Verify(x => x.Create(fileRoute.LoadBalancerOptions), Times.Once); + _soCreator.Verify(x => x.Create(fileRoute.SecurityOptions), Times.Once); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs index e79871eba..a00510cf8 100644 --- a/test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs @@ -1,68 +1,67 @@ -using Ocelot.Configuration; -using Ocelot.Configuration.Creator; +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Shouldly; using System.Collections.Generic; using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Configuration -{ - public class SecurityOptionsCreatorTests - { - private FileReRoute _fileReRoute; - private FileGlobalConfiguration _fileGlobalConfig; - private SecurityOptions _result; - private ISecurityOptionsCreator _creator; - - public SecurityOptionsCreatorTests() - { - _creator = new SecurityOptionsCreator(); - } - - [Fact] - public void should_create_security_config() - { - var ipAllowedList = new List() { "127.0.0.1", "192.168.1.1" }; - var ipBlockedList = new List() { "127.0.0.1", "192.168.1.1" }; - var fileReRoute = new FileReRoute - { - SecurityOptions = new FileSecurityOptions() - { - IPAllowedList = ipAllowedList, - IPBlockedList = ipBlockedList - } - }; - - var expected = new SecurityOptions(ipAllowedList, ipBlockedList); - - this.Given(x => x.GivenThe(fileReRoute)) - .When(x => x.WhenICreate()) - .Then(x => x.ThenTheResultIs(expected)) - .BDDfy(); - } - - private void GivenThe(FileReRoute reRoute) - { - _fileReRoute = reRoute; - } - - private void WhenICreate() - { - _result = _creator.Create(_fileReRoute.SecurityOptions); - } - - private void ThenTheResultIs(SecurityOptions expected) - { - for (int i = 0; i < expected.IPAllowedList.Count; i++) - { - _result.IPAllowedList[i].ShouldBe(expected.IPAllowedList[i]); - } - - for (int i = 0; i < expected.IPBlockedList.Count; i++) - { - _result.IPBlockedList[i].ShouldBe(expected.IPBlockedList[i]); - } - } - } -} +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class SecurityOptionsCreatorTests + { + private FileRoute _fileRoute; + private SecurityOptions _result; + private readonly ISecurityOptionsCreator _creator; + + public SecurityOptionsCreatorTests() + { + _creator = new SecurityOptionsCreator(); + } + + [Fact] + public void should_create_security_config() + { + var ipAllowedList = new List { "127.0.0.1", "192.168.1.1" }; + var ipBlockedList = new List { "127.0.0.1", "192.168.1.1" }; + var fileRoute = new FileRoute + { + SecurityOptions = new FileSecurityOptions + { + IPAllowedList = ipAllowedList, + IPBlockedList = ipBlockedList, + }, + }; + + var expected = new SecurityOptions(ipAllowedList, ipBlockedList); + + this.Given(x => x.GivenThe(fileRoute)) + .When(x => x.WhenICreate()) + .Then(x => x.ThenTheResultIs(expected)) + .BDDfy(); + } + + private void GivenThe(FileRoute route) + { + _fileRoute = route; + } + + private void WhenICreate() + { + _result = _creator.Create(_fileRoute.SecurityOptions); + } + + private void ThenTheResultIs(SecurityOptions expected) + { + for (var i = 0; i < expected.IPAllowedList.Count; i++) + { + _result.IPAllowedList[i].ShouldBe(expected.IPAllowedList[i]); + } + + for (var i = 0; i < expected.IPBlockedList.Count; i++) + { + _result.IPBlockedList[i].ShouldBe(expected.IPBlockedList[i]); + } + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs index 1fbf4f790..0232a6012 100644 --- a/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs @@ -2,8 +2,11 @@ using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; + using Shouldly; + using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Configuration @@ -26,16 +29,18 @@ public void should_create_service_provider_config() { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { + Scheme = "https", Host = "127.0.0.1", Port = 1234, Type = "ServiceFabric", Token = "testtoken", ConfigurationKey = "woo", - Namespace = "default" - } + Namespace = "default", + }, }; var expected = new ServiceProviderConfigurationBuilder() + .WithScheme("https") .WithHost("127.0.0.1") .WithPort(1234) .WithType("ServiceFabric") @@ -62,6 +67,7 @@ private void WhenICreate() private void ThenTheConfigIs(ServiceProviderConfiguration expected) { + _result.Scheme.ShouldBe(expected.Scheme); _result.Host.ShouldBe(expected.Host); _result.Port.ShouldBe(expected.Port); _result.Token.ShouldBe(expected.Token); diff --git a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs index 5d0684966..461292932 100644 --- a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs @@ -1,260 +1,263 @@ -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; using Ocelot.Values; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Configuration -{ - public class UpstreamTemplatePatternCreatorTests - { - private FileReRoute _fileReRoute; - private readonly UpstreamTemplatePatternCreator _creator; - private UpstreamPathTemplate _result; - - public UpstreamTemplatePatternCreatorTests() - { - _creator = new UpstreamTemplatePatternCreator(); - } - - [Fact] - public void should_match_up_to_next_slash() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/api/v{apiVersion}/cards", - Priority = 0 - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^(?i)/api/v[^/]+/cards$")) - .And(x => ThenThePriorityIs(0)) - .BDDfy(); - } - - [Fact] - public void should_use_re_route_priority() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/orders/{catchAll}", - Priority = 0 - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^(?i)/orders/.+$")) - .And(x => ThenThePriorityIs(0)) - .BDDfy(); - } - - [Fact] - public void should_use_zero_priority() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/{catchAll}", - Priority = 1 - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/.*")) - .And(x => ThenThePriorityIs(0)) - .BDDfy(); - } - - [Fact] - public void should_set_upstream_template_pattern_to_ignore_case_sensitivity() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/PRODUCTS/{productId}", - ReRouteIsCaseSensitive = false - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^(?i)/PRODUCTS/.+$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_match_forward_slash_or_no_forward_slash_if_template_end_with_forward_slash() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/PRODUCTS/", - ReRouteIsCaseSensitive = false - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^(?i)/PRODUCTS(/|)$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_set_upstream_template_pattern_to_respect_case_sensitivity() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/PRODUCTS/{productId}", - ReRouteIsCaseSensitive = true - }; - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/PRODUCTS/.+$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_create_template_pattern_that_matches_anything_to_end_of_string() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/api/products/{productId}", - ReRouteIsCaseSensitive = true - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/api/products/.+$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - [Fact] - public void should_create_template_pattern_that_matches_more_than_one_placeholder() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/api/products/{productId}/variants/{variantId}", - ReRouteIsCaseSensitive = true - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/api/products/[^/]+/variants/.+$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_create_template_pattern_that_matches_more_than_one_placeholder_with_trailing_slash() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/api/products/{productId}/variants/{variantId}/", - ReRouteIsCaseSensitive = true - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/api/products/[^/]+/variants/[^/]+(/|)$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_create_template_pattern_that_matches_to_end_of_string() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/" - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_create_template_pattern_that_matches_to_end_of_string_when_slash_and_placeholder() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/{url}" - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/.*")) - .And(x => ThenThePriorityIs(0)) - .BDDfy(); - } - - [Fact] - public void should_create_template_pattern_that_starts_with_placeholder_then_has_another_later() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/{productId}/products/variants/{variantId}/", - ReRouteIsCaseSensitive = true - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^/[^/]+/products/variants/[^/]+(/|)$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_create_template_pattern_that_matches_query_string() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}" - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^(?i)/api/subscriptions/[^/]+/updates\\?unitId=.+$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - [Fact] - public void should_create_template_pattern_that_matches_query_string_with_multiple_params() - { - var fileReRoute = new FileReRoute - { - UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}&productId={productId}" - }; - - this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) - .When(x => x.WhenICreateTheTemplatePattern()) - .Then(x => x.ThenTheFollowingIsReturned("^(?i)/api/subscriptions/[^/]+/updates\\?unitId=.+&productId=.+$")) - .And(x => ThenThePriorityIs(1)) - .BDDfy(); - } - - private void GivenTheFollowingFileReRoute(FileReRoute fileReRoute) - { - _fileReRoute = fileReRoute; - } - - private void WhenICreateTheTemplatePattern() - { - _result = _creator.Create(_fileReRoute); - } +using Shouldly; - private void ThenTheFollowingIsReturned(string expected) - { - _result.Template.ShouldBe(expected); - } +using TestStack.BDDfy; - private void ThenThePriorityIs(int v) - { - _result.Priority.ShouldBe(v); - } - } -} +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class UpstreamTemplatePatternCreatorTests + { + private FileRoute _fileRoute; + private readonly UpstreamTemplatePatternCreator _creator; + private UpstreamPathTemplate _result; + + public UpstreamTemplatePatternCreatorTests() + { + _creator = new UpstreamTemplatePatternCreator(); + } + + [Fact] + public void should_match_up_to_next_slash() + { + var fileRoute = new FileRoute + { + UpstreamPathTemplate = "/api/v{apiVersion}/cards", + Priority = 0, + }; + + this.Given(x => x.GivenTheFollowingFileRoute(fileRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^(?i)/api/v[^/]+/cards$")) + .And(x => ThenThePriorityIs(0)) + .BDDfy(); + } + + [Fact] + public void should_use_re_route_priority() + { + var fileRoute = new FileRoute + { + UpstreamPathTemplate = "/orders/{catchAll}", + Priority = 0, + }; + + this.Given(x => x.GivenTheFollowingFileRoute(fileRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^(?i)/orders/.+$")) + .And(x => ThenThePriorityIs(0)) + .BDDfy(); + } + + [Fact] + public void should_use_zero_priority() + { + var fileRoute = new FileRoute + { + UpstreamPathTemplate = "/{catchAll}", + Priority = 1, + }; + + this.Given(x => x.GivenTheFollowingFileRoute(fileRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/.*")) + .And(x => ThenThePriorityIs(0)) + .BDDfy(); + } + + [Fact] + public void should_set_upstream_template_pattern_to_ignore_case_sensitivity() + { + var fileRoute = new FileRoute + { + UpstreamPathTemplate = "/PRODUCTS/{productId}", + RouteIsCaseSensitive = false, + }; + + this.Given(x => x.GivenTheFollowingFileRoute(fileRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^(?i)/PRODUCTS/.+$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_match_forward_slash_or_no_forward_slash_if_template_end_with_forward_slash() + { + var fileRoute = new FileRoute + { + UpstreamPathTemplate = "/PRODUCTS/", + RouteIsCaseSensitive = false, + }; + + this.Given(x => x.GivenTheFollowingFileRoute(fileRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^(?i)/PRODUCTS(/|)$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_set_upstream_template_pattern_to_respect_case_sensitivity() + { + var fileRoute = new FileRoute + { + UpstreamPathTemplate = "/PRODUCTS/{productId}", + RouteIsCaseSensitive = true, + }; + this.Given(x => x.GivenTheFollowingFileRoute(fileRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/PRODUCTS/.+$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_anything_to_end_of_string() + { + var fileRoute = new FileRoute + { + UpstreamPathTemplate = "/api/products/{productId}", + RouteIsCaseSensitive = true, + }; + + this.Given(x => x.GivenTheFollowingFileRoute(fileRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/api/products/.+$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_more_than_one_placeholder() + { + var fileRoute = new FileRoute + { + UpstreamPathTemplate = "/api/products/{productId}/variants/{variantId}", + RouteIsCaseSensitive = true, + }; + + this.Given(x => x.GivenTheFollowingFileRoute(fileRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/api/products/[^/]+/variants/.+$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_more_than_one_placeholder_with_trailing_slash() + { + var fileRoute = new FileRoute + { + UpstreamPathTemplate = "/api/products/{productId}/variants/{variantId}/", + RouteIsCaseSensitive = true, + }; + + this.Given(x => x.GivenTheFollowingFileRoute(fileRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/api/products/[^/]+/variants/[^/]+(/|)$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_to_end_of_string() + { + var fileRoute = new FileRoute + { + UpstreamPathTemplate = "/", + }; + + this.Given(x => x.GivenTheFollowingFileRoute(fileRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_to_end_of_string_when_slash_and_placeholder() + { + var fileRoute = new FileRoute + { + UpstreamPathTemplate = "/{url}", + }; + + this.Given(x => x.GivenTheFollowingFileRoute(fileRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/.*")) + .And(x => ThenThePriorityIs(0)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_starts_with_placeholder_then_has_another_later() + { + var fileRoute = new FileRoute + { + UpstreamPathTemplate = "/{productId}/products/variants/{variantId}/", + RouteIsCaseSensitive = true, + }; + + this.Given(x => x.GivenTheFollowingFileRoute(fileRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/[^/]+/products/variants/[^/]+(/|)$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_query_string() + { + var fileRoute = new FileRoute + { + UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", + }; + + this.Given(x => x.GivenTheFollowingFileRoute(fileRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^(?i)/api/subscriptions/[^/]+/updates\\?unitId=.+$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_query_string_with_multiple_params() + { + var fileRoute = new FileRoute + { + UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}&productId={productId}", + }; + + this.Given(x => x.GivenTheFollowingFileRoute(fileRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^(?i)/api/subscriptions/[^/]+/updates\\?unitId=.+&productId=.+$")) + .And(x => ThenThePriorityIs(1)) + .BDDfy(); + } + + private void GivenTheFollowingFileRoute(FileRoute fileRoute) + { + _fileRoute = fileRoute; + } + + private void WhenICreateTheTemplatePattern() + { + _result = _creator.Create(_fileRoute); + } + + private void ThenTheFollowingIsReturned(string expected) + { + _result.Template.ShouldBe(expected); + } + + private void ThenThePriorityIs(int v) + { + _result.Priority.ShouldBe(v); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs index 7055c1ad6..f21911f14 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs @@ -1,1451 +1,1585 @@ -namespace Ocelot.UnitTests.Configuration.Validation -{ - using Microsoft.AspNetCore.Authentication; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Logging; - using Microsoft.Extensions.Options; - using Moq; - using Ocelot.Configuration.File; - using Ocelot.Configuration.Validator; - using Ocelot.Requester; - using Ocelot.Responses; - using Ocelot.ServiceDiscovery; - using Ocelot.ServiceDiscovery.Providers; - using Ocelot.Values; - using Requester; - using Shouldly; - using System.Collections.Generic; - using System.Security.Claims; - using System.Text.Encodings.Web; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - - public class FileConfigurationFluentValidatorTests - { - private IConfigurationValidator _configurationValidator; - private FileConfiguration _fileConfiguration; - private Response _result; - private readonly Mock _authProvider; - - public FileConfigurationFluentValidatorTests() - { - _authProvider = new Mock(); - var provider = new ServiceCollection() - .BuildServiceProvider(); - // Todo - replace with mocks - _configurationValidator = new FileConfigurationFluentValidator(provider, new ReRouteFluentValidator(_authProvider.Object, new HostAndPortValidator(), new FileQoSOptionsFluentValidator(provider)), new FileGlobalConfigurationFluentValidator(new FileQoSOptionsFluentValidator(provider))); - } - - [Fact] - public void configuration_is_valid_if_service_discovery_options_specified_and_has_service_fabric_as_option() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "test" - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Type = "ServiceFabric", - Port = 8500 - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_if_service_discovery_options_specified_and_has_service_discovery_handler() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "test" - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Type = "FakeServiceDiscoveryProvider", - Port = 8500 - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .And(x => x.GivenAServiceDiscoveryHandler()) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_if_service_discovery_options_specified_dynamically_and_has_service_discovery_handler() - { - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Type = "FakeServiceDiscoveryProvider", - Port = 8500 - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .And(x => x.GivenAServiceDiscoveryHandler()) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_service_discovery_options_specified_but_no_service_discovery_handler() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "test" - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Type = "FakeServiceDiscoveryProvider", - Port = 8500 - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorIs()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?")) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_service_discovery_options_specified_dynamically_but_service_discovery_handler() - { - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Type = "FakeServiceDiscoveryProvider", - Port = 8500 - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorIs()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?")) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_service_discovery_options_specified_but_no_service_discovery_handler_with_matching_name() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - ServiceName = "test" - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "localhost", - Type = "consul", - Port = 8500 - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .And(x => x.GivenAServiceDiscoveryHandler()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorIs()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot, errors are: Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?")) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_if_qos_options_specified_and_has_qos_handler() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura", - QoSOptions = new FileQoSOptions - { - TimeoutValue = 1, - ExceptionsAllowedBeforeBreaking = 1 - } - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .And(x => x.GivenAQoSHandler()) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_if_qos_options_specified_globally_and_has_qos_handler() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura", - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - QoSOptions = new FileQoSOptions - { - TimeoutValue = 1, - ExceptionsAllowedBeforeBreaking = 1 - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .And(x => x.GivenAQoSHandler()) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_qos_options_specified_but_no_qos_handler() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura", - QoSOptions = new FileQoSOptions - { - TimeoutValue = 1, - ExceptionsAllowedBeforeBreaking = 1 - } - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorIs()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?")) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_qos_options_specified_globally_but_no_qos_handler() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura", - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - QoSOptions = new FileQoSOptions - { - TimeoutValue = 1, - ExceptionsAllowedBeforeBreaking = 1 - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorIs()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?")) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_if_aggregates_are_valid() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Tom", - "Laura" - } - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_aggregates_are_duplicate_of_re_routes() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom", - UpstreamHost = "localhost" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/tom", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Tom", - "Laura" - }, - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "reRoute /tom has duplicate aggregate")) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_if_aggregates_are_not_duplicate_of_re_routes() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Post" }, - Key = "Tom", - UpstreamHost = "localhost" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/tom", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Tom", - "Laura" - }, - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_aggregates_are_duplicate_of_aggregates() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/lol", - UpstreamHttpMethod = new List { "Get" }, - Key = "Tom" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/tom", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Tom", - "Laura" - } - }, - new FileAggregateReRoute - { - UpstreamPathTemplate = "/tom", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Tom", - "Laura" - } - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "aggregate /tom has duplicate aggregate")) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_re_routes_dont_exist_for_aggregate() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Tom", - "Laura" - } - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "ReRoutes for aggregateReRoute / either do not exist or do not have correct ServiceName property")) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_aggregate_has_re_routes_with_specific_request_id_keys() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51878, - } - }, - UpstreamPathTemplate = "/laura", - UpstreamHttpMethod = new List { "Get" }, - Key = "Laura" - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - UpstreamPathTemplate = "/tom", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = "should_fail", - Key = "Tom" - } - }, - Aggregates = new List - { - new FileAggregateReRoute - { - UpstreamPathTemplate = "/", - UpstreamHost = "localhost", - ReRouteKeys = new List - { - "Tom", - "Laura" - } - } - } - }; - - this.Given(x => x.GivenAConfiguration(configuration)) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "aggregateReRoute / contains ReRoute with specific RequestIdKey, this is not possible with Aggregates")) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_scheme_in_downstream_or_upstream_template() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "http://www.bbc.co.uk/api/products/{productId}", - UpstreamPathTemplate = "http://asdf.com" - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .Then(x => x.ThenTheErrorIs()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} doesnt start with forward slash")) - .And(x => x.ThenTheErrorMessageAtPositionIs(1, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) - .And(x => x.ThenTheErrorMessageAtPositionIs(2, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} contains scheme")) - - .And(x => x.ThenTheErrorMessageAtPositionIs(3, "Upstream Path Template http://asdf.com contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) - .And(x => x.ThenTheErrorMessageAtPositionIs(4, "Upstream Path Template http://asdf.com doesnt start with forward slash")) - .And(x => x.ThenTheErrorMessageAtPositionIs(5, "Upstream Path Template http://asdf.com contains scheme")) - .BDDfy(); +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Validator; +using Ocelot.Requester; +using Ocelot.Responses; +using Ocelot.ServiceDiscovery; +using Ocelot.ServiceDiscovery.Providers; +using Ocelot.UnitTests.Requester; +using Ocelot.Values; +using Shouldly; +using System.Collections.Generic; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration.Validation +{ + public class FileConfigurationFluentValidatorTests + { + private IConfigurationValidator _configurationValidator; + private FileConfiguration _fileConfiguration; + private Response _result; + private readonly Mock _authProvider; + + public FileConfigurationFluentValidatorTests() + { + _authProvider = new Mock(); + var provider = new ServiceCollection() + .BuildServiceProvider(); + + // Todo - replace with mocks + _configurationValidator = new FileConfigurationFluentValidator(provider, new RouteFluentValidator(_authProvider.Object, new HostAndPortValidator(), new FileQoSOptionsFluentValidator(provider)), new FileGlobalConfigurationFluentValidator(new FileQoSOptionsFluentValidator(provider))); + } + + [Fact] + public void configuration_is_valid_if_service_discovery_options_specified_and_has_service_fabric_as_option() + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "test", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "https", + Host = "localhost", + Type = "ServiceFabric", + Port = 8500, + }, + }, + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_if_service_discovery_options_specified_and_has_service_discovery_handler() + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "test", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "https", + Host = "localhost", + Type = "FakeServiceDiscoveryProvider", + Port = 8500, + }, + }, + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .And(x => x.GivenAServiceDiscoveryHandler()) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_if_service_discovery_options_specified_dynamically_and_has_service_discovery_handler() + { + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "https", + Host = "localhost", + Type = "FakeServiceDiscoveryProvider", + Port = 8500, + }, + }, + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .And(x => x.GivenAServiceDiscoveryHandler()) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_service_discovery_options_specified_but_no_service_discovery_handler() + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "test", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "https", + Host = "localhost", + Type = "FakeServiceDiscoveryProvider", + Port = 8500, + }, + }, + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorIs()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?")) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_service_discovery_options_specified_dynamically_but_service_discovery_handler() + { + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "https", + Host = "localhost", + Type = "FakeServiceDiscoveryProvider", + Port = 8500, + }, + }, + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorIs()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?")) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_service_discovery_options_specified_but_no_service_discovery_handler_with_matching_name() + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = "test", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "https", + Host = "localhost", + Type = "consul", + Port = 8500, + }, + }, + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .And(x => x.GivenAServiceDiscoveryHandler()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorIs()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_if_qos_options_specified_and_has_qos_handler() + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51878, + }, + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + QoSOptions = new FileQoSOptions + { + TimeoutValue = 1, + ExceptionsAllowedBeforeBreaking = 1, + }, + }, + }, + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .And(x => x.GivenAQoSHandler()) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_if_qos_options_specified_globally_and_has_qos_handler() + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51878, + }, + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + QoSOptions = new FileQoSOptions + { + TimeoutValue = 1, + ExceptionsAllowedBeforeBreaking = 1, + }, + }, + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .And(x => x.GivenAQoSHandler()) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_qos_options_specified_but_no_qos_handler() + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51878, + }, + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + QoSOptions = new FileQoSOptions + { + TimeoutValue = 1, + ExceptionsAllowedBeforeBreaking = 1, + }, + }, + }, + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorIs()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot because either a Route or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?")) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_qos_options_specified_globally_but_no_qos_handler() + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51878, + }, + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + QoSOptions = new FileQoSOptions + { + TimeoutValue = 1, + ExceptionsAllowedBeforeBreaking = 1, + }, + }, + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorIs()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot because either a Route or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_if_aggregates_are_valid() + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51878, + }, + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51880, + }, + }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new List { "Get" }, + Key = "Tom", + }, + }, + Aggregates = new List + { + new() + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = new List + { + "Tom", + "Laura", + }, + }, + }, + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_aggregates_are_duplicate_of_re_routes() + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51878, + }, + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51880, + }, + }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new List { "Get" }, + Key = "Tom", + UpstreamHost = "localhost", + }, + }, + Aggregates = new List + { + new() + { + UpstreamPathTemplate = "/tom", + UpstreamHost = "localhost", + RouteKeys = new List + { + "Tom", + "Laura", + }, + }, + }, + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "route /tom has duplicate aggregate")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_if_aggregates_are_not_duplicate_of_re_routes() + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51878, + }, + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51880, + }, + }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new List { "Post" }, + Key = "Tom", + UpstreamHost = "localhost", + }, + }, + Aggregates = new List + { + new() + { + UpstreamPathTemplate = "/tom", + UpstreamHost = "localhost", + RouteKeys = new List + { + "Tom", + "Laura", + }, + }, + }, + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_aggregates_are_duplicate_of_aggregates() + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51878, + }, + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51880, + }, + }, + UpstreamPathTemplate = "/lol", + UpstreamHttpMethod = new List { "Get" }, + Key = "Tom", + }, + }, + Aggregates = new List + { + new() + { + UpstreamPathTemplate = "/tom", + UpstreamHost = "localhost", + RouteKeys = new List + { + "Tom", + "Laura", + }, + }, + new() + { + UpstreamPathTemplate = "/tom", + UpstreamHost = "localhost", + RouteKeys = new List + { + "Tom", + "Laura", + }, + }, + }, + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "aggregate /tom has duplicate aggregate")) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_re_routes_dont_exist_for_aggregate() + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51878, + }, + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + }, + }, + Aggregates = new List + { + new() + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = new List + { + "Tom", + "Laura", + }, + }, + }, + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Routes for aggregateRoute / either do not exist or do not have correct ServiceName property")) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_aggregate_has_re_routes_with_specific_request_id_keys() + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51878, + }, + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura", + }, + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51880, + }, + }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new List { "Get" }, + RequestIdKey = "should_fail", + Key = "Tom", + }, + }, + Aggregates = new List + { + new() + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + RouteKeys = new List + { + "Tom", + "Laura", + }, + }, + }, + }; + + this.Given(x => x.GivenAConfiguration(configuration)) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "aggregateRoute / contains Route with specific RequestIdKey, this is not possible with Aggregates")) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_scheme_in_downstream_or_upstream_template() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "http://www.bbc.co.uk/api/products/{productId}", + UpstreamPathTemplate = "http://asdf.com", + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .Then(x => x.ThenTheErrorIs()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} doesnt start with forward slash")) + .And(x => x.ThenTheErrorMessageAtPositionIs(1, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) + .And(x => x.ThenTheErrorMessageAtPositionIs(2, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} contains scheme")) + + .And(x => x.ThenTheErrorMessageAtPositionIs(3, "Upstream Path Template http://asdf.com contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) + .And(x => x.ThenTheErrorMessageAtPositionIs(4, "Upstream Path Template http://asdf.com doesnt start with forward slash")) + .And(x => x.ThenTheErrorMessageAtPositionIs(5, "Upstream Path Template http://asdf.com contains scheme")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_one_route() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + }, + }, + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_without_slash_prefix_downstream_path_template() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "api/products/", + UpstreamPathTemplate = "/asdf/", + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template api/products/ doesnt start with forward slash")) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_without_slash_prefix_upstream_path_template() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "api/prod/", + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Upstream Path Template api/prod/ doesnt start with forward slash")) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_upstream_url_contains_forward_slash_then_another_forward_slash() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "//api/prod/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + Port = 80, + }, + }, + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Upstream Path Template //api/prod/ contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_downstream_url_contains_forward_slash_then_another_forward_slash() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "//api/products/", + UpstreamPathTemplate = "/api/prod/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + Port = 80, + }, + }, + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template //api/products/ contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_valid_authentication_provider() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + }, + }, + AuthenticationOptions = new FileAuthenticationOptions() + { + AuthenticationProviderKey = "Test", + }, + }, + }, + })) + .And(x => x.GivenTheAuthSchemeExists("Test")) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_with_invalid_authentication_provider() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + AuthenticationOptions = new FileAuthenticationOptions() + { + AuthenticationProviderKey = "Test", + }, + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Authentication Options AuthenticationProviderKey:Test,AllowedScopes:[] is unsupported authentication provider")) + .BDDfy(); + } + + [Fact] + public void configuration_is_not_valid_with_duplicate_routes_all_verbs() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "bb.co.uk", + }, + }, + }, + new() + { + DownstreamPathTemplate = "/www/test/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "bb.co.uk", + }, + }, + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "route /asdf/ has duplicate")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_duplicate_routes_all_verbs_but_different_hosts() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "bb.co.uk", + }, + }, + UpstreamHost = "host1", + }, + new() + { + DownstreamPathTemplate = "/www/test/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "bb.co.uk", + }, + }, + UpstreamHost = "host2", + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_not_valid_with_duplicate_routes_specific_verbs() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + }, + }, + UpstreamHttpMethod = new List {"Get"}, + }, + new() + { + DownstreamPathTemplate = "/www/test/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + }, + }, + UpstreamHttpMethod = new List {"Get"}, + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "route /asdf/ has duplicate")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_duplicate_routes_different_verbs() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + }, + }, + }, + new() + { + DownstreamPathTemplate = "/www/test/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Post"}, + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + }, + }, + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_not_valid_with_duplicate_routes_with_duplicated_upstreamhosts() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + }, + }, + UpstreamHttpMethod = new List(), + UpstreamHost = "upstreamhost", + }, + new() + { + DownstreamPathTemplate = "/www/test/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + }, + }, + UpstreamHttpMethod = new List(), + UpstreamHost = "upstreamhost", + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "route /asdf/ has duplicate")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_duplicate_routes_but_different_upstreamhosts() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + }, + }, + UpstreamHttpMethod = new List(), + UpstreamHost = "upstreamhost111", + }, + new() + { + DownstreamPathTemplate = "/www/test/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + }, + }, + UpstreamHttpMethod = new List(), + UpstreamHost = "upstreamhost222", + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_duplicate_routes_but_one_upstreamhost_is_not_set() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + }, + }, + UpstreamHttpMethod = new List(), + UpstreamHost = "upstreamhost", + }, + new() + { + DownstreamPathTemplate = "/www/test/", + UpstreamPathTemplate = "/asdf/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + }, + }, + UpstreamHttpMethod = new List(), + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); } - [Fact] - public void configuration_is_valid_with_one_reroute() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", + [Fact] + public void configuration_is_invalid_with_invalid_rate_limit_configuration() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + }, + }, + RateLimitOptions = new FileRateLimitRule + { + Period = "1x", + EnableRateLimiting = true, + }, + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "RateLimitOptions.Period does not contain integer then s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_valid_rate_limit_configuration() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + }, + }, + RateLimitOptions = new FileRateLimitRule + { + Period = "1d", + EnableRateLimiting = true, + }, + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_using_service_discovery_and_service_name() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + ServiceName = "Test", + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "https", + Type = "servicefabric", + Host = "localhost", + Port = 1234, + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void configuration_is_invalid_when_not_using_service_discovery_and_host(string downstreamHost) + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + DownstreamHostAndPorts = new List + { + new() + { + Host = downstreamHost, + }, + }, + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!")) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_when_not_using_service_discovery_and_host_is_set() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + DownstreamHostAndPorts = new List + { + new() + { + Host = "bbc.co.uk", + }, + }, + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_when_no_downstream_but_has_host_and_port() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + DownstreamHostAndPorts = new List + { + new() + { + Host = "test", + }, + }, + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_not_valid_when_no_host_and_port() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + DownstreamHostAndPorts = new List + { + }, + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!")) + .BDDfy(); + } + + [Fact] + public void configuration_is_not_valid_when_host_and_port_is_empty() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "/asdf/", + UpstreamHttpMethod = new List {"Get"}, + DownstreamHostAndPorts = new List + { + new(), + }, + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!")) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_when_placeholder_is_used_twice_in_upstream_path_template() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/bar/{everything}", + DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort - { - Host = "bbc.co.uk" - } - }, - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_without_slash_prefix_downstream_path_template() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "api/products/", - UpstreamPathTemplate = "/asdf/" - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template api/products/ doesnt start with forward slash")) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_without_slash_prefix_upstream_path_template() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "api/prod/", - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Upstream Path Template api/prod/ doesnt start with forward slash")) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_upstream_url_contains_forward_slash_then_another_forward_slash() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "//api/prod/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - Port = 80 - } - }, - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Upstream Path Template //api/prod/ contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_if_downstream_url_contains_forward_slash_then_another_forward_slash() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "//api/products/", - UpstreamPathTemplate = "/api/prod/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - Port = 80 - } - }, - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template //api/products/ contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_with_valid_authentication_provider() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - } - }, - AuthenticationOptions = new FileAuthenticationOptions() - { - AuthenticationProviderKey = "Test" - } - } - } - })) - .And(x => x.GivenTheAuthSchemeExists("Test")) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_with_invalid_authentication_provider() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - AuthenticationOptions = new FileAuthenticationOptions() - { - AuthenticationProviderKey = "Test" - } - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "Authentication Options AuthenticationProviderKey:Test,AllowedScopes:[] is unsupported authentication provider")) - .BDDfy(); - } - - [Fact] - public void configuration_is_not_valid_with_duplicate_reroutes_all_verbs() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bb.co.uk" - } - }, - }, - new FileReRoute - { - DownstreamPathTemplate = "/www/test/", - UpstreamPathTemplate = "/asdf/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bb.co.uk" - } - }, - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "reRoute /asdf/ has duplicate")) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_with_duplicate_reroutes_all_verbs_but_different_hosts() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bb.co.uk" - } - }, - UpstreamHost = "host1" - }, - new FileReRoute - { - DownstreamPathTemplate = "/www/test/", - UpstreamPathTemplate = "/asdf/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bb.co.uk" - } - }, - UpstreamHost = "host2" - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_not_valid_with_duplicate_reroutes_specific_verbs() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - } - }, - UpstreamHttpMethod = new List {"Get"} - }, - new FileReRoute - { - DownstreamPathTemplate = "/www/test/", - UpstreamPathTemplate = "/asdf/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - } - }, - UpstreamHttpMethod = new List {"Get"} - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "reRoute /asdf/ has duplicate")) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_with_duplicate_reroutes_different_verbs() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - } - }, - }, - new FileReRoute - { - DownstreamPathTemplate = "/www/test/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Post"}, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - } - }, - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_with_invalid_rate_limit_configuration() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - } - }, - RateLimitOptions = new FileRateLimitRule - { - Period = "1x", - EnableRateLimiting = true - } - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "RateLimitOptions.Period does not contain integer then s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period")) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_with_valid_rate_limit_configuration() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk", - } - }, - RateLimitOptions = new FileRateLimitRule - { - Period = "1d", - EnableRateLimiting = true - } - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_with_using_service_discovery_and_service_name() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - ServiceName = "Test" - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Type = "servicefabric", - Host = "localhost", - Port = 1234 - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - public void configuration_is_invalid_when_not_using_service_discovery_and_host(string downstreamHost) - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = downstreamHost, - } - }, - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using ReRoute.Host or Ocelot cannot find your service!")) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_when_not_using_service_discovery_and_host_is_set() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "bbc.co.uk" - } - }, - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_valid_when_no_downstream_but_has_host_and_port() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "test" - } - } - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void configuration_is_not_valid_when_no_host_and_port() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - DownstreamHostAndPorts = new List - { - } - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!")) - .BDDfy(); - } - - [Fact] - public void configuration_is_not_valid_when_host_and_port_is_empty() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/products/", - UpstreamPathTemplate = "/asdf/", - UpstreamHttpMethod = new List {"Get"}, - DownstreamHostAndPorts = new List - { - new FileHostAndPort() - } - } - } - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using ReRoute.Host or Ocelot cannot find your service!")) - .BDDfy(); - } - - [Fact] - public void configuration_is_invalid_when_placeholder_is_used_twice_in_upstream_path_template() - { - this.Given(x => x.GivenAConfiguration(new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/bar/{everything}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort() { Host = "a.b.cd" }, - }, - UpstreamPathTemplate = "/foo/bar/{everything}/{everything}", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - })) - .When(x => x.WhenIValidateTheConfiguration()) - .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorMessageAtPositionIs(0, "reRoute /foo/bar/{everything}/{everything} has duplicated placeholder")) - .BDDfy(); - } - - private void GivenAConfiguration(FileConfiguration fileConfiguration) - { - _fileConfiguration = fileConfiguration; - } - - private void WhenIValidateTheConfiguration() - { - _result = _configurationValidator.IsValid(_fileConfiguration).Result; - } - - private void ThenTheResultIsValid() - { - _result.Data.IsError.ShouldBeFalse(); - } - - private void ThenTheResultIsNotValid() - { - _result.Data.IsError.ShouldBeTrue(); - } - - private void ThenTheErrorIs() - { - _result.Data.Errors[0].ShouldBeOfType(); - } - - private void ThenTheErrorMessageAtPositionIs(int index, string expected) - { - _result.Data.Errors[index].Message.ShouldBe(expected); - } - - private void GivenTheAuthSchemeExists(string name) - { - _authProvider.Setup(x => x.GetAllSchemesAsync()).ReturnsAsync(new List - { - new AuthenticationScheme(name, name, typeof(TestHandler)) - }); - } - - private void GivenAQoSHandler() - { - var collection = new ServiceCollection(); - QosDelegatingHandlerDelegate del = (a, b) => new FakeDelegatingHandler(); - collection.AddSingleton(del); - var provider = collection.BuildServiceProvider(); - _configurationValidator = new FileConfigurationFluentValidator(provider, new ReRouteFluentValidator(_authProvider.Object, new HostAndPortValidator(), new FileQoSOptionsFluentValidator(provider)), new FileGlobalConfigurationFluentValidator(new FileQoSOptionsFluentValidator(provider))); - } - - private void GivenAServiceDiscoveryHandler() - { - var collection = new ServiceCollection(); - ServiceDiscoveryFinderDelegate del = (a, b, c) => new FakeServiceDiscoveryProvider(); - collection.AddSingleton(del); - var provider = collection.BuildServiceProvider(); - _configurationValidator = new FileConfigurationFluentValidator(provider, new ReRouteFluentValidator(_authProvider.Object, new HostAndPortValidator(), new FileQoSOptionsFluentValidator(provider)), new FileGlobalConfigurationFluentValidator(new FileQoSOptionsFluentValidator(provider))); - } - - private class FakeServiceDiscoveryProvider : IServiceDiscoveryProvider - { - public Task> Get() - { - throw new System.NotImplementedException(); - } - } - - private class TestOptions : AuthenticationSchemeOptions - { - } - - private class TestHandler : AuthenticationHandler - { - public TestHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) - { - } - - protected override Task HandleAuthenticateAsync() - { - var principal = new ClaimsPrincipal(); - return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); - } - } - } -} + new() { Host = "a.b.cd" }, + }, + UpstreamPathTemplate = "/foo/bar/{everything}/{everything}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorMessageAtPositionIs(0, "route /foo/bar/{everything}/{everything} has duplicated placeholder")) + .BDDfy(); + } + + private void GivenAConfiguration(FileConfiguration fileConfiguration) + { + _fileConfiguration = fileConfiguration; + } + + private void WhenIValidateTheConfiguration() + { + _result = _configurationValidator.IsValid(_fileConfiguration).Result; + } + + private void ThenTheResultIsValid() + { + _result.Data.IsError.ShouldBeFalse(); + } + + private void ThenTheResultIsNotValid() + { + _result.Data.IsError.ShouldBeTrue(); + } + + private void ThenTheErrorIs() + { + _result.Data.Errors[0].ShouldBeOfType(); + } + + private void ThenTheErrorMessageAtPositionIs(int index, string expected) + { + _result.Data.Errors[index].Message.ShouldBe(expected); + } + + private void GivenTheAuthSchemeExists(string name) + { + _authProvider.Setup(x => x.GetAllSchemesAsync()).ReturnsAsync(new List + { + new(name, name, typeof(TestHandler)), + }); + } + + private void GivenAQoSHandler() + { + var collection = new ServiceCollection(); + QosDelegatingHandlerDelegate del = (a, b) => new FakeDelegatingHandler(); + collection.AddSingleton(del); + var provider = collection.BuildServiceProvider(); + _configurationValidator = new FileConfigurationFluentValidator(provider, new RouteFluentValidator(_authProvider.Object, new HostAndPortValidator(), new FileQoSOptionsFluentValidator(provider)), new FileGlobalConfigurationFluentValidator(new FileQoSOptionsFluentValidator(provider))); + } + + private void GivenAServiceDiscoveryHandler() + { + var collection = new ServiceCollection(); + ServiceDiscoveryFinderDelegate del = (a, b, c) => new FakeServiceDiscoveryProvider(); + collection.AddSingleton(del); + var provider = collection.BuildServiceProvider(); + _configurationValidator = new FileConfigurationFluentValidator(provider, new RouteFluentValidator(_authProvider.Object, new HostAndPortValidator(), new FileQoSOptionsFluentValidator(provider)), new FileGlobalConfigurationFluentValidator(new FileQoSOptionsFluentValidator(provider))); + } + + private class FakeServiceDiscoveryProvider : IServiceDiscoveryProvider + { + public Task> Get() + { + throw new System.NotImplementedException(); + } + } + + private class TestOptions : AuthenticationSchemeOptions + { + } + + private class TestHandler : AuthenticationHandler + { + public TestHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { + } + + protected override Task HandleAuthenticateAsync() + { + var principal = new ClaimsPrincipal(); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); + } + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs index 64178767f..5ddb0f98f 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs @@ -1,103 +1,108 @@ using FluentValidation.Results; -using Microsoft.Extensions.DependencyInjection; -using Ocelot.Configuration.File; -using Ocelot.Configuration.Validator; -using Ocelot.Requester; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Configuration.Validation -{ - public class FileQoSOptionsFluentValidatorTests - { - private FileQoSOptionsFluentValidator _validator; - private ServiceCollection _services; - private ValidationResult _result; - private FileQoSOptions _qosOptions; - - public FileQoSOptionsFluentValidatorTests() - { - _services = new ServiceCollection(); - var provider = _services.BuildServiceProvider(); - _validator = new FileQoSOptionsFluentValidator(provider); - } - - [Fact] - public void should_be_valid_as_nothing_set() - { - this.Given(_ => GivenThe(new FileQoSOptions())) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void should_be_valid_as_qos_delegate_set() - { - var qosOptions = new FileQoSOptions - { - TimeoutValue = 1, - ExceptionsAllowedBeforeBreaking = 1 - }; - this.Given(_ => GivenThe(qosOptions)) - .And(_ => GivenAQosDelegate()) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void should_be_invalid_as_no_qos_delegate() - { - var qosOptions = new FileQoSOptions - { - TimeoutValue = 1, - ExceptionsAllowedBeforeBreaking = 1 - }; - - this.Given(_ => GivenThe(qosOptions)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsInValid()) - .And(_ => ThenTheErrorIs()) - .BDDfy(); - } - - private void ThenTheErrorIs() - { - _result.Errors[0].ErrorMessage.ShouldBe("Unable to start Ocelot because either a ReRoute or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?"); - } - - private void ThenTheResultIsInValid() - { - _result.IsValid.ShouldBeFalse(); - } +using Microsoft.Extensions.DependencyInjection; - private void GivenAQosDelegate() - { - QosDelegatingHandlerDelegate fake = (a, b) => - { - return null; - }; - _services.AddSingleton(fake); - var provider = _services.BuildServiceProvider(); - _validator = new FileQoSOptionsFluentValidator(provider); - } +using Ocelot.Configuration.File; +using Ocelot.Configuration.Validator; +using Ocelot.Requester; - private void GivenThe(FileQoSOptions qosOptions) - { - _qosOptions = qosOptions; - } +using Shouldly; - private void WhenIValidate() - { - _result = _validator.Validate(_qosOptions); - } +using TestStack.BDDfy; - private void ThenTheResultIsValid() - { - _result.IsValid.ShouldBeTrue(); - } - } -} +using Xunit; + +namespace Ocelot.UnitTests.Configuration.Validation +{ + public class FileQoSOptionsFluentValidatorTests + { + private FileQoSOptionsFluentValidator _validator; + private readonly ServiceCollection _services; + private ValidationResult _result; + private FileQoSOptions _qosOptions; + + public FileQoSOptionsFluentValidatorTests() + { + _services = new ServiceCollection(); + var provider = _services.BuildServiceProvider(); + _validator = new FileQoSOptionsFluentValidator(provider); + } + + [Fact] + public void should_be_valid_as_nothing_set() + { + this.Given(_ => GivenThe(new FileQoSOptions())) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void should_be_valid_as_qos_delegate_set() + { + var qosOptions = new FileQoSOptions + { + TimeoutValue = 1, + ExceptionsAllowedBeforeBreaking = 1, + }; + + this.Given(_ => GivenThe(qosOptions)) + .And(_ => GivenAQosDelegate()) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void should_be_invalid_as_no_qos_delegate() + { + var qosOptions = new FileQoSOptions + { + TimeoutValue = 1, + ExceptionsAllowedBeforeBreaking = 1, + }; + + this.Given(_ => GivenThe(qosOptions)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsInValid()) + .And(_ => ThenTheErrorIs()) + .BDDfy(); + } + + private void ThenTheErrorIs() + { + _result.Errors[0].ErrorMessage.ShouldBe("Unable to start Ocelot because either a Route or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?"); + } + + private void ThenTheResultIsInValid() + { + _result.IsValid.ShouldBeFalse(); + } + + private void GivenAQosDelegate() + { + QosDelegatingHandlerDelegate fake = (a, b) => + { + return null; + }; + _services.AddSingleton(fake); + var provider = _services.BuildServiceProvider(); + _validator = new FileQoSOptionsFluentValidator(provider); + } + + private void GivenThe(FileQoSOptions qosOptions) + { + _qosOptions = qosOptions; + } + + private void WhenIValidate() + { + _result = _validator.Validate(_qosOptions); + } + + private void ThenTheResultIsValid() + { + _result.IsValid.ShouldBeTrue(); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs index 6052c24fb..ec8103ce0 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs @@ -1,77 +1,81 @@ using FluentValidation.Results; -using Ocelot.Configuration.File; -using Ocelot.Configuration.Validator; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Configuration.Validation -{ - public class HostAndPortValidatorTests - { - private HostAndPortValidator _validator; - private ValidationResult _result; - private FileHostAndPort _hostAndPort; - - public HostAndPortValidatorTests() - { - _validator = new HostAndPortValidator(); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - public void should_be_invalid_because_host_empty(string host) - { - var fileHostAndPort = new FileHostAndPort - { - Host = host - }; - - this.Given(_ => GivenThe(fileHostAndPort)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsInValid()) - .And(_ => ThenTheErorrIs()) - .BDDfy(); - } - [Fact] - public void should_be_valid_because_host_set() - { - var fileHostAndPort = new FileHostAndPort - { - Host = "test" - }; - - this.Given(_ => GivenThe(fileHostAndPort)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsValid()) - .BDDfy(); - } - - private void GivenThe(FileHostAndPort hostAndPort) - { - _hostAndPort = hostAndPort; - } - - private void WhenIValidate() - { - _result = _validator.Validate(_hostAndPort); - } +using Ocelot.Configuration.File; +using Ocelot.Configuration.Validator; - private void ThenTheResultIsValid() - { - _result.IsValid.ShouldBeTrue(); - } +using Shouldly; - private void ThenTheErorrIs() - { - _result.Errors[0].ErrorMessage.ShouldBe("When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using ReRoute.Host or Ocelot cannot find your service!"); - } +using TestStack.BDDfy; - private void ThenTheResultIsInValid() - { - _result.IsValid.ShouldBeFalse(); - } - } -} +using Xunit; + +namespace Ocelot.UnitTests.Configuration.Validation +{ + public class HostAndPortValidatorTests + { + private HostAndPortValidator _validator; + private ValidationResult _result; + private FileHostAndPort _hostAndPort; + + public HostAndPortValidatorTests() + { + _validator = new HostAndPortValidator(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void should_be_invalid_because_host_empty(string host) + { + var fileHostAndPort = new FileHostAndPort + { + Host = host, + }; + + this.Given(_ => GivenThe(fileHostAndPort)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsInValid()) + .And(_ => ThenTheErorrIs()) + .BDDfy(); + } + + [Fact] + public void should_be_valid_because_host_set() + { + var fileHostAndPort = new FileHostAndPort + { + Host = "test", + }; + + this.Given(_ => GivenThe(fileHostAndPort)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsValid()) + .BDDfy(); + } + + private void GivenThe(FileHostAndPort hostAndPort) + { + _hostAndPort = hostAndPort; + } + + private void WhenIValidate() + { + _result = _validator.Validate(_hostAndPort); + } + + private void ThenTheResultIsValid() + { + _result.IsValid.ShouldBeTrue(); + } + + private void ThenTheErorrIs() + { + _result.Errors[0].ErrorMessage.ShouldBe("When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!"); + } + + private void ThenTheResultIsInValid() + { + _result.IsValid.ShouldBeFalse(); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/Validation/ReRouteFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs similarity index 63% rename from test/Ocelot.UnitTests/Configuration/Validation/ReRouteFluentValidatorTests.cs rename to test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs index 9433bb891..2e0a9ca75 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/ReRouteFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs @@ -1,368 +1,432 @@ -namespace Ocelot.UnitTests.Configuration.Validation -{ - using FluentValidation.Results; - using Microsoft.AspNetCore.Authentication; - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Configuration.File; - using Ocelot.Configuration.Validator; - using Ocelot.Requester; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - - public class ReRouteFluentValidatorTests - { - private readonly ReRouteFluentValidator _validator; - private readonly Mock _authProvider; - private QosDelegatingHandlerDelegate _qosDelegatingHandler; - private Mock _serviceProvider; - private FileReRoute _reRoute; - private ValidationResult _result; - - public ReRouteFluentValidatorTests() - { - _authProvider = new Mock(); - _serviceProvider = new Mock(); - // Todo - replace with mocks - _validator = new ReRouteFluentValidator(_authProvider.Object, new HostAndPortValidator(), new FileQoSOptionsFluentValidator(_serviceProvider.Object)); - } - - [Fact] - public void downstream_path_template_should_not_be_empty() - { - var fileReRoute = new FileReRoute(); - - this.Given(_ => GivenThe(fileReRoute)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsInvalid()) - .And(_ => ThenTheErrorsContains("Downstream Path Template cannot be empty")) - .BDDfy(); - } - - [Fact] - public void upstream_path_template_should_not_be_empty() - { - var fileReRoute = new FileReRoute - { - DownstreamPathTemplate = "test" - }; - - this.Given(_ => GivenThe(fileReRoute)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsInvalid()) - .And(_ => ThenTheErrorsContains("Upstream Path Template cannot be empty")) - .BDDfy(); - } - - [Fact] - public void downstream_path_template_should_start_with_forward_slash() - { - var fileReRoute = new FileReRoute - { - DownstreamPathTemplate = "test" - }; - - this.Given(_ => GivenThe(fileReRoute)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsInvalid()) - .And(_ => ThenTheErrorsContains("Downstream Path Template test doesnt start with forward slash")) - .BDDfy(); - } - - [Fact] - public void downstream_path_template_should_not_contain_double_forward_slash() - { - var fileReRoute = new FileReRoute - { - DownstreamPathTemplate = "//test" - }; - - this.Given(_ => GivenThe(fileReRoute)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsInvalid()) - .And(_ => ThenTheErrorsContains("Downstream Path Template //test contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) - .BDDfy(); - } - - [Theory] - [InlineData("https://test")] - [InlineData("http://test")] - [InlineData("/test/http://")] - [InlineData("/test/https://")] - public void downstream_path_template_should_not_contain_scheme(string downstreamPathTemplate) - { - var fileReRoute = new FileReRoute - { - DownstreamPathTemplate = downstreamPathTemplate - }; - - this.Given(_ => GivenThe(fileReRoute)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsInvalid()) - .And(_ => ThenTheErrorsContains($"Downstream Path Template {downstreamPathTemplate} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) - .BDDfy(); - } - - [Fact] - public void upstream_path_template_should_start_with_forward_slash() - { - var fileReRoute = new FileReRoute - { - DownstreamPathTemplate = "/test", - UpstreamPathTemplate = "test" - }; - - this.Given(_ => GivenThe(fileReRoute)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsInvalid()) - .And(_ => ThenTheErrorsContains("Upstream Path Template test doesnt start with forward slash")) - .BDDfy(); - } - - [Fact] - public void upstream_path_template_should_not_contain_double_forward_slash() - { - var fileReRoute = new FileReRoute - { - DownstreamPathTemplate = "/test", - UpstreamPathTemplate = "//test" - }; - - this.Given(_ => GivenThe(fileReRoute)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsInvalid()) - .And(_ => ThenTheErrorsContains("Upstream Path Template //test contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) - .BDDfy(); - } - - [Theory] - [InlineData("https://test")] - [InlineData("http://test")] - [InlineData("/test/http://")] - [InlineData("/test/https://")] - public void upstream_path_template_should_not_contain_scheme(string upstreamPathTemplate) - { - var fileReRoute = new FileReRoute - { - DownstreamPathTemplate = "/test", - UpstreamPathTemplate = upstreamPathTemplate - }; - - this.Given(_ => GivenThe(fileReRoute)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsInvalid()) - .And(_ => ThenTheErrorsContains($"Upstream Path Template {upstreamPathTemplate} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) - .BDDfy(); - } - - [Fact] - public void should_not_be_valid_if_enable_rate_limiting_true_and_period_is_empty() - { - var fileReRoute = new FileReRoute - { - DownstreamPathTemplate = "/test", - UpstreamPathTemplate = "/test", - RateLimitOptions = new FileRateLimitRule - { - EnableRateLimiting = true - } - }; - - this.Given(_ => GivenThe(fileReRoute)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsInvalid()) - .And(_ => ThenTheErrorsContains("RateLimitOptions.Period is empty")) - .BDDfy(); - } - - [Fact] - public void should_not_be_valid_if_enable_rate_limiting_true_and_period_has_value() - { - var fileReRoute = new FileReRoute - { - DownstreamPathTemplate = "/test", - UpstreamPathTemplate = "/test", - RateLimitOptions = new FileRateLimitRule - { - EnableRateLimiting = true, - Period = "test" - } - }; - - this.Given(_ => GivenThe(fileReRoute)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsInvalid()) - .And(_ => ThenTheErrorsContains("RateLimitOptions.Period does not contain integer then s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period")) - .BDDfy(); - } - - [Fact] - public void should_not_be_valid_if_specified_authentication_provider_isnt_registered() - { - var fileReRoute = new FileReRoute - { - DownstreamPathTemplate = "/test", - UpstreamPathTemplate = "/test", - AuthenticationOptions = new FileAuthenticationOptions - { - AuthenticationProviderKey = "JwtLads" - } - }; - - this.Given(_ => GivenThe(fileReRoute)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsInvalid()) - .And(_ => ThenTheErrorsContains($"Authentication Options AuthenticationProviderKey:JwtLads,AllowedScopes:[] is unsupported authentication provider")) - .BDDfy(); - } - - [Fact] - public void should_not_be_valid_if_not_using_service_discovery_and_no_host_and_ports() - { - var fileReRoute = new FileReRoute - { - DownstreamPathTemplate = "/test", - UpstreamPathTemplate = "/test", - }; - - this.Given(_ => GivenThe(fileReRoute)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsInvalid()) - .And(_ => ThenTheErrorsContains("When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!")) - .BDDfy(); - } - - [Fact] - public void should_be_valid_if_using_service_discovery_and_no_host_and_ports() - { - var fileReRoute = new FileReRoute - { - DownstreamPathTemplate = "/test", - UpstreamPathTemplate = "/test", - ServiceName = "Lads" - }; - - this.Given(_ => GivenThe(fileReRoute)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void should_be_valid_re_route_using_host_and_port_and_paths() - { - var fileReRoute = new FileReRoute - { - DownstreamPathTemplate = "/test", - UpstreamPathTemplate = "/test", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 5000 - } - } - }; - - this.Given(_ => GivenThe(fileReRoute)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsValid()) - .BDDfy(); - } - - [Fact] - public void should_be_valid_if_specified_authentication_provider_is_registered() - { - const string key = "JwtLads"; - - var fileReRoute = new FileReRoute - { - DownstreamPathTemplate = "/test", - UpstreamPathTemplate = "/test", - AuthenticationOptions = new FileAuthenticationOptions - { - AuthenticationProviderKey = key - }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 5000 - } - } - }; - - this.Given(_ => GivenThe(fileReRoute)) - .And(_ => GivenAnAuthProvider(key)) - .When(_ => WhenIValidate()) - .Then(_ => ThenTheResultIsValid()) - .BDDfy(); - } - - private void GivenAnAuthProvider(string key) - { - var schemes = new List - { - new AuthenticationScheme(key, key, typeof(FakeAutheHandler)) - }; - - _authProvider - .Setup(x => x.GetAllSchemesAsync()) - .ReturnsAsync(schemes); - } - - private void ThenTheResultIsValid() - { - _result.IsValid.ShouldBeTrue(); - } - - private void GivenThe(FileReRoute reRoute) - { - _reRoute = reRoute; - } - - private void WhenIValidate() - { - _result = _validator.Validate(_reRoute); - } - - private void ThenTheResultIsInvalid() - { - _result.IsValid.ShouldBeFalse(); - } - - private void ThenTheErrorsContains(string expected) - { - _result.Errors.ShouldContain(x => x.ErrorMessage == expected); - } - - private class FakeAutheHandler : IAuthenticationHandler - { - public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) - { - throw new System.NotImplementedException(); - } - - public Task AuthenticateAsync() - { - throw new System.NotImplementedException(); - } - - public Task ChallengeAsync(AuthenticationProperties properties) - { - throw new System.NotImplementedException(); - } - - public Task ForbidAsync(AuthenticationProperties properties) - { - throw new System.NotImplementedException(); - } - } - } -} +using FluentValidation.Results; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Moq; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Validator; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration.Validation +{ + public class RouteFluentValidatorTests + { + private readonly RouteFluentValidator _validator; + private readonly Mock _authProvider; + private Mock _serviceProvider; + private FileRoute _route; + private ValidationResult _result; + + public RouteFluentValidatorTests() + { + _authProvider = new Mock(); + _serviceProvider = new Mock(); + + // Todo - replace with mocks + _validator = new RouteFluentValidator(_authProvider.Object, new HostAndPortValidator(), new FileQoSOptionsFluentValidator(_serviceProvider.Object)); + } + + [Fact] + public void downstream_path_template_should_not_be_empty() + { + var fileRoute = new FileRoute(); + + this.Given(_ => GivenThe(fileRoute)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsInvalid()) + .And(_ => ThenTheErrorsContains("Downstream Path Template cannot be empty")) + .BDDfy(); + } + + [Fact] + public void upstream_path_template_should_not_be_empty() + { + var fileRoute = new FileRoute + { + DownstreamPathTemplate = "test", + }; + + this.Given(_ => GivenThe(fileRoute)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsInvalid()) + .And(_ => ThenTheErrorsContains("Upstream Path Template cannot be empty")) + .BDDfy(); + } + + [Fact] + public void downstream_path_template_should_start_with_forward_slash() + { + var fileRoute = new FileRoute + { + DownstreamPathTemplate = "test", + }; + + this.Given(_ => GivenThe(fileRoute)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsInvalid()) + .And(_ => ThenTheErrorsContains("Downstream Path Template test doesnt start with forward slash")) + .BDDfy(); + } + + [Fact] + public void downstream_path_template_should_not_contain_double_forward_slash() + { + var fileRoute = new FileRoute + { + DownstreamPathTemplate = "//test", + }; + + this.Given(_ => GivenThe(fileRoute)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsInvalid()) + .And(_ => ThenTheErrorsContains("Downstream Path Template //test contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) + .BDDfy(); + } + + [Theory] + [InlineData("https://test")] + [InlineData("http://test")] + [InlineData("/test/http://")] + [InlineData("/test/https://")] + public void downstream_path_template_should_not_contain_scheme(string downstreamPathTemplate) + { + var fileRoute = new FileRoute + { + DownstreamPathTemplate = downstreamPathTemplate, + }; + + this.Given(_ => GivenThe(fileRoute)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsInvalid()) + .And(_ => ThenTheErrorsContains($"Downstream Path Template {downstreamPathTemplate} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) + .BDDfy(); + } + + [Fact] + public void upstream_path_template_should_start_with_forward_slash() + { + var fileRoute = new FileRoute + { + DownstreamPathTemplate = "/test", + UpstreamPathTemplate = "test", + }; + + this.Given(_ => GivenThe(fileRoute)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsInvalid()) + .And(_ => ThenTheErrorsContains("Upstream Path Template test doesnt start with forward slash")) + .BDDfy(); + } + + [Fact] + public void upstream_path_template_should_not_contain_double_forward_slash() + { + var fileRoute = new FileRoute + { + DownstreamPathTemplate = "/test", + UpstreamPathTemplate = "//test", + }; + + this.Given(_ => GivenThe(fileRoute)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsInvalid()) + .And(_ => ThenTheErrorsContains("Upstream Path Template //test contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) + .BDDfy(); + } + + [Theory] + [InlineData("https://test")] + [InlineData("http://test")] + [InlineData("/test/http://")] + [InlineData("/test/https://")] + public void upstream_path_template_should_not_contain_scheme(string upstreamPathTemplate) + { + var fileRoute = new FileRoute + { + DownstreamPathTemplate = "/test", + UpstreamPathTemplate = upstreamPathTemplate, + }; + + this.Given(_ => GivenThe(fileRoute)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsInvalid()) + .And(_ => ThenTheErrorsContains($"Upstream Path Template {upstreamPathTemplate} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature.")) + .BDDfy(); + } + + [Fact] + public void should_not_be_valid_if_enable_rate_limiting_true_and_period_is_empty() + { + var fileRoute = new FileRoute + { + DownstreamPathTemplate = "/test", + UpstreamPathTemplate = "/test", + RateLimitOptions = new FileRateLimitRule + { + EnableRateLimiting = true, + }, + }; + + this.Given(_ => GivenThe(fileRoute)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsInvalid()) + .And(_ => ThenTheErrorsContains("RateLimitOptions.Period is empty")) + .BDDfy(); + } + + [Fact] + public void should_not_be_valid_if_enable_rate_limiting_true_and_period_has_value() + { + var fileRoute = new FileRoute + { + DownstreamPathTemplate = "/test", + UpstreamPathTemplate = "/test", + RateLimitOptions = new FileRateLimitRule + { + EnableRateLimiting = true, + Period = "test", + }, + }; + + this.Given(_ => GivenThe(fileRoute)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsInvalid()) + .And(_ => ThenTheErrorsContains("RateLimitOptions.Period does not contain integer then s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period")) + .BDDfy(); + } + + [Fact] + public void should_not_be_valid_if_specified_authentication_provider_isnt_registered() + { + var fileRoute = new FileRoute + { + DownstreamPathTemplate = "/test", + UpstreamPathTemplate = "/test", + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "JwtLads", + }, + }; + + this.Given(_ => GivenThe(fileRoute)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsInvalid()) + .And(_ => ThenTheErrorsContains($"Authentication Options AuthenticationProviderKey:JwtLads,AllowedScopes:[] is unsupported authentication provider")) + .BDDfy(); + } + + [Fact] + public void should_not_be_valid_if_not_using_service_discovery_and_no_host_and_ports() + { + var fileRoute = new FileRoute + { + DownstreamPathTemplate = "/test", + UpstreamPathTemplate = "/test", + }; + + this.Given(_ => GivenThe(fileRoute)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsInvalid()) + .And(_ => ThenTheErrorsContains("When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!")) + .BDDfy(); + } + + [Fact] + public void should_be_valid_if_using_service_discovery_and_no_host_and_ports() + { + var fileRoute = new FileRoute + { + DownstreamPathTemplate = "/test", + UpstreamPathTemplate = "/test", + ServiceName = "Lads", + }; + + this.Given(_ => GivenThe(fileRoute)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void should_be_valid_re_route_using_host_and_port_and_paths() + { + var fileRoute = new FileRoute + { + DownstreamPathTemplate = "/test", + UpstreamPathTemplate = "/test", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 5000, + }, + }, + }; + + this.Given(_ => GivenThe(fileRoute)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void should_be_valid_if_specified_authentication_provider_is_registered() + { + const string key = "JwtLads"; + + var fileRoute = new FileRoute + { + DownstreamPathTemplate = "/test", + UpstreamPathTemplate = "/test", + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = key, + }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 5000, + }, + }, + }; + + this.Given(_ => GivenThe(fileRoute)) + .And(_ => GivenAnAuthProvider(key)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsValid()) + .BDDfy(); + } + + [Theory] + [InlineData("1.0")] + [InlineData("1.1")] + [InlineData("2.0")] + [InlineData("1,0")] + [InlineData("1,1")] + [InlineData("2,0")] + [InlineData("1")] + [InlineData("2")] + [InlineData("")] + [InlineData(null)] + public void should_be_valid_re_route_using_downstream_http_version(string version) + { + var fileRoute = new FileRoute + { + DownstreamPathTemplate = "/test", + UpstreamPathTemplate = "/test", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 5000, + }, + }, + DownstreamHttpVersion = version, + }; + + this.Given(_ => GivenThe(fileRoute)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsValid()) + .BDDfy(); + } + + [Theory] + [InlineData("retg1.1")] + [InlineData("re2.0")] + [InlineData("1,0a")] + [InlineData("a1,1")] + [InlineData("12,0")] + [InlineData("asdf")] + public void should_be_invalid_re_route_using_downstream_http_version(string version) + { + var fileRoute = new FileRoute + { + DownstreamPathTemplate = "/test", + UpstreamPathTemplate = "/test", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 5000, + }, + }, + DownstreamHttpVersion = version, + }; + + this.Given(_ => GivenThe(fileRoute)) + .When(_ => WhenIValidate()) + .Then(_ => ThenTheResultIsInvalid()) + .And(_ => ThenTheErrorsContains("'Downstream Http Version' is not in the correct format.")) + .BDDfy(); + } + + private void GivenAnAuthProvider(string key) + { + var schemes = new List + { + new(key, key, typeof(FakeAutheHandler)), + }; + + _authProvider + .Setup(x => x.GetAllSchemesAsync()) + .ReturnsAsync(schemes); + } + + private void ThenTheResultIsValid() + { + _result.IsValid.ShouldBeTrue(); + } + + private void GivenThe(FileRoute route) + { + _route = route; + } + + private async Task WhenIValidate() + { + _result = await _validator.ValidateAsync(_route); + } + + private void ThenTheResultIsInvalid() + { + _result.IsValid.ShouldBeFalse(); + } + + private void ThenTheErrorsContains(string expected) + { + _result.Errors.ShouldContain(x => x.ErrorMessage == expected); + } + + private class FakeAutheHandler : IAuthenticationHandler + { + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + throw new System.NotImplementedException(); + } + + public Task AuthenticateAsync() + { + throw new System.NotImplementedException(); + } + + public Task ChallengeAsync(AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + + public Task ForbidAsync(AuthenticationProperties properties) + { + throw new System.NotImplementedException(); + } + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs new file mode 100644 index 000000000..8d21558cb --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs @@ -0,0 +1,58 @@ +using System; + +using Ocelot.Configuration.Creator; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class VersionCreatorTests + { + private readonly HttpVersionCreator _creator; + private string _input; + private Version _result; + + public VersionCreatorTests() + { + _creator = new HttpVersionCreator(); + } + + [Fact] + public void should_create_version_based_on_input() + { + this.Given(_ => GivenTheInput("2.0")) + .When(_ => WhenICreate()) + .Then(_ => ThenTheResultIs(2, 0)) + .BDDfy(); + } + + [Fact] + public void should_default_to_version_one_point_one() + { + this.Given(_ => GivenTheInput(string.Empty)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheResultIs(1, 1)) + .BDDfy(); + } + + private void GivenTheInput(string input) + { + _input = input; + } + + private void WhenICreate() + { + _result = _creator.Create(_input); + } + + private void ThenTheResultIs(int major, int minor) + { + _result.Major.ShouldBe(major); + _result.Minor.ShouldBe(minor); + } + } +} diff --git a/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs index e655f1235..9d4d29bad 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs @@ -1,259 +1,263 @@ -namespace Ocelot.UnitTests.Consul -{ - using global::Consul; - using Microsoft.Extensions.Options; - using Moq; - using Newtonsoft.Json; - using Ocelot.Cache; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Configuration.File; - using Ocelot.Configuration.Repository; - using Ocelot.Logging; - using Provider.Consul; - using Responses; - using Shouldly; - using System.Collections.Generic; - using System.Linq; - using System.Text; - using System.Threading; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - - public class ConsulFileConfigurationRepositoryTests - { - private ConsulFileConfigurationRepository _repo; - private Mock> _options; - private Mock> _cache; - private Mock _factory; - private Mock _loggerFactory; - private Mock _client; - private Mock _kvEndpoint; - private FileConfiguration _fileConfiguration; - private Response _setResult; - private Response _getResult; - - public ConsulFileConfigurationRepositoryTests() - { - _cache = new Mock>(); - _loggerFactory = new Mock(); - - _options = new Mock>(); - _factory = new Mock(); - _client = new Mock(); - _kvEndpoint = new Mock(); - - _client - .Setup(x => x.KV) - .Returns(_kvEndpoint.Object); - - _factory - .Setup(x => x.Get(It.IsAny())) - .Returns(_client.Object); - - _options - .SetupGet(x => x.Value) - .Returns(() => _fileConfiguration); - } - - [Fact] - public void should_set_config() - { - var config = FakeFileConfiguration(); - - this.Given(_ => GivenIHaveAConfiguration(config)) - .And(_ => GivenWritingToConsulSucceeds()) - .When(_ => WhenISetTheConfiguration()) - .Then(_ => ThenTheConfigurationIsStoredAs(config)) - .BDDfy(); - } - - [Fact] - public void should_get_config() - { - var config = FakeFileConfiguration(); - - this.Given(_ => GivenIHaveAConfiguration(config)) - .And(_ => GivenFetchFromConsulSucceeds()) - .When(_ => WhenIGetTheConfiguration()) - .Then(_ => ThenTheConfigurationIs(config)) - .BDDfy(); - } - - [Fact] - public void should_get_null_config() - { - var config = FakeFileConfiguration(); - - this.Given(_ => GivenIHaveAConfiguration(config)) - .Given(_ => GivenFetchFromConsulReturnsNull()) - .When(_ => WhenIGetTheConfiguration()) - .Then(_ => ThenTheConfigurationIsNull()) - .BDDfy(); - } - - [Fact] - public void should_get_config_from_cache() - { - var config = FakeFileConfiguration(); - - this.Given(_ => GivenIHaveAConfiguration(config)) - .And(_ => GivenFetchFromCacheSucceeds()) - .When(_ => WhenIGetTheConfiguration()) - .Then(_ => ThenTheConfigurationIs(config)) - .BDDfy(); - } - - [Fact] - public void should_set_config_key() - { - var config = FakeFileConfiguration(); - - this.Given(_ => GivenIHaveAConfiguration(config)) - .And(_ => GivenTheConfigKeyComesFromFileConfig("Tom")) - .And(_ => GivenFetchFromConsulSucceeds()) - .When(_ => WhenIGetTheConfiguration()) - .And(_ => ThenTheConfigKeyIs("Tom")) - .BDDfy(); - } - - [Fact] - public void should_set_default_config_key() - { - var config = FakeFileConfiguration(); - - this.Given(_ => GivenIHaveAConfiguration(config)) - .And(_ => GivenFetchFromConsulSucceeds()) - .When(_ => WhenIGetTheConfiguration()) - .And(_ => ThenTheConfigKeyIs("InternalConfiguration")) - .BDDfy(); - } - - private void ThenTheConfigKeyIs(string expected) - { - _kvEndpoint - .Verify(x => x.Get(expected, It.IsAny()), Times.Once); - } - - private void GivenTheConfigKeyComesFromFileConfig(string key) - { - _fileConfiguration.GlobalConfiguration.ServiceDiscoveryProvider.ConfigurationKey = key; - _repo = new ConsulFileConfigurationRepository(_options.Object, _cache.Object, _factory.Object, _loggerFactory.Object); - } - - private void ThenTheConfigurationIsNull() - { - _getResult.Data.ShouldBeNull(); - } - - private void ThenTheConfigurationIs(FileConfiguration config) - { - var expected = JsonConvert.SerializeObject(config, Formatting.Indented); - var result = JsonConvert.SerializeObject(_getResult.Data, Formatting.Indented); - result.ShouldBe(expected); - } - - private async Task WhenIGetTheConfiguration() - { - _getResult = await _repo.Get(); - } - - private void GivenWritingToConsulSucceeds() - { - var response = new WriteResult(); - response.Response = true; - - _kvEndpoint - .Setup(x => x.Put(It.IsAny(), It.IsAny())).ReturnsAsync(response); - } - - private void GivenFetchFromCacheSucceeds() - { - _cache.Setup(x => x.Get(It.IsAny(), It.IsAny())).Returns(_fileConfiguration); - } - - private void GivenFetchFromConsulReturnsNull() - { - QueryResult result = new QueryResult(); - - _kvEndpoint - .Setup(x => x.Get(It.IsAny(), It.IsAny())) - .ReturnsAsync(result); - } - - private void GivenFetchFromConsulSucceeds() - { - var json = JsonConvert.SerializeObject(_fileConfiguration, Formatting.Indented); - - var bytes = Encoding.UTF8.GetBytes(json); - - var kvp = new KVPair("OcelotConfiguration"); - kvp.Value = bytes; - - var query = new QueryResult(); - query.Response = kvp; - - _kvEndpoint - .Setup(x => x.Get(It.IsAny(), It.IsAny())) - .ReturnsAsync(query); - } - - private void ThenTheConfigurationIsStoredAs(FileConfiguration config) - { - var json = JsonConvert.SerializeObject(config, Formatting.Indented); - - var bytes = Encoding.UTF8.GetBytes(json); - - _kvEndpoint - .Verify(x => x.Put(It.Is(k => k.Value.SequenceEqual(bytes)), It.IsAny()), Times.Once); - } - - private async Task WhenISetTheConfiguration() - { - _setResult = await _repo.Set(_fileConfiguration); - } - - private void GivenIHaveAConfiguration(FileConfiguration config) - { - _fileConfiguration = config; - - _repo = new ConsulFileConfigurationRepository(_options.Object, _cache.Object, _factory.Object, _loggerFactory.Object); - } - - private FileConfiguration FakeFileConfiguration() - { - var reRoutes = new List - { - new FileReRoute - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "123.12.12.12", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/asdfs/test/{test}" - } - }; - - var globalConfiguration = new FileGlobalConfiguration - { - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Port = 198, - Host = "blah" - } - }; - - return new FileConfiguration - { - GlobalConfiguration = globalConfiguration, - ReRoutes = reRoutes - }; - } - } -} +using global::Consul; +using Microsoft.Extensions.Options; +using Moq; +using Newtonsoft.Json; +using Ocelot.Cache; +using Ocelot.Configuration.File; +using Ocelot.Logging; +using Ocelot.Provider.Consul; +using Ocelot.Responses; +using Shouldly; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Consul +{ + public class ConsulFileConfigurationRepositoryTests + { + private ConsulFileConfigurationRepository _repo; + private readonly Mock> _options; + private readonly Mock> _cache; + private readonly Mock _factory; + private readonly Mock _loggerFactory; + private readonly Mock _client; + private readonly Mock _kvEndpoint; + private FileConfiguration _fileConfiguration; + private Response _setResult; + private Response _getResult; + + public ConsulFileConfigurationRepositoryTests() + { + _cache = new Mock>(); + _loggerFactory = new Mock(); + + _options = new Mock>(); + _factory = new Mock(); + _client = new Mock(); + _kvEndpoint = new Mock(); + + _client + .Setup(x => x.KV) + .Returns(_kvEndpoint.Object); + + _factory + .Setup(x => x.Get(It.IsAny())) + .Returns(_client.Object); + + _options + .SetupGet(x => x.Value) + .Returns(() => _fileConfiguration); + } + + [Fact] + public void should_set_config() + { + var config = FakeFileConfiguration(); + + this.Given(_ => GivenIHaveAConfiguration(config)) + .And(_ => GivenWritingToConsulSucceeds()) + .When(_ => WhenISetTheConfiguration()) + .Then(_ => ThenTheConfigurationIsStoredAs(config)) + .BDDfy(); + } + + [Fact] + public void should_get_config() + { + var config = FakeFileConfiguration(); + + this.Given(_ => GivenIHaveAConfiguration(config)) + .And(_ => GivenFetchFromConsulSucceeds()) + .When(_ => WhenIGetTheConfiguration()) + .Then(_ => ThenTheConfigurationIs(config)) + .BDDfy(); + } + + [Fact] + public void should_get_null_config() + { + var config = FakeFileConfiguration(); + + this.Given(_ => GivenIHaveAConfiguration(config)) + .Given(_ => GivenFetchFromConsulReturnsNull()) + .When(_ => WhenIGetTheConfiguration()) + .Then(_ => ThenTheConfigurationIsNull()) + .BDDfy(); + } + + [Fact] + public void should_get_config_from_cache() + { + var config = FakeFileConfiguration(); + + this.Given(_ => GivenIHaveAConfiguration(config)) + .And(_ => GivenFetchFromCacheSucceeds()) + .When(_ => WhenIGetTheConfiguration()) + .Then(_ => ThenTheConfigurationIs(config)) + .BDDfy(); + } + + [Fact] + public void should_set_config_key() + { + var config = FakeFileConfiguration(); + + this.Given(_ => GivenIHaveAConfiguration(config)) + .And(_ => GivenTheConfigKeyComesFromFileConfig("Tom")) + .And(_ => GivenFetchFromConsulSucceeds()) + .When(_ => WhenIGetTheConfiguration()) + .And(_ => ThenTheConfigKeyIs("Tom")) + .BDDfy(); + } + + [Fact] + public void should_set_default_config_key() + { + var config = FakeFileConfiguration(); + + this.Given(_ => GivenIHaveAConfiguration(config)) + .And(_ => GivenFetchFromConsulSucceeds()) + .When(_ => WhenIGetTheConfiguration()) + .And(_ => ThenTheConfigKeyIs("InternalConfiguration")) + .BDDfy(); + } + + private void ThenTheConfigKeyIs(string expected) + { + _kvEndpoint + .Verify(x => x.Get(expected, It.IsAny()), Times.Once); + } + + private void GivenTheConfigKeyComesFromFileConfig(string key) + { + _fileConfiguration.GlobalConfiguration.ServiceDiscoveryProvider.ConfigurationKey = key; + _repo = new ConsulFileConfigurationRepository(_options.Object, _cache.Object, _factory.Object, _loggerFactory.Object); + } + + private void ThenTheConfigurationIsNull() + { + _getResult.Data.ShouldBeNull(); + } + + private void ThenTheConfigurationIs(FileConfiguration config) + { + var expected = JsonConvert.SerializeObject(config, Formatting.Indented); + var result = JsonConvert.SerializeObject(_getResult.Data, Formatting.Indented); + result.ShouldBe(expected); + } + + private async Task WhenIGetTheConfiguration() + { + _getResult = await _repo.Get(); + } + + private void GivenWritingToConsulSucceeds() + { + var response = new WriteResult + { + Response = true, + }; + + _kvEndpoint + .Setup(x => x.Put(It.IsAny(), It.IsAny())).ReturnsAsync(response); + } + + private void GivenFetchFromCacheSucceeds() + { + _cache.Setup(x => x.Get(It.IsAny(), It.IsAny())).Returns(_fileConfiguration); + } + + private void GivenFetchFromConsulReturnsNull() + { + var result = new QueryResult(); + + _kvEndpoint + .Setup(x => x.Get(It.IsAny(), It.IsAny())) + .ReturnsAsync(result); + } + + private void GivenFetchFromConsulSucceeds() + { + var json = JsonConvert.SerializeObject(_fileConfiguration, Formatting.Indented); + + var bytes = Encoding.UTF8.GetBytes(json); + + var kvp = new KVPair("OcelotConfiguration") + { + Value = bytes, + }; + + var query = new QueryResult + { + Response = kvp, + }; + + _kvEndpoint + .Setup(x => x.Get(It.IsAny(), It.IsAny())) + .ReturnsAsync(query); + } + + private void ThenTheConfigurationIsStoredAs(FileConfiguration config) + { + var json = JsonConvert.SerializeObject(config, Formatting.Indented); + + var bytes = Encoding.UTF8.GetBytes(json); + + _kvEndpoint + .Verify(x => x.Put(It.Is(k => k.Value.SequenceEqual(bytes)), It.IsAny()), Times.Once); + } + + private async Task WhenISetTheConfiguration() + { + _setResult = await _repo.Set(_fileConfiguration); + } + + private void GivenIHaveAConfiguration(FileConfiguration config) + { + _fileConfiguration = config; + + _repo = new ConsulFileConfigurationRepository(_options.Object, _cache.Object, _factory.Object, _loggerFactory.Object); + } + + private static FileConfiguration FakeFileConfiguration() + { + var routes = new List + { + new() + { + DownstreamHostAndPorts = new List + { + new() + { + Host = "123.12.12.12", + Port = 80, + }, + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/asdfs/test/{test}", + }, + }; + + var globalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "https", + Port = 198, + Host = "blah", + }, + }; + + return new FileConfiguration + { + GlobalConfiguration = globalConfiguration, + Routes = routes, + }; + } + } +} diff --git a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs index 8bc44b1fb..8890ae72b 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs @@ -1,30 +1,32 @@ -namespace Ocelot.UnitTests.Consul -{ - using global::Consul; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Moq; - using Newtonsoft.Json; - using Ocelot.Logging; - using Provider.Consul; - using Shouldly; - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using TestStack.BDDfy; - using Values; - using Xunit; +using global::Consul; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Moq; +using Newtonsoft.Json; +using Ocelot.Logging; +using Ocelot.Provider.Consul; +using Ocelot.Values; +using Shouldly; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using TestStack.BDDfy; +using Xunit; +using _Consul_ = Ocelot.Provider.Consul.Consul; +namespace Ocelot.UnitTests.Consul +{ public class ConsulServiceDiscoveryProviderTests : IDisposable { private IWebHost _fakeConsulBuilder; private readonly List _serviceEntries; - private Consul _provider; + private _Consul_ _provider; private readonly string _serviceName; private readonly int _port; private readonly string _consulHost; + private readonly string _consulScheme; private readonly string _fakeConsulServiceDiscoveryUrl; private List _services; private readonly Mock _factory; @@ -37,29 +39,30 @@ public ConsulServiceDiscoveryProviderTests() _serviceName = "test"; _port = 8500; _consulHost = "localhost"; - _fakeConsulServiceDiscoveryUrl = $"http://{_consulHost}:{_port}"; + _consulScheme = "http"; + _fakeConsulServiceDiscoveryUrl = $"{_consulScheme}://{_consulHost}:{_port}"; _serviceEntries = new List(); _factory = new Mock(); _clientFactory = new ConsulClientFactory(); _logger = new Mock(); - _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _factory.Setup(x => x.CreateLogger<_Consul_>()).Returns(_logger.Object); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - var config = new ConsulRegistryConfiguration(_consulHost, _port, _serviceName, null); - _provider = new Consul(config, _factory.Object, _clientFactory); + var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, _serviceName, null); + _provider = new _Consul_(config, _factory.Object, _clientFactory); } [Fact] public void should_return_service_from_consul() { - var serviceEntryOne = new ServiceEntry() + var serviceEntryOne = new ServiceEntry { - Service = new AgentService() + Service = new AgentService { Service = _serviceName, Address = "localhost", Port = 50881, ID = Guid.NewGuid().ToString(), - Tags = new string[0] + Tags = Array.Empty(), }, }; @@ -74,18 +77,18 @@ public void should_return_service_from_consul() public void should_use_token() { var token = "test token"; - var config = new ConsulRegistryConfiguration(_consulHost, _port, _serviceName, token); - _provider = new Consul(config, _factory.Object, _clientFactory); + var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _port, _serviceName, token); + _provider = new _Consul_(config, _factory.Object, _clientFactory); - var serviceEntryOne = new ServiceEntry() + var serviceEntryOne = new ServiceEntry { - Service = new AgentService() + Service = new AgentService { Service = _serviceName, Address = "localhost", Port = 50881, ID = Guid.NewGuid().ToString(), - Tags = new string[0] + Tags = Array.Empty(), }, }; @@ -93,34 +96,34 @@ public void should_use_token() .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) .When(_ => WhenIGetTheServices()) .Then(_ => ThenTheCountIs(1)) - .And(_ => _receivedToken.ShouldBe(token)) + .And(_ => ThenTheTokenIs(token)) .BDDfy(); } [Fact] public void should_not_return_services_with_invalid_address() { - var serviceEntryOne = new ServiceEntry() + var serviceEntryOne = new ServiceEntry { - Service = new AgentService() + Service = new AgentService { Service = _serviceName, Address = "http://localhost", Port = 50881, ID = Guid.NewGuid().ToString(), - Tags = new string[0] + Tags = Array.Empty(), }, }; - var serviceEntryTwo = new ServiceEntry() + var serviceEntryTwo = new ServiceEntry { - Service = new AgentService() + Service = new AgentService { Service = _serviceName, Address = "http://localhost", Port = 50888, ID = Guid.NewGuid().ToString(), - Tags = new string[0] + Tags = Array.Empty(), }, }; @@ -135,27 +138,27 @@ public void should_not_return_services_with_invalid_address() [Fact] public void should_not_return_services_with_empty_address() { - var serviceEntryOne = new ServiceEntry() + var serviceEntryOne = new ServiceEntry { - Service = new AgentService() + Service = new AgentService { Service = _serviceName, - Address = "", + Address = string.Empty, Port = 50881, ID = Guid.NewGuid().ToString(), - Tags = new string[0] + Tags = Array.Empty(), }, }; - var serviceEntryTwo = new ServiceEntry() + var serviceEntryTwo = new ServiceEntry { - Service = new AgentService() + Service = new AgentService { Service = _serviceName, Address = null, Port = 50888, ID = Guid.NewGuid().ToString(), - Tags = new string[0] + Tags = Array.Empty(), }, }; @@ -170,27 +173,27 @@ public void should_not_return_services_with_empty_address() [Fact] public void should_not_return_services_with_invalid_port() { - var serviceEntryOne = new ServiceEntry() + var serviceEntryOne = new ServiceEntry { - Service = new AgentService() + Service = new AgentService { Service = _serviceName, Address = "localhost", Port = -1, ID = Guid.NewGuid().ToString(), - Tags = new string[0] + Tags = Array.Empty(), }, }; - var serviceEntryTwo = new ServiceEntry() + var serviceEntryTwo = new ServiceEntry { - Service = new AgentService() + Service = new AgentService { Service = _serviceName, Address = "localhost", Port = 0, ID = Guid.NewGuid().ToString(), - Tags = new string[0] + Tags = Array.Empty(), }, }; @@ -251,6 +254,11 @@ private void WhenIGetTheServices() _services = _provider.Get().GetAwaiter().GetResult(); } + private void ThenTheTokenIs(string token) + { + _receivedToken.ShouldBe(token); + } + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) { foreach (var serviceEntry in serviceEntries) diff --git a/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs index cb5fc0b5d..763d63d2c 100644 --- a/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs @@ -1,22 +1,21 @@ -namespace Ocelot.UnitTests.Consul -{ - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Moq; - using Ocelot.DependencyInjection; - using Provider.Consul; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Reflection; - using TestStack.BDDfy; - using Xunit; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Ocelot.DependencyInjection; +using Ocelot.Provider.Consul; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Reflection; +using TestStack.BDDfy; +using Xunit; +namespace Ocelot.UnitTests.Consul +{ public class OcelotBuilderExtensionsTests { private readonly IServiceCollection _services; - private IServiceProvider _serviceProvider; private readonly IConfiguration _configRoot; private IOcelotBuilder _ocelotBuilder; private Exception _ex; @@ -25,12 +24,11 @@ public OcelotBuilderExtensionsTests() { _configRoot = new ConfigurationRoot(new List()); _services = new ServiceCollection(); - _services.AddSingleton(GetHostingEnvironment()); + _services.AddSingleton(GetHostingEnvironment()); _services.AddSingleton(_configRoot); } - - private IWebHostEnvironment GetHostingEnvironment() + private static IWebHostEnvironment GetHostingEnvironment() { var environment = new Mock(); environment diff --git a/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs index cc38e29c3..b3780fd21 100644 --- a/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs @@ -1,17 +1,17 @@ -namespace Ocelot.UnitTests.Consul -{ - using Moq; - using Ocelot.Infrastructure; - using Ocelot.Logging; - using Ocelot.ServiceDiscovery.Providers; - using Provider.Consul; - using Shouldly; - using System; - using System.Collections.Generic; - using TestStack.BDDfy; - using Values; - using Xunit; +using Moq; +using Ocelot.Infrastructure; +using Ocelot.Logging; +using Ocelot.Provider.Consul; +using Ocelot.ServiceDiscovery.Providers; +using Ocelot.Values; +using Shouldly; +using System; +using System.Collections.Generic; +using TestStack.BDDfy; +using Xunit; +namespace Ocelot.UnitTests.Consul +{ public class PollingConsulServiceDiscoveryProviderTests { private readonly int _delay; @@ -34,7 +34,7 @@ public PollingConsulServiceDiscoveryProviderTests() [Fact] public void should_return_service_from_consul() { - var service = new Service("", new ServiceHostAndPort("", 0), "", "", new List()); + var service = new Service(string.Empty, new ServiceHostAndPort(string.Empty, 0), string.Empty, string.Empty, new List()); this.Given(x => GivenConsulReturns(service)) .When(x => WhenIGetTheServices(1)) diff --git a/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs b/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs index c51e9cd7b..7b3085c53 100644 --- a/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/Consul/ProviderFactoryTests.cs @@ -1,57 +1,56 @@ -using Ocelot.Configuration.Builder; - -namespace Ocelot.UnitTests.Consul -{ - using Microsoft.Extensions.DependencyInjection; - using Moq; - using Ocelot.Configuration; - using Ocelot.Logging; - using Provider.Consul; - using Shouldly; - using System; - using Xunit; - - public class ProviderFactoryTests - { - private readonly IServiceProvider _provider; - - public ProviderFactoryTests() - { - var services = new ServiceCollection(); - var loggerFactory = new Mock(); - var logger = new Mock(); - loggerFactory.Setup(x => x.CreateLogger()).Returns(logger.Object); - loggerFactory.Setup(x => x.CreateLogger()).Returns(logger.Object); - var consulFactory = new Mock(); - services.AddSingleton(consulFactory.Object); - services.AddSingleton(loggerFactory.Object); - _provider = services.BuildServiceProvider(); - } - - [Fact] - public void should_return_ConsulServiceDiscoveryProvider() - { - var reRoute = new DownstreamReRouteBuilder() - .WithServiceName("") - .Build(); - - var provider = ConsulProviderFactory.Get(_provider, new ServiceProviderConfiguration("", "", 1, "", "", 1), reRoute); - provider.ShouldBeOfType(); - } - - [Fact] - public void should_return_PollingConsulServiceDiscoveryProvider() - { - var stopsPollerFromPolling = 10000; - - var reRoute = new DownstreamReRouteBuilder() - .WithServiceName("") - .Build(); - - var provider = ConsulProviderFactory.Get(_provider, new ServiceProviderConfiguration("pollconsul", "", 1, "", "", stopsPollerFromPolling), reRoute); - var pollProvider = provider as PollConsul; - pollProvider.ShouldNotBeNull(); - pollProvider.Dispose(); - } - } -} +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Provider.Consul; +using Shouldly; +using System; +using Xunit; + +namespace Ocelot.UnitTests.Consul +{ + public class ProviderFactoryTests + { + private readonly IServiceProvider _provider; + + public ProviderFactoryTests() + { + var services = new ServiceCollection(); + var loggerFactory = new Mock(); + var logger = new Mock(); + loggerFactory.Setup(x => x.CreateLogger()).Returns(logger.Object); + loggerFactory.Setup(x => x.CreateLogger()).Returns(logger.Object); + var consulFactory = new Mock(); + services.AddSingleton(consulFactory.Object); + services.AddSingleton(loggerFactory.Object); + _provider = services.BuildServiceProvider(); + } + + [Fact] + public void should_return_ConsulServiceDiscoveryProvider() + { + var route = new DownstreamRouteBuilder() + .WithServiceName(string.Empty) + .Build(); + + var provider = ConsulProviderFactory.Get(_provider, new ServiceProviderConfiguration(string.Empty, string.Empty, string.Empty, 1, string.Empty, string.Empty, 1), route); + provider.ShouldBeOfType(); + } + + [Fact] + public void should_return_PollingConsulServiceDiscoveryProvider() + { + var stopsPollerFromPolling = 10000; + + var route = new DownstreamRouteBuilder() + .WithServiceName(string.Empty) + .Build(); + + var provider = ConsulProviderFactory.Get(_provider, new ServiceProviderConfiguration("pollconsul", "http", string.Empty, 1, string.Empty, string.Empty, stopsPollerFromPolling), route); + var pollProvider = provider as PollConsul; + pollProvider.ShouldNotBeNull(); + pollProvider.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs index fbf1cae24..4e13e79fb 100644 --- a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs +++ b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs @@ -1,135 +1,140 @@ -using Microsoft.AspNetCore.Mvc; -using Moq; -using Ocelot.Configuration; -using Ocelot.Configuration.File; -using Ocelot.Configuration.Setter; -using Ocelot.Errors; -using Ocelot.Responses; -using Shouldly; -using System; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Controllers -{ - using Ocelot.Configuration.Repository; - - public class FileConfigurationControllerTests - { - private readonly FileConfigurationController _controller; - private readonly Mock _repo; - private readonly Mock _setter; - private IActionResult _result; - private FileConfiguration _fileConfiguration; - private readonly Mock _provider; - - public FileConfigurationControllerTests() - { - _provider = new Mock(); - _repo = new Mock(); - _setter = new Mock(); - _controller = new FileConfigurationController(_repo.Object, _setter.Object, _provider.Object); +using System; + +using Microsoft.AspNetCore.Mvc; + +using Moq; + +using Ocelot.Configuration; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Setter; +using Ocelot.Errors; +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; +using Ocelot.Configuration.Repository; + +namespace Ocelot.UnitTests.Controllers +{ + public class FileConfigurationControllerTests + { + private readonly FileConfigurationController _controller; + private readonly Mock _repo; + private readonly Mock _setter; + private IActionResult _result; + private FileConfiguration _fileConfiguration; + private readonly Mock _provider; + + public FileConfigurationControllerTests() + { + _provider = new Mock(); + _repo = new Mock(); + _setter = new Mock(); + _controller = new FileConfigurationController(_repo.Object, _setter.Object, _provider.Object); + } + + [Fact] + public void should_get_file_configuration() + { + var expected = new OkResponse(new FileConfiguration()); + + this.Given(x => x.GivenTheGetConfigurationReturns(expected)) + .When(x => x.WhenIGetTheFileConfiguration()) + .Then(x => x.TheTheGetFileConfigurationIsCalledCorrectly()) + .BDDfy(); } - [Fact] - public void should_get_file_configuration() - { - var expected = new Responses.OkResponse(new FileConfiguration()); - - this.Given(x => x.GivenTheGetConfigurationReturns(expected)) - .When(x => x.WhenIGetTheFileConfiguration()) - .Then(x => x.TheTheGetFileConfigurationIsCalledCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_return_error_when_cannot_get_config() - { - var expected = new Responses.ErrorResponse(It.IsAny()); + [Fact] + public void should_return_error_when_cannot_get_config() + { + var expected = new ErrorResponse(It.IsAny()); this.Given(x => x.GivenTheGetConfigurationReturns(expected)) .When(x => x.WhenIGetTheFileConfiguration()) .Then(x => x.TheTheGetFileConfigurationIsCalledCorrectly()) .And(x => x.ThenTheResponseIs()) - .BDDfy(); + .BDDfy(); + } + + [Fact] + public void should_post_file_configuration() + { + var expected = new FileConfiguration(); + + this.Given(x => GivenTheFileConfiguration(expected)) + .And(x => GivenTheConfigSetterReturns(new OkResponse())) + .When(x => WhenIPostTheFileConfiguration()) + .Then(x => x.ThenTheConfigrationSetterIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_return_error_when_cannot_set_config() + { + var expected = new FileConfiguration(); + + this.Given(x => GivenTheFileConfiguration(expected)) + .And(x => GivenTheConfigSetterReturns(new ErrorResponse(new FakeError()))) + .When(x => WhenIPostTheFileConfiguration()) + .Then(x => x.ThenTheConfigrationSetterIsCalledCorrectly()) + .And(x => ThenTheResponseIs()) + .BDDfy(); + } + + private void GivenTheConfigSetterReturns(Response response) + { + _setter + .Setup(x => x.Set(It.IsAny())) + .ReturnsAsync(response); + } + + private void ThenTheConfigrationSetterIsCalledCorrectly() + { + _setter + .Verify(x => x.Set(_fileConfiguration), Times.Once); + } + + private void WhenIPostTheFileConfiguration() + { + _result = _controller.Post(_fileConfiguration).Result; + } + + private void GivenTheFileConfiguration(FileConfiguration fileConfiguration) + { + _fileConfiguration = fileConfiguration; + } + + private void ThenTheResponseIs() + { + _result.ShouldBeOfType(); + } + + private void GivenTheGetConfigurationReturns(Response fileConfiguration) + { + _repo + .Setup(x => x.Get()) + .ReturnsAsync(fileConfiguration); } - [Fact] - public void should_post_file_configuration() - { - var expected = new FileConfiguration(); - - this.Given(x => GivenTheFileConfiguration(expected)) - .And(x => GivenTheConfigSetterReturns(new OkResponse())) - .When(x => WhenIPostTheFileConfiguration()) - .Then(x => x.ThenTheConfigrationSetterIsCalledCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_return_error_when_cannot_set_config() - { - var expected = new FileConfiguration(); - - this.Given(x => GivenTheFileConfiguration(expected)) - .And(x => GivenTheConfigSetterReturns(new ErrorResponse(new FakeError()))) - .When(x => WhenIPostTheFileConfiguration()) - .Then(x => x.ThenTheConfigrationSetterIsCalledCorrectly()) - .And(x => ThenTheResponseIs()) - .BDDfy(); - } - - private void GivenTheConfigSetterReturns(Response response) - { - _setter - .Setup(x => x.Set(It.IsAny())) - .ReturnsAsync(response); - } - - private void ThenTheConfigrationSetterIsCalledCorrectly() - { - _setter - .Verify(x => x.Set(_fileConfiguration), Times.Once); - } - - private void WhenIPostTheFileConfiguration() - { - _result = _controller.Post(_fileConfiguration).Result; - } - - private void GivenTheFileConfiguration(FileConfiguration fileConfiguration) - { - _fileConfiguration = fileConfiguration; - } - - private void ThenTheResponseIs() + private void WhenIGetTheFileConfiguration() { - _result.ShouldBeOfType(); - } - - private void GivenTheGetConfigurationReturns(Ocelot.Responses.Response fileConfiguration) - { - _repo - .Setup(x => x.Get()) - .ReturnsAsync(fileConfiguration); - } - - private void WhenIGetTheFileConfiguration() - { - _result = _controller.Get().Result; - } - - private void TheTheGetFileConfigurationIsCalledCorrectly() + _result = _controller.Get().Result; + } + + private void TheTheGetFileConfigurationIsCalledCorrectly() { _repo - .Verify(x => x.Get(), Times.Once); - } - - private class FakeError : Error - { - public FakeError() : base(string.Empty, OcelotErrorCode.CannotAddDataError) - { - } - } - } + .Verify(x => x.Get(), Times.Once); + } + + private class FakeError : Error + { + public FakeError() : base(string.Empty, OcelotErrorCode.CannotAddDataError, 404) + { + } + } + } } diff --git a/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs b/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs index 5f44797f8..a4db930a6 100644 --- a/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs +++ b/test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs @@ -1,16 +1,21 @@ -using Microsoft.AspNetCore.Mvc; -using Moq; -using Ocelot.Cache; -using Shouldly; -using TestStack.BDDfy; +using Microsoft.AspNetCore.Mvc; + +using Moq; + +using Ocelot.Cache; + +using Shouldly; + +using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Controllers { public class OutputCacheControllerTests { - private OutputCacheController _controller; - private Mock> _cache; + private readonly OutputCacheController _controller; + private readonly Mock> _cache; private IActionResult _result; public OutputCacheControllerTests() diff --git a/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs index 1d408ba41..0aedd79f1 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs @@ -1,332 +1,342 @@ -namespace Ocelot.UnitTests.DependencyInjection -{ - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Moq; - using Newtonsoft.Json; - using Ocelot.Configuration.File; - using Ocelot.DependencyInjection; - using Shouldly; - using System.Collections.Generic; - using System.IO; - using TestStack.BDDfy; - using Xunit; - - public class ConfigurationBuilderExtensionsTests - { - private IConfigurationRoot _configuration; - private string _result; - private IConfigurationRoot _configRoot; - private FileConfiguration _globalConfig; - private FileConfiguration _reRouteA; - private FileConfiguration _reRouteB; - private FileConfiguration _aggregate; - private FileConfiguration _envSpecific; - private Mock _hostingEnvironment; - - public ConfigurationBuilderExtensionsTests() - { - _hostingEnvironment = new Mock(); - // Clean up config files before each test - var subConfigFiles = new DirectoryInfo(".").GetFiles("ocelot.*.json"); - - foreach (var config in subConfigFiles) - { - config.Delete(); - } - } - - [Fact] - public void should_add_base_url_to_config() - { - this.Given(_ => GivenTheBaseUrl("test")) - .When(_ => WhenIGet("BaseUrl")) - .Then(_ => ThenTheResultIs("test")) - .BDDfy(); - } - - [Fact] - public void should_merge_files() - { - this.Given(_ => GivenMultipleConfigurationFiles("", false)) - .And(_ => GivenTheEnvironmentIs(null)) - .When(_ => WhenIAddOcelotConfiguration()) - .Then(_ => ThenTheConfigsAreMerged()) - .BDDfy(); - } - - [Fact] - public void should_merge_files_except_env() - { - this.Given(_ => GivenMultipleConfigurationFiles("", true)) - .And(_ => GivenTheEnvironmentIs("Env")) - .When(_ => WhenIAddOcelotConfiguration()) - .Then(_ => ThenTheConfigsAreMerged()) - .And(_ => NotContainsEnvSpecificConfig()) - .BDDfy(); - } - - [Fact] - public void should_merge_files_in_specific_folder() - { - string configFolder = "ConfigFiles"; - this.Given(_ => GivenMultipleConfigurationFiles(configFolder, false)) - .When(_ => WhenIAddOcelotConfigurationWithSpecificFolder(configFolder)) - .Then(_ => ThenTheConfigsAreMerged()) - .BDDfy(); - } - - private void GivenMultipleConfigurationFiles(string folder, bool addEnvSpecificConfig) - { - if (!string.IsNullOrEmpty(folder)) - { - Directory.CreateDirectory(folder); - } - - _globalConfig = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - BaseUrl = "BaseUrl", - RateLimitOptions = new FileRateLimitOptions - { - HttpStatusCode = 500, - ClientIdHeader = "ClientIdHeader", - DisableRateLimitHeaders = true, - QuotaExceededMessage = "QuotaExceededMessage", - RateLimitCounterPrefix = "RateLimitCounterPrefix" - }, - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "Host", - Port = 80, - Type = "Type" - }, - RequestIdKey = "RequestIdKey" - } - }; - - _reRouteA = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamScheme = "DownstreamScheme", - DownstreamPathTemplate = "DownstreamPathTemplate", - Key = "Key", - UpstreamHost = "UpstreamHost", - UpstreamHttpMethod = new List - { - "UpstreamHttpMethod" - }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "Host", - Port = 80 - } - } - } - } - }; - - _reRouteB = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamScheme = "DownstreamSchemeB", - DownstreamPathTemplate = "DownstreamPathTemplateB", - Key = "KeyB", - UpstreamHost = "UpstreamHostB", - UpstreamHttpMethod = new List - { - "UpstreamHttpMethodB" - }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "HostB", - Port = 80 - } - } - }, - new FileReRoute - { - DownstreamScheme = "DownstreamSchemeBB", - DownstreamPathTemplate = "DownstreamPathTemplateBB", - Key = "KeyBB", - UpstreamHost = "UpstreamHostBB", - UpstreamHttpMethod = new List - { - "UpstreamHttpMethodBB" - }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "HostBB", - Port = 80 - } - } - } - } - }; - - _aggregate = new FileConfiguration - { - Aggregates = new List - { - new FileAggregateReRoute - { - ReRouteKeys = new List - { - "KeyB", - "KeyBB" - }, - UpstreamPathTemplate = "UpstreamPathTemplate", - }, - new FileAggregateReRoute - { - ReRouteKeys = new List - { - "KeyB", - "KeyBB" - }, - UpstreamPathTemplate = "UpstreamPathTemplate", - } - } - }; - - _envSpecific = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamScheme = "DownstreamSchemeSpec", - DownstreamPathTemplate = "DownstreamPathTemplateSpec", - Key = "KeySpec", - UpstreamHost = "UpstreamHostSpec", - UpstreamHttpMethod = new List - { - "UpstreamHttpMethodSpec" - }, - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "HostSpec", - Port = 80 - } - } - } - } - }; - - string globalFilename = Path.Combine(folder, "ocelot.global.json"); - string reroutesAFilename = Path.Combine(folder, "ocelot.reRoutesA.json"); - string reroutesBFilename = Path.Combine(folder, "ocelot.reRoutesB.json"); - string aggregatesFilename = Path.Combine(folder, "ocelot.aggregates.json"); - - File.WriteAllText(globalFilename, JsonConvert.SerializeObject(_globalConfig)); - File.WriteAllText(reroutesAFilename, JsonConvert.SerializeObject(_reRouteA)); - File.WriteAllText(reroutesBFilename, JsonConvert.SerializeObject(_reRouteB)); - File.WriteAllText(aggregatesFilename, JsonConvert.SerializeObject(_aggregate)); - - if (addEnvSpecificConfig) - { - string envSpecificFilename = Path.Combine(folder, "ocelot.Env.json"); - File.WriteAllText(envSpecificFilename, JsonConvert.SerializeObject(_envSpecific)); - } - } - - private void GivenTheEnvironmentIs(string env) - { - _hostingEnvironment.SetupGet(x => x.EnvironmentName).Returns(env); - } - - private void WhenIAddOcelotConfiguration() - { - IConfigurationBuilder builder = new ConfigurationBuilder(); - - builder.AddOcelot(_hostingEnvironment.Object); - - _configRoot = builder.Build(); - } - - private void WhenIAddOcelotConfigurationWithSpecificFolder(string folder) - { - IConfigurationBuilder builder = new ConfigurationBuilder(); - builder.AddOcelot(folder, _hostingEnvironment.Object); - _configRoot = builder.Build(); - } - - private void ThenTheConfigsAreMerged() - { - var fc = (FileConfiguration)_configRoot.Get(typeof(FileConfiguration)); - - fc.GlobalConfiguration.BaseUrl.ShouldBe(_globalConfig.GlobalConfiguration.BaseUrl); - fc.GlobalConfiguration.RateLimitOptions.ClientIdHeader.ShouldBe(_globalConfig.GlobalConfiguration.RateLimitOptions.ClientIdHeader); - fc.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders.ShouldBe(_globalConfig.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders); - fc.GlobalConfiguration.RateLimitOptions.HttpStatusCode.ShouldBe(_globalConfig.GlobalConfiguration.RateLimitOptions.HttpStatusCode); - fc.GlobalConfiguration.RateLimitOptions.QuotaExceededMessage.ShouldBe(_globalConfig.GlobalConfiguration.RateLimitOptions.QuotaExceededMessage); - fc.GlobalConfiguration.RateLimitOptions.RateLimitCounterPrefix.ShouldBe(_globalConfig.GlobalConfiguration.RateLimitOptions.RateLimitCounterPrefix); - fc.GlobalConfiguration.RequestIdKey.ShouldBe(_globalConfig.GlobalConfiguration.RequestIdKey); - fc.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(_globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Host); - fc.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(_globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Port); - fc.GlobalConfiguration.ServiceDiscoveryProvider.Type.ShouldBe(_globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Type); - - fc.ReRoutes.Count.ShouldBe(_reRouteA.ReRoutes.Count + _reRouteB.ReRoutes.Count); - - fc.ReRoutes.ShouldContain(x => x.DownstreamPathTemplate == _reRouteA.ReRoutes[0].DownstreamPathTemplate); - fc.ReRoutes.ShouldContain(x => x.DownstreamPathTemplate == _reRouteB.ReRoutes[0].DownstreamPathTemplate); - fc.ReRoutes.ShouldContain(x => x.DownstreamPathTemplate == _reRouteB.ReRoutes[1].DownstreamPathTemplate); - - fc.ReRoutes.ShouldContain(x => x.DownstreamScheme == _reRouteA.ReRoutes[0].DownstreamScheme); - fc.ReRoutes.ShouldContain(x => x.DownstreamScheme == _reRouteB.ReRoutes[0].DownstreamScheme); - fc.ReRoutes.ShouldContain(x => x.DownstreamScheme == _reRouteB.ReRoutes[1].DownstreamScheme); - - fc.ReRoutes.ShouldContain(x => x.Key == _reRouteA.ReRoutes[0].Key); - fc.ReRoutes.ShouldContain(x => x.Key == _reRouteB.ReRoutes[0].Key); - fc.ReRoutes.ShouldContain(x => x.Key == _reRouteB.ReRoutes[1].Key); - - fc.ReRoutes.ShouldContain(x => x.UpstreamHost == _reRouteA.ReRoutes[0].UpstreamHost); - fc.ReRoutes.ShouldContain(x => x.UpstreamHost == _reRouteB.ReRoutes[0].UpstreamHost); - fc.ReRoutes.ShouldContain(x => x.UpstreamHost == _reRouteB.ReRoutes[1].UpstreamHost); - - fc.Aggregates.Count.ShouldBe(_aggregate.Aggregates.Count); - } - - private void NotContainsEnvSpecificConfig() - { - var fc = (FileConfiguration)_configRoot.Get(typeof(FileConfiguration)); - fc.ReRoutes.ShouldNotContain(x => x.DownstreamScheme == _envSpecific.ReRoutes[0].DownstreamScheme); - fc.ReRoutes.ShouldNotContain(x => x.DownstreamPathTemplate == _envSpecific.ReRoutes[0].DownstreamPathTemplate); - fc.ReRoutes.ShouldNotContain(x => x.Key == _envSpecific.ReRoutes[0].Key); - } - - private void GivenTheBaseUrl(string baseUrl) - { -#pragma warning disable CS0618 - var builder = new ConfigurationBuilder() - .AddOcelotBaseUrl(baseUrl); -#pragma warning restore CS0618 - _configuration = builder.Build(); - } - - private void WhenIGet(string key) - { - _result = _configuration.GetValue("BaseUrl", ""); - } - - private void ThenTheResultIs(string expected) - { - _result.ShouldBe(expected); - } - } -} +using System.Collections.Generic; +using System.IO; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; + +using Moq; + +using Newtonsoft.Json; + +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.DependencyInjection +{ + public class ConfigurationBuilderExtensionsTests + { + private IConfigurationRoot _configuration; + private string _result; + private IConfigurationRoot _configRoot; + private FileConfiguration _globalConfig; + private FileConfiguration _routeA; + private FileConfiguration _routeB; + private FileConfiguration _aggregate; + private FileConfiguration _envSpecific; + private readonly Mock _hostingEnvironment; + + public ConfigurationBuilderExtensionsTests() + { + _hostingEnvironment = new Mock(); + + // Clean up config files before each test + var subConfigFiles = new DirectoryInfo(".").GetFiles("ocelot.*.json"); + + foreach (var config in subConfigFiles) + { + config.Delete(); + } + } + + [Fact] + public void should_add_base_url_to_config() + { + this.Given(_ => GivenTheBaseUrl("test")) + .When(_ => WhenIGet("BaseUrl")) + .Then(_ => ThenTheResultIs("test")) + .BDDfy(); + } + + [Fact] + public void should_merge_files() + { + this.Given(_ => GivenMultipleConfigurationFiles(string.Empty, false)) + .And(_ => GivenTheEnvironmentIs(null)) + .When(_ => WhenIAddOcelotConfiguration()) + .Then(_ => ThenTheConfigsAreMerged()) + .BDDfy(); + } + + [Fact] + public void should_merge_files_except_env() + { + this.Given(_ => GivenMultipleConfigurationFiles(string.Empty, true)) + .And(_ => GivenTheEnvironmentIs("Env")) + .When(_ => WhenIAddOcelotConfiguration()) + .Then(_ => ThenTheConfigsAreMerged()) + .And(_ => NotContainsEnvSpecificConfig()) + .BDDfy(); + } + + [Fact] + public void should_merge_files_in_specific_folder() + { + var configFolder = "ConfigFiles"; + this.Given(_ => GivenMultipleConfigurationFiles(configFolder, false)) + .When(_ => WhenIAddOcelotConfigurationWithSpecificFolder(configFolder)) + .Then(_ => ThenTheConfigsAreMerged()) + .BDDfy(); + } + + private void GivenMultipleConfigurationFiles(string folder, bool addEnvSpecificConfig) + { + if (!string.IsNullOrEmpty(folder)) + { + Directory.CreateDirectory(folder); + } + + _globalConfig = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + BaseUrl = "BaseUrl", + RateLimitOptions = new FileRateLimitOptions + { + HttpStatusCode = 500, + ClientIdHeader = "ClientIdHeader", + DisableRateLimitHeaders = true, + QuotaExceededMessage = "QuotaExceededMessage", + RateLimitCounterPrefix = "RateLimitCounterPrefix", + }, + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Scheme = "https", + Host = "Host", + Port = 80, + Type = "Type", + }, + RequestIdKey = "RequestIdKey", + }, + }; + + _routeA = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamScheme = "DownstreamScheme", + DownstreamPathTemplate = "DownstreamPathTemplate", + Key = "Key", + UpstreamHost = "UpstreamHost", + UpstreamHttpMethod = new List + { + "UpstreamHttpMethod", + }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "Host", + Port = 80, + }, + }, + }, + }, + }; + + _routeB = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamScheme = "DownstreamSchemeB", + DownstreamPathTemplate = "DownstreamPathTemplateB", + Key = "KeyB", + UpstreamHost = "UpstreamHostB", + UpstreamHttpMethod = new List + { + "UpstreamHttpMethodB", + }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "HostB", + Port = 80, + }, + }, + }, + new() + { + DownstreamScheme = "DownstreamSchemeBB", + DownstreamPathTemplate = "DownstreamPathTemplateBB", + Key = "KeyBB", + UpstreamHost = "UpstreamHostBB", + UpstreamHttpMethod = new List + { + "UpstreamHttpMethodBB", + }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "HostBB", + Port = 80, + }, + }, + }, + }, + }; + + _aggregate = new FileConfiguration + { + Aggregates = new List + { + new() + { + RouteKeys = new List + { + "KeyB", + "KeyBB", + }, + UpstreamPathTemplate = "UpstreamPathTemplate", + }, + new() + { + RouteKeys = new List + { + "KeyB", + "KeyBB", + }, + UpstreamPathTemplate = "UpstreamPathTemplate", + }, + }, + }; + + _envSpecific = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamScheme = "DownstreamSchemeSpec", + DownstreamPathTemplate = "DownstreamPathTemplateSpec", + Key = "KeySpec", + UpstreamHost = "UpstreamHostSpec", + UpstreamHttpMethod = new List + { + "UpstreamHttpMethodSpec", + }, + DownstreamHostAndPorts = new List + { + new() + { + Host = "HostSpec", + Port = 80, + }, + }, + }, + }, + }; + + var globalFilename = Path.Combine(folder, "ocelot.global.json"); + var routesAFilename = Path.Combine(folder, "ocelot.routesA.json"); + var routesBFilename = Path.Combine(folder, "ocelot.routesB.json"); + var aggregatesFilename = Path.Combine(folder, "ocelot.aggregates.json"); + + File.WriteAllText(globalFilename, JsonConvert.SerializeObject(_globalConfig)); + File.WriteAllText(routesAFilename, JsonConvert.SerializeObject(_routeA)); + File.WriteAllText(routesBFilename, JsonConvert.SerializeObject(_routeB)); + File.WriteAllText(aggregatesFilename, JsonConvert.SerializeObject(_aggregate)); + + if (addEnvSpecificConfig) + { + var envSpecificFilename = Path.Combine(folder, "ocelot.Env.json"); + File.WriteAllText(envSpecificFilename, JsonConvert.SerializeObject(_envSpecific)); + } + } + + private void GivenTheEnvironmentIs(string env) + { + _hostingEnvironment.SetupGet(x => x.EnvironmentName).Returns(env); + } + + private void WhenIAddOcelotConfiguration() + { + IConfigurationBuilder builder = new ConfigurationBuilder(); + + builder.AddOcelot(_hostingEnvironment.Object); + + _configRoot = builder.Build(); + } + + private void WhenIAddOcelotConfigurationWithSpecificFolder(string folder) + { + IConfigurationBuilder builder = new ConfigurationBuilder(); + builder.AddOcelot(folder, _hostingEnvironment.Object); + _configRoot = builder.Build(); + } + + private void ThenTheConfigsAreMerged() + { + var fc = (FileConfiguration)_configRoot.Get(typeof(FileConfiguration)); + + fc.GlobalConfiguration.BaseUrl.ShouldBe(_globalConfig.GlobalConfiguration.BaseUrl); + fc.GlobalConfiguration.RateLimitOptions.ClientIdHeader.ShouldBe(_globalConfig.GlobalConfiguration.RateLimitOptions.ClientIdHeader); + fc.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders.ShouldBe(_globalConfig.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders); + fc.GlobalConfiguration.RateLimitOptions.HttpStatusCode.ShouldBe(_globalConfig.GlobalConfiguration.RateLimitOptions.HttpStatusCode); + fc.GlobalConfiguration.RateLimitOptions.QuotaExceededMessage.ShouldBe(_globalConfig.GlobalConfiguration.RateLimitOptions.QuotaExceededMessage); + fc.GlobalConfiguration.RateLimitOptions.RateLimitCounterPrefix.ShouldBe(_globalConfig.GlobalConfiguration.RateLimitOptions.RateLimitCounterPrefix); + fc.GlobalConfiguration.RequestIdKey.ShouldBe(_globalConfig.GlobalConfiguration.RequestIdKey); + fc.GlobalConfiguration.ServiceDiscoveryProvider.Scheme.ShouldBe(_globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Scheme); + fc.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(_globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Host); + fc.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(_globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Port); + fc.GlobalConfiguration.ServiceDiscoveryProvider.Type.ShouldBe(_globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Type); + + fc.Routes.Count.ShouldBe(_routeA.Routes.Count + _routeB.Routes.Count); + + fc.Routes.ShouldContain(x => x.DownstreamPathTemplate == _routeA.Routes[0].DownstreamPathTemplate); + fc.Routes.ShouldContain(x => x.DownstreamPathTemplate == _routeB.Routes[0].DownstreamPathTemplate); + fc.Routes.ShouldContain(x => x.DownstreamPathTemplate == _routeB.Routes[1].DownstreamPathTemplate); + + fc.Routes.ShouldContain(x => x.DownstreamScheme == _routeA.Routes[0].DownstreamScheme); + fc.Routes.ShouldContain(x => x.DownstreamScheme == _routeB.Routes[0].DownstreamScheme); + fc.Routes.ShouldContain(x => x.DownstreamScheme == _routeB.Routes[1].DownstreamScheme); + + fc.Routes.ShouldContain(x => x.Key == _routeA.Routes[0].Key); + fc.Routes.ShouldContain(x => x.Key == _routeB.Routes[0].Key); + fc.Routes.ShouldContain(x => x.Key == _routeB.Routes[1].Key); + + fc.Routes.ShouldContain(x => x.UpstreamHost == _routeA.Routes[0].UpstreamHost); + fc.Routes.ShouldContain(x => x.UpstreamHost == _routeB.Routes[0].UpstreamHost); + fc.Routes.ShouldContain(x => x.UpstreamHost == _routeB.Routes[1].UpstreamHost); + + fc.Aggregates.Count.ShouldBe(_aggregate.Aggregates.Count); + } + + private void NotContainsEnvSpecificConfig() + { + var fc = (FileConfiguration)_configRoot.Get(typeof(FileConfiguration)); + fc.Routes.ShouldNotContain(x => x.DownstreamScheme == _envSpecific.Routes[0].DownstreamScheme); + fc.Routes.ShouldNotContain(x => x.DownstreamPathTemplate == _envSpecific.Routes[0].DownstreamPathTemplate); + fc.Routes.ShouldNotContain(x => x.Key == _envSpecific.Routes[0].Key); + } + + private void GivenTheBaseUrl(string baseUrl) + { +#pragma warning disable CS0618 + var builder = new ConfigurationBuilder() + .AddOcelotBaseUrl(baseUrl); +#pragma warning restore CS0618 + _configuration = builder.Build(); + } + + private void WhenIGet(string key) + { + _result = _configuration.GetValue(key, string.Empty); + } + + private void ThenTheResultIs(string expected) + { + _result.ShouldBe(expected); + } + } +} diff --git a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs index 0f1ddb605..087fc6dfd 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs @@ -1,44 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +using Moq; + +using Ocelot.Multiplexer; + +using Ocelot.Configuration.Setter; +using Ocelot.DependencyInjection; +using Ocelot.Infrastructure; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Requester; + +using Ocelot.UnitTests.Requester; + +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Ocelot.Values; + +using Xunit; + +using static Ocelot.UnitTests.Multiplexing.UserDefinedResponseAggregatorTests; + namespace Ocelot.UnitTests.DependencyInjection { - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Moq; - using Ocelot.Configuration.Setter; - using Ocelot.DependencyInjection; - using Ocelot.Infrastructure; - using Ocelot.Middleware.Multiplexer; - using Ocelot.Requester; - using Ocelot.UnitTests.Requester; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net.Http; - using System.Reflection; - using TestStack.BDDfy; - using Xunit; - using static Ocelot.UnitTests.Middleware.UserDefinedResponseAggregatorTests; - public class OcelotBuilderTests { private readonly IServiceCollection _services; private IServiceProvider _serviceProvider; private readonly IConfiguration _configRoot; private IOcelotBuilder _ocelotBuilder; - private readonly int _maxRetries; private Exception _ex; public OcelotBuilderTests() { _configRoot = new ConfigurationRoot(new List()); _services = new ServiceCollection(); - _services.AddSingleton(GetHostingEnvironment()); + _services.AddSingleton(GetHostingEnvironment()); _services.AddSingleton(_configRoot); - _maxRetries = 100; } - private IWebHostEnvironment GetHostingEnvironment() + private static IWebHostEnvironment GetHostingEnvironment() { var environment = new Mock(); environment @@ -148,6 +162,42 @@ public void should_add_transient_defined_aggregators() .BDDfy(); } + [Fact] + public void should_add_custom_load_balancer_creators_by_default_ctor() + { + this.Given(x => WhenISetUpOcelotServices()) + .When(x => _ocelotBuilder.AddCustomLoadBalancer()) + .Then(x => ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators()) + .BDDfy(); + } + + [Fact] + public void should_add_custom_load_balancer_creators_by_factory_method() + { + this.Given(x => WhenISetUpOcelotServices()) + .When(x => _ocelotBuilder.AddCustomLoadBalancer(() => new FakeCustomLoadBalancer())) + .Then(x => ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators()) + .BDDfy(); + } + + [Fact] + public void should_add_custom_load_balancer_creators_by_di_factory_method() + { + this.Given(x => WhenISetUpOcelotServices()) + .When(x => _ocelotBuilder.AddCustomLoadBalancer(provider => new FakeCustomLoadBalancer())) + .Then(x => ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators()) + .BDDfy(); + } + + [Fact] + public void should_add_custom_load_balancer_creators_by_factory_method_with_arguments() + { + this.Given(x => WhenISetUpOcelotServices()) + .When(x => _ocelotBuilder.AddCustomLoadBalancer((route, discoveryProvider) => new FakeCustomLoadBalancer())) + .Then(x => ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators()) + .BDDfy(); + } + [Fact] public void should_replace_iplaceholder() { @@ -158,6 +208,15 @@ public void should_replace_iplaceholder() .BDDfy(); } + [Fact] + public void should_add_custom_load_balancer_creators() + { + this.Given(x => WhenISetUpOcelotServices()) + .When(x => _ocelotBuilder.AddCustomLoadBalancer((provider, route, discoveryProvider) => new FakeCustomLoadBalancer())) + .Then(x => ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators()) + .BDDfy(); + } + private void AddSingletonDefinedAggregator() where T : class, IDefinedAggregator { @@ -239,6 +298,17 @@ private void ThenTheProviderIsRegisteredAndReturnsSpecificAggregators handlers[1].ShouldBeOfType(); } + private void ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators() + { + _serviceProvider = _services.BuildServiceProvider(); + var creators = _serviceProvider.GetServices().ToList(); + creators.Count(c => c.GetType() == typeof(NoLoadBalancerCreator)).ShouldBe(1); + creators.Count(c => c.GetType() == typeof(RoundRobinCreator)).ShouldBe(1); + creators.Count(c => c.GetType() == typeof(CookieStickySessionsCreator)).ShouldBe(1); + creators.Count(c => c.GetType() == typeof(LeastConnectionCreator)).ShouldBe(1); + creators.Count(c => c.GetType() == typeof(DelegateInvokingLoadBalancerCreator)).ShouldBe(1); + } + private void ThenTheAggregatorsAreTransient() { var aggregators = _serviceProvider.GetServices().ToList(); @@ -323,5 +393,20 @@ private void ThenAnExceptionIsntThrown() { _ex.ShouldBeNull(); } + + private class FakeCustomLoadBalancer : ILoadBalancer + { + public Task> Lease(HttpContext httpContext) + { + // Not relevant for these tests + throw new NotImplementedException(); + } + + public void Release(ServiceHostAndPort hostAndPort) + { + // Not relevant for these tests + throw new NotImplementedException(); + } + } } } diff --git a/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs b/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs index 6e6f6c12d..0f80a0534 100644 --- a/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs +++ b/test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs @@ -1,4 +1,9 @@ -using Moq; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; + +using Moq; + using Ocelot.Configuration; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Errors; @@ -8,11 +13,11 @@ using Ocelot.Responses; using Ocelot.UnitTests.Responder; using Ocelot.Values; + using Shouldly; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; + using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.DownstreamPathManipulation @@ -39,13 +44,13 @@ public void should_change_downstream_path_request() { var claims = new List { - new Claim("test", "data"), + new("test", "data"), }; var placeHolderValues = new List(); this.Given( x => x.GivenAClaimToThing(new List { - new ClaimToThing("path-key", "", "", 0), + new("path-key", string.Empty, string.Empty, 0), })) .And(x => x.GivenClaims(claims)) .And(x => x.GivenDownstreamPathTemplate("/api/test/{path-key}")) @@ -62,16 +67,16 @@ public void should_replace_existing_placeholder_value() { var claims = new List { - new Claim("test", "data"), + new("test", "data"), }; var placeHolderValues = new List { - new PlaceholderNameAndValue ("{path-key}", "old_value"), + new("{path-key}", "old_value"), }; this.Given( x => x.GivenAClaimToThing(new List { - new ClaimToThing("path-key", "", "", 0), + new("path-key", string.Empty, string.Empty, 0), })) .And(x => x.GivenClaims(claims)) .And(x => x.GivenDownstreamPathTemplate("/api/test/{path-key}")) @@ -88,13 +93,13 @@ public void should_return_error_when_no_placeholder_in_downstream_path() { var claims = new List { - new Claim("test", "data"), + new("test", "data"), }; var placeHolderValues = new List(); this.Given( x => x.GivenAClaimToThing(new List { - new ClaimToThing("path-key", "", "", 0), + new("path-key", string.Empty, string.Empty, 0), })) .And(x => x.GivenClaims(claims)) .And(x => x.GivenDownstreamPathTemplate("/api/test")) @@ -110,13 +115,13 @@ private void should_return_error_when_claim_parser_returns_error() { var claims = new List { - new Claim("test", "data"), + new("test", "data"), }; var placeHolderValues = new List(); this.Given( x => x.GivenAClaimToThing(new List { - new ClaimToThing("path-key", "", "", 0), + new("path-key", string.Empty, string.Empty, 0), })) .And(x => x.GivenClaims(claims)) .And(x => x.GivenDownstreamPathTemplate("/api/test/{path-key}")) diff --git a/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs index 800033004..ca9f567bd 100644 --- a/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs @@ -1,21 +1,27 @@ using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using System.Net.Http; +using System.Security.Claims; +using System.Threading.Tasks; + using Moq; + using Ocelot.Configuration; using Ocelot.Configuration.Builder; -using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamPathManipulation.Middleware; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Logging; using Ocelot.Middleware; -using Ocelot.PathManipulation; -using Ocelot.PathManipulation.Middleware; using Ocelot.Request.Middleware; + +using Ocelot.PathManipulation; + using Ocelot.Responses; -using Ocelot.Values; -using System.Collections.Generic; -using System.Net.Http; -using System.Security.Claims; -using System.Threading.Tasks; + using TestStack.BDDfy; + +using Ocelot.Values; + using Xunit; namespace Ocelot.UnitTests.DownstreamPathManipulation @@ -23,34 +29,34 @@ namespace Ocelot.UnitTests.DownstreamPathManipulation public class ClaimsToDownstreamPathMiddlewareTests { private readonly Mock _changePath; - private Mock _loggerFactory; - private Mock _logger; - private ClaimsToDownstreamPathMiddleware _middleware; - private DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private readonly ClaimsToDownstreamPathMiddleware _middleware; + private readonly RequestDelegate _next; + private readonly HttpContext _httpContext; public ClaimsToDownstreamPathMiddlewareTests() { - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); + _httpContext = new DefaultHttpContext(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _changePath = new Mock(); - _downstreamContext.DownstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); + _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com"))); _middleware = new ClaimsToDownstreamPathMiddleware(_next, _loggerFactory.Object, _changePath.Object); } [Fact] public void should_call_add_queries_correctly() { - var downstreamRoute = new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() + var downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithClaimsToDownstreamPath(new List { - new ClaimToThing("UserId", "Subject", "", 0), + new("UserId", "Subject", string.Empty, 0), }) .WithUpstreamHttpMethod(new List { "Get" }) .Build()) @@ -62,12 +68,11 @@ public void should_call_add_queries_correctly() .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenChangeDownstreamPathIsCalledCorrectly()) .BDDfy(); - } private void WhenICallTheMiddleware() { - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); } private void GivenTheChangeDownstreamPathReturnsOk() @@ -87,15 +92,15 @@ private void ThenChangeDownstreamPathIsCalledCorrectly() .Verify(x => x.ChangeDownstreamPath( It.IsAny>(), It.IsAny>(), - _downstreamContext.DownstreamReRoute.DownstreamPathTemplate, - _downstreamContext.TemplatePlaceholderNameAndValues), Times.Once); + _httpContext.Items.DownstreamRoute().DownstreamPathTemplate, + _httpContext.Items.TemplatePlaceholderNameAndValues()), Times.Once); } - private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + private void GivenTheDownStreamRouteIs(Ocelot.DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute) { - _downstreamContext.TemplatePlaceholderNameAndValues = downstreamRoute.TemplatePlaceholderNameAndValues; - _downstreamContext.DownstreamReRoute = downstreamRoute.ReRoute.DownstreamReRoute[0]; - } + _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); + } } } diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs index a3348627b..ecfa27190 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteCreatorTests.cs @@ -1,305 +1,306 @@ -namespace Ocelot.UnitTests.DownstreamRouteFinder -{ - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Configuration.Creator; - using Ocelot.DownstreamRouteFinder; - using Ocelot.DownstreamRouteFinder.Finder; - using Ocelot.LoadBalancer.LoadBalancers; - using Responses; - using Shouldly; - using System.Collections.Generic; - using System.Net.Http; - using TestStack.BDDfy; - using Xunit; - - public class DownstreamRouteCreatorTests - { - private readonly DownstreamRouteCreator _creator; - private readonly QoSOptions _qoSOptions; - private readonly HttpHandlerOptions _handlerOptions; - private readonly LoadBalancerOptions _loadBalancerOptions; - private Response _result; - private string _upstreamHost; - private string _upstreamUrlPath; - private string _upstreamHttpMethod; - private IInternalConfiguration _configuration; - private Mock _qosOptionsCreator; - private Response _resultTwo; - private string _upstreamQuery; - - public DownstreamRouteCreatorTests() - { - _qosOptionsCreator = new Mock(); - _qoSOptions = new QoSOptionsBuilder().Build(); - _handlerOptions = new HttpHandlerOptionsBuilder().Build(); - _loadBalancerOptions = new LoadBalancerOptionsBuilder().WithType(nameof(NoLoadBalancer)).Build(); - _qosOptionsCreator - .Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny>())) - .Returns(_qoSOptions); +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; +using Ocelot.DownstreamRouteFinder.Finder; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Responses; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Net.Http; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.DownstreamRouteFinder +{ + public class DownstreamRouteCreatorTests + { + private readonly DownstreamRouteCreator _creator; + private readonly QoSOptions _qoSOptions; + private readonly HttpHandlerOptions _handlerOptions; + private readonly LoadBalancerOptions _loadBalancerOptions; + private Response _result; + private string _upstreamHost; + private string _upstreamUrlPath; + private string _upstreamHttpMethod; + private IInternalConfiguration _configuration; + private readonly Mock _qosOptionsCreator; + private Response _resultTwo; + private readonly string _upstreamQuery; + + public DownstreamRouteCreatorTests() + { + _qosOptionsCreator = new Mock(); + _qoSOptions = new QoSOptionsBuilder().Build(); + _handlerOptions = new HttpHandlerOptionsBuilder().Build(); + _loadBalancerOptions = new LoadBalancerOptionsBuilder().WithType(nameof(NoLoadBalancer)).Build(); + _qosOptionsCreator + .Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(_qoSOptions); _creator = new DownstreamRouteCreator(_qosOptionsCreator.Object); - } - - [Fact] - public void should_create_downstream_route() - { - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions); - - this.Given(_ => GivenTheConfiguration(configuration)) - .When(_ => WhenICreate()) - .Then(_ => ThenTheDownstreamRouteIsCreated()) - .BDDfy(); - } - - [Fact] - public void should_create_downstream_route_with_rate_limit_options() - { - var rateLimitOptions = new RateLimitOptionsBuilder() - .WithEnableRateLimiting(true) - .WithClientIdHeader("test") - .Build(); - - var downstreamReRoute = new DownstreamReRouteBuilder() - .WithServiceName("auth") - .WithRateLimitOptions(rateLimitOptions) - .Build(); - - var reRoute = new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) - .Build(); - - var reRoutes = new List { reRoute }; - - var configuration = new InternalConfiguration(reRoutes, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions); - - this.Given(_ => GivenTheConfiguration(configuration)) - .When(_ => WhenICreate()) - .Then(_ => ThenTheDownstreamRouteIsCreated()) - .And(_ => WithRateLimitOptions(rateLimitOptions)) - .BDDfy(); - } - - [Fact] - public void should_cache_downstream_route() - { - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions); - - this.Given(_ => GivenTheConfiguration(configuration, "/geoffisthebest/")) - .When(_ => WhenICreate()) - .And(_ => GivenTheConfiguration(configuration, "/geoffisthebest/")) - .When(_ => WhenICreateAgain()) - .Then(_ => ThenTheDownstreamRoutesAreTheSameReference()) - .BDDfy(); - } - - [Fact] - public void should_not_cache_downstream_route() - { - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions); - - this.Given(_ => GivenTheConfiguration(configuration, "/geoffistheworst/")) - .When(_ => WhenICreate()) - .And(_ => GivenTheConfiguration(configuration, "/geoffisthebest/")) - .When(_ => WhenICreateAgain()) - .Then(_ => ThenTheDownstreamRoutesAreTheNotSameReference()) - .BDDfy(); - } - - [Fact] - public void should_create_downstream_route_with_no_path() - { - var upstreamUrlPath = "/auth/"; - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions); - - this.Given(_ => GivenTheConfiguration(configuration, upstreamUrlPath)) - .When(_ => WhenICreate()) - .Then(_ => ThenTheDownstreamPathIsForwardSlash()) - .BDDfy(); - } - - [Fact] - public void should_create_downstream_route_with_only_first_segment_no_traling_slash() - { - var upstreamUrlPath = "/auth"; - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions); - - this.Given(_ => GivenTheConfiguration(configuration, upstreamUrlPath)) - .When(_ => WhenICreate()) - .Then(_ => ThenTheDownstreamPathIsForwardSlash()) - .BDDfy(); - } - - [Fact] - public void should_create_downstream_route_with_segments_no_traling_slash() - { - var upstreamUrlPath = "/auth/test"; - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions); - - this.Given(_ => GivenTheConfiguration(configuration, upstreamUrlPath)) - .When(_ => WhenICreate()) - .Then(_ => ThenThePathDoesNotHaveTrailingSlash()) - .BDDfy(); - } - - [Fact] - public void should_create_downstream_route_and_remove_query_string() - { - var upstreamUrlPath = "/auth/test?test=1&best=2"; - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions); - - this.Given(_ => GivenTheConfiguration(configuration, upstreamUrlPath)) - .When(_ => WhenICreate()) - .Then(_ => ThenTheQueryStringIsRemoved()) - .BDDfy(); - } - - [Fact] - public void should_create_downstream_route_for_sticky_sessions() - { - var loadBalancerOptions = new LoadBalancerOptionsBuilder().WithType(nameof(CookieStickySessions)).WithKey("boom").WithExpiryInMs(1).Build(); - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", loadBalancerOptions, "http", _qoSOptions, _handlerOptions); - - this.Given(_ => GivenTheConfiguration(configuration)) - .When(_ => WhenICreate()) - .Then(_ => ThenTheStickySessionLoadBalancerIsUsed(loadBalancerOptions)) - .BDDfy(); - } - - [Fact] - public void should_create_downstream_route_with_qos() - { - var qoSOptions = new QoSOptionsBuilder() - .WithExceptionsAllowedBeforeBreaking(1) - .WithTimeoutValue(1) - .Build(); - - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", qoSOptions, _handlerOptions); - - this.Given(_ => GivenTheConfiguration(configuration)) - .And(_ => GivenTheQosCreatorReturns(qoSOptions)) - .When(_ => WhenICreate()) - .Then(_ => ThenTheQosOptionsAreSet(qoSOptions)) - .BDDfy(); - } - - [Fact] - public void should_create_downstream_route_with_handler_options() - { - var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions); - - this.Given(_ => GivenTheConfiguration(configuration)) - .When(_ => WhenICreate()) - .Then(_ => ThenTheHandlerOptionsAreSet()) - .BDDfy(); - } - - private void GivenTheQosCreatorReturns(QoSOptions options) - { - _qosOptionsCreator - .Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny>())) - .Returns(options); - } - - private void WithRateLimitOptions(RateLimitOptions expected) - { - _result.Data.ReRoute.DownstreamReRoute[0].EnableEndpointEndpointRateLimiting.ShouldBeTrue(); - _result.Data.ReRoute.DownstreamReRoute[0].RateLimitOptions.EnableRateLimiting.ShouldBe(expected.EnableRateLimiting); - _result.Data.ReRoute.DownstreamReRoute[0].RateLimitOptions.ClientIdHeader.ShouldBe(expected.ClientIdHeader); - } - - private void ThenTheDownstreamRouteIsCreated() - { - _result.Data.ReRoute.DownstreamReRoute[0].DownstreamPathTemplate.Value.ShouldBe("/test"); - _result.Data.ReRoute.UpstreamHttpMethod[0].ShouldBe(HttpMethod.Get); - _result.Data.ReRoute.DownstreamReRoute[0].ServiceName.ShouldBe("auth"); - _result.Data.ReRoute.DownstreamReRoute[0].LoadBalancerKey.ShouldBe("/auth/test|GET"); - _result.Data.ReRoute.DownstreamReRoute[0].UseServiceDiscovery.ShouldBeTrue(); - _result.Data.ReRoute.DownstreamReRoute[0].HttpHandlerOptions.ShouldNotBeNull(); - _result.Data.ReRoute.DownstreamReRoute[0].QosOptions.ShouldNotBeNull(); - _result.Data.ReRoute.DownstreamReRoute[0].DownstreamScheme.ShouldBe("http"); - _result.Data.ReRoute.DownstreamReRoute[0].LoadBalancerOptions.Type.ShouldBe(nameof(NoLoadBalancer)); - _result.Data.ReRoute.DownstreamReRoute[0].HttpHandlerOptions.ShouldBe(_handlerOptions); - _result.Data.ReRoute.DownstreamReRoute[0].QosOptions.ShouldBe(_qoSOptions); - _result.Data.ReRoute.UpstreamTemplatePattern.ShouldNotBeNull(); - _result.Data.ReRoute.DownstreamReRoute[0].UpstreamPathTemplate.ShouldNotBeNull(); - } - - private void ThenTheDownstreamPathIsForwardSlash() - { - _result.Data.ReRoute.DownstreamReRoute[0].DownstreamPathTemplate.Value.ShouldBe("/"); - _result.Data.ReRoute.DownstreamReRoute[0].ServiceName.ShouldBe("auth"); - _result.Data.ReRoute.DownstreamReRoute[0].LoadBalancerKey.ShouldBe("/auth/|GET"); - } - - private void ThenThePathDoesNotHaveTrailingSlash() - { - _result.Data.ReRoute.DownstreamReRoute[0].DownstreamPathTemplate.Value.ShouldBe("/test"); - _result.Data.ReRoute.DownstreamReRoute[0].ServiceName.ShouldBe("auth"); - _result.Data.ReRoute.DownstreamReRoute[0].LoadBalancerKey.ShouldBe("/auth/test|GET"); - } - - private void ThenTheQueryStringIsRemoved() - { - _result.Data.ReRoute.DownstreamReRoute[0].DownstreamPathTemplate.Value.ShouldBe("/test"); - _result.Data.ReRoute.DownstreamReRoute[0].ServiceName.ShouldBe("auth"); - _result.Data.ReRoute.DownstreamReRoute[0].LoadBalancerKey.ShouldBe("/auth/test|GET"); - } - - private void ThenTheStickySessionLoadBalancerIsUsed(LoadBalancerOptions expected) - { - _result.Data.ReRoute.DownstreamReRoute[0].LoadBalancerKey.ShouldBe($"{nameof(CookieStickySessions)}:boom"); - _result.Data.ReRoute.DownstreamReRoute[0].LoadBalancerOptions.Type.ShouldBe(nameof(CookieStickySessions)); - _result.Data.ReRoute.DownstreamReRoute[0].LoadBalancerOptions.ShouldBe(expected); - } - - private void ThenTheQosOptionsAreSet(QoSOptions expected) - { - _result.Data.ReRoute.DownstreamReRoute[0].QosOptions.ShouldBe(expected); - _result.Data.ReRoute.DownstreamReRoute[0].QosOptions.UseQos.ShouldBeTrue(); - _qosOptionsCreator - .Verify(x => x.Create(expected, _upstreamUrlPath, It.IsAny>()), Times.Once); - } - - private void GivenTheConfiguration(IInternalConfiguration config) - { - _upstreamHost = "doesnt matter"; - _upstreamUrlPath = "/auth/test"; - _upstreamHttpMethod = "GET"; - _configuration = config; - } - - private void GivenTheConfiguration(IInternalConfiguration config, string upstreamUrlPath) - { - _upstreamHost = "doesnt matter"; - _upstreamUrlPath = upstreamUrlPath; - _upstreamHttpMethod = "GET"; - _configuration = config; - } - - private void ThenTheHandlerOptionsAreSet() - { - _result.Data.ReRoute.DownstreamReRoute[0].HttpHandlerOptions.ShouldBe(_handlerOptions); - } - - private void WhenICreate() - { - _result = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost); - } - - private void WhenICreateAgain() - { - _resultTwo = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost); - } - - private void ThenTheDownstreamRoutesAreTheSameReference() - { - _result.ShouldBe(_resultTwo); - } - - private void ThenTheDownstreamRoutesAreTheNotSameReference() - { - _result.ShouldNotBe(_resultTwo); - } - } -} + _upstreamQuery = string.Empty; + } + + [Fact] + public void should_create_downstream_route() + { + var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + + this.Given(_ => GivenTheConfiguration(configuration)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheDownstreamRouteIsCreated()) + .BDDfy(); + } + + [Fact] + public void should_create_downstream_route_with_rate_limit_options() + { + var rateLimitOptions = new RateLimitOptionsBuilder() + .WithEnableRateLimiting(true) + .WithClientIdHeader("test") + .Build(); + + var downstreamRoute = new DownstreamRouteBuilder() + .WithServiceName("auth") + .WithRateLimitOptions(rateLimitOptions) + .Build(); + + var route = new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) + .Build(); + + var routes = new List { route }; + + var configuration = new InternalConfiguration(routes, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + + this.Given(_ => GivenTheConfiguration(configuration)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheDownstreamRouteIsCreated()) + .And(_ => WithRateLimitOptions(rateLimitOptions)) + .BDDfy(); + } + + [Fact] + public void should_cache_downstream_route() + { + var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + + this.Given(_ => GivenTheConfiguration(configuration, "/geoffisthebest/")) + .When(_ => WhenICreate()) + .And(_ => GivenTheConfiguration(configuration, "/geoffisthebest/")) + .When(_ => WhenICreateAgain()) + .Then(_ => ThenTheDownstreamRoutesAreTheSameReference()) + .BDDfy(); + } + + [Fact] + public void should_not_cache_downstream_route() + { + var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + + this.Given(_ => GivenTheConfiguration(configuration, "/geoffistheworst/")) + .When(_ => WhenICreate()) + .And(_ => GivenTheConfiguration(configuration, "/geoffisthebest/")) + .When(_ => WhenICreateAgain()) + .Then(_ => ThenTheDownstreamRoutesAreTheNotSameReference()) + .BDDfy(); + } + + [Fact] + public void should_create_downstream_route_with_no_path() + { + var upstreamUrlPath = "/auth/"; + var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + + this.Given(_ => GivenTheConfiguration(configuration, upstreamUrlPath)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheDownstreamPathIsForwardSlash()) + .BDDfy(); + } + + [Fact] + public void should_create_downstream_route_with_only_first_segment_no_traling_slash() + { + var upstreamUrlPath = "/auth"; + var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + + this.Given(_ => GivenTheConfiguration(configuration, upstreamUrlPath)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheDownstreamPathIsForwardSlash()) + .BDDfy(); + } + + [Fact] + public void should_create_downstream_route_with_segments_no_traling_slash() + { + var upstreamUrlPath = "/auth/test"; + var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + + this.Given(_ => GivenTheConfiguration(configuration, upstreamUrlPath)) + .When(_ => WhenICreate()) + .Then(_ => ThenThePathDoesNotHaveTrailingSlash()) + .BDDfy(); + } + + [Fact] + public void should_create_downstream_route_and_remove_query_string() + { + var upstreamUrlPath = "/auth/test?test=1&best=2"; + var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + + this.Given(_ => GivenTheConfiguration(configuration, upstreamUrlPath)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheQueryStringIsRemoved()) + .BDDfy(); + } + + [Fact] + public void should_create_downstream_route_for_sticky_sessions() + { + var loadBalancerOptions = new LoadBalancerOptionsBuilder().WithType(nameof(CookieStickySessions)).WithKey("boom").WithExpiryInMs(1).Build(); + var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + + this.Given(_ => GivenTheConfiguration(configuration)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheStickySessionLoadBalancerIsUsed(loadBalancerOptions)) + .BDDfy(); + } + + [Fact] + public void should_create_downstream_route_with_qos() + { + var qoSOptions = new QoSOptionsBuilder() + .WithExceptionsAllowedBeforeBreaking(1) + .WithTimeoutValue(1) + .Build(); + + var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", qoSOptions, _handlerOptions, new Version("1.1")); + + this.Given(_ => GivenTheConfiguration(configuration)) + .And(_ => GivenTheQosCreatorReturns(qoSOptions)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheQosOptionsAreSet(qoSOptions)) + .BDDfy(); + } + + [Fact] + public void should_create_downstream_route_with_handler_options() + { + var configuration = new InternalConfiguration(null, "doesnt matter", null, "doesnt matter", _loadBalancerOptions, "http", _qoSOptions, _handlerOptions, new Version("1.1")); + + this.Given(_ => GivenTheConfiguration(configuration)) + .When(_ => WhenICreate()) + .Then(_ => ThenTheHandlerOptionsAreSet()) + .BDDfy(); + } + + private void GivenTheQosCreatorReturns(QoSOptions options) + { + _qosOptionsCreator + .Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(options); + } + + private void WithRateLimitOptions(RateLimitOptions expected) + { + _result.Data.Route.DownstreamRoute[0].EnableEndpointEndpointRateLimiting.ShouldBeTrue(); + _result.Data.Route.DownstreamRoute[0].RateLimitOptions.EnableRateLimiting.ShouldBe(expected.EnableRateLimiting); + _result.Data.Route.DownstreamRoute[0].RateLimitOptions.ClientIdHeader.ShouldBe(expected.ClientIdHeader); + } + + private void ThenTheDownstreamRouteIsCreated() + { + _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe("/test"); + _result.Data.Route.UpstreamHttpMethod[0].ShouldBe(HttpMethod.Get); + _result.Data.Route.DownstreamRoute[0].ServiceName.ShouldBe("auth"); + _result.Data.Route.DownstreamRoute[0].LoadBalancerKey.ShouldBe("/auth/test|GET"); + _result.Data.Route.DownstreamRoute[0].UseServiceDiscovery.ShouldBeTrue(); + _result.Data.Route.DownstreamRoute[0].HttpHandlerOptions.ShouldNotBeNull(); + _result.Data.Route.DownstreamRoute[0].QosOptions.ShouldNotBeNull(); + _result.Data.Route.DownstreamRoute[0].DownstreamScheme.ShouldBe("http"); + _result.Data.Route.DownstreamRoute[0].LoadBalancerOptions.Type.ShouldBe(nameof(NoLoadBalancer)); + _result.Data.Route.DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_handlerOptions); + _result.Data.Route.DownstreamRoute[0].QosOptions.ShouldBe(_qoSOptions); + _result.Data.Route.UpstreamTemplatePattern.ShouldNotBeNull(); + _result.Data.Route.DownstreamRoute[0].UpstreamPathTemplate.ShouldNotBeNull(); + } + + private void ThenTheDownstreamPathIsForwardSlash() + { + _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe("/"); + _result.Data.Route.DownstreamRoute[0].ServiceName.ShouldBe("auth"); + _result.Data.Route.DownstreamRoute[0].LoadBalancerKey.ShouldBe("/auth/|GET"); + } + + private void ThenThePathDoesNotHaveTrailingSlash() + { + _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe("/test"); + _result.Data.Route.DownstreamRoute[0].ServiceName.ShouldBe("auth"); + _result.Data.Route.DownstreamRoute[0].LoadBalancerKey.ShouldBe("/auth/test|GET"); + } + + private void ThenTheQueryStringIsRemoved() + { + _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe("/test"); + _result.Data.Route.DownstreamRoute[0].ServiceName.ShouldBe("auth"); + _result.Data.Route.DownstreamRoute[0].LoadBalancerKey.ShouldBe("/auth/test|GET"); + } + + private void ThenTheStickySessionLoadBalancerIsUsed(LoadBalancerOptions expected) + { + _result.Data.Route.DownstreamRoute[0].LoadBalancerKey.ShouldBe($"{nameof(CookieStickySessions)}:boom"); + _result.Data.Route.DownstreamRoute[0].LoadBalancerOptions.Type.ShouldBe(nameof(CookieStickySessions)); + _result.Data.Route.DownstreamRoute[0].LoadBalancerOptions.ShouldBe(expected); + } + + private void ThenTheQosOptionsAreSet(QoSOptions expected) + { + _result.Data.Route.DownstreamRoute[0].QosOptions.ShouldBe(expected); + _result.Data.Route.DownstreamRoute[0].QosOptions.UseQos.ShouldBeTrue(); + _qosOptionsCreator + .Verify(x => x.Create(expected, _upstreamUrlPath, It.IsAny>()), Times.Once); + } + + private void GivenTheConfiguration(IInternalConfiguration config) + { + _upstreamHost = "doesnt matter"; + _upstreamUrlPath = "/auth/test"; + _upstreamHttpMethod = "GET"; + _configuration = config; + } + + private void GivenTheConfiguration(IInternalConfiguration config, string upstreamUrlPath) + { + _upstreamHost = "doesnt matter"; + _upstreamUrlPath = upstreamUrlPath; + _upstreamHttpMethod = "GET"; + _configuration = config; + } + + private void ThenTheHandlerOptionsAreSet() + { + _result.Data.Route.DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_handlerOptions); + } + + private void WhenICreate() + { + _result = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost); + } + + private void WhenICreateAgain() + { + _resultTwo = _creator.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost); + } + + private void ThenTheDownstreamRoutesAreTheSameReference() + { + _result.ShouldBe(_resultTwo); + } + + private void ThenTheDownstreamRoutesAreTheNotSameReference() + { + _result.ShouldNotBe(_resultTwo); + } + } +} diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs index 3b3332588..8da8c8ca5 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs @@ -1,65 +1,70 @@ -namespace Ocelot.UnitTests.DownstreamRouteFinder -{ - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.DownstreamRouteFinder; - using Ocelot.DownstreamRouteFinder.Finder; - using Ocelot.DownstreamRouteFinder.Middleware; - using Ocelot.DownstreamRouteFinder.UrlMatcher; - using Ocelot.Logging; - using Ocelot.Middleware; - using Ocelot.Middleware.Multiplexer; - using Ocelot.Responses; - using Shouldly; - using System.Collections.Generic; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.Finder; +using Ocelot.DownstreamRouteFinder.Middleware; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Logging; +using Ocelot.Middleware; + +using Ocelot.Responses; +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.DownstreamRouteFinder +{ public class DownstreamRouteFinderMiddlewareTests { private readonly Mock _finder; private readonly Mock _factory; - private Response _downstreamRoute; + private Response _downstreamRoute; private IInternalConfiguration _config; - private Mock _loggerFactory; - private Mock _logger; + private readonly Mock _loggerFactory; + private readonly Mock _logger; private readonly DownstreamRouteFinderMiddleware _middleware; - private readonly DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; - private readonly Mock _multiplexer; + private readonly RequestDelegate _next; + private readonly HttpContext _httpContext; public DownstreamRouteFinderMiddlewareTests() { + _httpContext = new DefaultHttpContext(); _finder = new Mock(); _factory = new Mock(); _factory.Setup(x => x.Get(It.IsAny())).Returns(_finder.Object); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; - _multiplexer = new Mock(); - _middleware = new DownstreamRouteFinderMiddleware(_next, _loggerFactory.Object, _factory.Object, _multiplexer.Object); + _middleware = new DownstreamRouteFinderMiddleware(_next, _loggerFactory.Object, _factory.Object); } [Fact] public void should_call_scoped_data_repository_correctly() { - var config = new InternalConfiguration(null, null, new ServiceProviderConfigurationBuilder().Build(), "", new LoadBalancerOptionsBuilder().Build(), "", new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build()); + var config = new InternalConfiguration(null, null, new ServiceProviderConfigurationBuilder().Build(), string.Empty, new LoadBalancerOptionsBuilder().Build(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1")); - var downstreamReRoute = new DownstreamReRouteBuilder() + var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithUpstreamHttpMethod(new List { "Get" }) .Build(); this.Given(x => x.GivenTheDownStreamRouteFinderReturns( - new DownstreamRoute( + new DownstreamRouteHolder( new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) .Build()))) .And(x => GivenTheFollowingConfig(config)) @@ -70,18 +75,18 @@ public void should_call_scoped_data_repository_correctly() private void WhenICallTheMiddleware() { - _middleware.Invoke(_downstreamContext).GetAwaiter().GetType(); + _middleware.Invoke(_httpContext).GetAwaiter().GetType(); } private void GivenTheFollowingConfig(IInternalConfiguration config) { _config = config; - _downstreamContext.Configuration = config; + _httpContext.Items.SetIInternalConfiguration(config); } - private void GivenTheDownStreamRouteFinderReturns(DownstreamRoute downstreamRoute) + private void GivenTheDownStreamRouteFinderReturns(DownstreamRouteHolder downstreamRoute) { - _downstreamRoute = new OkResponse(downstreamRoute); + _downstreamRoute = new OkResponse(downstreamRoute); _finder .Setup(x => x.Get(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_downstreamRoute); @@ -89,8 +94,8 @@ private void GivenTheDownStreamRouteFinderReturns(DownstreamRoute downstreamRout private void ThenTheScopedDataRepositoryIsCalledCorrectly() { - _downstreamContext.TemplatePlaceholderNameAndValues.ShouldBe(_downstreamRoute.Data.TemplatePlaceholderNameAndValues); - _downstreamContext.Configuration.ServiceProviderConfiguration.ShouldBe(_config.ServiceProviderConfiguration); + _httpContext.Items.TemplatePlaceholderNameAndValues().ShouldBe(_downstreamRoute.Data.TemplatePlaceholderNameAndValues); + _httpContext.Items.IInternalConfiguration().ServiceProviderConfiguration.ShouldBe(_config.ServiceProviderConfiguration); } } } diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index 7419fe558..6d266ed97 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -1,769 +1,771 @@ using Moq; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.DownstreamRouteFinder; -using Ocelot.DownstreamRouteFinder.Finder; -using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.Responses; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.Finder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Responses; using Ocelot.Values; using Shouldly; +using System; using System.Collections.Generic; using TestStack.BDDfy; using Xunit; - -namespace Ocelot.UnitTests.DownstreamRouteFinder -{ - public class DownstreamRouteFinderTests - { - private readonly IDownstreamRouteProvider _downstreamRouteFinder; - private readonly Mock _mockMatcher; - private readonly Mock _finder; - private string _upstreamUrlPath; - private Response _result; - private List _reRoutesConfig; - private InternalConfiguration _config; - private Response _match; - private string _upstreamHttpMethod; - private string _upstreamHost; - private string _upstreamQuery; - - public DownstreamRouteFinderTests() - { - _mockMatcher = new Mock(); - _finder = new Mock(); - _downstreamRouteFinder = new Ocelot.DownstreamRouteFinder.Finder.DownstreamRouteFinder(_mockMatcher.Object, _finder.Object); - } - - [Fact] - public void should_return_highest_priority_when_first() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) - .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>(new List()))) - .And(x => x.GivenTheConfigurationIs(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) - .Build(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 0, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 0, false, "someUpstreamPath")) - .Build() - }, string.Empty, serviceProviderConfig)) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) - .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) - .When(x => x.WhenICallTheFinder()) - .Then(x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) - .WithUpstreamHttpMethod(new List { "Post" }) - .Build()) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) - .WithUpstreamHttpMethod(new List { "Post" }) - .Build() - ))) - .BDDfy(); - } - - [Fact] - public void should_return_highest_priority_when_lowest() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) - .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>(new List()))) - .And(x => x.GivenTheConfigurationIs(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 0, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 0, false, "someUpstreamPath")) - .Build(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) - .Build() - }, string.Empty, serviceProviderConfig)) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) - .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) - .When(x => x.WhenICallTheFinder()) - .Then(x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) - .WithUpstreamHttpMethod(new List { "Post" }) - .Build()) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) - .WithUpstreamHttpMethod(new List { "Post" }) - .Build() - ))) - .BDDfy(); - } - - [Fact] - public void should_return_route() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) - .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>( - new List()))) - .And(x => x.GivenTheConfigurationIs(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build() - }, string.Empty, serviceProviderConfig - )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) - .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) - .When(x => x.WhenICallTheFinder()) - .Then( - x => x.ThenTheFollowingIsReturned(new DownstreamRoute( - new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build() - ))) - .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_not_append_slash_to_upstream_url_path() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher")) - .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>( - new List()))) - .And(x => x.GivenTheConfigurationIs(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build() - }, string.Empty, serviceProviderConfig - )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) - .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) - .When(x => x.WhenICallTheFinder()) - .Then( - x => x.ThenTheFollowingIsReturned(new DownstreamRoute( - new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build() - ))) - .And(x => x.ThenTheUrlMatcherIsCalledCorrectly("matchInUrlMatcher")) - .BDDfy(); - } - - [Fact] - public void should_return_route_if_upstream_path_and_upstream_template_are_the_same() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) - .And( - x => - x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>(new List()))) - .And(x => x.GivenTheConfigurationIs(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build() - }, string.Empty, serviceProviderConfig - )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) - .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) - .When(x => x.WhenICallTheFinder()) - .Then( - x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build() - ))) - .BDDfy(); - } - - [Fact] - public void should_return_correct_route_for_http_verb() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) - .And( - x => - x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>(new List()))) - .And(x => x.GivenTheConfigurationIs(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 1, false, "someUpstreamPath")) - .Build(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPathForAPost") - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 1, false, "someUpstreamPath")) - .Build() - }, string.Empty, serviceProviderConfig - )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) - .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) - .When(x => x.WhenICallTheFinder()) - .Then( - x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPathForAPost") - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 1, false, "someUpstreamPath")) - .Build() - ))) - .BDDfy(); - } - - [Fact] - public void should_not_return_route() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("dontMatchPath/")) - .And(x => x.GivenTheConfigurationIs(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("somPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("somePath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("somePath", 1, false, "someUpstreamPath")) - .Build(), - }, string.Empty, serviceProviderConfig - )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(false)))) - .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) - .When(x => x.WhenICallTheFinder()) - .Then( - x => x.ThenAnErrorResponseIsReturned()) - .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_return_correct_route_for_http_verb_setting_multiple_upstream_http_method() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) - .And( - x => - x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>(new List()))) - .And(x => x.GivenTheConfigurationIs(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get", "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get", "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 1, false, "someUpstreamPath")) - .Build() - }, string.Empty, serviceProviderConfig - )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) - .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) - .When(x => x.WhenICallTheFinder()) - .Then( - x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 1, false, "someUpstreamPath")) - .Build() - ))) - .BDDfy(); - } - - [Fact] - public void should_return_correct_route_for_http_verb_setting_all_upstream_http_method() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) - .And( - x => - x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>(new List()))) - .And(x => x.GivenTheConfigurationIs(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List()) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List()) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 1, false, "someUpstreamPath")) - .Build() - }, string.Empty, serviceProviderConfig - )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) - .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) - .When(x => x.WhenICallTheFinder()) - .Then( - x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Post" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 1, false, "someUpstreamPath")) - .Build() - ))) - .BDDfy(); - } - - [Fact] - public void should_not_return_route_for_http_verb_not_setting_in_upstream_http_method() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) - .And( - x => - x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>(new List()))) - .And(x => x.GivenTheConfigurationIs(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get", "Patch", "Delete" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get", "Patch", "Delete" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 1, false, "someUpstreamPath")) - .Build() - }, string.Empty, serviceProviderConfig - )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) - .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) - .When(x => x.WhenICallTheFinder()) - .Then(x => x.ThenAnErrorResponseIsReturned()) - .And(x => x.ThenTheUrlMatcherIsNotCalled()) - .BDDfy(); - } - - [Fact] - public void should_return_route_when_host_matches() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) - .And(x => GivenTheUpstreamHostIs("MATCH")) - .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>( - new List()))) - .And(x => x.GivenTheConfigurationIs(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .WithUpstreamHost("MATCH") - .Build() - }, string.Empty, serviceProviderConfig - )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) - .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) - .When(x => x.WhenICallTheFinder()) - .Then( - x => x.ThenTheFollowingIsReturned(new DownstreamRoute( - new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build() - ))) - .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_return_route_when_upstreamhost_is_null() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) - .And(x => GivenTheUpstreamHostIs("MATCH")) - .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>( - new List()))) - .And(x => x.GivenTheConfigurationIs(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build() - }, string.Empty, serviceProviderConfig - )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) - .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) - .When(x => x.WhenICallTheFinder()) - .Then( - x => x.ThenTheFollowingIsReturned(new DownstreamRoute( - new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build() - ))) - .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_not_return_route_when_host_doesnt_match() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) - .And(x => GivenTheUpstreamHostIs("DONTMATCH")) - .And(x => x.GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List()))) - .And(x => x.GivenTheConfigurationIs(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .WithUpstreamHost("MATCH") - .Build(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { }) // empty list of methods - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { }) // empty list of methods - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .WithUpstreamHost("MATCH") - .Build() - }, string.Empty, serviceProviderConfig - )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) - .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) - .When(x => x.WhenICallTheFinder()) - .Then(x => x.ThenAnErrorResponseIsReturned()) - .And(x => x.ThenTheUrlMatcherIsNotCalled()) - .BDDfy(); - } - - [Fact] - public void should_not_return_route_when_host_doesnt_match_with_empty_upstream_http_method() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) - .And(x => GivenTheUpstreamHostIs("DONTMATCH")) - .And(x => x.GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List()))) - .And(x => x.GivenTheConfigurationIs(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List()) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List()) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .WithUpstreamHost("MATCH") - .Build() - }, string.Empty, serviceProviderConfig - )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) - .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) - .When(x => x.WhenICallTheFinder()) - .Then(x => x.ThenAnErrorResponseIsReturned()) - .And(x => x.ThenTheUrlMatcherIsNotCalled()) - .BDDfy(); - } - - [Fact] - public void should_return_route_when_host_does_match_with_empty_upstream_http_method() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) - .And(x => GivenTheUpstreamHostIs("MATCH")) - .And(x => x.GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List()))) - .And(x => x.GivenTheConfigurationIs(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List()) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List()) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .WithUpstreamHost("MATCH") - .Build() - }, string.Empty, serviceProviderConfig - )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) - .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) - .When(x => x.WhenICallTheFinder()) - .And(x => x.ThenTheUrlMatcherIsCalledCorrectly(1, 0)) - .BDDfy(); - } - - [Fact] - public void should_return_route_when_host_matches_but_null_host_on_same_path_first() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) - .And(x => GivenTheUpstreamHostIs("MATCH")) - .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( - new OkResponse>( - new List()))) - .And(x => x.GivenTheConfigurationIs(new List - { - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("THENULLPATH") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) - .WithUpstreamHost("MATCH") - .Build() - }, string.Empty, serviceProviderConfig - )) - .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) - .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) - .When(x => x.WhenICallTheFinder()) - .Then( - x => x.ThenTheFollowingIsReturned(new DownstreamRoute( - new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("someDownstreamPath") - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "test")) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "test")) - .Build() - ))) - .And(x => x.ThenTheUrlMatcherIsCalledCorrectly(1, 0)) - .And(x => x.ThenTheUrlMatcherIsCalledCorrectly(1, 1)) - .BDDfy(); - } - - private void GivenTheUpstreamHostIs(string upstreamHost) - { - _upstreamHost = upstreamHost; - } - - private void GivenTheTemplateVariableAndNameFinderReturns(Response> response) - { - _finder - .Setup(x => x.Find(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(response); - } - - private void GivenTheUpstreamHttpMethodIs(string upstreamHttpMethod) - { - _upstreamHttpMethod = upstreamHttpMethod; - } - - private void ThenAnErrorResponseIsReturned() - { - _result.IsError.ShouldBeTrue(); - } - - private void ThenTheUrlMatcherIsCalledCorrectly() - { - _mockMatcher - .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _reRoutesConfig[0].UpstreamTemplatePattern), Times.Once); - } - - private void ThenTheUrlMatcherIsCalledCorrectly(int times, int index = 0) - { - _mockMatcher - .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _reRoutesConfig[index].UpstreamTemplatePattern), Times.Exactly(times)); - } - - private void ThenTheUrlMatcherIsCalledCorrectly(string expectedUpstreamUrlPath) - { - _mockMatcher - .Verify(x => x.Match(expectedUpstreamUrlPath, _upstreamQuery, _reRoutesConfig[0].UpstreamTemplatePattern), Times.Once); - } - - private void ThenTheUrlMatcherIsNotCalled() - { - _mockMatcher - .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _reRoutesConfig[0].UpstreamTemplatePattern), Times.Never); - } - - private void GivenTheUrlMatcherReturns(Response match) - { - _match = match; - _mockMatcher - .Setup(x => x.Match(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(_match); - } - - private void GivenTheConfigurationIs(List reRoutesConfig, string adminPath, ServiceProviderConfiguration serviceProviderConfig) - { - _reRoutesConfig = reRoutesConfig; - _config = new InternalConfiguration(_reRoutesConfig, adminPath, serviceProviderConfig, "", new LoadBalancerOptionsBuilder().Build(), "", new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build()); - } - - private void GivenThereIsAnUpstreamUrlPath(string upstreamUrlPath) - { + +namespace Ocelot.UnitTests.DownstreamRouteFinder +{ + public class DownstreamRouteFinderTests + { + private readonly IDownstreamRouteProvider _downstreamRouteFinder; + private readonly Mock _mockMatcher; + private readonly Mock _finder; + private string _upstreamUrlPath; + private Response _result; + private List _routesConfig; + private InternalConfiguration _config; + private Response _match; + private string _upstreamHttpMethod; + private string _upstreamHost; + private string _upstreamQuery; + + public DownstreamRouteFinderTests() + { + _mockMatcher = new Mock(); + _finder = new Mock(); + _downstreamRouteFinder = new Ocelot.DownstreamRouteFinder.Finder.DownstreamRouteFinder(_mockMatcher.Object, _finder.Object); + } + + [Fact] + public void should_return_highest_priority_when_first() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>(new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) + .Build(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 0, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 0, false, "someUpstreamPath")) + .Build(), + }, string.Empty, serviceProviderConfig)) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) + .When(x => x.WhenICallTheFinder()) + .Then(x => x.ThenTheFollowingIsReturned(new DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) + .WithUpstreamHttpMethod(new List { "Post" }) + .Build()) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) + .WithUpstreamHttpMethod(new List { "Post" }) + .Build() + ))) + .BDDfy(); + } + + [Fact] + public void should_return_highest_priority_when_lowest() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>(new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 0, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 0, false, "someUpstreamPath")) + .Build(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) + .Build(), + }, string.Empty, serviceProviderConfig)) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) + .When(x => x.WhenICallTheFinder()) + .Then(x => x.ThenTheFollowingIsReturned(new DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) + .WithUpstreamHttpMethod(new List { "Post" }) + .Build()) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("test", 1, false, "someUpstreamPath")) + .WithUpstreamHttpMethod(new List { "Post" }) + .Build() + ))) + .BDDfy(); + } + + [Fact] + public void should_return_route() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>( + new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build(), + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenTheFollowingIsReturned(new DownstreamRouteHolder( + new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build() + ))) + .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_not_append_slash_to_upstream_url_path() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>( + new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build(), + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenTheFollowingIsReturned(new DownstreamRouteHolder( + new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build() + ))) + .And(x => x.ThenTheUrlMatcherIsCalledCorrectly("matchInUrlMatcher")) + .BDDfy(); + } + + [Fact] + public void should_return_route_if_upstream_path_and_upstream_template_are_the_same() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) + .And( + x => + x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>(new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build(), + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenTheFollowingIsReturned(new DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build() + ))) + .BDDfy(); + } + + [Fact] + public void should_return_correct_route_for_http_verb() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) + .And( + x => + x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>(new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) + .Build(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPathForAPost") + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) + .Build(), + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenTheFollowingIsReturned(new DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPathForAPost") + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) + .Build() + ))) + .BDDfy(); + } + + [Fact] + public void should_not_return_route() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("dontMatchPath/")) + .And(x => x.GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("somPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("somePath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("somePath", 1, false, "someUpstreamPath")) + .Build(), + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(false)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenAnErrorResponseIsReturned()) + .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_return_correct_route_for_http_verb_setting_multiple_upstream_http_method() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) + .And( + x => + x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>(new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get", "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get", "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) + .Build(), + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenTheFollowingIsReturned(new DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) + .Build() + ))) + .BDDfy(); + } + + [Fact] + public void should_return_correct_route_for_http_verb_setting_all_upstream_http_method() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) + .And( + x => + x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>(new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List()) + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List()) + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) + .Build(), + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenTheFollowingIsReturned(new DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Post" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) + .Build() + ))) + .BDDfy(); + } + + [Fact] + public void should_not_return_route_for_http_verb_not_setting_in_upstream_http_method() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) + .And( + x => + x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>(new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get", "Patch", "Delete" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get", "Patch", "Delete" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 1, false, "someUpstreamPath")) + .Build(), + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) + .When(x => x.WhenICallTheFinder()) + .Then(x => x.ThenAnErrorResponseIsReturned()) + .And(x => x.ThenTheUrlMatcherIsNotCalled()) + .BDDfy(); + } + + [Fact] + public void should_return_route_when_host_matches() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) + .And(x => GivenTheUpstreamHostIs("MATCH")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>( + new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .WithUpstreamHost("MATCH") + .Build(), + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenTheFollowingIsReturned(new DownstreamRouteHolder( + new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build() + ))) + .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_return_route_when_upstreamhost_is_null() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) + .And(x => GivenTheUpstreamHostIs("MATCH")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>( + new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build(), + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenTheFollowingIsReturned(new DownstreamRouteHolder( + new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build() + ))) + .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_not_return_route_when_host_doesnt_match() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) + .And(x => GivenTheUpstreamHostIs("DONTMATCH")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .WithUpstreamHost("MATCH") + .Build(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List()) // empty list of methods + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List()) // empty list of methods + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .WithUpstreamHost("MATCH") + .Build(), + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .When(x => x.WhenICallTheFinder()) + .Then(x => x.ThenAnErrorResponseIsReturned()) + .And(x => x.ThenTheUrlMatcherIsNotCalled()) + .BDDfy(); + } + + [Fact] + public void should_not_return_route_when_host_doesnt_match_with_empty_upstream_http_method() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) + .And(x => GivenTheUpstreamHostIs("DONTMATCH")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List()) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List()) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .WithUpstreamHost("MATCH") + .Build(), + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .When(x => x.WhenICallTheFinder()) + .Then(x => x.ThenAnErrorResponseIsReturned()) + .And(x => x.ThenTheUrlMatcherIsNotCalled()) + .BDDfy(); + } + + [Fact] + public void should_return_route_when_host_does_match_with_empty_upstream_http_method() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) + .And(x => GivenTheUpstreamHostIs("MATCH")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List()) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List()) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .WithUpstreamHost("MATCH") + .Build(), + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .When(x => x.WhenICallTheFinder()) + .And(x => x.ThenTheUrlMatcherIsCalledCorrectly(1, 0)) + .BDDfy(); + } + + [Fact] + public void should_return_route_when_host_matches_but_null_host_on_same_path_first() + { + var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher/")) + .And(x => GivenTheUpstreamHostIs("MATCH")) + .And(x => x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>( + new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("THENULLPATH") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "someUpstreamPath")) + .WithUpstreamHost("MATCH") + .Build(), + }, string.Empty, serviceProviderConfig + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenTheFollowingIsReturned(new DownstreamRouteHolder( + new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "test")) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(new UpstreamPathTemplate("someUpstreamPath", 1, false, "test")) + .Build() + ))) + .And(x => x.ThenTheUrlMatcherIsCalledCorrectly(1, 0)) + .And(x => x.ThenTheUrlMatcherIsCalledCorrectly(1, 1)) + .BDDfy(); + } + + private void GivenTheUpstreamHostIs(string upstreamHost) + { + _upstreamHost = upstreamHost; + } + + private void GivenTheTemplateVariableAndNameFinderReturns(Response> response) + { + _finder + .Setup(x => x.Find(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(response); + } + + private void GivenTheUpstreamHttpMethodIs(string upstreamHttpMethod) + { + _upstreamHttpMethod = upstreamHttpMethod; + } + + private void ThenAnErrorResponseIsReturned() + { + _result.IsError.ShouldBeTrue(); + } + + private void ThenTheUrlMatcherIsCalledCorrectly() + { + _mockMatcher + .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _routesConfig[0].UpstreamTemplatePattern), Times.Once); + } + + private void ThenTheUrlMatcherIsCalledCorrectly(int times, int index = 0) + { + _mockMatcher + .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _routesConfig[index].UpstreamTemplatePattern), Times.Exactly(times)); + } + + private void ThenTheUrlMatcherIsCalledCorrectly(string expectedUpstreamUrlPath) + { + _mockMatcher + .Verify(x => x.Match(expectedUpstreamUrlPath, _upstreamQuery, _routesConfig[0].UpstreamTemplatePattern), Times.Once); + } + + private void ThenTheUrlMatcherIsNotCalled() + { + _mockMatcher + .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _routesConfig[0].UpstreamTemplatePattern), Times.Never); + } + + private void GivenTheUrlMatcherReturns(Response match) + { + _match = match; + _mockMatcher + .Setup(x => x.Match(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_match); + } + + private void GivenTheConfigurationIs(List routesConfig, string adminPath, ServiceProviderConfiguration serviceProviderConfig) + { + _routesConfig = routesConfig; + _config = new InternalConfiguration(_routesConfig, adminPath, serviceProviderConfig, string.Empty, new LoadBalancerOptionsBuilder().Build(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1")); + } + + private void GivenThereIsAnUpstreamUrlPath(string upstreamUrlPath) + { _upstreamUrlPath = upstreamUrlPath; - } - - private void WhenICallTheFinder() - { - _result = _downstreamRouteFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost); - } - - private void ThenTheFollowingIsReturned(DownstreamRoute expected) - { - _result.Data.ReRoute.DownstreamReRoute[0].DownstreamPathTemplate.Value.ShouldBe(expected.ReRoute.DownstreamReRoute[0].DownstreamPathTemplate.Value); - _result.Data.ReRoute.UpstreamTemplatePattern.Priority.ShouldBe(expected.ReRoute.UpstreamTemplatePattern.Priority); - - for (int i = 0; i < _result.Data.TemplatePlaceholderNameAndValues.Count; i++) - { - _result.Data.TemplatePlaceholderNameAndValues[i].Name.ShouldBe(expected.TemplatePlaceholderNameAndValues[i].Name); - _result.Data.TemplatePlaceholderNameAndValues[i].Value.ShouldBe(expected.TemplatePlaceholderNameAndValues[i].Value); - } - - _result.IsError.ShouldBeFalse(); - } - } -} + _upstreamQuery = string.Empty; + } + + private void WhenICallTheFinder() + { + _result = _downstreamRouteFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost); + } + + private void ThenTheFollowingIsReturned(DownstreamRouteHolder expected) + { + _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe(expected.Route.DownstreamRoute[0].DownstreamPathTemplate.Value); + _result.Data.Route.UpstreamTemplatePattern.Priority.ShouldBe(expected.Route.UpstreamTemplatePattern.Priority); + + for (var i = 0; i < _result.Data.TemplatePlaceholderNameAndValues.Count; i++) + { + _result.Data.TemplatePlaceholderNameAndValues[i].Name.ShouldBe(expected.TemplatePlaceholderNameAndValues[i].Name); + _result.Data.TemplatePlaceholderNameAndValues[i].Value.ShouldBe(expected.TemplatePlaceholderNameAndValues[i].Value); + } + + _result.IsError.ShouldBeFalse(); + } + } +} diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs index 437033f79..a7090bb55 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs @@ -1,151 +1,165 @@ -namespace Ocelot.UnitTests.DownstreamRouteFinder +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Logging; +using Shouldly; +using System; +using System.Collections.Generic; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.DownstreamRouteFinder { - using Microsoft.Extensions.DependencyInjection; - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Configuration.Creator; using Ocelot.DownstreamRouteFinder.Finder; - using Ocelot.DownstreamRouteFinder.UrlMatcher; - using Ocelot.Logging; - using Shouldly; - using System.Collections.Generic; - using TestStack.BDDfy; - using Xunit; - public class DownstreamRouteProviderFactoryTests - { - private readonly DownstreamRouteProviderFactory _factory; - private IInternalConfiguration _config; - private IDownstreamRouteProvider _result; - private Mock _logger; - private Mock _loggerFactory; - - public DownstreamRouteProviderFactoryTests() - { - var services = new ServiceCollection(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - var provider = services.BuildServiceProvider(); - _logger = new Mock(); - _loggerFactory = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _factory = new DownstreamRouteProviderFactory(provider, _loggerFactory.Object); - } - - [Fact] - public void should_return_downstream_route_finder() - { - var reRoutes = new List - { - new ReRouteBuilder().Build() - }; - - this.Given(_ => GivenTheReRoutes(reRoutes)) - .When(_ => WhenIGet()) - .Then(_ => ThenTheResultShouldBe()) - .BDDfy(); - } - - [Fact] - public void should_return_downstream_route_finder_when_not_dynamic_re_route_and_service_discovery_on() - { - var spConfig = new ServiceProviderConfigurationBuilder().WithHost("test").WithPort(50).WithType("test").Build(); - var reRoutes = new List - { - new ReRouteBuilder().WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("woot").Build()).Build() - }; - - this.Given(_ => GivenTheReRoutes(reRoutes, spConfig)) - .When(_ => WhenIGet()) - .Then(_ => ThenTheResultShouldBe()) - .BDDfy(); - } - - [Fact] - public void should_return_downstream_route_finder_as_no_service_discovery_given_no_host() - { - var spConfig = new ServiceProviderConfigurationBuilder().WithHost("").WithPort(50).Build(); - var reRoutes = new List(); - - this.Given(_ => GivenTheReRoutes(reRoutes, spConfig)) - .When(_ => WhenIGet()) - .Then(_ => ThenTheResultShouldBe()) - .BDDfy(); - } - - [Fact] - public void should_return_downstream_route_finder_given_no_service_discovery_port() - { - var spConfig = new ServiceProviderConfigurationBuilder().WithHost("localhost").WithPort(0).Build(); - var reRoutes = new List(); - - this.Given(_ => GivenTheReRoutes(reRoutes, spConfig)) - .When(_ => WhenIGet()) - .Then(_ => ThenTheResultShouldBe()) - .BDDfy(); - } - - [Fact] - public void should_return_downstream_route_finder_given_no_service_discovery_type() - { - var spConfig = new ServiceProviderConfigurationBuilder().WithHost("localhost").WithPort(50).WithType("").Build(); - var reRoutes = new List(); - - this.Given(_ => GivenTheReRoutes(reRoutes, spConfig)) - .When(_ => WhenIGet()) - .Then(_ => ThenTheResultShouldBe()) - .BDDfy(); - } - - [Fact] - public void should_return_downstream_route_creator() - { - var spConfig = new ServiceProviderConfigurationBuilder().WithHost("test").WithPort(50).WithType("test").Build(); - var reRoutes = new List(); - - this.Given(_ => GivenTheReRoutes(reRoutes, spConfig)) - .When(_ => WhenIGet()) - .Then(_ => ThenTheResultShouldBe()) - .BDDfy(); - } - - [Fact] - public void should_return_downstream_route_creator_with_dynamic_re_route() - { - var spConfig = new ServiceProviderConfigurationBuilder().WithHost("test").WithPort(50).WithType("test").Build(); - var reRoutes = new List - { - new ReRouteBuilder().Build() - }; - - this.Given(_ => GivenTheReRoutes(reRoutes, spConfig)) - .When(_ => WhenIGet()) - .Then(_ => ThenTheResultShouldBe()) - .BDDfy(); - } - - private void ThenTheResultShouldBe() - { - _result.ShouldBeOfType(); - } - - private void WhenIGet() - { - _result = _factory.Get(_config); - } - - private void GivenTheReRoutes(List reRoutes) - { - _config = new InternalConfiguration(reRoutes, "", null, "", new LoadBalancerOptionsBuilder().Build(), "", new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build()); - } - - private void GivenTheReRoutes(List reRoutes, ServiceProviderConfiguration config) - { - _config = new InternalConfiguration(reRoutes, "", config, "", new LoadBalancerOptionsBuilder().Build(), "", new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build()); - } - } -} + public class DownstreamRouteProviderFactoryTests + { + private readonly DownstreamRouteProviderFactory _factory; + private IInternalConfiguration _config; + private IDownstreamRouteProvider _result; + private readonly Mock _logger; + private readonly Mock _loggerFactory; + + public DownstreamRouteProviderFactoryTests() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + var provider = services.BuildServiceProvider(); + _logger = new Mock(); + _loggerFactory = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _factory = new DownstreamRouteProviderFactory(provider, _loggerFactory.Object); + } + + [Fact] + public void should_return_downstream_route_finder() + { + var routes = new List + { + new RouteBuilder().Build(), + }; + + this.Given(_ => GivenTheRoutes(routes)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheResultShouldBe()) + .BDDfy(); + } + + [Fact] + public void should_return_downstream_route_finder_when_not_dynamic_re_route_and_service_discovery_on() + { + var spConfig = new ServiceProviderConfigurationBuilder().WithScheme("http").WithHost("test").WithPort(50).WithType("test").Build(); + var routes = new List + { + new RouteBuilder().WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("woot").Build()).Build(), + }; + + this.Given(_ => GivenTheRoutes(routes, spConfig)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheResultShouldBe()) + .BDDfy(); + } + + [Fact] + public void should_return_downstream_route_finder_as_no_service_discovery_given_no_scheme() + { + var spConfig = new ServiceProviderConfigurationBuilder().WithScheme(string.Empty).WithHost("test").WithPort(50).Build(); + var routes = new List(); + + this.Given(_ => GivenTheRoutes(routes, spConfig)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheResultShouldBe()) + .BDDfy(); + } + + [Fact] + public void should_return_downstream_route_finder_as_no_service_discovery_given_no_host() + { + var spConfig = new ServiceProviderConfigurationBuilder().WithScheme("http").WithHost(string.Empty).WithPort(50).Build(); + var routes = new List(); + + this.Given(_ => GivenTheRoutes(routes, spConfig)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheResultShouldBe()) + .BDDfy(); + } + + [Fact] + public void should_return_downstream_route_finder_given_no_service_discovery_port() + { + var spConfig = new ServiceProviderConfigurationBuilder().WithScheme("http").WithHost("localhost").WithPort(0).Build(); + var routes = new List(); + + this.Given(_ => GivenTheRoutes(routes, spConfig)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheResultShouldBe()) + .BDDfy(); + } + + [Fact] + public void should_return_downstream_route_finder_given_no_service_discovery_type() + { + var spConfig = new ServiceProviderConfigurationBuilder().WithScheme("http").WithHost("localhost").WithPort(50).WithType(string.Empty).Build(); + var routes = new List(); + + this.Given(_ => GivenTheRoutes(routes, spConfig)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheResultShouldBe()) + .BDDfy(); + } + + [Fact] + public void should_return_downstream_route_creator() + { + var spConfig = new ServiceProviderConfigurationBuilder().WithScheme("http").WithHost("test").WithPort(50).WithType("test").Build(); + var routes = new List(); + + this.Given(_ => GivenTheRoutes(routes, spConfig)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheResultShouldBe()) + .BDDfy(); + } + + [Fact] + public void should_return_downstream_route_creator_with_dynamic_re_route() + { + var spConfig = new ServiceProviderConfigurationBuilder().WithScheme("http").WithHost("test").WithPort(50).WithType("test").Build(); + var routes = new List + { + new RouteBuilder().Build(), + }; + + this.Given(_ => GivenTheRoutes(routes, spConfig)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheResultShouldBe()) + .BDDfy(); + } + + private void ThenTheResultShouldBe() + { + _result.ShouldBeOfType(); + } + + private void WhenIGet() + { + _result = _factory.Get(_config); + } + + private void GivenTheRoutes(List routes) + { + _config = new InternalConfiguration(routes, string.Empty, null, string.Empty, new LoadBalancerOptionsBuilder().Build(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1")); + } + + private void GivenTheRoutes(List routes, ServiceProviderConfiguration config) + { + _config = new InternalConfiguration(routes, string.Empty, config, string.Empty, new LoadBalancerOptionsBuilder().Build(), string.Empty, new QoSOptionsBuilder().Build(), new HttpHandlerOptionsBuilder().Build(), new Version("1.1")); + } + } +} diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs index 7909456b3..cb325568d 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs @@ -1,8 +1,11 @@ using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Responses; using Ocelot.Values; + using Shouldly; + using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher @@ -147,7 +150,7 @@ public void should_not_find_match() [Fact] public void can_match_down_stream_url() { - this.Given(x => x.GivenIHaveAUpstreamPath("")) + this.Given(x => x.GivenIHaveAUpstreamPath(string.Empty)) .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^$")) .When(x => x.WhenIMatchThePaths()) .And(x => x.ThenTheResultIsTrue()) diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs index ac911baf0..78e8e40e6 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs @@ -1,9 +1,13 @@ +using System.Collections.Generic; +using System.Linq; + using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Responses; + using Shouldly; -using System.Collections.Generic; -using System.Linq; + using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher @@ -24,8 +28,8 @@ public UrlPathPlaceholderNameAndValueFinderTests() [Fact] public void can_match_down_stream_url() { - this.Given(x => x.GivenIHaveAUpstreamPath("")) - .And(x => x.GivenIHaveAnUpstreamUrlTemplate("")) + this.Given(x => x.GivenIHaveAUpstreamPath(string.Empty)) + .And(x => x.GivenIHaveAnUpstreamUrlTemplate(string.Empty)) .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) .And(x => x.ThenTheTemplatesVariablesAre(new List())) .BDDfy(); @@ -36,10 +40,10 @@ public void can_match_down_stream_url_with_nothing_then_placeholder_no_value_is_ { var expectedTemplates = new List { - new PlaceholderNameAndValue("{url}", "") + new("{url}", string.Empty), }; - this.Given(x => x.GivenIHaveAUpstreamPath("")) + this.Given(x => x.GivenIHaveAUpstreamPath(string.Empty)) .And(x => x.GivenIHaveAnUpstreamUrlTemplate("/{url}")) .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) .And(x => x.ThenTheTemplatesVariablesAre(expectedTemplates)) @@ -51,7 +55,7 @@ public void can_match_down_stream_url_with_nothing_then_placeholder_value_is_tes { var expectedTemplates = new List { - new PlaceholderNameAndValue("{url}", "test") + new("{url}", "test"), }; this.Given(x => x.GivenIHaveAUpstreamPath("/test")) @@ -66,7 +70,7 @@ public void should_match_everything_in_path_with_query() { var expectedTemplates = new List { - new PlaceholderNameAndValue("{everything}", "test/toot") + new("{everything}", "test/toot"), }; this.Given(x => x.GivenIHaveAUpstreamPath("/test/toot")) @@ -82,7 +86,7 @@ public void should_match_everything_in_path() { var expectedTemplates = new List { - new PlaceholderNameAndValue("{everything}", "test/toot") + new("{everything}", "test/toot"), }; this.Given(x => x.GivenIHaveAUpstreamPath("/test/toot")) @@ -97,7 +101,7 @@ public void can_match_down_stream_url_with_forward_slash_then_placeholder_no_val { var expectedTemplates = new List { - new PlaceholderNameAndValue("{url}", "") + new("{url}", string.Empty), }; this.Given(x => x.GivenIHaveAUpstreamPath("/")) @@ -110,9 +114,7 @@ public void can_match_down_stream_url_with_forward_slash_then_placeholder_no_val [Fact] public void can_match_down_stream_url_with_forward_slash() { - var expectedTemplates = new List - { - }; + var expectedTemplates = new List(); this.Given(x => x.GivenIHaveAUpstreamPath("/")) .And(x => x.GivenIHaveAnUpstreamUrlTemplate("/")) @@ -126,7 +128,7 @@ public void can_match_down_stream_url_with_forward_slash_then_placeholder_then_a { var expectedTemplates = new List { - new PlaceholderNameAndValue("{url}", "1") + new("{url}", "1"), }; this.Given(x => x.GivenIHaveAUpstreamPath("/1/products")) @@ -151,7 +153,7 @@ public void should_find_query_string() { var expectedTemplates = new List { - new PlaceholderNameAndValue("{productId}", "1") + new("{productId}", "1"), }; this.Given(x => x.GivenIHaveAUpstreamPath("/products")) @@ -167,7 +169,7 @@ public void should_find_query_string_dont_include_hardcoded() { var expectedTemplates = new List { - new PlaceholderNameAndValue("{productId}", "1") + new("{productId}", "1"), }; this.Given(x => x.GivenIHaveAUpstreamPath("/products")) @@ -183,8 +185,8 @@ public void should_find_multiple_query_string() { var expectedTemplates = new List { - new PlaceholderNameAndValue("{productId}", "1"), - new PlaceholderNameAndValue("{categoryId}", "2") + new("{productId}", "1"), + new("{categoryId}", "2"), }; this.Given(x => x.GivenIHaveAUpstreamPath("/products")) @@ -200,9 +202,9 @@ public void should_find_multiple_query_string_and_path() { var expectedTemplates = new List { - new PlaceholderNameAndValue("{productId}", "1"), - new PlaceholderNameAndValue("{categoryId}", "2"), - new PlaceholderNameAndValue("{account}", "3") + new("{productId}", "1"), + new("{categoryId}", "2"), + new("{account}", "3"), }; this.Given(x => x.GivenIHaveAUpstreamPath("/products/3")) @@ -218,9 +220,9 @@ public void should_find_multiple_query_string_and_path_that_ends_with_slash() { var expectedTemplates = new List { - new PlaceholderNameAndValue("{productId}", "1"), - new PlaceholderNameAndValue("{categoryId}", "2"), - new PlaceholderNameAndValue("{account}", "3") + new("{productId}", "1"), + new("{categoryId}", "2"), + new("{account}", "3"), }; this.Given(x => x.GivenIHaveAUpstreamPath("/products/3/")) @@ -266,7 +268,7 @@ public void can_match_down_stream_url_with_downstream_template_with_one_place_ho { var expectedTemplates = new List { - new PlaceholderNameAndValue("{productId}", "1") + new("{productId}", "1"), }; this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1")) @@ -281,8 +283,8 @@ public void can_match_down_stream_url_with_downstream_template_with_two_place_ho { var expectedTemplates = new List { - new PlaceholderNameAndValue("{productId}", "1"), - new PlaceholderNameAndValue("{categoryId}", "2") + new("{productId}", "1"), + new("{categoryId}", "2"), }; this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/2")) @@ -297,8 +299,8 @@ public void can_match_down_stream_url_with_downstream_template_with_two_place_ho { var expectedTemplates = new List { - new PlaceholderNameAndValue("{productId}", "1"), - new PlaceholderNameAndValue("{categoryId}", "2") + new("{productId}", "1"), + new("{categoryId}", "2"), }; this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2")) @@ -313,9 +315,9 @@ public void can_match_down_stream_url_with_downstream_template_with_three_place_ { var expectedTemplates = new List { - new PlaceholderNameAndValue("{productId}", "1"), - new PlaceholderNameAndValue("{categoryId}", "2"), - new PlaceholderNameAndValue("{variantId}", "123") + new("{productId}", "1"), + new("{categoryId}", "2"), + new("{variantId}", "123"), }; this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2/variant/123")) @@ -330,8 +332,8 @@ public void can_match_down_stream_url_with_downstream_template_with_three_place_ { var expectedTemplates = new List { - new PlaceholderNameAndValue("{productId}", "1"), - new PlaceholderNameAndValue("{categoryId}", "2") + new("{productId}", "1"), + new("{categoryId}", "2"), }; this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2/variant/")) @@ -346,7 +348,7 @@ public void can_match_down_stream_url_with_downstream_template_with_place_holder { var expectedTemplates = new List { - new PlaceholderNameAndValue("{finalUrlPath}", "product/products/categories/"), + new("{finalUrlPath}", "product/products/categories/"), }; this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/categories/")) diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs index 7ed6a02a3..cc027ad41 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs @@ -1,53 +1,63 @@ -namespace Ocelot.UnitTests.DownstreamUrlCreator -{ - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.DownstreamRouteFinder; - using Ocelot.DownstreamRouteFinder.UrlMatcher; - using Ocelot.DownstreamUrlCreator.Middleware; - using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; - using Ocelot.Logging; - using Ocelot.Middleware; - using Ocelot.Request.Middleware; - using Ocelot.Responses; - using Ocelot.Values; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Net.Http; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.DownstreamUrlCreator.Middleware; +using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.Request.Middleware; + +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Ocelot.Values; +using Xunit; + +namespace Ocelot.UnitTests.DownstreamUrlCreator +{ public class DownstreamUrlCreatorMiddlewareTests { private readonly Mock _downstreamUrlTemplateVariableReplacer; private OkResponse _downstreamPath; private readonly Mock _loggerFactory; - private Mock _logger; + private readonly Mock _logger; private DownstreamUrlCreatorMiddleware _middleware; - private readonly DownstreamContext _downstreamContext; - private readonly OcelotRequestDelegate _next; + private readonly RequestDelegate _next; private readonly HttpRequestMessage _request; + private readonly HttpContext _httpContext; + private Mock _repo; public DownstreamUrlCreatorMiddlewareTests() { - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); + _repo = new Mock(); + _httpContext = new DefaultHttpContext(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _downstreamUrlTemplateVariableReplacer = new Mock(); _request = new HttpRequestMessage(HttpMethod.Get, "https://my.url/abc/?q=123"); - _downstreamContext.DownstreamRequest = new DownstreamRequest(_request); _next = context => Task.CompletedTask; } [Fact] public void should_replace_scheme_and_path() { - var downstreamReRoute = new DownstreamReRouteBuilder() + var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithUpstreamHttpMethod(new List { "Get" }) .WithDownstreamScheme("https") @@ -57,10 +67,10 @@ public void should_replace_scheme_and_path() .Build(); this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRoute( + new DownstreamRouteHolder( new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) .Build()))) .And(x => x.GivenTheDownstreamRequestUriIs("http://my.url/abc?q=123")) @@ -75,7 +85,7 @@ public void should_replace_scheme_and_path() [Fact] public void should_replace_query_string() { - var downstreamReRoute = new DownstreamReRouteBuilder() + var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") .WithUpstreamHttpMethod(new List { "Get" }) .WithDownstreamScheme("https") @@ -85,14 +95,14 @@ public void should_replace_query_string() .Build(); this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRoute( + new DownstreamRouteHolder( new List { - new PlaceholderNameAndValue("{subscriptionId}", "1"), - new PlaceholderNameAndValue("{unitId}", "2") + new("{subscriptionId}", "1"), + new("{unitId}", "2"), }, - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) .Build()))) .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2")) @@ -100,14 +110,14 @@ public void should_replace_query_string() .And(x => x.GivenTheUrlReplacerWillReturn("api/units/1/2/updates")) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates")) - .And(x => ThenTheQueryStringIs("")) + .And(x => ThenTheQueryStringIs(string.Empty)) .BDDfy(); } [Fact] public void should_replace_query_string_but_leave_non_placeholder_queries() { - var downstreamReRoute = new DownstreamReRouteBuilder() + var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") .WithUpstreamHttpMethod(new List { "Get" }) .WithDownstreamScheme("https") @@ -117,14 +127,14 @@ public void should_replace_query_string_but_leave_non_placeholder_queries() .Build(); this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRoute( + new DownstreamRouteHolder( new List { - new PlaceholderNameAndValue("{subscriptionId}", "1"), - new PlaceholderNameAndValue("{unitId}", "2") + new("{subscriptionId}", "1"), + new("{unitId}", "2"), }, - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) .Build()))) .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2&productId=2")) @@ -139,7 +149,7 @@ public void should_replace_query_string_but_leave_non_placeholder_queries() [Fact] public void should_replace_query_string_exact_match() { - var downstreamReRoute = new DownstreamReRouteBuilder() + var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates/{unitIdIty}") .WithUpstreamHttpMethod(new List { "Get" }) .WithDownstreamScheme("https") @@ -149,15 +159,15 @@ public void should_replace_query_string_exact_match() .Build(); this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRoute( + new DownstreamRouteHolder( new List { - new PlaceholderNameAndValue("{subscriptionId}", "1"), - new PlaceholderNameAndValue("{unitId}", "2"), - new PlaceholderNameAndValue("{unitIdIty}", "3") + new("{subscriptionId}", "1"), + new("{unitId}", "2"), + new("{unitIdIty}", "3"), }, - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) .Build()))) .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2?unitIdIty=3")) @@ -165,14 +175,14 @@ public void should_replace_query_string_exact_match() .And(x => x.GivenTheUrlReplacerWillReturn("api/units/1/2/updates/3")) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates/3")) - .And(x => ThenTheQueryStringIs("")) + .And(x => ThenTheQueryStringIs(string.Empty)) .BDDfy(); } [Fact] public void should_not_create_service_fabric_url() { - var downstreamReRoute = new DownstreamReRouteBuilder() + var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithUpstreamHttpMethod(new List { "Get" }) .WithDownstreamScheme("https") @@ -185,10 +195,10 @@ public void should_not_create_service_fabric_url() .Build(); this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRoute( + new DownstreamRouteHolder( new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Get" }) .Build()))) .And(x => x.GivenTheDownstreamRequestUriIs("http://my.url/abc?q=123")) @@ -202,16 +212,16 @@ public void should_not_create_service_fabric_url() [Fact] public void should_create_service_fabric_url() { - var downstreamReRoute = new DownstreamReRouteBuilder() + var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamScheme("http") .WithServiceName("Ocelot/OcelotApp") .WithUseServiceDiscovery(true) .Build(); - var downstreamRoute = new DownstreamRoute( + var downstreamRouteHolder = new DownstreamRouteHolder( new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .Build()); var config = new ServiceProviderConfigurationBuilder() @@ -220,7 +230,7 @@ public void should_create_service_fabric_url() .WithPort(19081) .Build(); - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRouteHolder)) .And(x => GivenTheServiceProviderConfigIs(config)) .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:19081")) .And(x => x.GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp")) @@ -232,16 +242,16 @@ public void should_create_service_fabric_url() [Fact] public void should_create_service_fabric_url_with_query_string_for_stateless_service() { - var downstreamReRoute = new DownstreamReRouteBuilder() + var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamScheme("http") .WithServiceName("Ocelot/OcelotApp") .WithUseServiceDiscovery(true) .Build(); - var downstreamRoute = new DownstreamRoute( + var downstreamRouteHolder = new DownstreamRouteHolder( new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .Build()); var config = new ServiceProviderConfigurationBuilder() @@ -250,7 +260,7 @@ public void should_create_service_fabric_url_with_query_string_for_stateless_ser .WithPort(19081) .Build(); - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRouteHolder)) .And(x => GivenTheServiceProviderConfigIs(config)) .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:19081?Tom=test&laura=1")) .And(x => x.GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp")) @@ -262,16 +272,16 @@ 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() { - var downstreamReRoute = new DownstreamReRouteBuilder() + var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamScheme("http") .WithServiceName("Ocelot/OcelotApp") .WithUseServiceDiscovery(true) .Build(); - var downstreamRoute = new DownstreamRoute( + var downstreamRouteHolder = new DownstreamRouteHolder( new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .Build()); var config = new ServiceProviderConfigurationBuilder() @@ -280,7 +290,7 @@ public void should_create_service_fabric_url_with_query_string_for_stateful_serv .WithPort(19081) .Build(); - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRouteHolder)) .And(x => GivenTheServiceProviderConfigIs(config)) .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:19081?PartitionKind=test&PartitionKey=1")) .And(x => x.GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp")) @@ -292,10 +302,10 @@ 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() { - var downstreamRoute = new DownstreamRoute( + var downstreamRoute = new DownstreamRouteHolder( new List(), - new ReRouteBuilder().WithDownstreamReRoute( - new DownstreamReRouteBuilder() + new RouteBuilder().WithDownstreamRoute( + new DownstreamRouteBuilder() .WithDownstreamScheme("http") .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("/products").Build()) .WithUseServiceDiscovery(true) @@ -320,7 +330,7 @@ public void should_create_service_fabric_url_with_version_from_upstream_path_tem [Fact] public void issue_473_should_not_remove_additional_query_string() { - var downstreamReRoute = new DownstreamReRouteBuilder() + var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/Authorized/{action}?server={server}") .WithUpstreamHttpMethod(new List { "Post", "Get" }) .WithDownstreamScheme("http") @@ -331,14 +341,14 @@ public void issue_473_should_not_remove_additional_query_string() .Build(); this.Given(x => x.GivenTheDownStreamRouteIs( - new DownstreamRoute( + new DownstreamRouteHolder( new List { - new PlaceholderNameAndValue("{action}", "1"), - new PlaceholderNameAndValue("{server}", "2") + new("{action}", "1"), + new("{server}", "2"), }, - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .WithUpstreamHttpMethod(new List { "Post", "Get" }) .Build()))) .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/uc/Authorized/2/1/refresh?refreshToken=2288356cfb1338fdc5ff4ca558ec785118dfe1ff2864340937da8226863ff66d")) @@ -353,16 +363,16 @@ public void issue_473_should_not_remove_additional_query_string() [Fact] public void should_not_replace_by_empty_scheme() { - var downstreamReRoute = new DownstreamReRouteBuilder() - .WithDownstreamScheme("") + var downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamScheme(string.Empty) .WithServiceName("Ocelot/OcelotApp") .WithUseServiceDiscovery(true) .Build(); - var downstreamRoute = new DownstreamRoute( + var downstreamRouteHolder = new DownstreamRouteHolder( new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) + new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) .Build()); var config = new ServiceProviderConfigurationBuilder() @@ -371,7 +381,7 @@ public void should_not_replace_by_empty_scheme() .WithPort(19081) .Build(); - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRouteHolder)) .And(x => GivenTheServiceProviderConfigIs(config)) .And(x => x.GivenTheDownstreamRequestUriIs("https://localhost:19081?PartitionKind=test&PartitionKey=1")) .And(x => x.GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp")) @@ -382,26 +392,27 @@ public void should_not_replace_by_empty_scheme() private void GivenTheServiceProviderConfigIs(ServiceProviderConfiguration config) { - var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null); - _downstreamContext.Configuration = configuration; + var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null); + _httpContext.Items.SetIInternalConfiguration(configuration); } private void WhenICallTheMiddleware() { _middleware = new DownstreamUrlCreatorMiddleware(_next, _loggerFactory.Object, _downstreamUrlTemplateVariableReplacer.Object); - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); } - private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + private void GivenTheDownStreamRouteIs(DownstreamRouteHolder downstreamRoute) { - _downstreamContext.TemplatePlaceholderNameAndValues = downstreamRoute.TemplatePlaceholderNameAndValues; - _downstreamContext.DownstreamReRoute = downstreamRoute.ReRoute.DownstreamReRoute[0]; + _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + + _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); } private void GivenTheDownstreamRequestUriIs(string uri) { _request.RequestUri = new Uri(uri); - _downstreamContext.DownstreamRequest = new DownstreamRequest(_request); + _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(_request)); } private void GivenTheUrlReplacerWillReturnSequence(params string[] paths) @@ -425,12 +436,12 @@ private void GivenTheUrlReplacerWillReturn(string path) private void ThenTheDownstreamRequestUriIs(string expectedUri) { - _downstreamContext.DownstreamRequest.ToHttpRequestMessage().RequestUri.OriginalString.ShouldBe(expectedUri); + _httpContext.Items.DownstreamRequest().ToHttpRequestMessage().RequestUri.OriginalString.ShouldBe(expectedUri); } private void ThenTheQueryStringIs(string queryString) { - _downstreamContext.DownstreamRequest.Query.ShouldBe(queryString); + _httpContext.Items.DownstreamRequest().Query.ShouldBe(queryString); } } } diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs index 1c27898aa..b460eb85d 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs @@ -1,210 +1,214 @@ -using Ocelot.Configuration.Builder; -using Ocelot.DownstreamRouteFinder; -using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; -using Ocelot.Responses; -using Ocelot.Values; -using Shouldly; -using System.Collections.Generic; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer -{ - public class UpstreamUrlPathTemplateVariableReplacerTests - { - private DownstreamRoute _downstreamRoute; - private Response _result; - private readonly IDownstreamPathPlaceholderReplacer _downstreamPathReplacer; - - public UpstreamUrlPathTemplateVariableReplacerTests() - { - _downstreamPathReplacer = new DownstreamTemplatePathPlaceholderReplacer(); - } - - [Fact] - public void can_replace_no_template_variables() - { - this.Given(x => x.GivenThereIsAUrlMatch( - new DownstreamRoute( +using System.Collections.Generic; + +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; +using Ocelot.Responses; +using Ocelot.Values; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer +{ + public class UpstreamUrlPathTemplateVariableReplacerTests + { + private DownstreamRouteHolder _downstreamRoute; + private Response _result; + private readonly IDownstreamPathPlaceholderReplacer _downstreamPathReplacer; + + public UpstreamUrlPathTemplateVariableReplacerTests() + { + _downstreamPathReplacer = new DownstreamTemplatePathPlaceholderReplacer(); + } + + [Fact] + public void can_replace_no_template_variables() + { + this.Given(x => x.GivenThereIsAUrlMatch( + new DownstreamRouteHolder( new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .When(x => x.WhenIReplaceTheTemplateVariables()) - .Then(x => x.ThenTheDownstreamUrlPathIsReturned("")) - .BDDfy(); - } - - [Fact] - public void can_replace_no_template_variables_with_slash() - { - this.Given(x => x.GivenThereIsAUrlMatch( - new DownstreamRoute( + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned(string.Empty)) + .BDDfy(); + } + + [Fact] + public void can_replace_no_template_variables_with_slash() + { + this.Given(x => x.GivenThereIsAUrlMatch( + new DownstreamRouteHolder( new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("/") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .When(x => x.WhenIReplaceTheTemplateVariables()) - .Then(x => x.ThenTheDownstreamUrlPathIsReturned("/")) - .BDDfy(); - } - - [Fact] - public void can_replace_url_no_slash() - { - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("api") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .When(x => x.WhenIReplaceTheTemplateVariables()) - .Then(x => x.ThenTheDownstreamUrlPathIsReturned("api")) - .BDDfy(); - } - - [Fact] - public void can_replace_url_one_slash() - { - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("api/") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .When(x => x.WhenIReplaceTheTemplateVariables()) - .Then(x => x.ThenTheDownstreamUrlPathIsReturned("api/")) - .BDDfy(); - } - - [Fact] - public void can_replace_url_multiple_slash() - { - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("api/product/products/") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .When(x => x.WhenIReplaceTheTemplateVariables()) - .Then(x => x.ThenTheDownstreamUrlPathIsReturned("api/product/products/")) - .BDDfy(); - } - - [Fact] - public void can_replace_url_one_template_variable() - { - var templateVariables = new List() - { - new PlaceholderNameAndValue("{productId}", "1") - }; - - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("productservice/products/{productId}/") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .When(x => x.WhenIReplaceTheTemplateVariables()) - .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/products/1/")) - .BDDfy(); - } - - [Fact] - public void can_replace_url_one_template_variable_with_path_after() - { - var templateVariables = new List() - { - new PlaceholderNameAndValue("{productId}", "1") - }; - - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("productservice/products/{productId}/variants") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .When(x => x.WhenIReplaceTheTemplateVariables()) - .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/products/1/variants")) - .BDDfy(); - } - - [Fact] - public void can_replace_url_two_template_variable() - { - var templateVariables = new List() - { - new PlaceholderNameAndValue("{productId}", "1"), - new PlaceholderNameAndValue("{variantId}", "12") - }; - - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("productservice/products/{productId}/variants/{variantId}") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .When(x => x.WhenIReplaceTheTemplateVariables()) - .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/products/1/variants/12")) - .BDDfy(); + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("/") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("/")) + .BDDfy(); + } + + [Fact] + public void can_replace_url_no_slash() + { + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("api") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("api")) + .BDDfy(); + } + + [Fact] + public void can_replace_url_one_slash() + { + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("api/") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("api/")) + .BDDfy(); + } + + [Fact] + public void can_replace_url_multiple_slash() + { + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("api/product/products/") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("api/product/products/")) + .BDDfy(); } - [Fact] - public void can_replace_url_three_template_variable() - { - var templateVariables = new List() - { - new PlaceholderNameAndValue("{productId}", "1"), - new PlaceholderNameAndValue("{variantId}", "12"), - new PlaceholderNameAndValue("{categoryId}", "34") - }; - - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("productservice/category/{categoryId}/products/{productId}/variants/{variantId}") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()))) - .When(x => x.WhenIReplaceTheTemplateVariables()) - .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/category/34/products/1/variants/12")) - .BDDfy(); - } - - private void GivenThereIsAUrlMatch(DownstreamRoute downstreamRoute) - { - _downstreamRoute = downstreamRoute; - } - - private void WhenIReplaceTheTemplateVariables() - { - _result = _downstreamPathReplacer.Replace(_downstreamRoute.ReRoute.DownstreamReRoute[0].DownstreamPathTemplate.Value, _downstreamRoute.TemplatePlaceholderNameAndValues); - } - - private void ThenTheDownstreamUrlPathIsReturned(string expected) - { - _result.Data.Value.ShouldBe(expected); - } - } + [Fact] + public void can_replace_url_one_template_variable() + { + var templateVariables = new List + { + new("{productId}", "1"), + }; + + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRouteHolder(templateVariables, + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("productservice/products/{productId}/") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/products/1/")) + .BDDfy(); + } + + [Fact] + public void can_replace_url_one_template_variable_with_path_after() + { + var templateVariables = new List + { + new("{productId}", "1"), + }; + + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRouteHolder(templateVariables, + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("productservice/products/{productId}/variants") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/products/1/variants")) + .BDDfy(); + } + + [Fact] + public void can_replace_url_two_template_variable() + { + var templateVariables = new List + { + new("{productId}", "1"), + new("{variantId}", "12"), + }; + + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRouteHolder(templateVariables, + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("productservice/products/{productId}/variants/{variantId}") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/products/1/variants/12")) + .BDDfy(); + } + + [Fact] + public void can_replace_url_three_template_variable() + { + var templateVariables = new List + { + new("{productId}", "1"), + new("{variantId}", "12"), + new("{categoryId}", "34"), + }; + + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRouteHolder(templateVariables, + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("productservice/category/{categoryId}/products/{productId}/variants/{variantId}") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/category/34/products/1/variants/12")) + .BDDfy(); + } + + private void GivenThereIsAUrlMatch(DownstreamRouteHolder downstreamRoute) + { + _downstreamRoute = downstreamRoute; + } + + private void WhenIReplaceTheTemplateVariables() + { + _result = _downstreamPathReplacer.Replace(_downstreamRoute.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, _downstreamRoute.TemplatePlaceholderNameAndValues); + } + + private void ThenTheDownstreamUrlPathIsReturned(string expected) + { + _result.Data.Value.ShouldBe(expected); + } + } } diff --git a/test/Ocelot.UnitTests/Errors/ErrorTests.cs b/test/Ocelot.UnitTests/Errors/ErrorTests.cs index c1b92f8fa..f6ec52a04 100644 --- a/test/Ocelot.UnitTests/Errors/ErrorTests.cs +++ b/test/Ocelot.UnitTests/Errors/ErrorTests.cs @@ -1,5 +1,7 @@ -using Ocelot.Infrastructure.RequestData; -using Shouldly; +using Ocelot.Infrastructure.RequestData; + +using Shouldly; + using Xunit; namespace Ocelot.UnitTests.Errors diff --git a/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs b/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs index 3d86505e5..4d58c2de9 100644 --- a/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs @@ -1,37 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.Configuration; +using Ocelot.Errors; +using Ocelot.Errors.Middleware; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + namespace Ocelot.UnitTests.Errors { - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Repository; - using Ocelot.Errors; - using Ocelot.Errors.Middleware; - using Ocelot.Infrastructure.RequestData; - using Ocelot.Logging; - using Ocelot.Middleware; - using Shouldly; - using System; - using System.Net; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - public class ExceptionHandlerMiddlewareTests { private bool _shouldThrowAnException; - private readonly Mock _configRepo; private readonly Mock _repo; - private Mock _loggerFactory; - private Mock _logger; + private readonly Mock _loggerFactory; + private readonly Mock _logger; private readonly ExceptionHandlerMiddleware _middleware; - private readonly DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; + private readonly RequestDelegate _next; + private readonly HttpContext _httpContext; public ExceptionHandlerMiddlewareTests() { - _configRepo = new Mock(); + _httpContext = new DefaultHttpContext(); _repo = new Mock(); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); @@ -44,15 +47,16 @@ public ExceptionHandlerMiddlewareTests() throw new Exception("BOOM"); } - context.HttpContext.Response.StatusCode = (int)HttpStatusCode.OK; + _httpContext.Response.StatusCode = (int)HttpStatusCode.OK; }; - _middleware = new ExceptionHandlerMiddleware(_next, _loggerFactory.Object, _configRepo.Object, _repo.Object); + + _middleware = new ExceptionHandlerMiddleware(_next, _loggerFactory.Object, _repo.Object); } [Fact] public void NoDownstreamException() { - var config = new InternalConfiguration(null, null, null, null, null, null, null, null); + var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null); this.Given(_ => GivenAnExceptionWillNotBeThrownDownstream()) .And(_ => GivenTheConfigurationIs(config)) @@ -65,7 +69,7 @@ public void NoDownstreamException() [Fact] public void DownstreamException() { - var config = new InternalConfiguration(null, null, null, null, null, null, null, null); + var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null); this.Given(_ => GivenAnExceptionWillBeThrownDownstream()) .And(_ => GivenTheConfigurationIs(config)) @@ -77,7 +81,7 @@ public void DownstreamException() [Fact] public void ShouldSetRequestId() { - var config = new InternalConfiguration(null, null, null, "requestidkey", null, null, null, null); + var config = new InternalConfiguration(null, null, null, "requestidkey", null, null, null, null, null); this.Given(_ => GivenAnExceptionWillNotBeThrownDownstream()) .And(_ => GivenTheConfigurationIs(config)) @@ -90,7 +94,7 @@ public void ShouldSetRequestId() [Fact] public void ShouldSetAspDotNetRequestId() { - var config = new InternalConfiguration(null, null, null, null, null, null, null, null); + var config = new InternalConfiguration(null, null, null, null, null, null, null, null, null); this.Given(_ => GivenAnExceptionWillNotBeThrownDownstream()) .And(_ => GivenTheConfigurationIs(config)) @@ -100,16 +104,6 @@ public void ShouldSetAspDotNetRequestId() .BDDfy(); } - [Fact] - public void should_throw_exception_if_config_provider_returns_error() - { - this.Given(_ => GivenAnExceptionWillNotBeThrownDownstream()) - .And(_ => GivenTheConfigReturnsError()) - .When(_ => WhenICallTheMiddlewareWithTheRequestIdKey("requestidkey", "1234")) - .Then(_ => ThenAnExceptionIsThrown()) - .BDDfy(); - } - [Fact] public void should_throw_exception_if_config_provider_throws() { @@ -122,32 +116,27 @@ public void should_throw_exception_if_config_provider_throws() private void WhenICallTheMiddlewareWithTheRequestIdKey(string key, string value) { - _downstreamContext.HttpContext.Request.Headers.Add(key, value); - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); + _httpContext.Request.Headers.Add(key, value); + /* + _httpContext.Setup(x => x.Request.Headers).Returns(new HeaderDictionary() { { key, value } }); + */ + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); } private void WhenICallTheMiddleware() { - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); } private void GivenTheConfigThrows() { - var ex = new Exception("outer", new Exception("inner")); - _configRepo - .Setup(x => x.Get()).Throws(ex); + // this will break when we handle not having the configuratio in the items dictionary + _httpContext.Items = new Dictionary(); } private void ThenAnExceptionIsThrown() { - _downstreamContext.HttpContext.Response.StatusCode.ShouldBe(500); - } - - private void GivenTheConfigReturnsError() - { - var response = new Responses.ErrorResponse(new FakeError()); - _configRepo - .Setup(x => x.Get()).Returns(response); + _httpContext.Response.StatusCode.ShouldBe(500); } private void TheRequestIdIsSet(string key, string value) @@ -157,9 +146,7 @@ private void TheRequestIdIsSet(string key, string value) private void GivenTheConfigurationIs(IInternalConfiguration config) { - var response = new Responses.OkResponse(config); - _configRepo - .Setup(x => x.Get()).Returns(response); + _httpContext.Items.Add("IInternalConfiguration", config); } private void GivenAnExceptionWillNotBeThrownDownstream() @@ -174,12 +161,12 @@ private void GivenAnExceptionWillBeThrownDownstream() private void ThenTheResponseIsOk() { - _downstreamContext.HttpContext.Response.StatusCode.ShouldBe(200); + _httpContext.Response.StatusCode.ShouldBe(200); } private void ThenTheResponseIsError() { - _downstreamContext.HttpContext.Response.StatusCode.ShouldBe(500); + _httpContext.Response.StatusCode.ShouldBe(500); } private void TheAspDotnetRequestIdIsSet() @@ -190,7 +177,7 @@ private void TheAspDotnetRequestIdIsSet() private class FakeError : Error { internal FakeError() - : base("meh", OcelotErrorCode.CannotAddDataError) + : base("meh", OcelotErrorCode.CannotAddDataError, 404) { } } diff --git a/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs b/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs index fc7f3c824..7f19408c3 100644 --- a/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs +++ b/test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs @@ -1,47 +1,47 @@ -namespace Ocelot.UnitTests.Eureka -{ - using Microsoft.AspNetCore.Builder; - using Microsoft.Extensions.DependencyInjection; - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Configuration.Repository; - using Provider.Eureka; - using Responses; - using Shouldly; - using Steeltoe.Common.Discovery; - using System.Threading.Tasks; - using Xunit; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Repository; +using Ocelot.Provider.Eureka; +using Ocelot.Responses; +using Shouldly; +using Steeltoe.Discovery; +using System.Threading.Tasks; +using Xunit; +namespace Ocelot.UnitTests.Eureka +{ public class EurekaMiddlewareConfigurationProviderTests { [Fact] - public void should_not_build() + public void ShouldNotBuild() { var configRepo = new Mock(); configRepo.Setup(x => x.Get()) - .Returns(new OkResponse(new InternalConfiguration(null, null, null, null, null, null, null, null))); + .Returns(new OkResponse(new InternalConfiguration(null, null, null, null, null, null, null, null, null))); var services = new ServiceCollection(); - services.AddSingleton(configRepo.Object); + services.AddSingleton(configRepo.Object); var sp = services.BuildServiceProvider(); var provider = EurekaMiddlewareConfigurationProvider.Get(new ApplicationBuilder(sp)); - provider.ShouldBeOfType(); + provider.Status.ShouldBe(TaskStatus.RanToCompletion); } [Fact] - public void should_build() + public void ShouldBuild() { var serviceProviderConfig = new ServiceProviderConfigurationBuilder().WithType("eureka").Build(); var client = new Mock(); var configRepo = new Mock(); configRepo.Setup(x => x.Get()) - .Returns(new OkResponse(new InternalConfiguration(null, null, serviceProviderConfig, null, null, null, null, null))); + .Returns(new OkResponse(new InternalConfiguration(null, null, serviceProviderConfig, null, null, null, null, null, null))); var services = new ServiceCollection(); - services.AddSingleton(configRepo.Object); - services.AddSingleton(client.Object); + services.AddSingleton(configRepo.Object); + services.AddSingleton(client.Object); var sp = services.BuildServiceProvider(); var provider = EurekaMiddlewareConfigurationProvider.Get(new ApplicationBuilder(sp)); - provider.ShouldBeOfType(); + provider.Status.ShouldBe(TaskStatus.RanToCompletion); } } } diff --git a/test/Ocelot.UnitTests/Eureka/EurekaProviderFactoryTests.cs b/test/Ocelot.UnitTests/Eureka/EurekaProviderFactoryTests.cs index 04e40db4a..119e6b328 100644 --- a/test/Ocelot.UnitTests/Eureka/EurekaProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/Eureka/EurekaProviderFactoryTests.cs @@ -1,37 +1,37 @@ -namespace Ocelot.UnitTests.Eureka -{ - using Microsoft.Extensions.DependencyInjection; - using Moq; - using Ocelot.Configuration.Builder; - using Provider.Eureka; - using Shouldly; - using Steeltoe.Common.Discovery; - using Xunit; - - public class EurekaProviderFactoryTests - { - [Fact] - public void should_not_get() - { - var config = new ServiceProviderConfigurationBuilder().Build(); - var sp = new ServiceCollection().BuildServiceProvider(); - var provider = EurekaProviderFactory.Get(sp, config, null); - provider.ShouldBeNull(); - } - - [Fact] - public void should_get() - { - var config = new ServiceProviderConfigurationBuilder().WithType("eureka").Build(); - var client = new Mock(); - var services = new ServiceCollection(); - services.AddSingleton(client.Object); - var sp = services.BuildServiceProvider(); - var reRoute = new DownstreamReRouteBuilder() - .WithServiceName("") - .Build(); - var provider = EurekaProviderFactory.Get(sp, config, reRoute); - provider.ShouldBeOfType(); - } - } -} +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Ocelot.Configuration.Builder; +using Ocelot.Provider.Eureka; +using Shouldly; +using Steeltoe.Discovery; +using Xunit; + +namespace Ocelot.UnitTests.Eureka +{ + public class EurekaProviderFactoryTests + { + [Fact] + public void should_not_get() + { + var config = new ServiceProviderConfigurationBuilder().Build(); + var sp = new ServiceCollection().BuildServiceProvider(); + var provider = EurekaProviderFactory.Get(sp, config, null); + provider.ShouldBeNull(); + } + + [Fact] + public void should_get() + { + var config = new ServiceProviderConfigurationBuilder().WithType("eureka").Build(); + var client = new Mock(); + var services = new ServiceCollection(); + services.AddSingleton(client.Object); + var sp = services.BuildServiceProvider(); + var route = new DownstreamRouteBuilder() + .WithServiceName(string.Empty) + .Build(); + var provider = EurekaProviderFactory.Get(sp, config, route); + provider.ShouldBeOfType(); + } + } +} diff --git a/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs index ec332605e..b6162b235 100644 --- a/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs @@ -1,117 +1,118 @@ -namespace Ocelot.UnitTests.Eureka -{ - using Moq; - using Provider.Eureka; - using Shouldly; - using Steeltoe.Common.Discovery; - using System; - using System.Collections.Generic; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Values; - using Xunit; - - public class EurekaServiceDiscoveryProviderTests - { - private readonly Eureka _provider; - private readonly Mock _client; - private readonly string _serviceId; - private List _instances; - private List _result; - - public EurekaServiceDiscoveryProviderTests() - { - _serviceId = "Laura"; - _client = new Mock(); - _provider = new Eureka(_serviceId, _client.Object); - } - - [Fact] - public void should_return_empty_services() - { - this.When(_ => WhenIGet()) - .Then(_ => ThenTheCountIs(0)) - .BDDfy(); - } - - [Fact] - public void should_return_service_from_client() - { - var instances = new List - { - new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()) - }; - - this.Given(_ => GivenThe(instances)) - .When(_ => WhenIGet()) - .Then(_ => ThenTheCountIs(1)) - .And(_ => ThenTheClientIsCalledCorrectly()) - .And(_ => ThenTheServiceIsMapped()) - .BDDfy(); - } - - [Fact] - public void should_return_services_from_client() - { - var instances = new List - { - new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()), - new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()) - }; - - this.Given(_ => GivenThe(instances)) - .When(_ => WhenIGet()) - .Then(_ => ThenTheCountIs(2)) - .And(_ => ThenTheClientIsCalledCorrectly()) - .BDDfy(); - } - - private void ThenTheServiceIsMapped() - { - _result[0].HostAndPort.DownstreamHost.ShouldBe("somehost"); - _result[0].HostAndPort.DownstreamPort.ShouldBe(801); - _result[0].Name.ShouldBe(_serviceId); - } - - private void ThenTheCountIs(int expected) - { - _result.Count.ShouldBe(expected); - } - - private void ThenTheClientIsCalledCorrectly() - { - _client.Verify(x => x.GetInstances(_serviceId), Times.Once); - } - - private async Task WhenIGet() - { - _result = await _provider.Get(); - } - - private void GivenThe(List instances) - { - _instances = instances; - _client.Setup(x => x.GetInstances(It.IsAny())).Returns(instances); - } - } - - public class EurekaService : IServiceInstance - { - public EurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary metadata) - { - ServiceId = serviceId; - Host = host; - Port = port; - IsSecure = isSecure; - Uri = uri; - Metadata = metadata; - } - - public string ServiceId { get; } - public string Host { get; } - public int Port { get; } - public bool IsSecure { get; } - public Uri Uri { get; } - public IDictionary Metadata { get; } - } -} +using Moq; +using Ocelot.Values; +using Shouldly; +using Steeltoe.Common.Discovery; +using Steeltoe.Discovery; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using TestStack.BDDfy; +using Xunit; +using _Eureka_ = Ocelot.Provider.Eureka.Eureka; + +namespace Ocelot.UnitTests.Eureka +{ + public class EurekaServiceDiscoveryProviderTests + { + private readonly _Eureka_ _provider; + private readonly Mock _client; + private readonly string _serviceId; + private List _instances; + private List _result; + + public EurekaServiceDiscoveryProviderTests() + { + _serviceId = "Laura"; + _client = new Mock(); + _provider = new _Eureka_(_serviceId, _client.Object); + } + + [Fact] + public void should_return_empty_services() + { + this.When(_ => WhenIGet()) + .Then(_ => ThenTheCountIs(0)) + .BDDfy(); + } + + [Fact] + public void should_return_service_from_client() + { + var instances = new List + { + new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()), + }; + + this.Given(_ => GivenThe(instances)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheCountIs(1)) + .And(_ => ThenTheClientIsCalledCorrectly()) + .And(_ => ThenTheServiceIsMapped()) + .BDDfy(); + } + + [Fact] + public void should_return_services_from_client() + { + var instances = new List + { + new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()), + new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()), + }; + + this.Given(_ => GivenThe(instances)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheCountIs(2)) + .And(_ => ThenTheClientIsCalledCorrectly()) + .BDDfy(); + } + + private void ThenTheServiceIsMapped() + { + _result[0].HostAndPort.DownstreamHost.ShouldBe("somehost"); + _result[0].HostAndPort.DownstreamPort.ShouldBe(801); + _result[0].Name.ShouldBe(_serviceId); + } + + private void ThenTheCountIs(int expected) + { + _result.Count.ShouldBe(expected); + } + + private void ThenTheClientIsCalledCorrectly() + { + _client.Verify(x => x.GetInstances(_serviceId), Times.Once); + } + + private async Task WhenIGet() + { + _result = await _provider.Get(); + } + + private void GivenThe(List instances) + { + _instances = instances; + _client.Setup(x => x.GetInstances(It.IsAny())).Returns(instances); + } + } + + public class EurekaService : IServiceInstance + { + public EurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary metadata) + { + ServiceId = serviceId; + Host = host; + Port = port; + IsSecure = isSecure; + Uri = uri; + Metadata = metadata; + } + + public string ServiceId { get; } + public string Host { get; } + public int Port { get; } + public bool IsSecure { get; } + public Uri Uri { get; } + public IDictionary Metadata { get; } + } +} diff --git a/test/Ocelot.UnitTests/Eureka/OcelotPipelineExtensionsTests.cs b/test/Ocelot.UnitTests/Eureka/OcelotPipelineExtensionsTests.cs index 9ee61f216..dca5ac637 100644 --- a/test/Ocelot.UnitTests/Eureka/OcelotPipelineExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Eureka/OcelotPipelineExtensionsTests.cs @@ -1,59 +1,59 @@ -namespace Ocelot.UnitTests.Eureka -{ - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Ocelot.DependencyInjection; - using Ocelot.Middleware; - using Ocelot.Middleware.Pipeline; - using Pivotal.Discovery.Client; - using Shouldly; - using Steeltoe.Common.Discovery; - using Steeltoe.Discovery.Eureka; - using TestStack.BDDfy; - using Xunit; +//namespace Ocelot.UnitTests.Eureka +//{ +// using Microsoft.Extensions.Configuration; +// using Microsoft.Extensions.DependencyInjection; +// using Ocelot.DependencyInjection; +// using Ocelot.Middleware; +// using Ocelot.Middleware.Pipeline; +// using Steeltoe.Discovery.Client; +// using Shouldly; +// using Steeltoe.Common.Discovery; +// using Steeltoe.Discovery.Eureka; +// using TestStack.BDDfy; +// using Xunit; - public class OcelotPipelineExtensionsTests - { - private OcelotPipelineBuilder _builder; - private OcelotRequestDelegate _handlers; +// public class OcelotPipelineExtensionsTests +// { +// private OcelotPipelineBuilder _builder; +// private OcelotRequestDelegate _handlers; - [Fact] - public void should_set_up_pipeline() - { - this.Given(_ => GivenTheDepedenciesAreSetUp()) - .When(_ => WhenIBuild()) - .Then(_ => ThenThePipelineIsBuilt()) - .BDDfy(); - } +// [Fact] +// public void should_set_up_pipeline() +// { +// this.Given(_ => GivenTheDepedenciesAreSetUp()) +// .When(_ => WhenIBuild()) +// .Then(_ => ThenThePipelineIsBuilt()) +// .BDDfy(); +// } - private void ThenThePipelineIsBuilt() - { - _handlers.ShouldNotBeNull(); - } +// private void ThenThePipelineIsBuilt() +// { +// _handlers.ShouldNotBeNull(); +// } - private void WhenIBuild() - { - _handlers = _builder.BuildOcelotPipeline(new OcelotPipelineConfiguration()); - } +// private void WhenIBuild() +// { +// _handlers = _builder.BuildOcelotPipeline(new OcelotPipelineConfiguration()); +// } - private void GivenTheDepedenciesAreSetUp() - { - IConfigurationBuilder test = new ConfigurationBuilder(); - var root = test.Build(); - var services = new ServiceCollection(); - services.AddSingleton(root); - services.AddDiscoveryClient(new DiscoveryOptions - { - ClientType = DiscoveryClientType.EUREKA, - ClientOptions = new EurekaClientOptions() - { - ShouldFetchRegistry = false, - ShouldRegisterWithEureka = false - } - }); - services.AddOcelot(); - var provider = services.BuildServiceProvider(); - _builder = new OcelotPipelineBuilder(provider); - } - } -} +// private void GivenTheDepedenciesAreSetUp() +// { +// IConfigurationBuilder test = new ConfigurationBuilder(); +// var root = test.Build(); +// var services = new ServiceCollection(); +// services.AddSingleton(root); +// services.AddDiscoveryClient(new DiscoveryOptions +// { +// ClientType = DiscoveryClientType.EUREKA, +// ClientOptions = new EurekaClientOptions() +// { +// ShouldFetchRegistry = false, +// ShouldRegisterWithEureka = false +// } +// }); +// services.AddOcelot(); +// var provider = services.BuildServiceProvider(); +// _builder = new OcelotPipelineBuilder(provider); +// } +// } +//} diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs index 392712c3b..d1e6ae808 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs @@ -1,23 +1,27 @@ -using Moq; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; + +using Moq; + using Ocelot.Configuration; using Ocelot.Errors; using Ocelot.Headers; using Ocelot.Infrastructure.Claims.Parser; using Ocelot.Request.Middleware; using Ocelot.Responses; + using Shouldly; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Security.Claims; + using TestStack.BDDfy; + using Xunit; +using Ocelot.Infrastructure; +using Ocelot.Logging; namespace Ocelot.UnitTests.Headers { - using Ocelot.Infrastructure; - using Ocelot.Logging; - public class AddHeadersToRequestClaimToThingTests { private readonly AddHeadersToRequest _addHeadersToRequest; @@ -27,8 +31,8 @@ public class AddHeadersToRequestClaimToThingTests private List _configuration; private Response _result; private Response _claimValue; - private Mock _placeholders; - private Mock _factory; + private readonly Mock _placeholders; + private readonly Mock _factory; public AddHeadersToRequestClaimToThingTests() { @@ -44,13 +48,13 @@ public void should_add_headers_to_downstreamRequest() { var claims = new List { - new Claim("test", "data") + new("test", "data"), }; this.Given( x => x.GivenConfigurationHeaderExtractorProperties(new List { - new ClaimToThing("header-key", "", "", 0) + new("header-key", string.Empty, string.Empty, 0), })) .Given(x => x.GivenClaims(claims)) .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) @@ -66,11 +70,11 @@ public void should_replace_existing_headers_on_request() this.Given( x => x.GivenConfigurationHeaderExtractorProperties(new List { - new ClaimToThing("header-key", "", "", 0) + new("header-key", string.Empty, string.Empty, 0), })) .Given(x => x.GivenClaims(new List { - new Claim("test", "data") + new("test", "data"), })) .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) .And(x => x.GivenThatTheRequestContainsHeader("header-key", "initial")) @@ -86,12 +90,12 @@ public void should_return_error() this.Given( x => x.GivenConfigurationHeaderExtractorProperties(new List { - new ClaimToThing("", "", "", 0) + new(string.Empty, string.Empty, string.Empty, 0), })) .Given(x => x.GivenClaims(new List())) .And(x => x.GivenTheClaimParserReturns(new ErrorResponse(new List { - new AnyError() + new AnyError(), }))) .When(x => x.WhenIAddHeadersToTheRequest()) .Then(x => x.ThenTheResultIsError()) @@ -150,7 +154,7 @@ private void ThenTheHeaderIsAdded() private class AnyError : Error { public AnyError() - : base("blahh", OcelotErrorCode.UnknownError) + : base("blahh", OcelotErrorCode.UnknownError, 404) { } } diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs index cc2916a2d..f86430f6b 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs @@ -1,128 +1,121 @@ -namespace Ocelot.UnitTests.Headers -{ - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Configuration.Creator; - using Ocelot.Headers; - using Ocelot.Infrastructure; - using Ocelot.Infrastructure.Claims.Parser; - using Ocelot.Logging; - using Responder; - using Responses; - using Shouldly; - using TestStack.BDDfy; - using Xunit; - - public class AddHeadersToRequestPlainTests - { - private readonly AddHeadersToRequest _addHeadersToRequest; - private HttpContext _context; - private AddHeader _addedHeader; - private readonly Mock _placeholders; - private Mock _factory; - private readonly Mock _logger; - - public AddHeadersToRequestPlainTests() - { - _placeholders = new Mock(); - _factory = new Mock(); - _logger = new Mock(); - _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _addHeadersToRequest = new AddHeadersToRequest(Mock.Of(), _placeholders.Object, _factory.Object); - } - - [Fact] - public void should_log_error_if_cannot_find_placeholder() - { - _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new ErrorResponse(new AnyError())); - - this.Given(_ => GivenHttpRequestWithoutHeaders()) - .When(_ => WhenAddingHeader("X-Forwarded-For", "{RemoteIdAddress}")) - .Then(_ => ThenAnErrorIsLogged("X-Forwarded-For", "{RemoteIdAddress}")) - .BDDfy(); - } - - [Fact] - public void should_add_placeholder_to_downstream_request() - { - _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse("replaced")); - - this.Given(_ => GivenHttpRequestWithoutHeaders()) - .When(_ => WhenAddingHeader("X-Forwarded-For", "{RemoteIdAddress}")) - .Then(_ => ThenTheHeaderGetsTakenOverToTheRequestHeaders("replaced")) - .BDDfy(); - } - - [Fact] - public void should_add_plain_text_header_to_downstream_request() - { - this.Given(_ => GivenHttpRequestWithoutHeaders()) - .When(_ => WhenAddingHeader("X-Custom-Header", "PlainValue")) - .Then(_ => ThenTheHeaderGetsTakenOverToTheRequestHeaders()) - .BDDfy(); - } - - [Fact] - public void should_overwrite_existing_header_with_added_header() - { - this.Given(_ => GivenHttpRequestWithHeader("X-Custom-Header", "This should get overwritten")) - .When(_ => WhenAddingHeader("X-Custom-Header", "PlainValue")) - .Then(_ => ThenTheHeaderGetsTakenOverToTheRequestHeaders()) - .BDDfy(); - } - - private void ThenAnErrorIsLogged(string key, string value) - { - _logger.Verify(x => x.LogWarning($"Unable to add header to response {key}: {value}"), Times.Once); - } - - private void GivenHttpRequestWithoutHeaders() - { - _context = new DefaultHttpContext - { - Request = - { - Headers = - { - } - } - }; - } - - private void GivenHttpRequestWithHeader(string headerKey, string headerValue) - { - _context = new DefaultHttpContext - { - Request = - { - Headers = - { - { headerKey, headerValue } - } - } - }; - } - - private void WhenAddingHeader(string headerKey, string headerValue) - { - _addedHeader = new AddHeader(headerKey, headerValue); - _addHeadersToRequest.SetHeadersOnDownstreamRequest(new[] { _addedHeader }, _context); - } - - private void ThenTheHeaderGetsTakenOverToTheRequestHeaders() - { - var requestHeaders = _context.Request.Headers; - requestHeaders.ContainsKey(_addedHeader.Key).ShouldBeTrue($"Header {_addedHeader.Key} was expected but not there."); - var value = requestHeaders[_addedHeader.Key]; - value.ShouldNotBeNull($"Value of header {_addedHeader.Key} was expected to not be null."); - value.ToString().ShouldBe(_addedHeader.Value); - } - - private void ThenTheHeaderGetsTakenOverToTheRequestHeaders(string expected) - { - var requestHeaders = _context.Request.Headers; - var value = requestHeaders[_addedHeader.Key]; - value.ToString().ShouldBe(expected); - } - } -} +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Moq; +using Ocelot.Configuration.Creator; +using Ocelot.Headers; +using Ocelot.Infrastructure; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Logging; +using Ocelot.Responses; +using Ocelot.UnitTests.Responder; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Headers +{ + public class AddHeadersToRequestPlainTests + { + private readonly AddHeadersToRequest _addHeadersToRequest; + private HttpContext _context; + private AddHeader _addedHeader; + private readonly Mock _placeholders; + private readonly Mock _factory; + private readonly Mock _logger; + + public AddHeadersToRequestPlainTests() + { + _placeholders = new Mock(); + _factory = new Mock(); + _logger = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _addHeadersToRequest = new AddHeadersToRequest(Mock.Of(), _placeholders.Object, _factory.Object); + } + + [Fact] + public void should_log_error_if_cannot_find_placeholder() + { + _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new ErrorResponse(new AnyError())); + + this.Given(_ => GivenHttpRequestWithoutHeaders()) + .When(_ => WhenAddingHeader("X-Forwarded-For", "{RemoteIdAddress}")) + .Then(_ => ThenAnErrorIsLogged("X-Forwarded-For", "{RemoteIdAddress}")) + .BDDfy(); + } + + [Fact] + public void should_add_placeholder_to_downstream_request() + { + _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse("replaced")); + + this.Given(_ => GivenHttpRequestWithoutHeaders()) + .When(_ => WhenAddingHeader("X-Forwarded-For", "{RemoteIdAddress}")) + .Then(_ => ThenTheHeaderGetsTakenOverToTheRequestHeaders("replaced")) + .BDDfy(); + } + + [Fact] + public void should_add_plain_text_header_to_downstream_request() + { + this.Given(_ => GivenHttpRequestWithoutHeaders()) + .When(_ => WhenAddingHeader("X-Custom-Header", "PlainValue")) + .Then(_ => ThenTheHeaderGetsTakenOverToTheRequestHeaders()) + .BDDfy(); + } + + [Fact] + public void should_overwrite_existing_header_with_added_header() + { + this.Given(_ => GivenHttpRequestWithHeader("X-Custom-Header", "This should get overwritten")) + .When(_ => WhenAddingHeader("X-Custom-Header", "PlainValue")) + .Then(_ => ThenTheHeaderGetsTakenOverToTheRequestHeaders()) + .BDDfy(); + } + + private void ThenAnErrorIsLogged(string key, string value) + { + _logger.Verify(x => x.LogWarning($"Unable to add header to response {key}: {value}"), Times.Once); + } + + private void GivenHttpRequestWithoutHeaders() + { + _context = new DefaultHttpContext(); + } + + private void GivenHttpRequestWithHeader(string headerKey, string headerValue) + { + _context = new DefaultHttpContext + { + Request = + { + Headers = + { + { headerKey, headerValue }, + }, + }, + }; + } + + private void WhenAddingHeader(string headerKey, string headerValue) + { + _addedHeader = new AddHeader(headerKey, headerValue); + _addHeadersToRequest.SetHeadersOnDownstreamRequest(new[] { _addedHeader }, _context); + } + + private void ThenTheHeaderGetsTakenOverToTheRequestHeaders() + { + var requestHeaders = _context.Request.Headers; + requestHeaders.ContainsKey(_addedHeader.Key).ShouldBeTrue($"Header {_addedHeader.Key} was expected but not there."); + var value = requestHeaders[_addedHeader.Key]; + value.ShouldNotBe(default(StringValues), $"Value of header {_addedHeader.Key} was expected to not be null."); + value.ToString().ShouldBe(_addedHeader.Value); + } + + private void ThenTheHeaderGetsTakenOverToTheRequestHeaders(string expected) + { + var requestHeaders = _context.Request.Headers; + var value = requestHeaders[_addedHeader.Key]; + value.ToString().ShouldBe(expected); + } + } +} diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs index d9c6bfa45..4c0960cb9 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs @@ -1,4 +1,9 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; + using Moq; + using Ocelot.Configuration.Creator; using Ocelot.Headers; using Ocelot.Infrastructure; @@ -6,11 +11,11 @@ using Ocelot.Middleware; using Ocelot.Responses; using Ocelot.UnitTests.Responder; + using Shouldly; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; + using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Headers @@ -21,7 +26,7 @@ public class AddHeadersToResponseTests private readonly Mock _placeholders; private DownstreamResponse _response; private List _addHeaders; - private Mock _factory; + private readonly Mock _factory; private readonly Mock _logger; public AddHeadersToResponseTests() @@ -38,7 +43,7 @@ public void should_add_header() { var addHeaders = new List { - new AddHeader("Laura", "Tom") + new("Laura", "Tom"), }; this.Given(_ => GivenAResponseMessage()) @@ -53,7 +58,7 @@ public void should_add_trace_id_placeholder() { var addHeaders = new List { - new AddHeader("Trace-Id", "{TraceId}") + new("Trace-Id", "{TraceId}"), }; var traceId = "123"; @@ -71,8 +76,8 @@ public void should_add_trace_id_placeholder_and_normal() { var addHeaders = new List { - new AddHeader("Trace-Id", "{TraceId}"), - new AddHeader("Tom", "Laura") + new("Trace-Id", "{TraceId}"), + new("Tom", "Laura"), }; var traceId = "123"; @@ -91,7 +96,7 @@ public void should_do_nothing_and_log_error() { var addHeaders = new List { - new AddHeader("Trace-Id", "{TraceId}") + new("Trace-Id", "{TraceId}"), }; this.Given(_ => GivenAResponseMessage()) diff --git a/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs index 20ec9b70a..ebca5c926 100644 --- a/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs @@ -1,56 +1,54 @@ -using Ocelot.Middleware; +using Microsoft.AspNetCore.Http; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Headers; +using Ocelot.Headers.Middleware; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.Request.Middleware; +using Ocelot.Responses; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using TestStack.BDDfy; +using Xunit; namespace Ocelot.UnitTests.Headers { - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.DownstreamRouteFinder; - using Ocelot.DownstreamRouteFinder.UrlMatcher; - using Ocelot.Headers; - using Ocelot.Headers.Middleware; - using Ocelot.Logging; - using Ocelot.Request.Middleware; - using Ocelot.Responses; - using System.Collections.Generic; - using System.Net.Http; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - public class ClaimsToHeadersMiddlewareTests { private readonly Mock _addHeaders; - private Response _downstreamRoute; - private Mock _loggerFactory; - private Mock _logger; - private ClaimsToHeadersMiddleware _middleware; - private DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; + private Response _downstreamRoute; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private readonly ClaimsToHeadersMiddleware _middleware; + private readonly RequestDelegate _next; + private readonly HttpContext _httpContext; public ClaimsToHeadersMiddlewareTests() { + _httpContext = new DefaultHttpContext(); _addHeaders = new Mock(); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _middleware = new ClaimsToHeadersMiddleware(_next, _loggerFactory.Object, _addHeaders.Object); - _downstreamContext.DownstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); + _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com"))); } [Fact] public void should_call_add_headers_to_request_correctly() { - var downstreamRoute = new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() + var downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithClaimsToHeaders(new List { - new ClaimToThing("UserId", "Subject", "", 0) + new("UserId", "Subject", string.Empty, 0), }) .WithUpstreamHttpMethod(new List { "Get" }) .Build()) @@ -66,14 +64,16 @@ public void should_call_add_headers_to_request_correctly() private void WhenICallTheMiddleware() { - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); } - private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + private void GivenTheDownStreamRouteIs(Ocelot.DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute) { - _downstreamRoute = new OkResponse(downstreamRoute); - _downstreamContext.TemplatePlaceholderNameAndValues = downstreamRoute.TemplatePlaceholderNameAndValues; - _downstreamContext.DownstreamReRoute = downstreamRoute.ReRoute.DownstreamReRoute[0]; + _downstreamRoute = new OkResponse(downstreamRoute); + + _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + + _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); } private void GivenTheAddHeadersToDownstreamRequestReturnsOk() @@ -92,7 +92,7 @@ private void ThenTheAddHeadersToRequestIsCalledCorrectly() .Verify(x => x.SetHeadersOnDownstreamRequest( It.IsAny>(), It.IsAny>(), - _downstreamContext.DownstreamRequest), Times.Once); + _httpContext.Items.DownstreamRequest()), Times.Once); } } } diff --git a/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs b/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs index e3ff5050b..0b04883b6 100644 --- a/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs @@ -1,10 +1,15 @@ -using Microsoft.AspNetCore.Http; +using System.Collections.Generic; + +using Microsoft.AspNetCore.Http; + using Ocelot.Configuration; using Ocelot.Headers; -using Ocelot.Responses; -using Shouldly; -using System.Collections.Generic; -using TestStack.BDDfy; +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Headers @@ -13,7 +18,7 @@ public class HttpContextRequestHeaderReplacerTests { private HttpContext _context; private List _fAndRs; - private HttpContextRequestHeaderReplacer _replacer; + private readonly HttpContextRequestHeaderReplacer _replacer; private Response _result; public HttpContextRequestHeaderReplacerTests() @@ -27,8 +32,7 @@ public void should_replace_headers() var context = new DefaultHttpContext(); context.Request.Headers.Add("test", "test"); - var fAndRs = new List(); - fAndRs.Add(new HeaderFindAndReplace("test", "test", "chiken", 0)); + var fAndRs = new List { new("test", "test", "chiken", 0) }; this.Given(x => GivenTheFollowingHttpRequest(context)) .And(x => GivenTheFollowingHeaderReplacements(fAndRs)) diff --git a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs index aa00cc34f..0ce56233a 100644 --- a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs @@ -1,119 +1,123 @@ -namespace Ocelot.UnitTests.Headers -{ - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Authorisation.Middleware; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.DownstreamRouteFinder; - using Ocelot.Headers; - using Ocelot.Headers.Middleware; - using Ocelot.Logging; - using Ocelot.Middleware; - using Ocelot.Request.Middleware; - using System.Collections.Generic; - using System.Net.Http; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - - public class HttpHeadersTransformationMiddlewareTests - { - private readonly Mock _preReplacer; - private readonly Mock _postReplacer; - private Mock _loggerFactory; - private Mock _logger; - private readonly HttpHeadersTransformationMiddleware _middleware; - private readonly DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; - private readonly Mock _addHeadersToResponse; - private readonly Mock _addHeadersToRequest; - - public HttpHeadersTransformationMiddlewareTests() - { - _preReplacer = new Mock(); - _postReplacer = new Mock(); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _next = context => Task.CompletedTask; - _addHeadersToResponse = new Mock(); - _addHeadersToRequest = new Mock(); - _middleware = new HttpHeadersTransformationMiddleware( - _next, _loggerFactory.Object, _preReplacer.Object, - _postReplacer.Object, _addHeadersToResponse.Object, _addHeadersToRequest.Object); - } - - [Fact] - public void should_call_pre_and_post_header_transforms() - { - this.Given(x => GivenTheFollowingRequest()) - .And(x => GivenTheDownstreamRequestIs()) - .And(x => GivenTheReRouteHasPreFindAndReplaceSetUp()) - .And(x => GivenTheHttpResponseMessageIs()) - .When(x => WhenICallTheMiddleware()) - .Then(x => ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly()) - .Then(x => ThenAddHeadersToRequestIsCalledCorrectly()) - .And(x => ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly()) - .And(x => ThenAddHeadersToResponseIsCalledCorrectly()) - .BDDfy(); - } - - private void ThenAddHeadersToResponseIsCalledCorrectly() - { - _addHeadersToResponse - .Verify(x => x.Add(_downstreamContext.DownstreamReRoute.AddHeadersToDownstream, _downstreamContext.DownstreamResponse), Times.Once); - } - - private void ThenAddHeadersToRequestIsCalledCorrectly() - { - _addHeadersToRequest - .Verify(x => x.SetHeadersOnDownstreamRequest(_downstreamContext.DownstreamReRoute.AddHeadersToUpstream, _downstreamContext.HttpContext), Times.Once); - } - - private void WhenICallTheMiddleware() - { - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); - } - - private void GivenTheDownstreamRequestIs() - { - _downstreamContext.DownstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); - } - - private void GivenTheHttpResponseMessageIs() - { - _downstreamContext.DownstreamResponse = new DownstreamResponse(new HttpResponseMessage()); - } - - private void GivenTheReRouteHasPreFindAndReplaceSetUp() - { - var fAndRs = new List(); - var reRoute = new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder().WithUpstreamHeaderFindAndReplace(fAndRs) - .WithDownstreamHeaderFindAndReplace(fAndRs).Build()) - .Build(); - - var dR = new DownstreamRoute(null, reRoute); - - _downstreamContext.TemplatePlaceholderNameAndValues = dR.TemplatePlaceholderNameAndValues; - _downstreamContext.DownstreamReRoute = dR.ReRoute.DownstreamReRoute[0]; - } - - private void ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly() - { - _preReplacer.Verify(x => x.Replace(It.IsAny(), It.IsAny>()), Times.Once); - } - - private void ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly() - { - _postReplacer.Verify(x => x.Replace(It.IsAny(), It.IsAny>()), Times.Once); - } - - private void GivenTheFollowingRequest() - { - _downstreamContext.HttpContext.Request.Headers.Add("test", "test"); - } - } +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.Authorization.Middleware; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Headers; +using Ocelot.Headers.Middleware; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.Request.Middleware; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Headers +{ + public class HttpHeadersTransformationMiddlewareTests + { + private readonly Mock _preReplacer; + private readonly Mock _postReplacer; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private readonly HttpHeadersTransformationMiddleware _middleware; + private readonly RequestDelegate _next; + private readonly Mock _addHeadersToResponse; + private readonly Mock _addHeadersToRequest; + private readonly HttpContext _httpContext; + + public HttpHeadersTransformationMiddlewareTests() + { + _httpContext = new DefaultHttpContext(); + _preReplacer = new Mock(); + _postReplacer = new Mock(); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _next = context => Task.CompletedTask; + _addHeadersToResponse = new Mock(); + _addHeadersToRequest = new Mock(); + _middleware = new HttpHeadersTransformationMiddleware( + _next, _loggerFactory.Object, _preReplacer.Object, + _postReplacer.Object, _addHeadersToResponse.Object, _addHeadersToRequest.Object); + } + + [Fact] + public void should_call_pre_and_post_header_transforms() + { + this.Given(x => GivenTheFollowingRequest()) + .And(x => GivenTheDownstreamRequestIs()) + .And(x => GivenTheRouteHasPreFindAndReplaceSetUp()) + .And(x => GivenTheHttpResponseMessageIs()) + .When(x => WhenICallTheMiddleware()) + .Then(x => ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly()) + .Then(x => ThenAddHeadersToRequestIsCalledCorrectly()) + .And(x => ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly()) + .And(x => ThenAddHeadersToResponseIsCalledCorrectly()) + .BDDfy(); + } + + private void ThenAddHeadersToResponseIsCalledCorrectly() + { + _addHeadersToResponse + .Verify(x => x.Add(_httpContext.Items.DownstreamRoute().AddHeadersToDownstream, _httpContext.Items.DownstreamResponse()), Times.Once); + } + + private void ThenAddHeadersToRequestIsCalledCorrectly() + { + _addHeadersToRequest + .Verify(x => x.SetHeadersOnDownstreamRequest(_httpContext.Items.DownstreamRoute().AddHeadersToUpstream, _httpContext), Times.Once); + } + + private void WhenICallTheMiddleware() + { + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); + } + + private void GivenTheDownstreamRequestIs() + { + _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com"))); + } + + private void GivenTheHttpResponseMessageIs() + { + _httpContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new HttpResponseMessage())); + } + + private void GivenTheRouteHasPreFindAndReplaceSetUp() + { + var fAndRs = new List(); + var route = new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder().WithUpstreamHeaderFindAndReplace(fAndRs) + .WithDownstreamHeaderFindAndReplace(fAndRs).Build()) + .Build(); + + var dR = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(null, route); + + _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(dR.TemplatePlaceholderNameAndValues); + _httpContext.Items.UpsertDownstreamRoute(dR.Route.DownstreamRoute[0]); + } + + private void ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly() + { + _preReplacer.Verify(x => x.Replace(It.IsAny(), It.IsAny>()), Times.Once); + } + + private void ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly() + { + _postReplacer.Verify(x => x.Replace(It.IsAny(), It.IsAny>()), Times.Once); + } + + private void GivenTheFollowingRequest() + { + _httpContext.Request.Headers.Add("test", "test"); + } + } } diff --git a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs index f21e9c23f..97530526a 100644 --- a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs @@ -1,33 +1,33 @@ +using Microsoft.AspNetCore.Http; +using Moq; +using Ocelot.Configuration; +using Ocelot.Headers; +using Ocelot.Infrastructure; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Middleware; +using Ocelot.Request.Middleware; +using Ocelot.Responses; +using Shouldly; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using TestStack.BDDfy; +using Xunit; + namespace Ocelot.UnitTests.Headers { - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Configuration; - using Ocelot.Headers; - using Ocelot.Infrastructure; - using Ocelot.Infrastructure.RequestData; - using Ocelot.Middleware; - using Ocelot.Request.Middleware; - using Ocelot.Responses; - using Shouldly; - using System.Collections.Generic; - using System.Linq; - using System.Net; - using System.Net.Http; - using TestStack.BDDfy; - using Xunit; - public class HttpResponseHeaderReplacerTests { private DownstreamResponse _response; - private Placeholders _placeholders; + private readonly Placeholders _placeholders; private readonly HttpResponseHeaderReplacer _replacer; private List _headerFindAndReplaces; private Response _result; private DownstreamRequest _request; - private Mock _finder; - private Mock _repo; - private Mock _accessor; + private readonly Mock _finder; + private readonly Mock _repo; + private readonly Mock _accessor; public HttpResponseHeaderReplacerTests() { @@ -42,12 +42,12 @@ public HttpResponseHeaderReplacerTests() public void should_replace_headers() { var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, - new List>>() + new List>> { - new KeyValuePair>("test", new List {"test"}) - }, ""); + new("test", new List {"test"}), + }, string.Empty); - var fAndRs = new List { new HeaderFindAndReplace("test", "test", "chiken", 0) }; + var fAndRs = new List { new("test", "test", "chiken", 0) }; this.Given(x => GivenTheHttpResponse(response)) .And(x => GivenTheFollowingHeaderReplacements(fAndRs)) @@ -60,10 +60,10 @@ public void should_replace_headers() public void should_not_replace_headers() { var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, - new List>>() + new List>> { - new KeyValuePair>("test", new List {"test"}) - }, ""); + new("test", new List {"test"}), + }, string.Empty); var fAndRs = new List(); @@ -83,14 +83,14 @@ public void should_replace_downstream_base_url_with_ocelot_base_url() new HttpRequestMessage(HttpMethod.Get, "http://test.com") { RequestUri = new System.Uri(downstreamUrl) }; var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, - new List>>() + new List>> { - new KeyValuePair>("Location", new List {downstreamUrl}) - }, ""); + new("Location", new List {downstreamUrl}), + }, string.Empty); var fAndRs = new List { - new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0) + new("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0), }; this.Given(x => GivenTheHttpResponse(response)) @@ -110,14 +110,14 @@ public void should_replace_downstream_base_url_with_ocelot_base_url_with_port() new HttpRequestMessage(HttpMethod.Get, "http://test.com") { RequestUri = new System.Uri(downstreamUrl) }; var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, - new List>>() + new List>> { - new KeyValuePair>("Location", new List {downstreamUrl}) - }, ""); + new("Location", new List {downstreamUrl}), + }, string.Empty); var fAndRs = new List { - new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com:123/", 0) + new("Location", "{DownstreamBaseUrl}", "http://ocelot.com:123/", 0), }; this.Given(x => GivenTheHttpResponse(response)) @@ -137,14 +137,14 @@ public void should_replace_downstream_base_url_with_ocelot_base_url_and_path() new HttpRequestMessage(HttpMethod.Get, "http://test.com") { RequestUri = new System.Uri(downstreamUrl) }; var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, - new List>>() + new List>> { - new KeyValuePair>("Location", new List {downstreamUrl}) - }, ""); + new("Location", new List {downstreamUrl}), + }, string.Empty); var fAndRs = new List { - new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0) + new("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0), }; this.Given(x => GivenTheHttpResponse(response)) @@ -164,14 +164,14 @@ public void should_replace_downstream_base_url_with_ocelot_base_url_with_path_an new HttpRequestMessage(HttpMethod.Get, "http://test.com") { RequestUri = new System.Uri(downstreamUrl) }; var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, - new List>>() + new List>> { - new KeyValuePair>("Location", new List {downstreamUrl}) - }, ""); + new("Location", new List {downstreamUrl}), + }, string.Empty); var fAndRs = new List { - new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com:123/", 0) + new("Location", "{DownstreamBaseUrl}", "http://ocelot.com:123/", 0), }; this.Given(x => GivenTheHttpResponse(response)) @@ -191,14 +191,14 @@ public void should_replace_downstream_base_url_and_port_with_ocelot_base_url() new HttpRequestMessage(HttpMethod.Get, "http://test.com") { RequestUri = new System.Uri(downstreamUrl) }; var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, - new List>>() + new List>> { - new KeyValuePair>("Location", new List {downstreamUrl}) - }, ""); + new("Location", new List {downstreamUrl}), + }, string.Empty); var fAndRs = new List { - new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0) + new("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0), }; this.Given(x => GivenTheHttpResponse(response)) @@ -218,14 +218,14 @@ public void should_replace_downstream_base_url_and_port_with_ocelot_base_url_and new HttpRequestMessage(HttpMethod.Get, "http://test.com") { RequestUri = new System.Uri(downstreamUrl) }; var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, - new List>>() + new List>> { - new KeyValuePair>("Location", new List {downstreamUrl}) - }, ""); + new("Location", new List {downstreamUrl}), + }, string.Empty); var fAndRs = new List { - new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com:321/", 0) + new("Location", "{DownstreamBaseUrl}", "http://ocelot.com:321/", 0), }; this.Given(x => GivenTheHttpResponse(response)) @@ -263,8 +263,11 @@ private void GivenTheHttpResponse(DownstreamResponse response) private void WhenICallTheReplacer() { - var context = new DownstreamContext(new DefaultHttpContext()) { DownstreamResponse = _response, DownstreamRequest = _request }; - _result = _replacer.Replace(context, _headerFindAndReplaces); + var httpContext = new DefaultHttpContext(); + httpContext.Items.UpsertDownstreamResponse(_response); + httpContext.Items.UpsertDownstreamRequest(_request); + + _result = _replacer.Replace(httpContext, _headerFindAndReplaces); } private void ThenTheHeaderShouldBe(string key, string value) diff --git a/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs b/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs index e0aa48063..5c76ef5dd 100644 --- a/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs +++ b/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs @@ -1,8 +1,12 @@ -using Ocelot.Middleware; -using Ocelot.Responses; -using Shouldly; -using System.Collections.Generic; -using TestStack.BDDfy; +using System.Collections.Generic; + +using Ocelot.Middleware; +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Headers @@ -21,9 +25,9 @@ public RemoveHeadersTests() [Fact] public void should_remove_header() { - var headers = new List
() + var headers = new List
{ - new Header("Transfer-Encoding", new List {"chunked"}) + new("Transfer-Encoding", new List {"chunked"}), }; this.Given(x => x.GivenAHttpContext(headers)) diff --git a/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs b/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs index 321e0a07a..4e4c6ce75 100644 --- a/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs @@ -1,15 +1,19 @@ -using Ocelot.Errors; +using Ocelot.Errors; +using System.Collections.Generic; +using System.Security.Claims; + +using Ocelot.Infrastructure.Claims.Parser; + +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; namespace Ocelot.UnitTests.Infrastructure { - using Ocelot.Infrastructure.Claims.Parser; - using Responses; - using Shouldly; - using System.Collections.Generic; - using System.Security.Claims; - using TestStack.BDDfy; - using Xunit; - public class ClaimParserTests { private readonly IClaimsParser _claimsParser; @@ -43,7 +47,7 @@ public void should_return_error_response_when_cannot_find_requested_claim() .When(x => x.WhenICallTheParser()) .Then(x => x.ThenTheResultIs(new ErrorResponse(new List { - new CannotFindClaimError($"Cannot find claim for key: {_key}") + new CannotFindClaimError($"Cannot find claim for key: {_key}"), }))) .BDDfy(); } @@ -70,7 +74,7 @@ public void should_return_error_response_if_index_too_large() .When(x => x.WhenICallTheParser()) .Then(x => x.ThenTheResultIs(new ErrorResponse(new List { - new CannotFindClaimError($"Cannot find claim for key: {_key}, delimiter: {_delimiter}, index: {_index}") + new CannotFindClaimError($"Cannot find claim for key: {_key}, delimiter: {_delimiter}, index: {_index}"), }))) .BDDfy(); } @@ -85,7 +89,7 @@ public void should_return_error_response_if_index_too_small() .When(x => x.WhenICallTheParser()) .Then(x => x.ThenTheResultIs(new ErrorResponse(new List { - new CannotFindClaimError($"Cannot find claim for key: {_key}, delimiter: {_delimiter}, index: {_index}") + new CannotFindClaimError($"Cannot find claim for key: {_key}, delimiter: {_delimiter}, index: {_index}"), }))) .BDDfy(); } diff --git a/test/Ocelot.UnitTests/Infrastructure/ConfigAwarePlaceholdersTests.cs b/test/Ocelot.UnitTests/Infrastructure/ConfigAwarePlaceholdersTests.cs index ed88fc9d6..d9681e50e 100644 --- a/test/Ocelot.UnitTests/Infrastructure/ConfigAwarePlaceholdersTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/ConfigAwarePlaceholdersTests.cs @@ -1,13 +1,19 @@ +using System; + +using Microsoft.Extensions.Configuration; + +using Moq; + +using Ocelot.Infrastructure; + +using Ocelot.Responses; + +using Shouldly; + +using Xunit; + namespace Ocelot.UnitTests.Infrastructure { - using System; - using Moq; - using Ocelot.Infrastructure; - using Responses; - using Shouldly; - using Microsoft.Extensions.Configuration; - using Xunit; - public class ConfigAwarePlaceholdersTests { private readonly IPlaceholders _placeholders; @@ -28,7 +34,7 @@ public void should_return_value_from_underlying_placeholders() { var baseUrl = "http://www.bbc.co.uk"; const string key = "{BaseUrl}"; - + _basePlaceholders.Setup(x => x.Get(key)).Returns(new OkResponse(baseUrl)); var result = _placeholders.Get(key); result.Data.ShouldBe(baseUrl); @@ -38,9 +44,8 @@ public void should_return_value_from_underlying_placeholders() public void should_return_value_from_config_with_same_name_as_placeholder_if_underlying_placeholder_not_found() { const string expected = "http://foo-bar.co.uk"; - var baseUrl = "http://www.bbc.co.uk"; const string key = "{BaseUrl}"; - + _basePlaceholders.Setup(x => x.Get(key)).Returns(new ErrorResponse(new FakeError())); var result = _placeholders.Get(key); result.Data.ShouldBe(expected); @@ -52,7 +57,7 @@ public void should_return_value_from_config_with_same_name_as_placeholder_if_und public void should_return_value_from_config(string key) { const string expected = "foo"; - + _basePlaceholders.Setup(x => x.Get(key)).Returns(new ErrorResponse(new FakeError())); var result = _placeholders.Get(key); result.Data.ShouldBe(expected); diff --git a/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs b/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs index 0effbed5a..d5a386adc 100644 --- a/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs @@ -1,88 +1,92 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Infrastructure.RequestData; -using Ocelot.Responses; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Infrastructure -{ - public class HttpDataRepositoryTests - { - private readonly HttpContext _httpContext; - private IHttpContextAccessor _httpContextAccessor; - private readonly HttpDataRepository _httpDataRepository; - private object _result; - - public HttpDataRepositoryTests() - { - _httpContext = new DefaultHttpContext(); - _httpContextAccessor = new HttpContextAccessor { HttpContext = _httpContext }; - _httpDataRepository = new HttpDataRepository(_httpContextAccessor); - } - - /* - TODO - Additional tests -> Type mistmatch aka Add string, request int - TODO - Additional tests -> HttpContent null. This should never happen - */ - - [Fact] - public void get_returns_correct_key_from_http_context() - { - this.Given(x => x.GivenAHttpContextContaining("key", "string")) - .When(x => x.GetIsCalledWithKey("key")) - .Then(x => x.ThenTheResultIsAnOkResponse("string")) - .BDDfy(); - } - - [Fact] - public void get_returns_error_response_if_the_key_is_not_found() //Therefore does not return null - { - this.Given(x => x.GivenAHttpContextContaining("key", "string")) - .When(x => x.GetIsCalledWithKey("keyDoesNotExist")) - .Then(x => x.ThenTheResultIsAnErrorReposnse("string1")) - .BDDfy(); - } - - [Fact] - public void should_update() - { - this.Given(x => x.GivenAHttpContextContaining("key", "string")) - .And(x => x.UpdateIsCalledWith("key", "new string")) - .When(x => x.GetIsCalledWithKey("key")) - .Then(x => x.ThenTheResultIsAnOkResponse("new string")) - .BDDfy(); - } - - private void UpdateIsCalledWith(string key, string value) - { - _httpDataRepository.Update(key, value); - } - - private void GivenAHttpContextContaining(string key, object o) - { - _httpContext.Items.Add(key, o); - } - - private void GetIsCalledWithKey(string key) - { - _result = _httpDataRepository.Get(key); - } - - private void ThenTheResultIsAnErrorReposnse(object resultValue) - { - _result.ShouldBeOfType>(); - ((ErrorResponse)_result).Data.ShouldBeNull(); - ((ErrorResponse)_result).IsError.ShouldBe(true); - ((ErrorResponse)_result).Errors.ShouldHaveSingleItem() - .ShouldBeOfType() - .Message.ShouldStartWith("Unable to find data for key: "); - } - - private void ThenTheResultIsAnOkResponse(object resultValue) - { - _result.ShouldBeOfType>(); - ((OkResponse)_result).Data.ShouldBe(resultValue); - } - } +using Microsoft.AspNetCore.Http; + +using Ocelot.Infrastructure.RequestData; +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Infrastructure +{ + public class HttpDataRepositoryTests + { + private readonly HttpContext _httpContext; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly HttpDataRepository _httpDataRepository; + private object _result; + + public HttpDataRepositoryTests() + { + _httpContext = new DefaultHttpContext(); + _httpContextAccessor = new HttpContextAccessor { HttpContext = _httpContext }; + _httpDataRepository = new HttpDataRepository(_httpContextAccessor); + } + + /* + TODO - Additional tests -> Type mistmatch aka Add string, request int + TODO - Additional tests -> HttpContent null. This should never happen + */ + + [Fact] + public void get_returns_correct_key_from_http_context() + { + this.Given(x => x.GivenAHttpContextContaining("key", "string")) + .When(x => x.GetIsCalledWithKey("key")) + .Then(x => x.ThenTheResultIsAnOkResponse("string")) + .BDDfy(); + } + + [Fact] + public void get_returns_error_response_if_the_key_is_not_found() //Therefore does not return null + { + this.Given(x => x.GivenAHttpContextContaining("key", "string")) + .When(x => x.GetIsCalledWithKey("keyDoesNotExist")) + .Then(x => x.ThenTheResultIsAnErrorReposnse("string1")) + .BDDfy(); + } + + [Fact] + public void should_update() + { + this.Given(x => x.GivenAHttpContextContaining("key", "string")) + .And(x => x.UpdateIsCalledWith("key", "new string")) + .When(x => x.GetIsCalledWithKey("key")) + .Then(x => x.ThenTheResultIsAnOkResponse("new string")) + .BDDfy(); + } + + private void UpdateIsCalledWith(string key, string value) + { + _httpDataRepository.Update(key, value); + } + + private void GivenAHttpContextContaining(string key, object o) + { + _httpContext.Items.Add(key, o); + } + + private void GetIsCalledWithKey(string key) + { + _result = _httpDataRepository.Get(key); + } + + private void ThenTheResultIsAnErrorReposnse(object resultValue) + { + _result.ShouldBeOfType>(); + ((ErrorResponse)_result).Data.ShouldBe(default(T)); + ((ErrorResponse)_result).IsError.ShouldBe(true); + ((ErrorResponse)_result).Errors.ShouldHaveSingleItem() + .ShouldBeOfType() + .Message.ShouldStartWith("Unable to find data for key: "); + } + + private void ThenTheResultIsAnOkResponse(object resultValue) + { + _result.ShouldBeOfType>(); + ((OkResponse)_result).Data.ShouldBe(resultValue); + } + } } diff --git a/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs b/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs index e69de29bb..8b1378917 100644 --- a/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs +++ b/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs @@ -0,0 +1 @@ + diff --git a/test/Ocelot.UnitTests/Infrastructure/InMemoryBusTests.cs b/test/Ocelot.UnitTests/Infrastructure/InMemoryBusTests.cs index a46cbe102..ec908bd13 100644 --- a/test/Ocelot.UnitTests/Infrastructure/InMemoryBusTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/InMemoryBusTests.cs @@ -1,6 +1,9 @@ +using System.Threading.Tasks; + using Ocelot.Infrastructure; + using Shouldly; -using System.Threading.Tasks; + using Xunit; namespace Ocelot.UnitTests.Infrastructure diff --git a/test/Ocelot.UnitTests/Infrastructure/PlaceholdersTests.cs b/test/Ocelot.UnitTests/Infrastructure/PlaceholdersTests.cs index ccd489023..bfc6430be 100644 --- a/test/Ocelot.UnitTests/Infrastructure/PlaceholdersTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/PlaceholdersTests.cs @@ -1,18 +1,24 @@ +using System; +using System.Net; +using System.Net.Http; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.Infrastructure; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Middleware; +using Ocelot.Request.Middleware; + +using Ocelot.Responses; + +using Shouldly; + +using Xunit; + namespace Ocelot.UnitTests.Infrastructure { - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Infrastructure; - using Ocelot.Infrastructure.RequestData; - using Ocelot.Middleware; - using Ocelot.Request.Middleware; - using Ocelot.Responses; - using Shouldly; - using System; - using System.Net; - using System.Net.Http; - using Xunit; - public class PlaceholdersTests { private readonly IPlaceholders _placeholders; @@ -40,7 +46,7 @@ public void should_return_base_url() [Fact] public void should_return_remote_ip_address() { - var httpContext = new DefaultHttpContext() { Connection = { RemoteIpAddress = IPAddress.Any } }; + var httpContext = new DefaultHttpContext { Connection = { RemoteIpAddress = IPAddress.Any } }; _accessor.Setup(x => x.HttpContext).Returns(httpContext); var result = _placeholders.Get("{RemoteIpAddress}"); result.Data.ShouldBe(httpContext.Connection.RemoteIpAddress.ToString()); @@ -123,5 +129,33 @@ public void should_return_error_when_removed() result.IsError.ShouldBeTrue(); result.Errors[0].Message.ShouldBe("Unable to remove placeholder: {Test}, placeholder does not exists"); } + + [Fact] + public void should_return_upstreamHost() + { + var upstreamHost = "UpstreamHostA"; + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers.Add("Host", upstreamHost); + _accessor.Setup(x => x.HttpContext).Returns(httpContext); + var result = _placeholders.Get("{UpstreamHost}"); + result.Data.ShouldBe(upstreamHost); + } + + [Fact] + public void should_return_error_when_finding_upstbecause_Host_not_set() + { + var httpContext = new DefaultHttpContext(); + _accessor.Setup(x => x.HttpContext).Returns(httpContext); + var result = _placeholders.Get("{UpstreamHost}"); + result.IsError.ShouldBeTrue(); + } + + [Fact] + public void should_return_error_when_finding_upstream_host_because_exception_thrown() + { + _accessor.Setup(x => x.HttpContext).Throws(new Exception()); + var result = _placeholders.Get("{UpstreamHost}"); + result.IsError.ShouldBeTrue(); + } } } diff --git a/test/Ocelot.UnitTests/Infrastructure/ScopesAuthoriserTests.cs b/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs similarity index 78% rename from test/Ocelot.UnitTests/Infrastructure/ScopesAuthoriserTests.cs rename to test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs index 76901f23e..0e2bb03d5 100644 --- a/test/Ocelot.UnitTests/Infrastructure/ScopesAuthoriserTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs @@ -1,123 +1,128 @@ -using Moq; -using Ocelot.Authorisation; -using Ocelot.Errors; -using Ocelot.Infrastructure.Claims.Parser; -using Ocelot.Responses; -using Shouldly; -using System.Collections.Generic; -using System.Security.Claims; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Infrastructure -{ - public class ScopesAuthoriserTests - { - private ScopesAuthoriser _authoriser; - public Mock _parser; - private ClaimsPrincipal _principal; - private List _allowedScopes; - private Response _result; - - public ScopesAuthoriserTests() - { - _parser = new Mock(); - _authoriser = new ScopesAuthoriser(_parser.Object); - } - - [Fact] - public void should_return_ok_if_no_allowed_scopes() - { - this.Given(_ => GivenTheFollowing(new ClaimsPrincipal())) - .And(_ => GivenTheFollowing(new List())) - .When(_ => WhenIAuthorise()) - .Then(_ => ThenTheFollowingIsReturned(new OkResponse(true))) - .BDDfy(); - } - - [Fact] - public void should_return_ok_if_null_allowed_scopes() - { - this.Given(_ => GivenTheFollowing(new ClaimsPrincipal())) - .And(_ => GivenTheFollowing((List)null)) - .When(_ => WhenIAuthorise()) - .Then(_ => ThenTheFollowingIsReturned(new OkResponse(true))) - .BDDfy(); - } - - [Fact] - public void should_return_error_if_claims_parser_returns_error() - { - var fakeError = new FakeError(); - this.Given(_ => GivenTheFollowing(new ClaimsPrincipal())) - .And(_ => GivenTheParserReturns(new ErrorResponse>(fakeError))) - .And(_ => GivenTheFollowing(new List() { "doesntmatter" })) - .When(_ => WhenIAuthorise()) - .Then(_ => ThenTheFollowingIsReturned(new ErrorResponse(fakeError))) - .BDDfy(); - } - - [Fact] - public void should_match_scopes_and_return_ok_result() - { - var claimsPrincipal = new ClaimsPrincipal(); - var allowedScopes = new List() { "someScope" }; - - this.Given(_ => GivenTheFollowing(claimsPrincipal)) - .And(_ => GivenTheParserReturns(new OkResponse>(allowedScopes))) - .And(_ => GivenTheFollowing(allowedScopes)) - .When(_ => WhenIAuthorise()) - .Then(_ => ThenTheFollowingIsReturned(new OkResponse(true))) - .BDDfy(); - } - - [Fact] - public void should_not_match_scopes_and_return_error_result() - { - var fakeError = new FakeError(); - var claimsPrincipal = new ClaimsPrincipal(); - var allowedScopes = new List() { "someScope" }; - var userScopes = new List() { "anotherScope" }; - - this.Given(_ => GivenTheFollowing(claimsPrincipal)) - .And(_ => GivenTheParserReturns(new OkResponse>(userScopes))) - .And(_ => GivenTheFollowing(allowedScopes)) - .When(_ => WhenIAuthorise()) - .Then(_ => ThenTheFollowingIsReturned(new ErrorResponse(fakeError))) - .BDDfy(); - } - - private void GivenTheParserReturns(Response> response) - { - _parser.Setup(x => x.GetValuesByClaimType(It.IsAny>(), It.IsAny())).Returns(response); - } - - private void GivenTheFollowing(ClaimsPrincipal principal) - { - _principal = principal; - } - - private void GivenTheFollowing(List allowedScopes) - { - _allowedScopes = allowedScopes; - } - - private void WhenIAuthorise() - { - _result = _authoriser.Authorise(_principal, _allowedScopes); - } - - private void ThenTheFollowingIsReturned(Response expected) - { - _result.Data.ShouldBe(expected.Data); - _result.IsError.ShouldBe(expected.IsError); - } +using System.Collections.Generic; +using System.Security.Claims; + +using Moq; + +using Ocelot.Authorization; +using Ocelot.Errors; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Infrastructure +{ + public class ScopesAuthorizerTests + { + private readonly ScopesAuthorizer _authorizer; + public Mock _parser; + private ClaimsPrincipal _principal; + private List _allowedScopes; + private Response _result; + + public ScopesAuthorizerTests() + { + _parser = new Mock(); + _authorizer = new ScopesAuthorizer(_parser.Object); + } + + [Fact] + public void should_return_ok_if_no_allowed_scopes() + { + this.Given(_ => GivenTheFollowing(new ClaimsPrincipal())) + .And(_ => GivenTheFollowing(new List())) + .When(_ => WhenIAuthorize()) + .Then(_ => ThenTheFollowingIsReturned(new OkResponse(true))) + .BDDfy(); + } + + [Fact] + public void should_return_ok_if_null_allowed_scopes() + { + this.Given(_ => GivenTheFollowing(new ClaimsPrincipal())) + .And(_ => GivenTheFollowing((List)null)) + .When(_ => WhenIAuthorize()) + .Then(_ => ThenTheFollowingIsReturned(new OkResponse(true))) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_claims_parser_returns_error() + { + var fakeError = new FakeError(); + this.Given(_ => GivenTheFollowing(new ClaimsPrincipal())) + .And(_ => GivenTheParserReturns(new ErrorResponse>(fakeError))) + .And(_ => GivenTheFollowing(new List { "doesntmatter" })) + .When(_ => WhenIAuthorize()) + .Then(_ => ThenTheFollowingIsReturned(new ErrorResponse(fakeError))) + .BDDfy(); + } + + [Fact] + public void should_match_scopes_and_return_ok_result() + { + var claimsPrincipal = new ClaimsPrincipal(); + var allowedScopes = new List { "someScope" }; + + this.Given(_ => GivenTheFollowing(claimsPrincipal)) + .And(_ => GivenTheParserReturns(new OkResponse>(allowedScopes))) + .And(_ => GivenTheFollowing(allowedScopes)) + .When(_ => WhenIAuthorize()) + .Then(_ => ThenTheFollowingIsReturned(new OkResponse(true))) + .BDDfy(); + } + + [Fact] + public void should_not_match_scopes_and_return_error_result() + { + var fakeError = new FakeError(); + var claimsPrincipal = new ClaimsPrincipal(); + var allowedScopes = new List { "someScope" }; + var userScopes = new List { "anotherScope" }; + + this.Given(_ => GivenTheFollowing(claimsPrincipal)) + .And(_ => GivenTheParserReturns(new OkResponse>(userScopes))) + .And(_ => GivenTheFollowing(allowedScopes)) + .When(_ => WhenIAuthorize()) + .Then(_ => ThenTheFollowingIsReturned(new ErrorResponse(fakeError))) + .BDDfy(); + } + + private void GivenTheParserReturns(Response> response) + { + _parser.Setup(x => x.GetValuesByClaimType(It.IsAny>(), It.IsAny())).Returns(response); + } + + private void GivenTheFollowing(ClaimsPrincipal principal) + { + _principal = principal; + } + + private void GivenTheFollowing(List allowedScopes) + { + _allowedScopes = allowedScopes; + } + + private void WhenIAuthorize() + { + _result = _authorizer.Authorize(_principal, _allowedScopes); + } + + private void ThenTheFollowingIsReturned(Response expected) + { + _result.Data.ShouldBe(expected.Data); + _result.IsError.ShouldBe(expected.IsError); + } } public class FakeError : Error { - public FakeError() : base("fake error", OcelotErrorCode.CannotAddDataError) + public FakeError() : base("fake error", OcelotErrorCode.CannotAddDataError, 404) { } - } + } } diff --git a/test/Ocelot.UnitTests/Infrastructure/StringExtensionsTests.cs b/test/Ocelot.UnitTests/Infrastructure/StringExtensionsTests.cs index 06d81a6f1..e7d02722d 100644 --- a/test/Ocelot.UnitTests/Infrastructure/StringExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/StringExtensionsTests.cs @@ -1,5 +1,7 @@ using Ocelot.Infrastructure.Extensions; + using Shouldly; + using Xunit; namespace Ocelot.UnitTests.Infrastructure diff --git a/test/Ocelot.UnitTests/Kubernetes/KubeServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Kubernetes/KubeServiceDiscoveryProviderTests.cs index 8534194d4..b98b80d02 100644 --- a/test/Ocelot.UnitTests/Kubernetes/KubeServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Kubernetes/KubeServiceDiscoveryProviderTests.cs @@ -1,153 +1,162 @@ -using KubeClient; -using KubeClient.Models; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Moq; -using Newtonsoft.Json; -using Ocelot.Logging; -using Ocelot.Provider.Kubernetes; -using Ocelot.Values; -using Shouldly; -using System; -using System.Collections.Generic; -using System.IO; +using System; +using System.Collections.Generic; +using System.IO; using System.Linq; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Kubernetes -{ - public class KubeServiceDiscoveryProviderTests : IDisposable - { - private IWebHost _fakeKubeBuilder; - private ServiceV1 _serviceEntries; - private Kube _provider; - private readonly string _serviceName; - private readonly string _namespaces; - private readonly int _port; - private readonly string _kubeHost; - private readonly string _fakekubeServiceDiscoveryUrl; - private List _services; - private readonly Mock _factory; - private readonly Mock _logger; - private string _receivedToken; - private readonly IKubeApiClient _clientFactory; - - public KubeServiceDiscoveryProviderTests() - { - _serviceName = "test"; - _namespaces = "dev"; - _port = 8001; - _kubeHost = "localhost"; - _fakekubeServiceDiscoveryUrl = $"http://{_kubeHost}:{_port}"; - _serviceEntries = new ServiceV1(); - _factory = new Mock(); - - var option = new KubeClientOptions - { - ApiEndPoint = new Uri(_fakekubeServiceDiscoveryUrl), - AccessToken = "txpc696iUhbVoudg164r93CxDTrKRVWG", - AuthStrategy = KubeClient.KubeAuthStrategy.BearerToken, - AllowInsecure = true - }; - _clientFactory = KubeApiClient.Create(option); - _logger = new Mock(); - _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - var config = new KubeRegistryConfiguration() - { - KeyOfServiceInK8s = _serviceName, - KubeNamespace = _namespaces - }; - _provider = new Kube(config, _factory.Object, _clientFactory); - } - - [Fact] - public void should_return_service_from_k8s() - { - var token = "Bearer txpc696iUhbVoudg164r93CxDTrKRVWG"; - var serviceEntryOne = new ServiceV1() - { - Kind = "service", - ApiVersion = "1.0", - Metadata = new ObjectMetaV1() - { - Namespace = "dev" - }, - Spec = new ServiceSpecV1() - { - ClusterIP = "localhost" - }, - Status = new ServiceStatusV1() - { - LoadBalancer = new LoadBalancerStatusV1() - } - }; - - serviceEntryOne.Spec.Ports.Add( - new ServicePortV1() - { - Port = 80 - } - ); - - this.Given(x => GivenThereIsAFakeKubeServiceDiscoveryProvider(_fakekubeServiceDiscoveryUrl, _serviceName, _namespaces)) - .And(x => GivenTheServicesAreRegisteredWithKube(serviceEntryOne)) - .When(x => WhenIGetTheServices()) - .Then(x => ThenTheCountIs(1)) - .And(_ => _receivedToken.ShouldBe(token)) - .BDDfy(); - } +using KubeClient; +using KubeClient.Models; - private void ThenTheCountIs(int count) - { - _services.Count.ShouldBe(count); - } +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; - private void WhenIGetTheServices() - { - _services = _provider.Get().GetAwaiter().GetResult(); - } +using Moq; - private void GivenTheServicesAreRegisteredWithKube(ServiceV1 serviceEntries) - { - _serviceEntries = serviceEntries; - } +using Newtonsoft.Json; - private void GivenThereIsAFakeKubeServiceDiscoveryProvider(string url, string serviceName, string namespaces) - { - _fakeKubeBuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.Run(async context => - { - if (context.Request.Path.Value == $"/api/v1/namespaces/{namespaces}/services/{serviceName}") - { - if (context.Request.Headers.TryGetValue("Authorization", out var values)) - { - _receivedToken = values.First(); - } +using Ocelot.Logging; +using Ocelot.Provider.Kubernetes; +using Ocelot.Values; - var json = JsonConvert.SerializeObject(_serviceEntries); - context.Response.Headers.Add("Content-Type", "application/json"); - await context.Response.WriteAsync(json); - } - }); - }) - .Build(); +using Shouldly; - _fakeKubeBuilder.Start(); - } +using TestStack.BDDfy; - public void Dispose() - { - _fakeKubeBuilder?.Dispose(); - } - } -} +using Xunit; + +namespace Ocelot.UnitTests.Kubernetes +{ + public class KubeServiceDiscoveryProviderTests : IDisposable + { + private IWebHost _fakeKubeBuilder; + private readonly KubernetesServiceDiscoveryProvider _provider; + private EndpointsV1 _endpointEntries; + private readonly string _serviceName; + private readonly string _namespaces; + private readonly int _port; + private readonly string _kubeHost; + private readonly string _fakekubeServiceDiscoveryUrl; + private List _services; + private readonly Mock _factory; + private readonly Mock _logger; + private string _receivedToken; + private readonly IKubeApiClient _clientFactory; + + public KubeServiceDiscoveryProviderTests() + { + _serviceName = "test"; + _namespaces = "dev"; + _port = 5567; + _kubeHost = "localhost"; + _fakekubeServiceDiscoveryUrl = $"http://{_kubeHost}:{_port}"; + _endpointEntries = new EndpointsV1(); + _factory = new Mock(); + + var option = new KubeClientOptions + { + ApiEndPoint = new Uri(_fakekubeServiceDiscoveryUrl), + AccessToken = "txpc696iUhbVoudg164r93CxDTrKRVWG", + AuthStrategy = KubeAuthStrategy.BearerToken, + AllowInsecure = true, + }; + + _clientFactory = KubeApiClient.Create(option); + _logger = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + var config = new KubeRegistryConfiguration + { + KeyOfServiceInK8s = _serviceName, + KubeNamespace = _namespaces, + }; + _provider = new KubernetesServiceDiscoveryProvider(config, _factory.Object, _clientFactory); + } + + [Fact] + public void should_return_service_from_k8s() + { + var token = "Bearer txpc696iUhbVoudg164r93CxDTrKRVWG"; + var endPointEntryOne = new EndpointsV1 + { + Kind = "endpoint", + ApiVersion = "1.0", + Metadata = new ObjectMetaV1 + { + Namespace = "dev", + }, + }; + var endpointSubsetV1 = new EndpointSubsetV1(); + endpointSubsetV1.Addresses.Add(new EndpointAddressV1 + { + Ip = "127.0.0.1", + Hostname = "localhost", + }); + endpointSubsetV1.Ports.Add(new EndpointPortV1 + { + Port = 80, + }); + endPointEntryOne.Subsets.Add(endpointSubsetV1); + + this.Given(x => GivenThereIsAFakeKubeServiceDiscoveryProvider(_fakekubeServiceDiscoveryUrl, _serviceName, _namespaces)) + .And(x => GivenTheServicesAreRegisteredWithKube(endPointEntryOne)) + .When(x => WhenIGetTheServices()) + .Then(x => ThenTheCountIs(1)) + .And(_ => ThenTheTokenIs(token)) + .BDDfy(); + } + + private void ThenTheTokenIs(string token) + { + _receivedToken.ShouldBe(token); + } + + private void ThenTheCountIs(int count) + { + _services.Count.ShouldBe(count); + } + + private void WhenIGetTheServices() + { + _services = _provider.Get().GetAwaiter().GetResult(); + } + + private void GivenTheServicesAreRegisteredWithKube(EndpointsV1 endpointEntries) + { + _endpointEntries = endpointEntries; + } + + private void GivenThereIsAFakeKubeServiceDiscoveryProvider(string url, string serviceName, string namespaces) + { + _fakeKubeBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + if (context.Request.Path.Value == $"/api/v1/namespaces/{namespaces}/endpoints/{serviceName}") + { + if (context.Request.Headers.TryGetValue("Authorization", out var values)) + { + _receivedToken = values.First(); + } + + var json = JsonConvert.SerializeObject(_endpointEntries); + context.Response.Headers.Add("Content-Type", "application/json"); + await context.Response.WriteAsync(json); + } + }); + }) + .Build(); + + _fakeKubeBuilder.Start(); + } + + public void Dispose() + { + _fakeKubeBuilder?.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs index f858c1beb..1e4a9f99f 100644 --- a/test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs @@ -1,14 +1,20 @@ -using Microsoft.AspNetCore.Hosting; +using System; +using System.Collections.Generic; +using System.Reflection; + +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; + using Moq; + using Ocelot.DependencyInjection; using Ocelot.Provider.Kubernetes; + using Shouldly; -using System; -using System.Collections.Generic; -using System.Reflection; + using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Kubernetes @@ -16,7 +22,6 @@ namespace Ocelot.UnitTests.Kubernetes public class OcelotBuilderExtensionsTests { private readonly IServiceCollection _services; - private IServiceProvider _serviceProvider; private readonly IConfiguration _configRoot; private IOcelotBuilder _ocelotBuilder; private Exception _ex; @@ -25,11 +30,11 @@ public OcelotBuilderExtensionsTests() { _configRoot = new ConfigurationRoot(new List()); _services = new ServiceCollection(); - _services.AddSingleton(GetHostingEnvironment()); + _services.AddSingleton(GetHostingEnvironment()); _services.AddSingleton(_configRoot); } - private IWebHostEnvironment GetHostingEnvironment() + private static IWebHostEnvironment GetHostingEnvironment() { var environment = new Mock(); environment diff --git a/test/Ocelot.UnitTests/Kubernetes/PollingKubeServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Kubernetes/PollingKubeServiceDiscoveryProviderTests.cs index 0009ec026..0d80adc89 100644 --- a/test/Ocelot.UnitTests/Kubernetes/PollingKubeServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Kubernetes/PollingKubeServiceDiscoveryProviderTests.cs @@ -1,13 +1,18 @@ -using Moq; +using System; +using System.Collections.Generic; + +using Moq; + using Ocelot.Infrastructure; using Ocelot.Logging; using Ocelot.Provider.Kubernetes; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; + using Shouldly; -using System; -using System.Collections.Generic; + using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Kubernetes @@ -15,7 +20,7 @@ namespace Ocelot.UnitTests.Kubernetes public class PollingKubeServiceDiscoveryProviderTests { private readonly int _delay; - private PollKube _provider; + private PollKubernetes _provider; private readonly List _services; private readonly Mock _factory; private readonly Mock _logger; @@ -28,14 +33,14 @@ public PollingKubeServiceDiscoveryProviderTests() _delay = 1; _factory = new Mock(); _logger = new Mock(); - _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _kubeServiceDiscoveryProvider = new Mock(); } [Fact] public void should_return_service_from_kube() { - var service = new Service("", new ServiceHostAndPort("", 0), "", "", new List()); + var service = new Service(string.Empty, new ServiceHostAndPort(string.Empty, 0), string.Empty, string.Empty, new List()); this.Given(x => GivenKubeReturns(service)) .When(x => WhenIGetTheServices(1)) @@ -56,7 +61,7 @@ private void ThenTheCountIs(int count) private void WhenIGetTheServices(int expected) { - _provider = new PollKube(_delay, _factory.Object, _kubeServiceDiscoveryProvider.Object); + _provider = new PollKubernetes(_delay, _factory.Object, _kubeServiceDiscoveryProvider.Object); var result = Wait.WaitFor(3000).Until(() => { diff --git a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs new file mode 100644 index 000000000..4f0a8e697 --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs @@ -0,0 +1,74 @@ +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Responses; +using Ocelot.ServiceDiscovery.Providers; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class CookieStickySessionsCreatorTests + { + private readonly CookieStickySessionsCreator _creator; + private readonly Mock _serviceProvider; + private DownstreamRoute _route; + private Response _loadBalancer; + private string _typeName; + + public CookieStickySessionsCreatorTests() + { + _creator = new CookieStickySessionsCreator(); + _serviceProvider = new Mock(); + } + + [Fact] + public void should_return_instance_of_expected_load_balancer_type() + { + var route = new DownstreamRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("myType", "myKey", 1000)) + .Build(); + + this.Given(x => x.GivenARoute(route)) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_expected_name() + { + this.When(x => x.WhenIGetTheLoadBalancerTypeName()) + .Then(x => x.ThenTheLoadBalancerTypeIs("CookieStickySessions")) + .BDDfy(); + } + + private void GivenARoute(DownstreamRoute route) + { + _route = route; + } + + private void WhenIGetTheLoadBalancer() + { + _loadBalancer = _creator.Create(_route, _serviceProvider.Object); + } + + private void WhenIGetTheLoadBalancerTypeName() + { + _typeName = _creator.Type; + } + + private void ThenTheLoadBalancerIsReturned() + where T : ILoadBalancer + { + _loadBalancer.Data.ShouldBeOfType(); + } + + private void ThenTheLoadBalancerTypeIs(string type) + { + _typeName.ShouldBe(type); + } + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs index 401c2db12..1a2983e25 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs @@ -1,39 +1,47 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.Infrastructure; +using Ocelot.LoadBalancer.LoadBalancers; + +using Ocelot.UnitTests.Responder; + +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Ocelot.Values; + +using Xunit; + namespace Ocelot.UnitTests.LoadBalancer { - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Infrastructure; - using Ocelot.LoadBalancer.LoadBalancers; - using Ocelot.Middleware; - using Ocelot.Responses; - using Ocelot.UnitTests.Responder; - using Ocelot.Values; - using Shouldly; - using System; - using System.Collections; - using System.Collections.Generic; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - public class CookieStickySessionsTests { private readonly CookieStickySessions _stickySessions; private readonly Mock _loadBalancer; private readonly int _defaultExpiryInMs; - private DownstreamContext _downstreamContext; private Response _result; private Response _firstHostAndPort; private Response _secondHostAndPort; private readonly FakeBus _bus; + private HttpContext _httpContext; public CookieStickySessionsTests() { + _httpContext = new DefaultHttpContext(); _bus = new FakeBus(); _loadBalancer = new Mock(); _defaultExpiryInMs = 0; _stickySessions = new CookieStickySessions(_loadBalancer.Object, "sessionid", _defaultExpiryInMs, _bus); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); } [Fact] @@ -89,7 +97,7 @@ public void should_return_error() [Fact] public void should_release() { - _stickySessions.Release(new ServiceHostAndPort("", 0)); + _stickySessions.Release(new ServiceHostAndPort(string.Empty, 0)); } private void ThenTheLoadBalancerIsCalled() @@ -116,7 +124,7 @@ private void ThenAnErrorIsReturned() private void GivenTheLoadBalancerReturnsError() { _loadBalancer - .Setup(x => x.Lease(It.IsAny())) + .Setup(x => x.Lease(It.IsAny())) .ReturnsAsync(new ErrorResponse(new AnyError())); } @@ -138,14 +146,14 @@ private async Task WhenIMakeTwoRequetsWithDifferentSessionValues() var cookiesTwo = new FakeCookies(); cookiesTwo.AddCookie("sessionid", "123"); contextTwo.Request.Cookies = cookiesTwo; - _firstHostAndPort = await _stickySessions.Lease(new DownstreamContext(contextOne)); - _secondHostAndPort = await _stickySessions.Lease(new DownstreamContext(contextTwo)); + _firstHostAndPort = await _stickySessions.Lease(contextOne); + _secondHostAndPort = await _stickySessions.Lease(contextTwo); } private void GivenTheLoadBalancerReturnsSequence() { _loadBalancer - .SetupSequence(x => x.Lease(It.IsAny())) + .SetupSequence(x => x.Lease(It.IsAny())) .ReturnsAsync(new OkResponse(new ServiceHostAndPort("one", 80))) .ReturnsAsync(new OkResponse(new ServiceHostAndPort("two", 80))); } @@ -158,8 +166,8 @@ private void ThenTheFirstAndSecondResponseAreTheSame() private async Task WhenILeaseTwiceInARow() { - _firstHostAndPort = await _stickySessions.Lease(_downstreamContext); - _secondHostAndPort = await _stickySessions.Lease(_downstreamContext); + _firstHostAndPort = await _stickySessions.Lease(_httpContext); + _secondHostAndPort = await _stickySessions.Lease(_httpContext); } private void GivenTheDownstreamRequestHasSessionId(string value) @@ -168,19 +176,19 @@ private void GivenTheDownstreamRequestHasSessionId(string value) var cookies = new FakeCookies(); cookies.AddCookie("sessionid", value); context.Request.Cookies = cookies; - _downstreamContext = new DownstreamContext(context); + _httpContext = context; } private void GivenTheLoadBalancerReturns() { _loadBalancer - .Setup(x => x.Lease(It.IsAny())) - .ReturnsAsync(new OkResponse(new ServiceHostAndPort("", 80))); + .Setup(x => x.Lease(It.IsAny())) + .ReturnsAsync(new OkResponse(new ServiceHostAndPort(string.Empty, 80))); } private async Task WhenILease() { - _result = await _stickySessions.Lease(_downstreamContext); + _result = await _stickySessions.Lease(_httpContext); } private void ThenTheHostAndPortIsNotNull() @@ -196,7 +204,7 @@ private void ThenTheStickySessionWillTimeout() internal class FakeCookies : IRequestCookieCollection { - private readonly Dictionary _cookies = new Dictionary(); + private readonly Dictionary _cookies = new(); public string this[string key] => _cookies[key]; diff --git a/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs new file mode 100644 index 000000000..9d16d552a --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs @@ -0,0 +1,132 @@ +using System; +using System.Threading.Tasks; + +using Moq; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Responses; +using Ocelot.ServiceDiscovery.Providers; +using Ocelot.Values; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; +using Microsoft.AspNetCore.Http; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class DelegateInvokingLoadBalancerCreatorTests + { + private DelegateInvokingLoadBalancerCreator _creator; + private Func _creatorFunc; + private readonly Mock _serviceProvider; + private DownstreamRoute _route; + private Response _loadBalancer; + private string _typeName; + + public DelegateInvokingLoadBalancerCreatorTests() + { + _creatorFunc = (route, serviceDiscoveryProvider) => + new FakeLoadBalancer(route, serviceDiscoveryProvider); + _creator = new DelegateInvokingLoadBalancerCreator(_creatorFunc); + _serviceProvider = new Mock(); + } + + [Fact] + public void should_return_expected_name() + { + this.When(x => x.WhenIGetTheLoadBalancerTypeName()) + .Then(x => x.ThenTheLoadBalancerTypeIs("FakeLoadBalancer")) + .BDDfy(); + } + + [Fact] + public void should_return_result_of_specified_creator_func() + { + var route = new DownstreamRouteBuilder() + .Build(); + + this.Given(x => x.GivenARoute(route)) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_error() + { + var route = new DownstreamRouteBuilder() + .Build(); + + this.Given(x => x.GivenARoute(route)) + .And(x => x.GivenTheCreatorFuncThrows()) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenAnErrorIsReturned()) + .BDDfy(); + } + + private void GivenTheCreatorFuncThrows() + { + _creatorFunc = (route, serviceDiscoveryProvider) => throw new Exception(); + + _creator = new DelegateInvokingLoadBalancerCreator(_creatorFunc); + } + + private void ThenAnErrorIsReturned() + { + _loadBalancer.IsError.ShouldBeTrue(); + } + + private void GivenARoute(DownstreamRoute route) + { + _route = route; + } + + private void WhenIGetTheLoadBalancer() + { + _loadBalancer = _creator.Create(_route, _serviceProvider.Object); + } + + private void WhenIGetTheLoadBalancerTypeName() + { + _typeName = _creator.Type; + } + + private void ThenTheLoadBalancerIsReturned() + where T : ILoadBalancer + { + _loadBalancer.Data.ShouldBeOfType(); + } + + private void ThenTheLoadBalancerTypeIs(string type) + { + _typeName.ShouldBe(type); + } + + private class FakeLoadBalancer : ILoadBalancer + { + public FakeLoadBalancer(DownstreamRoute downstreamRoute, IServiceDiscoveryProvider serviceDiscoveryProvider) + { + DownstreamRoute = downstreamRoute; + ServiceDiscoveryProvider = serviceDiscoveryProvider; + } + + public DownstreamRoute DownstreamRoute { get; } + public IServiceDiscoveryProvider ServiceDiscoveryProvider { get; } + + public Task> Lease(HttpContext httpContext) + { + throw new NotImplementedException(); + } + + public void Release(ServiceHostAndPort hostAndPort) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs new file mode 100644 index 000000000..87b1c82cd --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs @@ -0,0 +1,79 @@ +using Moq; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.ServiceDiscovery.Providers; + +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class LeastConnectionCreatorTests + { + private readonly LeastConnectionCreator _creator; + private readonly Mock _serviceProvider; + private DownstreamRoute _route; + private Response _loadBalancer; + private string _typeName; + + public LeastConnectionCreatorTests() + { + _creator = new LeastConnectionCreator(); + _serviceProvider = new Mock(); + } + + [Fact] + public void should_return_instance_of_expected_load_balancer_type() + { + var route = new DownstreamRouteBuilder() + .WithServiceName("myService") + .Build(); + + this.Given(x => x.GivenARoute(route)) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_expected_name() + { + this.When(x => x.WhenIGetTheLoadBalancerTypeName()) + .Then(x => x.ThenTheLoadBalancerTypeIs("LeastConnection")) + .BDDfy(); + } + + private void GivenARoute(DownstreamRoute route) + { + _route = route; + } + + private void WhenIGetTheLoadBalancer() + { + _loadBalancer = _creator.Create(_route, _serviceProvider.Object); + } + + private void WhenIGetTheLoadBalancerTypeName() + { + _typeName = _creator.Type; + } + + private void ThenTheLoadBalancerIsReturned() + where T : ILoadBalancer + { + _loadBalancer.Data.ShouldBeOfType(); + } + + private void ThenTheLoadBalancerTypeIs(string type) + { + _typeName.ShouldBe(type); + } + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs index 1f610dd13..2cb28f80b 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs @@ -1,13 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + using Microsoft.AspNetCore.Http; + using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.Middleware; using Ocelot.Responses; using Ocelot.Values; + using Shouldly; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; + using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.LoadBalancer @@ -18,12 +22,12 @@ public class LeastConnectionTests private Response _result; private LeastConnection _leastConnection; private List _services; - private Random _random; - private DownstreamContext _context; + private readonly Random _random; + private readonly HttpContext _httpContext; public LeastConnectionTests() { - _context = new DownstreamContext(new DefaultHttpContext()); + _httpContext = new DefaultHttpContext(); _random = new Random(); } @@ -34,8 +38,8 @@ public void should_be_able_to_lease_and_release_concurrently() var availableServices = new List { - new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), - new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + new(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), + new(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, Array.Empty()), }; _services = availableServices; @@ -58,40 +62,40 @@ public void should_handle_service_returning_to_available() var availableServices = new List { - new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), - new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + new(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), + new(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, Array.Empty()), }; _leastConnection = new LeastConnection(() => Task.FromResult(availableServices), serviceName); - var hostAndPortOne = _leastConnection.Lease(_context).Result; + var hostAndPortOne = _leastConnection.Lease(_httpContext).Result; hostAndPortOne.Data.DownstreamHost.ShouldBe("127.0.0.1"); - var hostAndPortTwo = _leastConnection.Lease(_context).Result; + var hostAndPortTwo = _leastConnection.Lease(_httpContext).Result; hostAndPortTwo.Data.DownstreamHost.ShouldBe("127.0.0.2"); _leastConnection.Release(hostAndPortOne.Data); _leastConnection.Release(hostAndPortTwo.Data); availableServices = new List { - new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + new(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), }; - hostAndPortOne = _leastConnection.Lease(_context).Result; + hostAndPortOne = _leastConnection.Lease(_httpContext).Result; hostAndPortOne.Data.DownstreamHost.ShouldBe("127.0.0.1"); - hostAndPortTwo = _leastConnection.Lease(_context).Result; + hostAndPortTwo = _leastConnection.Lease(_httpContext).Result; hostAndPortTwo.Data.DownstreamHost.ShouldBe("127.0.0.1"); _leastConnection.Release(hostAndPortOne.Data); _leastConnection.Release(hostAndPortTwo.Data); availableServices = new List { - new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), - new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + new(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), + new(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, Array.Empty()), }; - hostAndPortOne = _leastConnection.Lease(_context).Result; + hostAndPortOne = _leastConnection.Lease(_httpContext).Result; hostAndPortOne.Data.DownstreamHost.ShouldBe("127.0.0.1"); - hostAndPortTwo = _leastConnection.Lease(_context).Result; + hostAndPortTwo = _leastConnection.Lease(_httpContext).Result; hostAndPortTwo.Data.DownstreamHost.ShouldBe("127.0.0.2"); _leastConnection.Release(hostAndPortOne.Data); _leastConnection.Release(hostAndPortTwo.Data); @@ -99,7 +103,7 @@ public void should_handle_service_returning_to_available() private async Task LeaseDelayAndRelease() { - var hostAndPort = await _leastConnection.Lease(_context); + var hostAndPort = await _leastConnection.Lease(_httpContext); await Task.Delay(_random.Next(1, 100)); _leastConnection.Release(hostAndPort.Data); } @@ -113,7 +117,7 @@ public void should_get_next_url() var availableServices = new List { - new Service(serviceName, hostAndPort, string.Empty, string.Empty, new string[0]) + new(serviceName, hostAndPort, string.Empty, string.Empty, Array.Empty()), }; this.Given(x => x.GivenAHostAndPort(hostAndPort)) @@ -130,23 +134,23 @@ public void should_serve_from_service_with_least_connections() var availableServices = new List { - new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), - new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), - new Service(serviceName, new ServiceHostAndPort("127.0.0.3", 80), string.Empty, string.Empty, new string[0]) + new(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), + new(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, Array.Empty()), + new(serviceName, new ServiceHostAndPort("127.0.0.3", 80), string.Empty, string.Empty, Array.Empty()), }; _services = availableServices; _leastConnection = new LeastConnection(() => Task.FromResult(_services), serviceName); - var response = _leastConnection.Lease(_context).Result; + var response = _leastConnection.Lease(_httpContext).Result; response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); - response = _leastConnection.Lease(_context).Result; + response = _leastConnection.Lease(_httpContext).Result; response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); - response = _leastConnection.Lease(_context).Result; + response = _leastConnection.Lease(_httpContext).Result; response.Data.DownstreamHost.ShouldBe(availableServices[2].HostAndPort.DownstreamHost); } @@ -158,26 +162,26 @@ public void should_build_connections_per_service() var availableServices = new List { - new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), - new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + new(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), + new(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, Array.Empty()), }; _services = availableServices; _leastConnection = new LeastConnection(() => Task.FromResult(_services), serviceName); - var response = _leastConnection.Lease(_context).Result; + var response = _leastConnection.Lease(_httpContext).Result; response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); - response = _leastConnection.Lease(_context).Result; + response = _leastConnection.Lease(_httpContext).Result; response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); - response = _leastConnection.Lease(_context).Result; + response = _leastConnection.Lease(_httpContext).Result; response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); - response = _leastConnection.Lease(_context).Result; + response = _leastConnection.Lease(_httpContext).Result; response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); } @@ -189,33 +193,33 @@ public void should_release_connection() var availableServices = new List { - new Service(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), - new Service(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + new(serviceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), + new(serviceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, Array.Empty()), }; _services = availableServices; _leastConnection = new LeastConnection(() => Task.FromResult(_services), serviceName); - var response = _leastConnection.Lease(_context).Result; + var response = _leastConnection.Lease(_httpContext).Result; response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); - response = _leastConnection.Lease(_context).Result; + response = _leastConnection.Lease(_httpContext).Result; response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); - response = _leastConnection.Lease(_context).Result; + response = _leastConnection.Lease(_httpContext).Result; response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); - response = _leastConnection.Lease(_context).Result; + response = _leastConnection.Lease(_httpContext).Result; response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); //release this so 2 should have 1 connection and we should get 2 back as our next host and port _leastConnection.Release(availableServices[1].HostAndPort); - response = _leastConnection.Lease(_context).Result; + response = _leastConnection.Lease(_httpContext).Result; response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); } @@ -276,7 +280,7 @@ private void GivenAHostAndPort(ServiceHostAndPort hostAndPort) private void WhenIGetTheNextHostAndPort() { - _result = _leastConnection.Lease(_context).Result; + _result = _leastConnection.Lease(_httpContext).Result; } private void ThenTheNextHostAndPortIsReturned() diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs index 8df47cea0..702f87d11 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs @@ -1,23 +1,35 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + using Moq; + using Ocelot.Configuration; using Ocelot.Configuration.Builder; +using Ocelot.Infrastructure.RequestData; using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.Responses; using Ocelot.ServiceDiscovery; using Ocelot.ServiceDiscovery.Providers; +using Ocelot.Values; + using Shouldly; -using System.Collections.Generic; + using TestStack.BDDfy; + using Xunit; +using System; + +using Microsoft.AspNetCore.Http; namespace Ocelot.UnitTests.LoadBalancer { public class LoadBalancerFactoryTests { - private DownstreamReRoute _reRoute; + private DownstreamRoute _route; private readonly LoadBalancerFactory _factory; private Response _result; private readonly Mock _serviceProviderFactory; + private readonly IEnumerable _loadBalancerCreators; private readonly Mock _serviceProvider; private ServiceProviderConfiguration _serviceProviderConfig; @@ -25,65 +37,89 @@ public LoadBalancerFactoryTests() { _serviceProviderFactory = new Mock(); _serviceProvider = new Mock(); - _factory = new LoadBalancerFactory(_serviceProviderFactory.Object); + _loadBalancerCreators = new ILoadBalancerCreator[] + { + new FakeLoadBalancerCreator(), + new FakeLoadBalancerCreator(), + new FakeLoadBalancerCreator(nameof(NoLoadBalancer)), + new BrokenLoadBalancerCreator(), + }; + _factory = new LoadBalancerFactory(_serviceProviderFactory.Object, _loadBalancerCreators); } [Fact] - public void should_return_no_load_balancer() + public void should_return_no_load_balancer_by_default() { - var reRoute = new DownstreamReRouteBuilder() + var route = new DownstreamRouteBuilder() .WithUpstreamHttpMethod(new List { "Get" }) .Build(); - this.Given(x => x.GivenAReRoute(reRoute)) + this.Given(x => x.GivenARoute(route)) .And(x => GivenAServiceProviderConfig(new ServiceProviderConfigurationBuilder().Build())) .And(x => x.GivenTheServiceProviderFactoryReturns()) .When(x => x.WhenIGetTheLoadBalancer()) - .Then(x => x.ThenTheLoadBalancerIsReturned()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) .BDDfy(); } [Fact] - public void should_return_round_robin_load_balancer() + public void should_return_matching_load_balancer() { - var reRoute = new DownstreamReRouteBuilder() - .WithLoadBalancerOptions(new LoadBalancerOptions("RoundRobin", "", 0)) + var route = new DownstreamRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("FakeLoadBalancerTwo", string.Empty, 0)) .WithUpstreamHttpMethod(new List { "Get" }) .Build(); - this.Given(x => x.GivenAReRoute(reRoute)) + this.Given(x => x.GivenARoute(route)) .And(x => GivenAServiceProviderConfig(new ServiceProviderConfigurationBuilder().Build())) .And(x => x.GivenTheServiceProviderFactoryReturns()) .When(x => x.WhenIGetTheLoadBalancer()) - .Then(x => x.ThenTheLoadBalancerIsReturned()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) .BDDfy(); } [Fact] - public void should_return_round_least_connection_balancer() + public void should_return_error_response_if_cannot_find_load_balancer_creator() { - var reRoute = new DownstreamReRouteBuilder() - .WithLoadBalancerOptions(new LoadBalancerOptions("LeastConnection", "", 0)) + var route = new DownstreamRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("DoesntExistLoadBalancer", string.Empty, 0)) .WithUpstreamHttpMethod(new List { "Get" }) .Build(); - this.Given(x => x.GivenAReRoute(reRoute)) + this.Given(x => x.GivenARoute(route)) .And(x => GivenAServiceProviderConfig(new ServiceProviderConfigurationBuilder().Build())) .And(x => x.GivenTheServiceProviderFactoryReturns()) .When(x => x.WhenIGetTheLoadBalancer()) - .Then(x => x.ThenTheLoadBalancerIsReturned()) + .Then(x => x.ThenAnErrorResponseIsReturned()) + .And(x => x.ThenTheErrorMessageIsCorrect()) + .BDDfy(); + } + + [Fact] + public void should_return_error_response_if_creator_errors() + { + var route = new DownstreamRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("BrokenLoadBalancer", string.Empty, 0)) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build(); + + this.Given(x => x.GivenARoute(route)) + .And(x => GivenAServiceProviderConfig(new ServiceProviderConfigurationBuilder().Build())) + .And(x => x.GivenTheServiceProviderFactoryReturns()) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenAnErrorResponseIsReturned()) .BDDfy(); } [Fact] public void should_call_service_provider() { - var reRoute = new DownstreamReRouteBuilder() - .WithLoadBalancerOptions(new LoadBalancerOptions("RoundRobin", "", 0)) + var route = new DownstreamRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("FakeLoadBalancerOne", string.Empty, 0)) .WithUpstreamHttpMethod(new List { "Get" }) .Build(); - this.Given(x => x.GivenAReRoute(reRoute)) + this.Given(x => x.GivenARoute(route)) .And(x => GivenAServiceProviderConfig(new ServiceProviderConfigurationBuilder().Build())) .And(x => x.GivenTheServiceProviderFactoryReturns()) .When(x => x.WhenIGetTheLoadBalancer()) @@ -92,18 +128,18 @@ public void should_call_service_provider() } [Fact] - public void should_return_sticky_session() + public void should_return_error_response_when_call_to_service_provider_fails() { - var reRoute = new DownstreamReRouteBuilder() - .WithLoadBalancerOptions(new LoadBalancerOptions("CookieStickySessions", "", 0)) + var route = new DownstreamRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("FakeLoadBalancerOne", string.Empty, 0)) .WithUpstreamHttpMethod(new List { "Get" }) .Build(); - this.Given(x => x.GivenAReRoute(reRoute)) + this.Given(x => x.GivenARoute(route)) .And(x => GivenAServiceProviderConfig(new ServiceProviderConfigurationBuilder().Build())) - .And(x => x.GivenTheServiceProviderFactoryReturns()) + .And(x => x.GivenTheServiceProviderFactoryFails()) .When(x => x.WhenIGetTheLoadBalancer()) - .Then(x => x.ThenTheLoadBalancerIsReturned()) + .Then(x => x.ThenAnErrorResponseIsReturned()) .BDDfy(); } @@ -115,29 +151,135 @@ private void GivenAServiceProviderConfig(ServiceProviderConfiguration servicePro private void GivenTheServiceProviderFactoryReturns() { _serviceProviderFactory - .Setup(x => x.Get(It.IsAny(), It.IsAny())) + .Setup(x => x.Get(It.IsAny(), It.IsAny())) .Returns(new OkResponse(_serviceProvider.Object)); } + private void GivenTheServiceProviderFactoryFails() + { + _serviceProviderFactory + .Setup(x => x.Get(It.IsAny(), It.IsAny())) + .Returns(new ErrorResponse(new CannotFindDataError("For tests"))); + } + private void ThenTheServiceProviderIsCalledCorrectly() { _serviceProviderFactory - .Verify(x => x.Get(It.IsAny(), It.IsAny()), Times.Once); + .Verify(x => x.Get(It.IsAny(), It.IsAny()), Times.Once); } - private void GivenAReRoute(DownstreamReRoute reRoute) + private void GivenARoute(DownstreamRoute route) { - _reRoute = reRoute; + _route = route; } private void WhenIGetTheLoadBalancer() { - _result = _factory.Get(_reRoute, _serviceProviderConfig).Result; + _result = _factory.Get(_route, _serviceProviderConfig); } private void ThenTheLoadBalancerIsReturned() { _result.Data.ShouldBeOfType(); } + + private void ThenAnErrorResponseIsReturned() + { + _result.IsError.ShouldBeTrue(); + } + + private void ThenTheErrorMessageIsCorrect() + { + _result.Errors[0].Message.ShouldBe("Could not find load balancer creator for Type: DoesntExistLoadBalancer, please check your config specified the correct load balancer and that you have registered a class with the same name."); + } + + private class FakeLoadBalancerCreator : ILoadBalancerCreator + where T : ILoadBalancer, new() + { + public FakeLoadBalancerCreator() + { + Type = typeof(T).Name; + } + + public FakeLoadBalancerCreator(string type) + { + Type = type; + } + + public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) + { + return new OkResponse(new T()); + } + + public string Type { get; } + } + + private class BrokenLoadBalancerCreator : ILoadBalancerCreator + where T : ILoadBalancer, new() + { + public BrokenLoadBalancerCreator() + { + Type = typeof(T).Name; + } + + public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) + { + return new ErrorResponse(new ErrorInvokingLoadBalancerCreator(new Exception())); + } + + public string Type { get; } + } + + private class FakeLoadBalancerOne : ILoadBalancer + { + public Task> Lease(HttpContext httpContext) + { + throw new NotImplementedException(); + } + + public void Release(ServiceHostAndPort hostAndPort) + { + throw new NotImplementedException(); + } + } + + private class FakeLoadBalancerTwo : ILoadBalancer + { + public Task> Lease(HttpContext httpContext) + { + throw new NotImplementedException(); + } + + public void Release(ServiceHostAndPort hostAndPort) + { + throw new NotImplementedException(); + } + } + + private class FakeNoLoadBalancer : ILoadBalancer + { + public Task> Lease(HttpContext httpContext) + { + throw new NotImplementedException(); + } + + public void Release(ServiceHostAndPort hostAndPort) + { + throw new NotImplementedException(); + } + } + + private class BrokenLoadBalancer : ILoadBalancer + { + public Task> Lease(HttpContext httpContext) + { + throw new NotImplementedException(); + } + + public void Release(ServiceHostAndPort hostAndPort) + { + throw new NotImplementedException(); + } + } } } diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs index 57252b2bc..b11a9575c 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs @@ -1,21 +1,26 @@ -using Moq; +using System; +using System.Threading.Tasks; + +using Moq; + using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.Middleware; using Ocelot.Responses; using Ocelot.Values; + using Shouldly; -using System; -using System.Threading.Tasks; + using TestStack.BDDfy; + using Xunit; +using Microsoft.AspNetCore.Http; namespace Ocelot.UnitTests.LoadBalancer { public class LoadBalancerHouseTests { - private DownstreamReRoute _reRoute; + private DownstreamRoute _route; private ILoadBalancer _loadBalancer; private readonly LoadBalancerHouse _loadBalancerHouse; private Response _getResult; @@ -26,17 +31,17 @@ public LoadBalancerHouseTests() { _factory = new Mock(); _loadBalancerHouse = new LoadBalancerHouse(_factory.Object); - _serviceProviderConfig = new ServiceProviderConfiguration("myType", "myHost", 123, string.Empty, "configKey", 0); + _serviceProviderConfig = new ServiceProviderConfiguration("myType", "myScheme", "myHost", 123, string.Empty, "configKey", 0); } [Fact] public void should_store_load_balancer_on_first_request() { - var reRoute = new DownstreamReRouteBuilder() + var route = new DownstreamRouteBuilder() .WithLoadBalancerKey("test") .Build(); - this.Given(x => x.GivenThereIsALoadBalancer(reRoute, new FakeLoadBalancer())) + this.Given(x => x.GivenThereIsALoadBalancer(route, new FakeLoadBalancer())) .Then(x => x.ThenItIsAdded()) .BDDfy(); } @@ -44,13 +49,13 @@ public void should_store_load_balancer_on_first_request() [Fact] public void should_not_store_load_balancer_on_second_request() { - var reRoute = new DownstreamReRouteBuilder() - .WithLoadBalancerOptions(new LoadBalancerOptions("FakeLoadBalancer", "", 0)) + var route = new DownstreamRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("FakeLoadBalancer", string.Empty, 0)) .WithLoadBalancerKey("test") .Build(); - this.Given(x => x.GivenThereIsALoadBalancer(reRoute, new FakeLoadBalancer())) - .When(x => x.WhenWeGetTheLoadBalancer(reRoute)) + this.Given(x => x.GivenThereIsALoadBalancer(route, new FakeLoadBalancer())) + .When(x => x.WhenWeGetTheLoadBalancer(route)) .Then(x => x.ThenItIsReturned()) .BDDfy(); } @@ -58,21 +63,21 @@ public void should_not_store_load_balancer_on_second_request() [Fact] public void should_store_load_balancers_by_key() { - var reRoute = new DownstreamReRouteBuilder() - .WithLoadBalancerOptions(new LoadBalancerOptions("FakeLoadBalancer", "", 0)) + var route = new DownstreamRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("FakeLoadBalancer", string.Empty, 0)) .WithLoadBalancerKey("test") .Build(); - var reRouteTwo = new DownstreamReRouteBuilder() - .WithLoadBalancerOptions(new LoadBalancerOptions("FakeRoundRobinLoadBalancer", "", 0)) + var routeTwo = new DownstreamRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("FakeRoundRobinLoadBalancer", string.Empty, 0)) .WithLoadBalancerKey("testtwo") .Build(); - this.Given(x => x.GivenThereIsALoadBalancer(reRoute, new FakeLoadBalancer())) - .And(x => x.GivenThereIsALoadBalancer(reRouteTwo, new FakeRoundRobinLoadBalancer())) - .When(x => x.WhenWeGetTheLoadBalancer(reRoute)) + this.Given(x => x.GivenThereIsALoadBalancer(route, new FakeLoadBalancer())) + .And(x => x.GivenThereIsALoadBalancer(routeTwo, new FakeRoundRobinLoadBalancer())) + .When(x => x.WhenWeGetTheLoadBalancer(route)) .Then(x => x.ThenTheLoadBalancerIs()) - .When(x => x.WhenWeGetTheLoadBalancer(reRouteTwo)) + .When(x => x.WhenWeGetTheLoadBalancer(routeTwo)) .Then(x => x.ThenTheLoadBalancerIs()) .BDDfy(); } @@ -80,39 +85,39 @@ public void should_store_load_balancers_by_key() [Fact] public void should_return_error_if_exception() { - var reRoute = new DownstreamReRouteBuilder().Build(); + var route = new DownstreamRouteBuilder().Build(); - this.When(x => x.WhenWeGetTheLoadBalancer(reRoute)) + this.When(x => x.WhenWeGetTheLoadBalancer(route)) .Then(x => x.ThenAnErrorIsReturned()) .BDDfy(); } [Fact] - public void should_get_new_load_balancer_if_reroute_load_balancer_has_changed() + public void should_get_new_load_balancer_if_route_load_balancer_has_changed() { - var reRoute = new DownstreamReRouteBuilder() - .WithLoadBalancerOptions(new LoadBalancerOptions("FakeLoadBalancer", "", 0)) + var route = new DownstreamRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("FakeLoadBalancer", string.Empty, 0)) .WithLoadBalancerKey("test") .Build(); - var reRouteTwo = new DownstreamReRouteBuilder() - .WithLoadBalancerOptions(new LoadBalancerOptions("LeastConnection", "", 0)) + var routeTwo = new DownstreamRouteBuilder() + .WithLoadBalancerOptions(new LoadBalancerOptions("LeastConnection", string.Empty, 0)) .WithLoadBalancerKey("test") .Build(); - this.Given(x => x.GivenThereIsALoadBalancer(reRoute, new FakeLoadBalancer())) - .When(x => x.WhenWeGetTheLoadBalancer(reRoute)) + this.Given(x => x.GivenThereIsALoadBalancer(route, new FakeLoadBalancer())) + .When(x => x.WhenWeGetTheLoadBalancer(route)) .Then(x => x.ThenTheLoadBalancerIs()) - .When(x => x.WhenIGetTheReRouteWithTheSameKeyButDifferentLoadBalancer(reRouteTwo)) + .When(x => x.WhenIGetTheRouteWithTheSameKeyButDifferentLoadBalancer(routeTwo)) .Then(x => x.ThenTheLoadBalancerIs()) .BDDfy(); } - private void WhenIGetTheReRouteWithTheSameKeyButDifferentLoadBalancer(DownstreamReRoute reRoute) + private void WhenIGetTheRouteWithTheSameKeyButDifferentLoadBalancer(DownstreamRoute route) { - _reRoute = reRoute; - _factory.Setup(x => x.Get(_reRoute, _serviceProviderConfig)).ReturnsAsync(new OkResponse(new LeastConnection(null, null))); - _getResult = _loadBalancerHouse.Get(_reRoute, _serviceProviderConfig).Result; + _route = route; + _factory.Setup(x => x.Get(_route, _serviceProviderConfig)).Returns(new OkResponse(new LeastConnection(null, null))); + _getResult = _loadBalancerHouse.Get(_route, _serviceProviderConfig); } private void ThenAnErrorIsReturned() @@ -131,31 +136,31 @@ private void ThenItIsAdded() _getResult.IsError.ShouldBe(false); _getResult.ShouldBeOfType>(); _getResult.Data.ShouldBe(_loadBalancer); - _factory.Verify(x => x.Get(_reRoute, _serviceProviderConfig), Times.Once); + _factory.Verify(x => x.Get(_route, _serviceProviderConfig), Times.Once); } - private void GivenThereIsALoadBalancer(DownstreamReRoute reRoute, ILoadBalancer loadBalancer) + private void GivenThereIsALoadBalancer(DownstreamRoute route, ILoadBalancer loadBalancer) { - _reRoute = reRoute; + _route = route; _loadBalancer = loadBalancer; - _factory.Setup(x => x.Get(_reRoute, _serviceProviderConfig)).ReturnsAsync(new OkResponse(loadBalancer)); - _getResult = _loadBalancerHouse.Get(reRoute, _serviceProviderConfig).Result; + _factory.Setup(x => x.Get(_route, _serviceProviderConfig)).Returns(new OkResponse(loadBalancer)); + _getResult = _loadBalancerHouse.Get(route, _serviceProviderConfig); } - private void WhenWeGetTheLoadBalancer(DownstreamReRoute reRoute) + private void WhenWeGetTheLoadBalancer(DownstreamRoute route) { - _getResult = _loadBalancerHouse.Get(reRoute, _serviceProviderConfig).Result; + _getResult = _loadBalancerHouse.Get(route, _serviceProviderConfig); } private void ThenItIsReturned() { _getResult.Data.ShouldBe(_loadBalancer); - _factory.Verify(x => x.Get(_reRoute, _serviceProviderConfig), Times.Once); + _factory.Verify(x => x.Get(_route, _serviceProviderConfig), Times.Once); } private class FakeLoadBalancer : ILoadBalancer { - public Task> Lease(DownstreamContext context) + public Task> Lease(HttpContext httpContext) { throw new NotImplementedException(); } @@ -168,7 +173,7 @@ public void Release(ServiceHostAndPort hostAndPort) private class FakeRoundRobinLoadBalancer : ILoadBalancer { - public Task> Lease(DownstreamContext context) + public Task> Lease(HttpContext httpContext) { throw new NotImplementedException(); } diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs index 2705b3b77..dac633fe2 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs @@ -1,27 +1,34 @@ using System; -using System.Linq.Expressions; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Errors; +using Ocelot.Infrastructure.RequestData; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Middleware; +using Ocelot.Logging; using Ocelot.Middleware; +using Ocelot.Request.Middleware; + +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Ocelot.Values; + +using Xunit; namespace Ocelot.UnitTests.LoadBalancer { - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Errors; - using Ocelot.LoadBalancer.LoadBalancers; - using Ocelot.LoadBalancer.Middleware; - using Ocelot.Logging; - using Ocelot.Request.Middleware; - using Ocelot.Responses; - using Ocelot.Values; - using Shouldly; - using System.Collections.Generic; - using System.Net.Http; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - public class LoadBalancerMiddlewareTests { private readonly Mock _loadBalancerHouse; @@ -29,32 +36,33 @@ public class LoadBalancerMiddlewareTests private ServiceHostAndPort _hostAndPort; private ErrorResponse _getLoadBalancerHouseError; private ErrorResponse _getHostAndPortError; - private HttpRequestMessage _downstreamRequest; + private readonly HttpRequestMessage _downstreamRequest; private ServiceProviderConfiguration _config; - private Mock _loggerFactory; - private Mock _logger; + private readonly Mock _loggerFactory; + private readonly Mock _logger; private LoadBalancingMiddleware _middleware; - private DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; + private readonly RequestDelegate _next; + private readonly HttpContext _httpContext; + private Mock _repo; public LoadBalancerMiddlewareTests() { + _repo = new Mock(); + _httpContext = new DefaultHttpContext(); _loadBalancerHouse = new Mock(); _loadBalancer = new Mock(); _loadBalancerHouse = new Mock(); _downstreamRequest = new HttpRequestMessage(HttpMethod.Get, "http://test.com/"); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; - _downstreamContext.DownstreamRequest = new DownstreamRequest(_downstreamRequest); } [Fact] public void should_call_scoped_data_repository_correctly() { - var downstreamRoute = new DownstreamReRouteBuilder() + var downstreamRoute = new DownstreamRouteBuilder() .WithUpstreamHttpMethod(new List { "Get" }) .Build(); @@ -74,7 +82,7 @@ public void should_call_scoped_data_repository_correctly() [Fact] public void should_set_pipeline_error_if_cannot_get_load_balancer() { - var downstreamRoute = new DownstreamReRouteBuilder() + var downstreamRoute = new DownstreamRouteBuilder() .WithUpstreamHttpMethod(new List { "Get" }) .Build(); @@ -93,7 +101,7 @@ public void should_set_pipeline_error_if_cannot_get_load_balancer() [Fact] public void should_set_pipeline_error_if_cannot_get_least() { - var downstreamRoute = new DownstreamReRouteBuilder() + var downstreamRoute = new DownstreamRouteBuilder() .WithUpstreamHttpMethod(new List { "Get" }) .Build(); @@ -113,7 +121,7 @@ public void should_set_pipeline_error_if_cannot_get_least() [Fact] public void should_set_scheme() { - var downstreamRoute = new DownstreamReRouteBuilder() + var downstreamRoute = new DownstreamRouteBuilder() .WithUpstreamHttpMethod(new List { "Get" }) .Build(); @@ -133,34 +141,34 @@ public void should_set_scheme() private void WhenICallTheMiddleware() { _middleware = new LoadBalancingMiddleware(_next, _loggerFactory.Object, _loadBalancerHouse.Object); - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); } private void GivenTheConfigurationIs(ServiceProviderConfiguration config) { _config = config; - var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null); - _downstreamContext.Configuration = configuration; + var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null); + _httpContext.Items.SetIInternalConfiguration(configuration); } private void GivenTheDownStreamUrlIs(string downstreamUrl) { - _downstreamRequest.RequestUri = new System.Uri(downstreamUrl); - _downstreamContext.DownstreamRequest = new DownstreamRequest(_downstreamRequest); + _downstreamRequest.RequestUri = new Uri(downstreamUrl); + _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(_downstreamRequest)); } private void GivenTheLoadBalancerReturnsAnError() { - _getHostAndPortError = new ErrorResponse(new List() { new ServicesAreNullError($"services were null for bah") }); + _getHostAndPortError = new ErrorResponse(new List { new ServicesAreNullError("services were null for bah") }); _loadBalancer - .Setup(x => x.Lease(It.IsAny())) + .Setup(x => x.Lease(It.IsAny())) .ReturnsAsync(_getHostAndPortError); } private void GivenTheLoadBalancerReturnsOk() { _loadBalancer - .Setup(x => x.Lease(It.IsAny())) + .Setup(x => x.Lease(It.IsAny())) .ReturnsAsync(new OkResponse(new ServiceHostAndPort("abc", 123, "https"))); } @@ -168,63 +176,63 @@ private void GivenTheLoadBalancerReturns() { _hostAndPort = new ServiceHostAndPort("127.0.0.1", 80); _loadBalancer - .Setup(x => x.Lease(It.IsAny())) + .Setup(x => x.Lease(It.IsAny())) .ReturnsAsync(new OkResponse(_hostAndPort)); } - private void GivenTheDownStreamRouteIs(DownstreamReRoute downstreamRoute, List placeholder) + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute, List placeholder) { - _downstreamContext.TemplatePlaceholderNameAndValues = placeholder; - _downstreamContext.DownstreamReRoute = downstreamRoute; + _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(placeholder); + _httpContext.Items.UpsertDownstreamRoute(downstreamRoute); } private void GivenTheLoadBalancerHouseReturns() { _loadBalancerHouse - .Setup(x => x.Get(It.IsAny(), It.IsAny())) - .ReturnsAsync(new OkResponse(_loadBalancer.Object)); + .Setup(x => x.Get(It.IsAny(), It.IsAny())) + .Returns(new OkResponse(_loadBalancer.Object)); } private void GivenTheLoadBalancerHouseReturnsAnError() { - _getLoadBalancerHouseError = new ErrorResponse(new List() + _getLoadBalancerHouseError = new ErrorResponse(new List { - new UnableToFindLoadBalancerError($"unabe to find load balancer for bah") + new UnableToFindLoadBalancerError("unabe to find load balancer for bah"), }); _loadBalancerHouse - .Setup(x => x.Get(It.IsAny(), It.IsAny())) - .ReturnsAsync(_getLoadBalancerHouseError); + .Setup(x => x.Get(It.IsAny(), It.IsAny())) + .Returns(_getLoadBalancerHouseError); } private void ThenAnErrorStatingLoadBalancerCouldNotBeFoundIsSetOnPipeline() { - _downstreamContext.IsError.ShouldBeTrue(); - _downstreamContext.Errors.ShouldBe(_getLoadBalancerHouseError.Errors); + _httpContext.Items.Errors().Count.ShouldBeGreaterThan(0); + _httpContext.Items.Errors().ShouldBe(_getLoadBalancerHouseError.Errors); } private void ThenAnErrorSayingReleaseFailedIsSetOnThePipeline() { - _downstreamContext.IsError.ShouldBeTrue(); - _downstreamContext.Errors.ShouldBe(It.IsAny>()); + _httpContext.Items.Errors().Count.ShouldBeGreaterThan(0); + _httpContext.Items.Errors().ShouldBe(It.IsAny>()); } private void ThenAnErrorStatingHostAndPortCouldNotBeFoundIsSetOnPipeline() { - _downstreamContext.IsError.ShouldBeTrue(); - _downstreamContext.Errors.ShouldBe(_getHostAndPortError.Errors); + _httpContext.Items.Errors().Count.ShouldBeGreaterThan(0); + _httpContext.Items.Errors().ShouldBe(_getHostAndPortError.Errors); } private void ThenAnHostAndPortIsSetOnPipeline() { - _downstreamContext.DownstreamRequest.Host.ShouldBeEquivalentTo("abc"); - _downstreamContext.DownstreamRequest.Port.ShouldBeEquivalentTo(123); - _downstreamContext.DownstreamRequest.Scheme.ShouldBeEquivalentTo("https"); + _httpContext.Items.DownstreamRequest().Host.ShouldBeEquivalentTo("abc"); + _httpContext.Items.DownstreamRequest().Port.ShouldBeEquivalentTo(123); + _httpContext.Items.DownstreamRequest().Scheme.ShouldBeEquivalentTo("https"); } private void ThenTheDownstreamUrlIsReplacedWith(string expectedUri) { - _downstreamContext.DownstreamRequest.ToHttpRequestMessage().RequestUri.OriginalString.ShouldBe(expectedUri); + _httpContext.Items.DownstreamRequest().ToHttpRequestMessage().RequestUri.OriginalString.ShouldBe(expectedUri); } } } diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerOptionsTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerOptionsTests.cs index 6486b83f3..029770e1b 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerOptionsTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerOptionsTests.cs @@ -1,6 +1,8 @@ using Ocelot.Configuration; using Ocelot.LoadBalancer.LoadBalancers; + using Shouldly; + using Xunit; namespace Ocelot.UnitTests.LoadBalancer diff --git a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs new file mode 100644 index 000000000..9600b9e79 --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs @@ -0,0 +1,78 @@ +using Moq; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.ServiceDiscovery.Providers; + +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class NoLoadBalancerCreatorTests + { + private readonly NoLoadBalancerCreator _creator; + private readonly Mock _serviceProvider; + private DownstreamRoute _route; + private Response _loadBalancer; + private string _typeName; + + public NoLoadBalancerCreatorTests() + { + _creator = new NoLoadBalancerCreator(); + _serviceProvider = new Mock(); + } + + [Fact] + public void should_return_instance_of_expected_load_balancer_type() + { + var route = new DownstreamRouteBuilder() + .Build(); + + this.Given(x => x.GivenARoute(route)) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_expected_name() + { + this.When(x => x.WhenIGetTheLoadBalancerTypeName()) + .Then(x => x.ThenTheLoadBalancerTypeIs("NoLoadBalancer")) + .BDDfy(); + } + + private void GivenARoute(DownstreamRoute route) + { + _route = route; + } + + private void WhenIGetTheLoadBalancer() + { + _loadBalancer = _creator.Create(_route, _serviceProvider.Object); + } + + private void WhenIGetTheLoadBalancerTypeName() + { + _typeName = _creator.Type; + } + + private void ThenTheLoadBalancerIsReturned() + where T : ILoadBalancer + { + _loadBalancer.Data.ShouldBeOfType(); + } + + private void ThenTheLoadBalancerTypeIs(string type) + { + _typeName.ShouldBe(type); + } + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs index 5f10c3bc8..84d562139 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs @@ -1,19 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + using Microsoft.AspNetCore.Http; + using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.Middleware; using Ocelot.Responses; using Ocelot.Values; + using Shouldly; -using System.Collections.Generic; -using System.Threading.Tasks; + using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.LoadBalancer { public class NoLoadBalancerTests { - private List _services; + private readonly List _services; private NoLoadBalancer _loadBalancer; private Response _result; @@ -30,7 +35,7 @@ public void should_return_host_and_port() var services = new List { - new Service("product", hostAndPort, string.Empty, string.Empty, new string[0]) + new("product", hostAndPort, string.Empty, string.Empty, Array.Empty()), }; this.Given(x => x.GivenServices(services)) @@ -54,7 +59,7 @@ public void should_return_error_if_no_services_then_when_services_available_retu var services = new List { - new Service("product", hostAndPort, string.Empty, string.Empty, new string[0]) + new("product", hostAndPort, string.Empty, string.Empty, Array.Empty()), }; this.Given(_ => WhenIGetTheNextHostAndPort()) @@ -91,7 +96,7 @@ private void GivenServices(List services) private void WhenIGetTheNextHostAndPort() { - _result = _loadBalancer.Lease(new DownstreamContext(new DefaultHttpContext())).Result; + _result = _loadBalancer.Lease(new DefaultHttpContext()).Result; } private void ThenTheHostAndPortIs(ServiceHostAndPort expected) diff --git a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs new file mode 100644 index 000000000..60797841c --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs @@ -0,0 +1,73 @@ +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Responses; +using Ocelot.ServiceDiscovery.Providers; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class RoundRobinCreatorTests + { + private readonly RoundRobinCreator _creator; + private readonly Mock _serviceProvider; + private DownstreamRoute _route; + private Response _loadBalancer; + private string _typeName; + + public RoundRobinCreatorTests() + { + _creator = new RoundRobinCreator(); + _serviceProvider = new Mock(); + } + + [Fact] + public void should_return_instance_of_expected_load_balancer_type() + { + var route = new DownstreamRouteBuilder() + .Build(); + + this.Given(x => x.GivenARoute(route)) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_expected_name() + { + this.When(x => x.WhenIGetTheLoadBalancerTypeName()) + .Then(x => x.ThenTheLoadBalancerTypeIs("RoundRobin")) + .BDDfy(); + } + + private void GivenARoute(DownstreamRoute route) + { + _route = route; + } + + private void WhenIGetTheLoadBalancer() + { + _loadBalancer = _creator.Create(_route, _serviceProvider.Object); + } + + private void WhenIGetTheLoadBalancerTypeName() + { + _typeName = _creator.Type; + } + + private void ThenTheLoadBalancerIsReturned() + where T : ILoadBalancer + { + _loadBalancer.Data.ShouldBeOfType(); + } + + private void ThenTheLoadBalancerTypeIs(string type) + { + _typeName.ShouldBe(type); + } + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs index ed5fc3bc6..fbeb9eadc 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs @@ -1,13 +1,18 @@ -using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + using Ocelot.LoadBalancer.LoadBalancers; -using Ocelot.Middleware; using Ocelot.Responses; using Ocelot.Values; + using Shouldly; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading.Tasks; + using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.LoadBalancer @@ -17,17 +22,16 @@ public class RoundRobinTests private readonly RoundRobin _roundRobin; private readonly List _services; private Response _hostAndPort; - private DownstreamContext _context; + private readonly HttpContext _httpContext; public RoundRobinTests() { - _context = new DownstreamContext(new DefaultHttpContext()); - + _httpContext = new DefaultHttpContext(); _services = new List { - new Service("product", new ServiceHostAndPort("127.0.0.1", 5000), string.Empty, string.Empty, new string[0]), - new Service("product", new ServiceHostAndPort("127.0.0.1", 5001), string.Empty, string.Empty, new string[0]), - new Service("product", new ServiceHostAndPort("127.0.0.1", 5001), string.Empty, string.Empty, new string[0]) + new("product", new ServiceHostAndPort("127.0.0.1", 5000), string.Empty, string.Empty, Array.Empty()), + new("product", new ServiceHostAndPort("127.0.0.1", 5001), string.Empty, string.Empty, Array.Empty()), + new("product", new ServiceHostAndPort("127.0.0.1", 5001), string.Empty, string.Empty, Array.Empty()), }; _roundRobin = new RoundRobin(() => Task.FromResult(_services)); @@ -52,18 +56,18 @@ public void should_go_back_to_first_address_after_finished_last() while (stopWatch.ElapsedMilliseconds < 1000) { - var address = _roundRobin.Lease(_context).Result; + var address = _roundRobin.Lease(_httpContext).Result; address.Data.ShouldBe(_services[0].HostAndPort); - address = _roundRobin.Lease(_context).Result; + address = _roundRobin.Lease(_httpContext).Result; address.Data.ShouldBe(_services[1].HostAndPort); - address = _roundRobin.Lease(_context).Result; + address = _roundRobin.Lease(_httpContext).Result; address.Data.ShouldBe(_services[2].HostAndPort); } } private void GivenIGetTheNextAddress() { - _hostAndPort = _roundRobin.Lease(_context).Result; + _hostAndPort = _roundRobin.Lease(_httpContext).Result; } private void ThenTheNextAddressIndexIs(int index) diff --git a/test/Ocelot.UnitTests/Logging/AspDotNetLoggerTests.cs b/test/Ocelot.UnitTests/Logging/AspDotNetLoggerTests.cs index c4e2d8bf8..7d5c79748 100644 --- a/test/Ocelot.UnitTests/Logging/AspDotNetLoggerTests.cs +++ b/test/Ocelot.UnitTests/Logging/AspDotNetLoggerTests.cs @@ -1,12 +1,16 @@ +using System; + +using Microsoft.Extensions.Logging; + +using Moq; + +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; + +using Xunit; + namespace Ocelot.UnitTests.Logging { - using Microsoft.Extensions.Logging; - using Moq; - using Ocelot.Infrastructure.RequestData; - using Ocelot.Logging; - using System; - using Xunit; - public class AspDotNetLoggerTests { private readonly Mock> _coreLogger; diff --git a/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs b/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs index 41507e119..32e49842d 100644 --- a/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs +++ b/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs @@ -1,10 +1,14 @@ +using System; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; + using Moq; + using Ocelot.Logging; -using Ocelot.Middleware; -using System; + using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Logging @@ -12,16 +16,17 @@ namespace Ocelot.UnitTests.Logging public class OcelotDiagnosticListenerTests { private readonly OcelotDiagnosticListener _listener; - private Mock _factory; + private readonly Mock _factory; private readonly Mock _logger; - private IServiceCollection _serviceCollection; - private IServiceProvider _serviceProvider; - private DownstreamContext _downstreamContext; + private readonly IServiceCollection _serviceCollection; + private readonly IServiceProvider _serviceProvider; private string _name; private Exception _exception; + private readonly HttpContext _httpContext; public OcelotDiagnosticListenerTests() { + _httpContext = new DefaultHttpContext(); _factory = new Mock(); _logger = new Mock(); _serviceCollection = new ServiceCollection(); @@ -30,44 +35,12 @@ public OcelotDiagnosticListenerTests() _listener = new OcelotDiagnosticListener(_factory.Object, _serviceProvider); } - [Fact] - public void should_trace_ocelot_middleware_started() - { - this.Given(_ => GivenAMiddlewareName()) - .And(_ => GivenAContext()) - .When(_ => WhenOcelotMiddlewareStartedCalled()) - .Then(_ => ThenTheLogIs($"Ocelot.MiddlewareStarted: {_name}; {_downstreamContext.HttpContext.Request.Path}")) - .BDDfy(); - } - - [Fact] - public void should_trace_ocelot_middleware_finished() - { - this.Given(_ => GivenAMiddlewareName()) - .And(_ => GivenAContext()) - .When(_ => WhenOcelotMiddlewareFinishedCalled()) - .Then(_ => ThenTheLogIs($"Ocelot.MiddlewareFinished: {_name}; {_downstreamContext.HttpContext.Request.Path}")) - .BDDfy(); - } - - [Fact] - public void should_trace_ocelot_middleware_exception() - { - this.Given(_ => GivenAMiddlewareName()) - .And(_ => GivenAContext()) - .And(_ => GivenAException(new Exception("oh no"))) - .When(_ => WhenOcelotMiddlewareExceptionCalled()) - .Then(_ => ThenTheLogIs($"Ocelot.MiddlewareException: {_name}; {_exception.Message};")) - .BDDfy(); - } - [Fact] public void should_trace_middleware_started() { this.Given(_ => GivenAMiddlewareName()) - .And(_ => GivenAContext()) .When(_ => WhenMiddlewareStartedCalled()) - .Then(_ => ThenTheLogIs($"MiddlewareStarting: {_name}; {_downstreamContext.HttpContext.Request.Path}")) + .Then(_ => ThenTheLogIs($"MiddlewareStarting: {_name}; {_httpContext.Request.Path}")) .BDDfy(); } @@ -75,9 +48,8 @@ public void should_trace_middleware_started() public void should_trace_middleware_finished() { this.Given(_ => GivenAMiddlewareName()) - .And(_ => GivenAContext()) .When(_ => WhenMiddlewareFinishedCalled()) - .Then(_ => ThenTheLogIs($"MiddlewareFinished: {_name}; {_downstreamContext.HttpContext.Response.StatusCode}")) + .Then(_ => ThenTheLogIs($"MiddlewareFinished: {_name}; {_httpContext.Response.StatusCode}")) .BDDfy(); } @@ -85,7 +57,6 @@ public void should_trace_middleware_finished() public void should_trace_middleware_exception() { this.Given(_ => GivenAMiddlewareName()) - .And(_ => GivenAContext()) .And(_ => GivenAException(new Exception("oh no"))) .When(_ => WhenMiddlewareExceptionCalled()) .Then(_ => ThenTheLogIs($"MiddlewareException: {_name}; {_exception.Message};")) @@ -97,29 +68,14 @@ private void GivenAException(Exception exception) _exception = exception; } - private void WhenOcelotMiddlewareStartedCalled() - { - _listener.OcelotMiddlewareStarted(_downstreamContext, _name); - } - - private void WhenOcelotMiddlewareFinishedCalled() - { - _listener.OcelotMiddlewareFinished(_downstreamContext, _name); - } - - private void WhenOcelotMiddlewareExceptionCalled() - { - _listener.OcelotMiddlewareException(_exception, _downstreamContext, _name); - } - private void WhenMiddlewareStartedCalled() { - _listener.OnMiddlewareStarting(_downstreamContext.HttpContext, _name); + _listener.OnMiddlewareStarting(_httpContext, _name); } private void WhenMiddlewareFinishedCalled() { - _listener.OnMiddlewareFinished(_downstreamContext.HttpContext, _name); + _listener.OnMiddlewareFinished(_httpContext, _name); } private void WhenMiddlewareExceptionCalled() @@ -127,11 +83,6 @@ private void WhenMiddlewareExceptionCalled() _listener.OnMiddlewareException(_exception, _name); } - private void GivenAContext() - { - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - } - private void GivenAMiddlewareName() { _name = "name"; diff --git a/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs b/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs index 7a8e4d8f9..ceecc60ec 100644 --- a/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs +++ b/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs @@ -1,9 +1,14 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Configuration.Memory; -using Ocelot.Middleware; -using Shouldly; -using System.Collections.Generic; -using TestStack.BDDfy; +using System.Collections.Generic; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; + +using Ocelot.Middleware; + +using Shouldly; + +using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Middleware @@ -12,7 +17,7 @@ public class BaseUrlFinderTests { private BaseUrlFinder _baseUrlFinder; private IConfiguration _config; - private List> _data; + private readonly List> _data; private string _result; public BaseUrlFinderTests() @@ -59,11 +64,14 @@ private void GivenTheFileBaseUrlIs(string configValue) private void WhenIFindTheUrl() { - var source = new MemoryConfigurationSource(); - source.InitialData = _data; + var source = new MemoryConfigurationSource + { + InitialData = _data, + }; var provider = new MemoryConfigurationProvider(source); - _config = new ConfigurationRoot(new List() { - provider + _config = new ConfigurationRoot(new List + { + provider, }); _baseUrlFinder = new BaseUrlFinder(_config); _result = _baseUrlFinder.Find(); diff --git a/test/Ocelot.UnitTests/Middleware/MultiplexerTests.cs b/test/Ocelot.UnitTests/Middleware/MultiplexerTests.cs deleted file mode 100644 index 7615dbfb5..000000000 --- a/test/Ocelot.UnitTests/Middleware/MultiplexerTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Moq; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Middleware; -using Ocelot.Middleware.Multiplexer; -using Shouldly; -using System.Threading.Tasks; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Middleware -{ - public class MultiplexerTests - { - private readonly Multiplexer _multiplexer; - private readonly DownstreamContext _context; - private ReRoute _reRoute; - private readonly OcelotRequestDelegate _pipeline; - private int _count; - private Mock _aggregator; - private Mock _factory; - - public MultiplexerTests() - { - _factory = new Mock(); - _aggregator = new Mock(); - _context = new DownstreamContext(new DefaultHttpContext()); - _pipeline = context => Task.FromResult(_count++); - _factory.Setup(x => x.Get(It.IsAny())).Returns(_aggregator.Object); - _multiplexer = new Multiplexer(_factory.Object); - } - - [Fact] - public void should_multiplex() - { - var reRoute = new ReRouteBuilder().WithDownstreamReRoute(new DownstreamReRouteBuilder().Build()).WithDownstreamReRoute(new DownstreamReRouteBuilder().Build()).Build(); - - this.Given(x => GivenTheFollowing(reRoute)) - .When(x => WhenIMultiplex()) - .Then(x => ThePipelineIsCalled(2)) - .BDDfy(); - } - - [Fact] - public void should_not_multiplex() - { - var reRoute = new ReRouteBuilder().WithDownstreamReRoute(new DownstreamReRouteBuilder().Build()).Build(); - - this.Given(x => GivenTheFollowing(reRoute)) - .When(x => WhenIMultiplex()) - .Then(x => ThePipelineIsCalled(1)) - .BDDfy(); - } - - private void GivenTheFollowing(ReRoute reRoute) - { - _reRoute = reRoute; - } - - private void WhenIMultiplex() - { - _multiplexer.Multiplex(_context, _reRoute, _pipeline).GetAwaiter().GetResult(); - } - - private void ThePipelineIsCalled(int expected) - { - _count.ShouldBe(expected); - } - } -} diff --git a/test/Ocelot.UnitTests/Middleware/OcelotMiddlewareTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotMiddlewareTests.cs deleted file mode 100644 index ca5c34efd..000000000 --- a/test/Ocelot.UnitTests/Middleware/OcelotMiddlewareTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Moq; -using Ocelot.Errors; -using Ocelot.Logging; -using Ocelot.Middleware; -using Ocelot.UnitTests.Responder; -using System.Collections.Generic; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Middleware -{ - public class OcelotMiddlewareTests - { - private Mock _logger; - private FakeMiddleware _middleware; - private List _errors; - - public OcelotMiddlewareTests() - { - _errors = new List(); - _logger = new Mock(); - _middleware = new FakeMiddleware(_logger.Object); - } - - [Fact] - public void should_log_error() - { - this.Given(x => GivenAnError(new AnyError())) - .When(x => WhenISetTheError()) - .Then(x => ThenTheErrorIsLogged(1)) - .BDDfy(); - } - - [Fact] - public void should_log_errors() - { - this.Given(x => GivenAnError(new AnyError())) - .And(x => GivenAnError(new AnyError())) - .When(x => WhenISetTheErrors()) - .Then(x => ThenTheErrorIsLogged(2)) - .BDDfy(); - } - - private void WhenISetTheErrors() - { - _middleware.SetPipelineError(new DownstreamContext(new DefaultHttpContext()), _errors); - } - - private void ThenTheErrorIsLogged(int times) - { - _logger.Verify(x => x.LogWarning("blahh"), Times.Exactly(times)); - } - - private void WhenISetTheError() - { - _middleware.SetPipelineError(new DownstreamContext(new DefaultHttpContext()), _errors[0]); - } - - private void GivenAnError(Error error) - { - _errors.Add(error); - } - } - - public class FakeMiddleware : OcelotMiddleware - { - public FakeMiddleware(IOcelotLogger logger) - : base(logger) - { - } - } -} diff --git a/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs index 2e097b086..ddcd87425 100644 --- a/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs @@ -1,23 +1,28 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +using Ocelot.DependencyInjection; +using Ocelot.DownstreamRouteFinder.Middleware; +using Ocelot.DownstreamUrlCreator.Middleware; +using Ocelot.LoadBalancer.Middleware; +using Ocelot.Middleware; +using Ocelot.Request.Middleware; +using Ocelot.WebSockets.Middleware; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + namespace Ocelot.UnitTests.Middleware { - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Ocelot.DependencyInjection; - using Ocelot.DownstreamRouteFinder.Middleware; - using Ocelot.DownstreamUrlCreator.Middleware; - using Ocelot.LoadBalancer.Middleware; - using Ocelot.Middleware; - using Ocelot.Middleware.Pipeline; - using Ocelot.Request.Middleware; - using Ocelot.WebSockets.Middleware; - using Shouldly; - using TestStack.BDDfy; - using Xunit; - public class OcelotPipelineExtensionsTests { - private OcelotPipelineBuilder _builder; - private OcelotRequestDelegate _handlers; + private ApplicationBuilder _builder; + private RequestDelegate _handlers; [Fact] public void should_set_up_pipeline() @@ -49,16 +54,14 @@ private void WhenIBuild() private void WhenIExpandBuild() { - OcelotPipelineConfiguration configuration = new OcelotPipelineConfiguration(); - configuration.MapWhenOcelotPipeline.Add((app) => + var configuration = new OcelotPipelineConfiguration(); + configuration.MapWhenOcelotPipeline.Add((httpContext) => httpContext.WebSockets.IsWebSocketRequest, app => { app.UseDownstreamRouteFinderMiddleware(); app.UseDownstreamRequestInitialiser(); app.UseLoadBalancingMiddleware(); app.UseDownstreamUrlCreatorMiddleware(); app.UseWebSocketsProxyMiddleware(); - - return context => context.HttpContext.WebSockets.IsWebSocketRequest; }); _handlers = _builder.BuildOcelotPipeline(new OcelotPipelineConfiguration()); } @@ -71,7 +74,7 @@ private void GivenTheDepedenciesAreSetUp() services.AddSingleton(root); services.AddOcelot(); var provider = services.BuildServiceProvider(); - _builder = new OcelotPipelineBuilder(provider); + _builder = new ApplicationBuilder(provider); } } } diff --git a/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs index 21e8dd63c..e53f72698 100644 --- a/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs +++ b/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs @@ -1,41 +1,47 @@ using System; +using System.Collections.Generic; +using System.Reflection; using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +using Moq; + +using Ocelot.DependencyInjection; +using Ocelot.Errors.Middleware; +using Ocelot.Logging; +using Ocelot.Middleware; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + namespace Ocelot.UnitTests.Middleware { - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Moq; - using Ocelot.DependencyInjection; - using Ocelot.Logging; - using Ocelot.Middleware; - using Ocelot.Middleware.Pipeline; - using Shouldly; - using System.Collections.Generic; - using System.Reflection; - using TestStack.BDDfy; - using Xunit; - public class OcelotPiplineBuilderTests { private readonly IServiceCollection _services; private readonly IConfiguration _configRoot; - private DownstreamContext _downstreamContext; private int _counter; + private readonly HttpContext _httpContext; public OcelotPiplineBuilderTests() { _configRoot = new ConfigurationRoot(new List()); _services = new ServiceCollection(); - _services.AddSingleton(GetHostingEnvironment()); - _services.AddSingleton(_configRoot); + _services.AddSingleton(GetHostingEnvironment()); + _services.AddSingleton(_configRoot); _services.AddOcelot(); + _httpContext = new DefaultHttpContext(); } - - private IWebHostEnvironment GetHostingEnvironment() + private static IWebHostEnvironment GetHostingEnvironment() { var environment = new Mock(); environment @@ -64,61 +70,58 @@ public void should_build_func() private void WhenIUseAGeneric() { var provider = _services.BuildServiceProvider(); - IOcelotPipelineBuilder builder = new OcelotPipelineBuilder(provider); - builder = builder.UseMiddleware(); + IApplicationBuilder builder = new ApplicationBuilder(provider); + builder = builder.UseMiddleware(); var del = builder.Build(); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - del.Invoke(_downstreamContext); + del.Invoke(_httpContext); } private void ThenTheGenericIsInThePipeline() { - _downstreamContext.HttpContext.Response.StatusCode.ShouldBe(500); + _httpContext.Response.StatusCode.ShouldBe(500); } private void WhenIUseAFunc() { _counter = 0; var provider = _services.BuildServiceProvider(); - IOcelotPipelineBuilder builder = new OcelotPipelineBuilder(provider); + IApplicationBuilder builder = new ApplicationBuilder(provider); builder = builder.Use(async (ctx, next) => { _counter++; await next.Invoke(); }); var del = builder.Build(); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - del.Invoke(_downstreamContext); + del.Invoke(_httpContext); } private void ThenTheFuncIsInThePipeline() { _counter.ShouldBe(1); - _downstreamContext.HttpContext.Response.StatusCode.ShouldBe(404); + _httpContext.Response.StatusCode.ShouldBe(404); } [Fact] public void Middleware_Multi_Parameters_Invoke() { var provider = _services.BuildServiceProvider(); - IOcelotPipelineBuilder builder = new OcelotPipelineBuilder(provider); + IApplicationBuilder builder = new ApplicationBuilder(provider); builder = builder.UseMiddleware(); var del = builder.Build(); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - del.Invoke(_downstreamContext); + del.Invoke(_httpContext); } private class MultiParametersInvokeMiddleware : OcelotMiddleware { - private readonly OcelotRequestDelegate _next; + private readonly RequestDelegate _next; - public MultiParametersInvokeMiddleware(OcelotRequestDelegate next) + public MultiParametersInvokeMiddleware(RequestDelegate next) : base(new FakeLogger()) { _next = next; } - public Task Invoke(DownstreamContext context, IServiceProvider serviceProvider) + public Task Invoke(HttpContext context, IServiceProvider serviceProvider) { return Task.CompletedTask; } diff --git a/test/Ocelot.UnitTests/Middleware/SimpleJsonResponseAggregatorTests.cs b/test/Ocelot.UnitTests/Middleware/SimpleJsonResponseAggregatorTests.cs deleted file mode 100644 index 8d17d792a..000000000 --- a/test/Ocelot.UnitTests/Middleware/SimpleJsonResponseAggregatorTests.cs +++ /dev/null @@ -1,222 +0,0 @@ -using Castle.Components.DictionaryAdapter; -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Configuration.File; -using Ocelot.Middleware; -using Ocelot.Middleware.Multiplexer; -using Ocelot.UnitTests.Responder; -using Ocelot.Values; -using Shouldly; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Text; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Middleware -{ - public class SimpleJsonResponseAggregatorTests - { - private readonly SimpleJsonResponseAggregator _aggregator; - private List _downstreamContexts; - private DownstreamContext _upstreamContext; - private ReRoute _reRoute; - - public SimpleJsonResponseAggregatorTests() - { - _aggregator = new SimpleJsonResponseAggregator(); - } - - [Fact] - public void should_aggregate_n_responses_and_set_response_content_on_upstream_context_withConfig() - { - var commentsDownstreamReRoute = new DownstreamReRouteBuilder().WithKey("Comments").Build(); - - var userDetailsDownstreamReRoute = new DownstreamReRouteBuilder().WithKey("UserDetails") - .WithUpstreamPathTemplate(new UpstreamPathTemplate("", 0, false, "/v1/users/{userId}")) - .Build(); - - var downstreamReRoutes = new List - { - commentsDownstreamReRoute, - userDetailsDownstreamReRoute - }; - - var reRoute = new ReRouteBuilder() - .WithDownstreamReRoutes(downstreamReRoutes) - .WithAggregateReRouteConfig(new List() - { - new AggregateReRouteConfig(){ReRouteKey = "UserDetails",JsonPath = "$[*].writerId",Parameter = "userId"} - }) - .Build(); - - var commentsResponseContent = @"[{""id"":1,""writerId"":1,""postId"":1,""text"":""text1""},{""id"":2,""writerId"":2,""postId"":2,""text"":""text2""},{""id"":3,""writerId"":2,""postId"":1,""text"":""text21""}]"; - var commentsDownstreamContext = new DownstreamContext(new DefaultHttpContext()) - { - DownstreamResponse = new DownstreamResponse(new StringContent(commentsResponseContent, Encoding.UTF8, "application/json"), HttpStatusCode.OK, new EditableList>>(), "some reason"), - DownstreamReRoute = commentsDownstreamReRoute - }; - - var userDetailsResponseContent = @"[{""id"":1,""firstName"":""abolfazl"",""lastName"":""rajabpour""},{""id"":2,""firstName"":""reza"",""lastName"":""rezaei""}]"; - var userDetailsDownstreamContext = new DownstreamContext(new DefaultHttpContext()) - { - DownstreamResponse = new DownstreamResponse(new StringContent(userDetailsResponseContent, Encoding.UTF8, "application/json"), HttpStatusCode.OK, new List>>(), "some reason"), - DownstreamReRoute = userDetailsDownstreamReRoute - }; - - var downstreamContexts = new List { commentsDownstreamContext, userDetailsDownstreamContext }; - - var expected = "{\"Comments\":" + commentsResponseContent + ",\"UserDetails\":" + userDetailsResponseContent + "}"; - - this.Given(x => GivenTheUpstreamContext(new DownstreamContext(new DefaultHttpContext()))) - .And(x => GivenTheReRoute(reRoute)) - .And(x => GivenTheDownstreamContext(downstreamContexts)) - .When(x => WhenIAggregate()) - .Then(x => ThenTheContentIs(expected)) - .And(x => ThenTheContentTypeIs("application/json")) - .And(x => ThenTheReasonPhraseIs("cannot return from aggregate..which reason phrase would you use?")) - .BDDfy(); - } - - [Fact] - public void should_aggregate_n_responses_and_set_response_content_on_upstream_context() - { - var billDownstreamReRoute = new DownstreamReRouteBuilder().WithKey("Bill").Build(); - - var georgeDownstreamReRoute = new DownstreamReRouteBuilder().WithKey("George").Build(); - - var downstreamReRoutes = new List - { - billDownstreamReRoute, - georgeDownstreamReRoute - }; - - var reRoute = new ReRouteBuilder() - .WithDownstreamReRoutes(downstreamReRoutes) - .Build(); - - var billDownstreamContext = new DownstreamContext(new DefaultHttpContext()) - { - DownstreamResponse = new DownstreamResponse(new StringContent("Bill says hi"), HttpStatusCode.OK, new EditableList>>(), "some reason"), - DownstreamReRoute = billDownstreamReRoute - }; - - var georgeDownstreamContext = new DownstreamContext(new DefaultHttpContext()) - { - DownstreamResponse = new DownstreamResponse(new StringContent("George says hi"), HttpStatusCode.OK, new List>>(), "some reason"), - DownstreamReRoute = georgeDownstreamReRoute - }; - - var downstreamContexts = new List { billDownstreamContext, georgeDownstreamContext }; - - var expected = "{\"Bill\":Bill says hi,\"George\":George says hi}"; - - this.Given(x => GivenTheUpstreamContext(new DownstreamContext(new DefaultHttpContext()))) - .And(x => GivenTheReRoute(reRoute)) - .And(x => GivenTheDownstreamContext(downstreamContexts)) - .When(x => WhenIAggregate()) - .Then(x => ThenTheContentIs(expected)) - .And(x => ThenTheContentTypeIs("application/json")) - .And(x => ThenTheReasonPhraseIs("cannot return from aggregate..which reason phrase would you use?")) - .BDDfy(); - } - - [Fact] - public void should_return_error_if_any_downstreams_have_errored() - { - var billDownstreamReRoute = new DownstreamReRouteBuilder().WithKey("Bill").Build(); - - var georgeDownstreamReRoute = new DownstreamReRouteBuilder().WithKey("George").Build(); - - var downstreamReRoutes = new List - { - billDownstreamReRoute, - georgeDownstreamReRoute - }; - - var reRoute = new ReRouteBuilder() - .WithDownstreamReRoutes(downstreamReRoutes) - .Build(); - - var billDownstreamContext = new DownstreamContext(new DefaultHttpContext()) - { - DownstreamResponse = new DownstreamResponse(new StringContent("Bill says hi"), HttpStatusCode.OK, new List>>(), "some reason"), - DownstreamReRoute = billDownstreamReRoute - }; - - var georgeDownstreamContext = new DownstreamContext(new DefaultHttpContext()) - { - DownstreamResponse = new DownstreamResponse(new StringContent("Error"), HttpStatusCode.OK, new List>>(), "some reason"), - DownstreamReRoute = georgeDownstreamReRoute, - }; - - georgeDownstreamContext.Errors.Add(new AnyError()); - - var downstreamContexts = new List { billDownstreamContext, georgeDownstreamContext }; - - var expected = "Error"; - - this.Given(x => GivenTheUpstreamContext(new DownstreamContext(new DefaultHttpContext()))) - .And(x => GivenTheReRoute(reRoute)) - .And(x => GivenTheDownstreamContext(downstreamContexts)) - .When(x => WhenIAggregate()) - .Then(x => ThenTheContentIs(expected)) - .And(x => ThenTheErrorIsMapped()) - .BDDfy(); - } - - private void ThenTheReasonPhraseIs(string expected) - { - _upstreamContext.DownstreamResponse.ReasonPhrase.ShouldBe(expected); - } - - private void ThenTheErrorIsMapped() - { - _upstreamContext.Errors.ShouldBe(_downstreamContexts[1].Errors); - _upstreamContext.DownstreamResponse.ShouldBe(_downstreamContexts[1].DownstreamResponse); - } - - private void GivenTheReRoute(ReRoute reRoute) - { - _reRoute = reRoute; - } - - private void GivenTheUpstreamContext(DownstreamContext upstreamContext) - { - _upstreamContext = upstreamContext; - } - - private void GivenTheDownstreamContext(List downstreamContexts) - { - _downstreamContexts = downstreamContexts; - } - - private void WhenIAggregate() - { - _aggregator.Aggregate(_reRoute, _upstreamContext, _downstreamContexts).GetAwaiter().GetResult(); - } - - private void ThenTheContentIs(string expected) - { - var content = _upstreamContext.DownstreamResponse.Content.ReadAsStringAsync() - .GetAwaiter() - .GetResult(); - - content.ShouldBe(expected); - } - - private void ThenTheContentTypeIs(string expected) - { - _upstreamContext.DownstreamResponse.Content.Headers.ContentType.MediaType.ShouldBe(expected); - } - - private void ThenTheUpstreamContextIsMappedForNonAggregate() - { - _upstreamContext.DownstreamRequest.ShouldBe(_downstreamContexts[0].DownstreamRequest); - _upstreamContext.DownstreamResponse.ShouldBe(_downstreamContexts[0].DownstreamResponse); - _upstreamContext.Errors.ShouldBe(_downstreamContexts[0].Errors); - } - } -} diff --git a/test/Ocelot.UnitTests/Middleware/UserDefinedResponseAggregatorTests.cs b/test/Ocelot.UnitTests/Middleware/UserDefinedResponseAggregatorTests.cs deleted file mode 100644 index 78d538786..000000000 --- a/test/Ocelot.UnitTests/Middleware/UserDefinedResponseAggregatorTests.cs +++ /dev/null @@ -1,152 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Moq; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Middleware; -using Ocelot.Middleware.Multiplexer; -using Ocelot.Responses; -using Ocelot.UnitTests.Responder; -using Shouldly; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Middleware -{ - public class UserDefinedResponseAggregatorTests - { - private readonly UserDefinedResponseAggregator _aggregator; - private readonly Mock _provider; - private ReRoute _reRoute; - private List _contexts; - private DownstreamContext _context; - - public UserDefinedResponseAggregatorTests() - { - _provider = new Mock(); - _aggregator = new UserDefinedResponseAggregator(_provider.Object); - } - - [Fact] - public void should_call_aggregator() - { - var reRoute = new ReRouteBuilder().Build(); - - var context = new DownstreamContext(new DefaultHttpContext()); - - var contexts = new List - { - new DownstreamContext(new DefaultHttpContext()) - { - DownstreamResponse = new DownstreamResponse(new StringContent("Tom"), HttpStatusCode.OK, new List>>(), "some reason") - }, - new DownstreamContext(new DefaultHttpContext()) - { - DownstreamResponse = new DownstreamResponse(new StringContent("Laura"), HttpStatusCode.OK, new List>>(), "some reason") - } - }; - - this.Given(_ => GivenTheProviderReturnsAggregator()) - .And(_ => GivenReRoute(reRoute)) - .And(_ => GivenContexts(contexts)) - .And(_ => GivenContext(context)) - .When(_ => WhenIAggregate()) - .Then(_ => ThenTheProviderIsCalled()) - .And(_ => ThenTheContentIsCorrect()) - .BDDfy(); - } - - [Fact] - public void should_not_find_aggregator() - { - var reRoute = new ReRouteBuilder().Build(); - - var context = new DownstreamContext(new DefaultHttpContext()); - - var contexts = new List - { - new DownstreamContext(new DefaultHttpContext()) - { - DownstreamResponse = new DownstreamResponse(new StringContent("Tom"), HttpStatusCode.OK, new List>>(), "some reason") - }, - new DownstreamContext(new DefaultHttpContext()) - { - DownstreamResponse = new DownstreamResponse(new StringContent("Laura"), HttpStatusCode.OK, new List>>(), "some reason") - } - }; - - this.Given(_ => GivenTheProviderReturnsError()) - .And(_ => GivenReRoute(reRoute)) - .And(_ => GivenContexts(contexts)) - .And(_ => GivenContext(context)) - .When(_ => WhenIAggregate()) - .Then(_ => ThenTheProviderIsCalled()) - .And(_ => ThenTheErrorIsReturned()) - .BDDfy(); - } - - private void ThenTheErrorIsReturned() - { - _context.IsError.ShouldBeTrue(); - _context.Errors.Count.ShouldBe(1); - } - - private void GivenTheProviderReturnsError() - { - _provider.Setup(x => x.Get(It.IsAny())).Returns(new ErrorResponse(new AnyError())); - } - - private async Task ThenTheContentIsCorrect() - { - var content = await _context.DownstreamResponse.Content.ReadAsStringAsync(); - content.ShouldBe("Tom, Laura"); - } - - private void ThenTheProviderIsCalled() - { - _provider.Verify(x => x.Get(_reRoute), Times.Once); - } - - private void GivenContext(DownstreamContext context) - { - _context = context; - } - - private void GivenContexts(List contexts) - { - _contexts = contexts; - } - - private async Task WhenIAggregate() - { - await _aggregator.Aggregate(_reRoute, _context, _contexts); - } - - private void GivenTheProviderReturnsAggregator() - { - var aggregator = new TestDefinedAggregator(); - _provider.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse(aggregator)); - } - - private void GivenReRoute(ReRoute reRoute) - { - _reRoute = reRoute; - } - - public class TestDefinedAggregator : IDefinedAggregator - { - public async Task Aggregate(List responses) - { - var tom = await responses[0].DownstreamResponse.Content.ReadAsStringAsync(); - var laura = await responses[1].DownstreamResponse.Content.ReadAsStringAsync(); - var content = $"{tom}, {laura}"; - var headers = responses.SelectMany(x => x.DownstreamResponse.Headers).ToList(); - return new DownstreamResponse(new StringContent(content), HttpStatusCode.OK, headers, "some reason"); - } - } - } -} diff --git a/test/Ocelot.UnitTests/Middleware/DefinedAggregatorProviderTests.cs b/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs similarity index 80% rename from test/Ocelot.UnitTests/Middleware/DefinedAggregatorProviderTests.cs rename to test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs index 1874e94b1..09626c379 100644 --- a/test/Ocelot.UnitTests/Middleware/DefinedAggregatorProviderTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs @@ -1,86 +1,91 @@ using Microsoft.Extensions.DependencyInjection; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Middleware.Multiplexer; -using Ocelot.Responses; -using Shouldly; -using TestStack.BDDfy; -using Xunit; -using static Ocelot.UnitTests.Middleware.UserDefinedResponseAggregatorTests; - -namespace Ocelot.UnitTests.Middleware -{ - public class DefinedAggregatorProviderTests - { - private ServiceLocatorDefinedAggregatorProvider _provider; - private Response _aggregator; - private ReRoute _reRoute; - - [Fact] - public void should_find_aggregator() - { - var reRoute = new ReRouteBuilder() - .WithAggregator("TestDefinedAggregator") - .Build(); - - this.Given(_ => GivenDefinedAggregator()) - .And(_ => GivenReRoute(reRoute)) - .When(_ => WhenIGet()) - .Then(_ => ThenTheAggregatorIsReturned()) - .BDDfy(); - } - [Fact] - public void should_not_find_aggregator() - { - var reRoute = new ReRouteBuilder() - .WithAggregator("TestDefinedAggregator") - .Build(); - - this.Given(_ => GivenNoDefinedAggregator()) - .And(_ => GivenReRoute(reRoute)) - .When(_ => WhenIGet()) - .Then(_ => ThenAnErrorIsReturned()) - .BDDfy(); - } - - private void GivenDefinedAggregator() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(); - var services = serviceCollection.BuildServiceProvider(); - _provider = new ServiceLocatorDefinedAggregatorProvider(services); - } - - private void ThenTheAggregatorIsReturned() - { - _aggregator.Data.ShouldNotBeNull(); - _aggregator.Data.ShouldBeOfType(); - _aggregator.IsError.ShouldBeFalse(); - } +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Multiplexer; +using Ocelot.Responses; - private void GivenNoDefinedAggregator() - { - var serviceCollection = new ServiceCollection(); - var services = serviceCollection.BuildServiceProvider(); - _provider = new ServiceLocatorDefinedAggregatorProvider(services); - } +using Shouldly; - private void GivenReRoute(ReRoute reRoute) - { - _reRoute = reRoute; - } +using TestStack.BDDfy; - private void WhenIGet() - { - _aggregator = _provider.Get(_reRoute); - } +using Xunit; - private void ThenAnErrorIsReturned() - { - _aggregator.IsError.ShouldBeTrue(); - _aggregator.Errors[0].Message.ShouldBe("Could not find Aggregator: TestDefinedAggregator"); - _aggregator.Errors[0].ShouldBeOfType(); - } - } -} +using static Ocelot.UnitTests.Multiplexing.UserDefinedResponseAggregatorTests; + +namespace Ocelot.UnitTests.Multiplexing +{ + public class DefinedAggregatorProviderTests + { + private ServiceLocatorDefinedAggregatorProvider _provider; + private Response _aggregator; + private Route _route; + + [Fact] + public void should_find_aggregator() + { + var route = new RouteBuilder() + .WithAggregator("TestDefinedAggregator") + .Build(); + + this.Given(_ => GivenDefinedAggregator()) + .And(_ => GivenRoute(route)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheAggregatorIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_not_find_aggregator() + { + var route = new RouteBuilder() + .WithAggregator("TestDefinedAggregator") + .Build(); + + this.Given(_ => GivenNoDefinedAggregator()) + .And(_ => GivenRoute(route)) + .When(_ => WhenIGet()) + .Then(_ => ThenAnErrorIsReturned()) + .BDDfy(); + } + + private void GivenDefinedAggregator() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(); + var services = serviceCollection.BuildServiceProvider(); + _provider = new ServiceLocatorDefinedAggregatorProvider(services); + } + + private void ThenTheAggregatorIsReturned() + { + _aggregator.Data.ShouldNotBeNull(); + _aggregator.Data.ShouldBeOfType(); + _aggregator.IsError.ShouldBeFalse(); + } + + private void GivenNoDefinedAggregator() + { + var serviceCollection = new ServiceCollection(); + var services = serviceCollection.BuildServiceProvider(); + _provider = new ServiceLocatorDefinedAggregatorProvider(services); + } + + private void GivenRoute(Route route) + { + _route = route; + } + + private void WhenIGet() + { + _aggregator = _provider.Get(_route); + } + + private void ThenAnErrorIsReturned() + { + _aggregator.IsError.ShouldBeTrue(); + _aggregator.Errors[0].Message.ShouldBe("Could not find Aggregator: TestDefinedAggregator"); + _aggregator.Errors[0].ShouldBeOfType(); + } + } +} diff --git a/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs b/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs new file mode 100644 index 000000000..d4e1577dd --- /dev/null +++ b/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.Multiplexer; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Logging; +using Ocelot.Middleware; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Multiplexing +{ + public class MultiplexingMiddlewareTests + { + private readonly MultiplexingMiddleware _middleware; + private Ocelot.DownstreamRouteFinder.DownstreamRouteHolder _downstreamRoute; + private int _count; + private readonly Mock _aggregator; + private readonly Mock _factory; + private readonly HttpContext _httpContext; + private readonly RequestDelegate _next; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + + public MultiplexingMiddlewareTests() + { + _httpContext = new DefaultHttpContext(); + _factory = new Mock(); + _aggregator = new Mock(); + _factory.Setup(x => x.Get(It.IsAny())).Returns(_aggregator.Object); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _next = context => Task.FromResult(_count++); + _middleware = new MultiplexingMiddleware(_next, _loggerFactory.Object, _factory.Object); + } + + [Fact] + public void should_multiplex() + { + var route = new RouteBuilder().WithDownstreamRoute(new DownstreamRouteBuilder().Build()).WithDownstreamRoute(new DownstreamRouteBuilder().Build()).Build(); + + this.Given(x => GivenTheFollowing(route)) + .When(x => WhenIMultiplex()) + .Then(x => ThePipelineIsCalled(2)) + .BDDfy(); + } + + [Fact] + public void should_not_multiplex() + { + var route = new RouteBuilder().WithDownstreamRoute(new DownstreamRouteBuilder().Build()).Build(); + + this.Given(x => GivenTheFollowing(route)) + .When(x => WhenIMultiplex()) + .Then(x => ThePipelineIsCalled(1)) + .BDDfy(); + } + + private void GivenTheFollowing(Route route) + { + _downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), route); + _httpContext.Items.UpsertDownstreamRoute(_downstreamRoute); + } + + private void WhenIMultiplex() + { + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); + } + + private void ThePipelineIsCalled(int expected) + { + _count.ShouldBe(expected); + } + } +} diff --git a/test/Ocelot.UnitTests/Middleware/ResponseAggregatorFactoryTests.cs b/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs similarity index 65% rename from test/Ocelot.UnitTests/Middleware/ResponseAggregatorFactoryTests.cs rename to test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs index 73f80f6f6..8608998b7 100644 --- a/test/Ocelot.UnitTests/Middleware/ResponseAggregatorFactoryTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs @@ -1,65 +1,70 @@ -namespace Ocelot.UnitTests.Middleware -{ - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Middleware.Multiplexer; - using Shouldly; - using TestStack.BDDfy; - using Xunit; +using Moq; - public class ResponseAggregatorFactoryTests - { - private readonly InMemoryResponseAggregatorFactory _factory; - private Mock _provider; - private ReRoute _reRoute; - private IResponseAggregator _aggregator; +using Ocelot.Multiplexer; - public ResponseAggregatorFactoryTests() - { - _provider = new Mock(); - _aggregator = new SimpleJsonResponseAggregator(); - _factory = new InMemoryResponseAggregatorFactory(_provider.Object, _aggregator); - } +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; - [Fact] - public void should_return_simple_json_aggregator() - { - var reRoute = new ReRouteBuilder() - .Build(); +using Shouldly; - this.Given(_ => GivenReRoute(reRoute)) - .When(_ => WhenIGet()) - .Then(_ => ThenTheAggregatorIs()) - .BDDfy(); - } +using TestStack.BDDfy; - [Fact] - public void should_return_user_defined_aggregator() - { - var reRoute = new ReRouteBuilder() - .WithAggregator("doesntmatter") - .Build(); - - this.Given(_ => GivenReRoute(reRoute)) - .When(_ => WhenIGet()) - .Then(_ => ThenTheAggregatorIs()) - .BDDfy(); - } - - private void GivenReRoute(ReRoute reRoute) - { - _reRoute = reRoute; - } - - private void WhenIGet() - { - _aggregator = _factory.Get(_reRoute); - } - - private void ThenTheAggregatorIs() - { - _aggregator.ShouldBeOfType(); - } - } -} +using Xunit; + +namespace Ocelot.UnitTests.Multiplexing +{ + public class ResponseAggregatorFactoryTests + { + private readonly InMemoryResponseAggregatorFactory _factory; + private readonly Mock _provider; + private Route _route; + private IResponseAggregator _aggregator; + + public ResponseAggregatorFactoryTests() + { + _provider = new Mock(); + _aggregator = new SimpleJsonResponseAggregator(); + _factory = new InMemoryResponseAggregatorFactory(_provider.Object, _aggregator); + } + + [Fact] + public void should_return_simple_json_aggregator() + { + var route = new RouteBuilder() + .Build(); + + this.Given(_ => GivenRoute(route)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheAggregatorIs()) + .BDDfy(); + } + + [Fact] + public void should_return_user_defined_aggregator() + { + var route = new RouteBuilder() + .WithAggregator("doesntmatter") + .Build(); + + this.Given(_ => GivenRoute(route)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheAggregatorIs()) + .BDDfy(); + } + + private void GivenRoute(Route route) + { + _route = route; + } + + private void WhenIGet() + { + _aggregator = _factory.Get(_route); + } + + private void ThenTheAggregatorIs() + { + _aggregator.ShouldBeOfType(); + } + } +} diff --git a/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs b/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs new file mode 100644 index 000000000..ad36990a9 --- /dev/null +++ b/test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs @@ -0,0 +1,217 @@ +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; + +using Castle.Components.DictionaryAdapter; + +using Microsoft.AspNetCore.Http; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.File; +using Ocelot.Middleware; +using Ocelot.Multiplexer; +using Ocelot.UnitTests.Responder; +using Ocelot.Values; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Multiplexing +{ + public class SimpleJsonResponseAggregatorTests + { + private readonly SimpleJsonResponseAggregator _aggregator; + private List _downstreamContexts; + private HttpContext _upstreamContext; + private Route _route; + + public SimpleJsonResponseAggregatorTests() + { + _aggregator = new SimpleJsonResponseAggregator(); + } + + [Fact] + public void should_aggregate_n_responses_and_set_response_content_on_upstream_context_withConfig() + { + var commentsDownstreamRoute = new DownstreamRouteBuilder().WithKey("Comments").Build(); + + var userDetailsDownstreamRoute = new DownstreamRouteBuilder().WithKey("UserDetails") + .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 0, false, "/v1/users/{userId}")) + .Build(); + + var downstreamRoutes = new List + { + commentsDownstreamRoute, + userDetailsDownstreamRoute, + }; + + var route = new RouteBuilder() + .WithDownstreamRoutes(downstreamRoutes) + .WithAggregateRouteConfig(new List + { + new(){RouteKey = "UserDetails",JsonPath = "$[*].writerId",Parameter = "userId"}, + }) + .Build(); + + var commentsResponseContent = @"[{string.Emptyidstring.Empty:1,string.EmptywriterIdstring.Empty:1,string.EmptypostIdstring.Empty:1,string.Emptytextstring.Empty:string.Emptytext1string.Empty},{string.Emptyidstring.Empty:2,string.EmptywriterIdstring.Empty:2,string.EmptypostIdstring.Empty:2,string.Emptytextstring.Empty:string.Emptytext2string.Empty},{string.Emptyidstring.Empty:3,string.EmptywriterIdstring.Empty:2,string.EmptypostIdstring.Empty:1,string.Emptytextstring.Empty:string.Emptytext21string.Empty}]"; + + var commentsDownstreamContext = new DefaultHttpContext(); + commentsDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent(commentsResponseContent, Encoding.UTF8, "application/json"), HttpStatusCode.OK, new EditableList>>(), "some reason")); + commentsDownstreamContext.Items.UpsertDownstreamRoute(commentsDownstreamRoute); + + var userDetailsResponseContent = @"[{string.Emptyidstring.Empty:1,string.EmptyfirstNamestring.Empty:string.Emptyabolfazlstring.Empty,string.EmptylastNamestring.Empty:string.Emptyrajabpourstring.Empty},{string.Emptyidstring.Empty:2,string.EmptyfirstNamestring.Empty:string.Emptyrezastring.Empty,string.EmptylastNamestring.Empty:string.Emptyrezaeistring.Empty}]"; + var userDetailsDownstreamContext = new DefaultHttpContext(); + userDetailsDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent(userDetailsResponseContent, Encoding.UTF8, "application/json"), HttpStatusCode.OK, new List>>(), "some reason")); + userDetailsDownstreamContext.Items.UpsertDownstreamRoute(userDetailsDownstreamRoute); + + var downstreamContexts = new List { commentsDownstreamContext, userDetailsDownstreamContext }; + + var expected = "{\"Comments\":" + commentsResponseContent + ",\"UserDetails\":" + userDetailsResponseContent + "}"; + + this.Given(x => GivenTheUpstreamContext(new DefaultHttpContext())) + .And(x => GivenTheRoute(route)) + .And(x => GivenTheDownstreamContext(downstreamContexts)) + .When(x => WhenIAggregate()) + .Then(x => ThenTheContentIs(expected)) + .And(x => ThenTheContentTypeIs("application/json")) + .And(x => ThenTheReasonPhraseIs("cannot return from aggregate..which reason phrase would you use?")) + .BDDfy(); + } + + [Fact] + public void should_aggregate_n_responses_and_set_response_content_on_upstream_context() + { + var billDownstreamRoute = new DownstreamRouteBuilder().WithKey("Bill").Build(); + + var georgeDownstreamRoute = new DownstreamRouteBuilder().WithKey("George").Build(); + + var downstreamRoutes = new List + { + billDownstreamRoute, + georgeDownstreamRoute, + }; + + var route = new RouteBuilder() + .WithDownstreamRoutes(downstreamRoutes) + .Build(); + + var billDownstreamContext = new DefaultHttpContext(); + billDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("Bill says hi"), HttpStatusCode.OK, new EditableList>>(), "some reason")); + billDownstreamContext.Items.UpsertDownstreamRoute(billDownstreamRoute); + + var georgeDownstreamContext = new DefaultHttpContext(); + georgeDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("George says hi"), HttpStatusCode.OK, new List>>(), "some reason")); + georgeDownstreamContext.Items.UpsertDownstreamRoute(georgeDownstreamRoute); + + var downstreamContexts = new List { billDownstreamContext, georgeDownstreamContext }; + + var expected = "{\"Bill\":Bill says hi,\"George\":George says hi}"; + + this.Given(x => GivenTheUpstreamContext(new DefaultHttpContext())) + .And(x => GivenTheRoute(route)) + .And(x => GivenTheDownstreamContext(downstreamContexts)) + .When(x => WhenIAggregate()) + .Then(x => ThenTheContentIs(expected)) + .And(x => ThenTheContentTypeIs("application/json")) + .And(x => ThenTheReasonPhraseIs("cannot return from aggregate..which reason phrase would you use?")) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_any_downstreams_have_errored() + { + var billDownstreamRoute = new DownstreamRouteBuilder().WithKey("Bill").Build(); + + var georgeDownstreamRoute = new DownstreamRouteBuilder().WithKey("George").Build(); + + var downstreamRoutes = new List + { + billDownstreamRoute, + georgeDownstreamRoute, + }; + + var route = new RouteBuilder() + .WithDownstreamRoutes(downstreamRoutes) + .Build(); + + var billDownstreamContext = new DefaultHttpContext(); + billDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("Bill says hi"), HttpStatusCode.OK, new List>>(), "some reason")); + billDownstreamContext.Items.UpsertDownstreamRoute(billDownstreamRoute); + + var georgeDownstreamContext = new DefaultHttpContext(); + georgeDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("Error"), HttpStatusCode.OK, new List>>(), "some reason")); + georgeDownstreamContext.Items.UpsertDownstreamRoute(georgeDownstreamRoute); + + georgeDownstreamContext.Items.SetError(new AnyError()); + + var downstreamContexts = new List { billDownstreamContext, georgeDownstreamContext }; + + var expected = "Error"; + + this.Given(x => GivenTheUpstreamContext(new DefaultHttpContext())) + .And(x => GivenTheRoute(route)) + .And(x => GivenTheDownstreamContext(downstreamContexts)) + .When(x => WhenIAggregate()) + .Then(x => ThenTheContentIs(expected)) + .And(x => ThenTheErrorIsMapped()) + .BDDfy(); + } + + private void ThenTheReasonPhraseIs(string expected) + { + _upstreamContext.Items.DownstreamResponse().ReasonPhrase.ShouldBe(expected); + } + + private void ThenTheErrorIsMapped() + { + _upstreamContext.Items.Errors().ShouldBe(_downstreamContexts[1].Items.Errors()); + _upstreamContext.Items.DownstreamResponse().ShouldBe(_downstreamContexts[1].Items.DownstreamResponse()); + } + + private void GivenTheRoute(Route route) + { + _route = route; + } + + private void GivenTheUpstreamContext(HttpContext upstreamContext) + { + _upstreamContext = upstreamContext; + } + + private void GivenTheDownstreamContext(List downstreamContexts) + { + _downstreamContexts = downstreamContexts; + } + + private void WhenIAggregate() + { + _aggregator.Aggregate(_route, _upstreamContext, _downstreamContexts).GetAwaiter().GetResult(); + } + + private void ThenTheContentIs(string expected) + { + var content = _upstreamContext.Items.DownstreamResponse().Content.ReadAsStringAsync() + .GetAwaiter() + .GetResult(); + + content.ShouldBe(expected); + } + + private void ThenTheContentTypeIs(string expected) + { + _upstreamContext.Items.DownstreamResponse().Content.Headers.ContentType.MediaType.ShouldBe(expected); + } + + private void ThenTheUpstreamContextIsMappedForNonAggregate() + { + _upstreamContext.Items.DownstreamRequest().ShouldBe(_downstreamContexts[0].Items.DownstreamRequest()); + _upstreamContext.Items.DownstreamRequest().ShouldBe(_downstreamContexts[0].Items.DownstreamRequest()); + _upstreamContext.Items.Errors().ShouldBe(_downstreamContexts[0].Items.Errors()); + } + } +} diff --git a/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs b/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs new file mode 100644 index 000000000..339de63bc --- /dev/null +++ b/test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs @@ -0,0 +1,161 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.Multiplexer; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Middleware; + +using Ocelot.UnitTests.Responder; + +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Multiplexing +{ + public class UserDefinedResponseAggregatorTests + { + private readonly UserDefinedResponseAggregator _aggregator; + private readonly Mock _provider; + private Route _route; + private List _contexts; + private HttpContext _context; + + public UserDefinedResponseAggregatorTests() + { + _provider = new Mock(); + _aggregator = new UserDefinedResponseAggregator(_provider.Object); + } + + [Fact] + public void should_call_aggregator() + { + var route = new RouteBuilder().Build(); + + var context = new DefaultHttpContext(); + + var contextA = new DefaultHttpContext(); + contextA.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("Tom"), HttpStatusCode.OK, new List>>(), "some reason")); + + var contextB = new DefaultHttpContext(); + contextB.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("Laura"), HttpStatusCode.OK, new List>>(), "some reason")); + + var contexts = new List + { + contextA, + contextB, + }; + + this.Given(_ => GivenTheProviderReturnsAggregator()) + .And(_ => GivenRoute(route)) + .And(_ => GivenContexts(contexts)) + .And(_ => GivenContext(context)) + .When(_ => WhenIAggregate()) + .Then(_ => ThenTheProviderIsCalled()) + .And(_ => ThenTheContentIsCorrect()) + .BDDfy(); + } + + [Fact] + public void should_not_find_aggregator() + { + var route = new RouteBuilder().Build(); + + var context = new DefaultHttpContext(); + + var contextA = new DefaultHttpContext(); + contextA.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("Tom"), HttpStatusCode.OK, new List>>(), "some reason")); + + var contextB = new DefaultHttpContext(); + contextB.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("Laura"), HttpStatusCode.OK, new List>>(), "some reason")); + + var contexts = new List + { + contextA, + contextB, + }; + + this.Given(_ => GivenTheProviderReturnsError()) + .And(_ => GivenRoute(route)) + .And(_ => GivenContexts(contexts)) + .And(_ => GivenContext(context)) + .When(_ => WhenIAggregate()) + .Then(_ => ThenTheProviderIsCalled()) + .And(_ => ThenTheErrorIsReturned()) + .BDDfy(); + } + + private void ThenTheErrorIsReturned() + { + _context.Items.Errors().Count.ShouldBeGreaterThan(0); + _context.Items.Errors().Count.ShouldBe(1); + } + + private void GivenTheProviderReturnsError() + { + _provider.Setup(x => x.Get(It.IsAny())).Returns(new ErrorResponse(new AnyError())); + } + + private async Task ThenTheContentIsCorrect() + { + var content = await _context.Items.DownstreamResponse().Content.ReadAsStringAsync(); + content.ShouldBe("Tom, Laura"); + } + + private void ThenTheProviderIsCalled() + { + _provider.Verify(x => x.Get(_route), Times.Once); + } + + private void GivenContext(HttpContext context) + { + _context = context; + } + + private void GivenContexts(List contexts) + { + _contexts = contexts; + } + + private async Task WhenIAggregate() + { + await _aggregator.Aggregate(_route, _context, _contexts); + } + + private void GivenTheProviderReturnsAggregator() + { + var aggregator = new TestDefinedAggregator(); + _provider.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse(aggregator)); + } + + private void GivenRoute(Route route) + { + _route = route; + } + + public class TestDefinedAggregator : IDefinedAggregator + { + public async Task Aggregate(List responses) + { + var tom = await responses[0].Items.DownstreamResponse().Content.ReadAsStringAsync(); + var laura = await responses[1].Items.DownstreamResponse().Content.ReadAsStringAsync(); + var content = $"{tom}, {laura}"; + var headers = responses.SelectMany(x => x.Items.DownstreamResponse().Headers).ToList(); + return new DownstreamResponse(new StringContent(content), HttpStatusCode.OK, headers, "some reason"); + } + } + } +} diff --git a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj index b999c7be6..bee0691fe 100644 --- a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +++ b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj @@ -1,97 +1,96 @@ - - - - 0.0.0-dev - netcoreapp3.1 - Ocelot.UnitTests - Ocelot.UnitTests - Exe - true - osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64 - false - false - false - ..\..\codeanalysis.ruleset - - - - full - True - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - - - - - - - all - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - \ No newline at end of file + + + + 0.0.0-dev + net7.0 + Ocelot.UnitTests + Ocelot.UnitTests + Exe + true + osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64 + false + false + false + ..\..\codeanalysis.ruleset + True + 1591 + + + + full + True + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + + + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + diff --git a/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs index 448546497..bde033646 100644 --- a/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs @@ -1,45 +1,51 @@ -namespace Ocelot.UnitTests.Polly -{ - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Moq; - using Ocelot.Configuration.Builder; - using Ocelot.DependencyInjection; - using Ocelot.Logging; - using Ocelot.Requester; - using Provider.Polly; - using Shouldly; - using System.IO; - using Xunit; +using System.IO; - public class OcelotBuilderExtensionsTests - { - [Fact] - public void should_build() - { - var loggerFactory = new Mock(); - var services = new ServiceCollection(); - var options = new QoSOptionsBuilder() - .WithTimeoutValue(100) - .WithExceptionsAllowedBeforeBreaking(1) - .WithDurationOfBreak(200) - .Build(); - var reRoute = new DownstreamReRouteBuilder().WithQosOptions(options) - .Build(); +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; - var configuration = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .Build(); - services - .AddOcelot(configuration) - .AddPolly(); - var provider = services.BuildServiceProvider(); +using Moq; - var handler = provider.GetService(); - handler.ShouldNotBeNull(); +using Ocelot.Configuration.Builder; +using Ocelot.DependencyInjection; +using Ocelot.Logging; +using Ocelot.Requester; - var delgatingHandler = handler(reRoute, loggerFactory.Object); - delgatingHandler.ShouldNotBeNull(); - } - } -} +using Ocelot.Provider.Polly; + +using Shouldly; + +using Xunit; + +namespace Ocelot.UnitTests.Polly +{ + public class OcelotBuilderExtensionsTests + { + [Fact] + public void should_build() + { + var loggerFactory = new Mock(); + var services = new ServiceCollection(); + var options = new QoSOptionsBuilder() + .WithTimeoutValue(100) + .WithExceptionsAllowedBeforeBreaking(1) + .WithDurationOfBreak(200) + .Build(); + var route = new DownstreamRouteBuilder().WithQosOptions(options) + .Build(); + + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .Build(); + services + .AddOcelot(configuration) + .AddPolly(); + var provider = services.BuildServiceProvider(); + + var handler = provider.GetService(); + handler.ShouldNotBeNull(); + + var delgatingHandler = handler(route, loggerFactory.Object); + delgatingHandler.ShouldNotBeNull(); + } + } +} diff --git a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs index 4a4cc805d..08392efa6 100644 --- a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs +++ b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs @@ -1,27 +1,31 @@ -namespace Ocelot.UnitTests.Polly -{ - using Moq; - using Ocelot.Configuration.Builder; - using Ocelot.Logging; - using Provider.Polly; - using Shouldly; - using Xunit; +using Moq; - public class PollyQoSProviderTests - { - [Fact] - public void should_build() - { - var options = new QoSOptionsBuilder() - .WithTimeoutValue(100) - .WithExceptionsAllowedBeforeBreaking(1) - .WithDurationOfBreak(200) - .Build(); - var reRoute = new DownstreamReRouteBuilder().WithQosOptions(options) - .Build(); - var factory = new Mock(); - var pollyQoSProvider = new PollyQoSProvider(reRoute, factory.Object); - pollyQoSProvider.CircuitBreaker.ShouldNotBeNull(); - } - } -} +using Ocelot.Configuration.Builder; +using Ocelot.Logging; + +using Ocelot.Provider.Polly; + +using Shouldly; + +using Xunit; + +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(); + } + } +} diff --git a/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs b/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs index d82fbc5be..f8a766f5f 100644 --- a/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs +++ b/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs @@ -1,194 +1,199 @@ -using Moq; -using Ocelot.Configuration; -using Ocelot.Errors; -using Ocelot.Infrastructure.Claims.Parser; -using Ocelot.QueryStrings; -using Ocelot.Request.Middleware; -using Ocelot.Responses; -using Shouldly; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Security.Claims; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.QueryStrings -{ - public class AddQueriesToRequestTests - { - private readonly AddQueriesToRequest _addQueriesToRequest; - private DownstreamRequest _downstreamRequest; - private readonly Mock _parser; - private List _configuration; - private List _claims; - private Response _result; - private Response _claimValue; - private HttpRequestMessage _request; - - public AddQueriesToRequestTests() - { - _request = new HttpRequestMessage(HttpMethod.Post, "http://my.url/abc?q=123"); - _parser = new Mock(); - _addQueriesToRequest = new AddQueriesToRequest(_parser.Object); - _downstreamRequest = new DownstreamRequest(_request); - } - - [Fact] - public void should_add_new_queries_to_downstream_request() - { - var claims = new List - { - new Claim("test", "data") - }; - - this.Given( - x => x.GivenAClaimToThing(new List - { - new ClaimToThing("query-key", "", "", 0) - })) - .Given(x => x.GivenClaims(claims)) - .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) - .When(x => x.WhenIAddQueriesToTheRequest()) - .Then(x => x.ThenTheResultIsSuccess()) - .And(x => x.ThenTheQueryIsAdded()) - .BDDfy(); - } - - [Fact] - public void should_add_new_queries_to_downstream_request_and_preserve_other_queries() - { - var claims = new List - { - new Claim("test", "data") - }; - - this.Given( - x => x.GivenAClaimToThing(new List - { - new ClaimToThing("query-key", "", "", 0) - })) - .Given(x => x.GivenClaims(claims)) - .And(x => GivenTheDownstreamRequestHasQueryString("?test=1&test=2")) - .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) - .When(x => x.WhenIAddQueriesToTheRequest()) - .Then(x => x.ThenTheResultIsSuccess()) - .And(x => x.ThenTheQueryIsAdded()) - .And(x => TheTheQueryStringIs("?test=1&test=2&query-key=value")) - .BDDfy(); - } - - private void TheTheQueryStringIs(string expected) - { - _downstreamRequest.Query.ShouldBe(expected); - } - - [Fact] - public void should_replace_existing_queries_on_downstream_request() - { - var claims = new List - { - new Claim("test", "data") - }; - - this.Given( - x => x.GivenAClaimToThing(new List - { - new ClaimToThing("query-key", "", "", 0) - })) - .And(x => x.GivenClaims(claims)) - .And(x => x.GivenTheDownstreamRequestHasQueryString("query-key", "initial")) - .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) - .When(x => x.WhenIAddQueriesToTheRequest()) - .Then(x => x.ThenTheResultIsSuccess()) - .And(x => x.ThenTheQueryIsAdded()) - .BDDfy(); - } - - [Fact] - public void should_return_error() - { - this.Given( - x => x.GivenAClaimToThing(new List - { - new ClaimToThing("", "", "", 0) - })) - .Given(x => x.GivenClaims(new List())) - .And(x => x.GivenTheClaimParserReturns(new ErrorResponse(new List - { - new AnyError() - }))) - .When(x => x.WhenIAddQueriesToTheRequest()) - .Then(x => x.ThenTheResultIsError()) - .BDDfy(); - } - - private void ThenTheQueryIsAdded() - { - var queries = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_downstreamRequest.ToHttpRequestMessage().RequestUri.OriginalString); - var query = queries.First(x => x.Key == "query-key"); - query.Value.First().ShouldBe(_claimValue.Data); - } - - private void GivenAClaimToThing(List configuration) - { - _configuration = configuration; - } - - private void GivenClaims(List claims) - { - _claims = claims; - } - - private void GivenTheDownstreamRequestHasQueryString(string queryString) - { - _request = new HttpRequestMessage(HttpMethod.Post, $"http://my.url/abc{queryString}"); - _downstreamRequest = new DownstreamRequest(_request); - } - - private void GivenTheDownstreamRequestHasQueryString(string key, string value) - { - var newUri = Microsoft.AspNetCore.WebUtilities.QueryHelpers - .AddQueryString(_downstreamRequest.ToHttpRequestMessage().RequestUri.OriginalString, key, value); - - _request.RequestUri = new Uri(newUri); - } - - private void GivenTheClaimParserReturns(Response claimValue) - { - _claimValue = claimValue; - _parser - .Setup( - x => +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; + +using Moq; + +using Ocelot.Configuration; +using Ocelot.Errors; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.QueryStrings; +using Ocelot.Request.Middleware; +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.QueryStrings +{ + public class AddQueriesToRequestTests + { + private readonly AddQueriesToRequest _addQueriesToRequest; + private DownstreamRequest _downstreamRequest; + private readonly Mock _parser; + private List _configuration; + private List _claims; + private Response _result; + private Response _claimValue; + private HttpRequestMessage _request; + + public AddQueriesToRequestTests() + { + _request = new HttpRequestMessage(HttpMethod.Post, "http://my.url/abc?q=123"); + _parser = new Mock(); + _addQueriesToRequest = new AddQueriesToRequest(_parser.Object); + _downstreamRequest = new DownstreamRequest(_request); + } + + [Fact] + public void should_add_new_queries_to_downstream_request() + { + var claims = new List + { + new("test", "data"), + }; + + this.Given( + x => x.GivenAClaimToThing(new List + { + new("query-key", string.Empty, string.Empty, 0), + })) + .Given(x => x.GivenClaims(claims)) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .When(x => x.WhenIAddQueriesToTheRequest()) + .Then(x => x.ThenTheResultIsSuccess()) + .And(x => x.ThenTheQueryIsAdded()) + .BDDfy(); + } + + [Fact] + public void should_add_new_queries_to_downstream_request_and_preserve_other_queries() + { + var claims = new List + { + new("test", "data"), + }; + + this.Given( + x => x.GivenAClaimToThing(new List + { + new("query-key", string.Empty, string.Empty, 0), + })) + .Given(x => x.GivenClaims(claims)) + .And(x => GivenTheDownstreamRequestHasQueryString("?test=1&test=2")) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .When(x => x.WhenIAddQueriesToTheRequest()) + .Then(x => x.ThenTheResultIsSuccess()) + .And(x => x.ThenTheQueryIsAdded()) + .And(x => TheTheQueryStringIs("?test=1&test=2&query-key=value")) + .BDDfy(); + } + + private void TheTheQueryStringIs(string expected) + { + _downstreamRequest.Query.ShouldBe(expected); + } + + [Fact] + public void should_replace_existing_queries_on_downstream_request() + { + var claims = new List + { + new("test", "data"), + }; + + this.Given( + x => x.GivenAClaimToThing(new List + { + new("query-key", string.Empty, string.Empty, 0), + })) + .And(x => x.GivenClaims(claims)) + .And(x => x.GivenTheDownstreamRequestHasQueryString("query-key", "initial")) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .When(x => x.WhenIAddQueriesToTheRequest()) + .Then(x => x.ThenTheResultIsSuccess()) + .And(x => x.ThenTheQueryIsAdded()) + .BDDfy(); + } + + [Fact] + public void should_return_error() + { + this.Given( + x => x.GivenAClaimToThing(new List + { + new(string.Empty, string.Empty, string.Empty, 0), + })) + .Given(x => x.GivenClaims(new List())) + .And(x => x.GivenTheClaimParserReturns(new ErrorResponse(new List + { + new AnyError(), + }))) + .When(x => x.WhenIAddQueriesToTheRequest()) + .Then(x => x.ThenTheResultIsError()) + .BDDfy(); + } + + private void ThenTheQueryIsAdded() + { + var queries = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_downstreamRequest.ToHttpRequestMessage().RequestUri.OriginalString); + var query = queries.First(x => x.Key == "query-key"); + query.Value.First().ShouldBe(_claimValue.Data); + } + + private void GivenAClaimToThing(List configuration) + { + _configuration = configuration; + } + + private void GivenClaims(List claims) + { + _claims = claims; + } + + private void GivenTheDownstreamRequestHasQueryString(string queryString) + { + _request = new HttpRequestMessage(HttpMethod.Post, $"http://my.url/abc{queryString}"); + _downstreamRequest = new DownstreamRequest(_request); + } + + private void GivenTheDownstreamRequestHasQueryString(string key, string value) + { + var newUri = Microsoft.AspNetCore.WebUtilities.QueryHelpers + .AddQueryString(_downstreamRequest.ToHttpRequestMessage().RequestUri.OriginalString, key, value); + + _request.RequestUri = new Uri(newUri); + } + + private void GivenTheClaimParserReturns(Response claimValue) + { + _claimValue = claimValue; + _parser + .Setup( + x => x.GetValue(It.IsAny>(), It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(_claimValue); - } - - private void WhenIAddQueriesToTheRequest() - { - _result = _addQueriesToRequest.SetQueriesOnDownstreamRequest(_configuration, _claims, _downstreamRequest); - } - - private void ThenTheResultIsSuccess() - { - _result.IsError.ShouldBe(false); - } - - private void ThenTheResultIsError() - { - _result.IsError.ShouldBe(true); - } - - private class AnyError : Error - { + It.IsAny(), + It.IsAny())) + .Returns(_claimValue); + } + + private void WhenIAddQueriesToTheRequest() + { + _result = _addQueriesToRequest.SetQueriesOnDownstreamRequest(_configuration, _claims, _downstreamRequest); + } + + private void ThenTheResultIsSuccess() + { + _result.IsError.ShouldBe(false); + } + + private void ThenTheResultIsError() + { + _result.IsError.ShouldBe(true); + } + + private class AnyError : Error + { public AnyError() - : base("blahh", OcelotErrorCode.UnknownError) - { - } - } - } + : base("blahh", OcelotErrorCode.UnknownError, 404) + { + } + } + } } diff --git a/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs b/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs index d2716faec..653ac4c63 100644 --- a/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs @@ -1,56 +1,63 @@ using Ocelot.Middleware; +using System.Collections.Generic; +using System.Net.Http; +using System.Security.Claims; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.QueryStrings; +using Ocelot.QueryStrings.Middleware; +using Ocelot.Request.Middleware; + +using Ocelot.Responses; + +using TestStack.BDDfy; + +using Xunit; namespace Ocelot.UnitTests.QueryStrings { - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.DownstreamRouteFinder; - using Ocelot.DownstreamRouteFinder.UrlMatcher; - using Ocelot.Logging; - using Ocelot.QueryStrings; - using Ocelot.QueryStrings.Middleware; - using Ocelot.Request.Middleware; - using Ocelot.Responses; - using System.Collections.Generic; - using System.Net.Http; - using System.Security.Claims; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - public class ClaimsToQueryStringMiddlewareTests { private readonly Mock _addQueries; - private Mock _loggerFactory; - private Mock _logger; - private ClaimsToQueryStringMiddleware _middleware; - private DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private readonly ClaimsToQueryStringMiddleware _middleware; + private readonly RequestDelegate _next; + private readonly HttpContext _httpContext; + private Mock _repo; public ClaimsToQueryStringMiddlewareTests() { - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); + _repo = new Mock(); + _httpContext = new DefaultHttpContext(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _addQueries = new Mock(); - _downstreamContext.DownstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); + _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com"))); _middleware = new ClaimsToQueryStringMiddleware(_next, _loggerFactory.Object, _addQueries.Object); } [Fact] public void should_call_add_queries_correctly() { - var downstreamRoute = new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() + var downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithClaimsToQueries(new List { - new ClaimToThing("UserId", "Subject", "", 0) + new("UserId", "Subject", string.Empty, 0), }) .WithUpstreamHttpMethod(new List { "Get" }) .Build()) @@ -66,7 +73,7 @@ public void should_call_add_queries_correctly() private void WhenICallTheMiddleware() { - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); } private void GivenTheAddHeadersToRequestReturnsOk() @@ -85,13 +92,14 @@ private void ThenTheAddQueriesToRequestIsCalledCorrectly() .Verify(x => x.SetQueriesOnDownstreamRequest( It.IsAny>(), It.IsAny>(), - _downstreamContext.DownstreamRequest), Times.Once); + _httpContext.Items.DownstreamRequest()), Times.Once); } - private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + private void GivenTheDownStreamRouteIs(Ocelot.DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute) { - _downstreamContext.TemplatePlaceholderNameAndValues = downstreamRoute.TemplatePlaceholderNameAndValues; - _downstreamContext.DownstreamReRoute = downstreamRoute.ReRoute.DownstreamReRoute[0]; + _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + + _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); } } } diff --git a/test/Ocelot.UnitTests/Rafty/OcelotAdministrationBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Rafty/OcelotAdministrationBuilderExtensionsTests.cs deleted file mode 100644 index 212d92931..000000000 --- a/test/Ocelot.UnitTests/Rafty/OcelotAdministrationBuilderExtensionsTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -namespace Ocelot.UnitTests.Rafty -{ - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Moq; - using Ocelot.Administration; - using Ocelot.DependencyInjection; - using Provider.Rafty; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Reflection; - using TestStack.BDDfy; - using Xunit; - - public class OcelotAdministrationBuilderExtensionsTests - { - private readonly IServiceCollection _services; - private IServiceProvider _serviceProvider; - private readonly IConfiguration _configRoot; - private IOcelotBuilder _ocelotBuilder; - private Exception _ex; - - public OcelotAdministrationBuilderExtensionsTests() - { - _configRoot = new ConfigurationRoot(new List()); - _services = new ServiceCollection(); - _services.AddSingleton(GetHostingEnvironment()); - _services.AddSingleton(_configRoot); - } - - private IWebHostEnvironment GetHostingEnvironment() - { - var environment = new Mock(); - environment - .Setup(e => e.ApplicationName) - .Returns(typeof(OcelotAdministrationBuilderExtensionsTests).GetTypeInfo().Assembly.GetName().Name); - - return environment.Object; - } - - [Fact] - public void should_set_up_rafty() - { - this.Given(x => WhenISetUpOcelotServices()) - .When(x => WhenISetUpRafty()) - .Then(x => ThenAnExceptionIsntThrown()) - .Then(x => ThenTheCorrectAdminPathIsRegitered()) - .BDDfy(); - } - - private void WhenISetUpRafty() - { - try - { - _ocelotBuilder.AddAdministration("/administration", "secret").AddRafty(); - } - catch (Exception e) - { - _ex = e; - } - } - - private void WhenISetUpOcelotServices() - { - try - { - _ocelotBuilder = _services.AddOcelot(_configRoot); - } - catch (Exception e) - { - _ex = e; - } - } - - private void ThenAnExceptionIsntThrown() - { - _ex.ShouldBeNull(); - } - - private void ThenTheCorrectAdminPathIsRegitered() - { - _serviceProvider = _services.BuildServiceProvider(); - var path = _serviceProvider.GetService(); - path.Path.ShouldBe("/administration"); - } - } -} diff --git a/test/Ocelot.UnitTests/Rafty/OcelotFiniteStateMachineTests.cs b/test/Ocelot.UnitTests/Rafty/OcelotFiniteStateMachineTests.cs deleted file mode 100644 index 53dc3476c..000000000 --- a/test/Ocelot.UnitTests/Rafty/OcelotFiniteStateMachineTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace Ocelot.UnitTests.Rafty -{ - using Moq; - using Ocelot.Configuration.Setter; - using Provider.Rafty; - using TestStack.BDDfy; - using Xunit; - - public class OcelotFiniteStateMachineTests - { - private UpdateFileConfiguration _command; - private readonly OcelotFiniteStateMachine _fsm; - private readonly Mock _setter; - - public OcelotFiniteStateMachineTests() - { - _setter = new Mock(); - _fsm = new OcelotFiniteStateMachine(_setter.Object); - } - - [Fact] - public void should_handle_update_file_configuration_command() - { - this.Given(x => GivenACommand(new UpdateFileConfiguration(new Ocelot.Configuration.File.FileConfiguration()))) - .When(x => WhenTheCommandIsHandled()) - .Then(x => ThenTheStateIsUpdated()) - .BDDfy(); - } - - private void GivenACommand(UpdateFileConfiguration command) - { - _command = command; - } - - private void WhenTheCommandIsHandled() - { - _fsm.Handle(new global::Rafty.Log.LogEntry(_command, _command.GetType(), 0)).Wait(); - } - - private void ThenTheStateIsUpdated() - { - _setter.Verify(x => x.Set(_command.Configuration), Times.Once); - } - } -} diff --git a/test/Ocelot.UnitTests/Rafty/RaftyFileConfigurationSetterTests.cs b/test/Ocelot.UnitTests/Rafty/RaftyFileConfigurationSetterTests.cs deleted file mode 100644 index fac7d2fa9..000000000 --- a/test/Ocelot.UnitTests/Rafty/RaftyFileConfigurationSetterTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace Ocelot.UnitTests.Rafty -{ - using global::Rafty.Concensus.Node; - using global::Rafty.Infrastructure; - using Moq; - using Ocelot.Configuration.File; - using Provider.Rafty; - using Shouldly; - using System.Threading.Tasks; - using Xunit; - - public class RaftyFileConfigurationSetterTests - { - private readonly RaftyFileConfigurationSetter _setter; - private readonly Mock _node; - - public RaftyFileConfigurationSetterTests() - { - _node = new Mock(); - _setter = new RaftyFileConfigurationSetter(_node.Object); - } - - [Fact] - public async Task should_return_ok() - { - var fileConfig = new FileConfiguration(); - - var response = new OkResponse(new UpdateFileConfiguration(fileConfig)); - - _node.Setup(x => x.Accept(It.IsAny())) - .ReturnsAsync(response); - - var result = await _setter.Set(fileConfig); - result.IsError.ShouldBeFalse(); - } - - [Fact] - public async Task should_return_not_ok() - { - var fileConfig = new FileConfiguration(); - - var response = new ErrorResponse("error", new UpdateFileConfiguration(fileConfig)); - - _node.Setup(x => x.Accept(It.IsAny())) - .ReturnsAsync(response); - - var result = await _setter.Set(fileConfig); - - result.IsError.ShouldBeTrue(); - } - } -} diff --git a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs index d0f69cec6..dbee5a2f6 100644 --- a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs @@ -1,179 +1,191 @@ -using Ocelot.Middleware; - -namespace Ocelot.UnitTests.RateLimit -{ - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.Caching.Memory; - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.DownstreamRouteFinder; - using Ocelot.Logging; - using Ocelot.RateLimit; - using Ocelot.RateLimit.Middleware; - using Ocelot.Request.Middleware; - using Shouldly; - using System.Collections.Generic; - using System.IO; - using System.Net.Http; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - - public class ClientRateLimitMiddlewareTests - { - private int _responseStatusCode; - private IRateLimitCounterHandler _rateLimitCounterHandler; - private Mock _loggerFactory; - private Mock _logger; - private readonly ClientRateLimitMiddleware _middleware; - private readonly DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; - private readonly string _url; - - public ClientRateLimitMiddlewareTests() - { - _url = "http://localhost:51879"; - var cacheEntryOptions = new MemoryCacheOptions(); - _rateLimitCounterHandler = new MemoryCacheRateLimitCounterHandler(new MemoryCache(cacheEntryOptions)); - var httpContext = new DefaultHttpContext(); - _downstreamContext = new DownstreamContext(httpContext); - _downstreamContext.HttpContext.Response.Body = new FakeStream(); - - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _next = context => Task.CompletedTask; - _middleware = new ClientRateLimitMiddleware(_next, _loggerFactory.Object, _rateLimitCounterHandler); - } - - [Fact] - public void should_call_middleware_and_ratelimiting() - { - var upstreamTemplate = new UpstreamPathTemplateBuilder().Build(); - - var downstreamReRoute = new DownstreamReRouteBuilder() - .WithEnableRateLimiting(true) - .WithRateLimitOptions(new RateLimitOptions(true, "ClientId", () => new List(), false, "", "", new RateLimitRule("1s", 100, 3), 429)) - .WithUpstreamHttpMethod(new List { "Get" }) - .WithUpstreamPathTemplate(upstreamTemplate) - .Build(); - - var reRoute = new ReRouteBuilder() - .WithDownstreamReRoute(downstreamReRoute) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build(); - - var downstreamRoute = new DownstreamRoute(new List(), reRoute); - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) - .When(x => x.WhenICallTheMiddlewareMultipleTime(2)) - .Then(x => x.ThenresponseStatusCodeIs200()) - .When(x => x.WhenICallTheMiddlewareMultipleTime(3)) - .Then(x => x.ThenresponseStatusCodeIs429()) - .BDDfy(); - } - - [Fact] - public void should_call_middleware_withWhitelistClient() - { - var downstreamRoute = new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithEnableRateLimiting(true) - .WithRateLimitOptions( - new Ocelot.Configuration.RateLimitOptions(true, "ClientId", () => new List() { "ocelotclient2" }, false, "", "", new RateLimitRule("1s", 100, 3), 429)) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) - .When(x => x.WhenICallTheMiddlewareWithWhiteClient()) - .Then(x => x.ThenresponseStatusCodeIs200()) - .BDDfy(); - } - - private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) - { - _downstreamContext.TemplatePlaceholderNameAndValues = downstreamRoute.TemplatePlaceholderNameAndValues; - _downstreamContext.DownstreamReRoute = downstreamRoute.ReRoute.DownstreamReRoute[0]; - } - - private void WhenICallTheMiddlewareMultipleTime(int times) - { - var clientId = "ocelotclient1"; - - for (int i = 0; i < times; i++) - { - var request = new HttpRequestMessage(new HttpMethod("GET"), _url); - request.Headers.Add("ClientId", clientId); - _downstreamContext.DownstreamRequest = new DownstreamRequest(request); - - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); - _responseStatusCode = (int)_downstreamContext.HttpContext.Response.StatusCode; - } - } - - private void WhenICallTheMiddlewareWithWhiteClient() - { - var clientId = "ocelotclient2"; - - for (int i = 0; i < 10; i++) - { - var request = new HttpRequestMessage(new HttpMethod("GET"), _url); - request.Headers.Add("ClientId", clientId); - _downstreamContext.DownstreamRequest = new DownstreamRequest(request); - _downstreamContext.HttpContext.Request.Headers.TryAdd("ClientId", clientId); - - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); - _responseStatusCode = (int)_downstreamContext.HttpContext.Response.StatusCode; - } - } - - private void ThenresponseStatusCodeIs429() - { - _responseStatusCode.ShouldBe(429); - } - - private void ThenresponseStatusCodeIs200() - { - _responseStatusCode.ShouldBe(200); - } - } - - internal class FakeStream : Stream - { - public override void Flush() - { - //do nothing - //throw new System.NotImplementedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new System.NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new System.NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new System.NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - //do nothing - } - - public override bool CanRead { get; } - public override bool CanSeek { get; } - public override bool CanWrite => true; - public override long Length { get; } - public override long Position { get; set; } - } -} +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.RateLimit; +using Ocelot.RateLimit.Middleware; +using Ocelot.Request.Middleware; +using Shouldly; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.RateLimit +{ + public class ClientRateLimitMiddlewareTests + { + private readonly IRateLimitCounterHandler _rateLimitCounterHandler; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private readonly ClientRateLimitMiddleware _middleware; + private readonly RequestDelegate _next; + private DownstreamResponse _downstreamResponse; + private readonly string _url; + + public ClientRateLimitMiddlewareTests() + { + _url = "http://localhost:51879"; + var cacheEntryOptions = new MemoryCacheOptions(); + _rateLimitCounterHandler = new MemoryCacheRateLimitCounterHandler(new MemoryCache(cacheEntryOptions)); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _next = context => Task.CompletedTask; + _middleware = new ClientRateLimitMiddleware(_next, _loggerFactory.Object, _rateLimitCounterHandler); + } + + [Fact] + public void should_call_middleware_and_ratelimiting() + { + var upstreamTemplate = new UpstreamPathTemplateBuilder().Build(); + + var downstreamRoute = new DownstreamRouteBuilder() + .WithEnableRateLimiting(true) + .WithRateLimitOptions(new RateLimitOptions(true, "ClientId", () => new List(), false, string.Empty, string.Empty, new RateLimitRule("1s", 100, 3), 429)) + .WithUpstreamHttpMethod(new List { "Get" }) + .WithUpstreamPathTemplate(upstreamTemplate) + .Build(); + + var route = new RouteBuilder() + .WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build(); + + var downstreamRouteHolder = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), route); + + this.Given(x => x.WhenICallTheMiddlewareMultipleTimes(2, downstreamRouteHolder)) + .Then(x => x.ThenThereIsNoDownstreamResponse()) + .When(x => x.WhenICallTheMiddlewareMultipleTimes(3, downstreamRouteHolder)) + .Then(x => x.ThenTheResponseIs429()) + .BDDfy(); + } + + [Fact] + public void should_call_middleware_withWhitelistClient() + { + var downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithEnableRateLimiting(true) + .WithRateLimitOptions( + new RateLimitOptions(true, "ClientId", () => new List { "ocelotclient2" }, false, string.Empty, string.Empty, new RateLimitRule("1s", 100, 3), 429)) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()); + + this.Given(x => x.WhenICallTheMiddlewareWithWhiteClient(downstreamRoute)) + .Then(x => x.ThenThereIsNoDownstreamResponse()) + .BDDfy(); + } + + private void WhenICallTheMiddlewareMultipleTimes(int times, Ocelot.DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute) + { + var httpContexts = new List(); + + for (var i = 0; i < times; i++) + { + var httpContext = new DefaultHttpContext + { + Response = + { + Body = new FakeStream(), + }, + }; + httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); + httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + httpContext.Items.UpsertDownstreamRoute(downstreamRoute); + var clientId = "ocelotclient1"; + var request = new HttpRequestMessage(new HttpMethod("GET"), _url); + httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(request)); + httpContext.Request.Headers.TryAdd("ClientId", clientId); + httpContexts.Add(httpContext); + } + + foreach (var httpContext in httpContexts) + { + _middleware.Invoke(httpContext).GetAwaiter().GetResult(); + var ds = httpContext.Items.DownstreamResponse(); + _downstreamResponse = ds; + } + } + + private void WhenICallTheMiddlewareWithWhiteClient(Ocelot.DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute) + { + var clientId = "ocelotclient2"; + + for (var i = 0; i < 10; i++) + { + var httpContext = new DefaultHttpContext + { + Response = + { + Body = new FakeStream(), + }, + }; + httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); + httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + httpContext.Items.UpsertDownstreamRoute(downstreamRoute); + var request = new HttpRequestMessage(new HttpMethod("GET"), _url); + request.Headers.Add("ClientId", clientId); + httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(request)); + httpContext.Request.Headers.TryAdd("ClientId", clientId); + _middleware.Invoke(httpContext).GetAwaiter().GetResult(); + var ds = httpContext.Items.DownstreamResponse(); + _downstreamResponse = ds; + } + } + + private void ThenTheResponseIs429() + { + var code = (int)_downstreamResponse.StatusCode; + code.ShouldBe(429); + } + + private void ThenThereIsNoDownstreamResponse() + { + _downstreamResponse.ShouldBeNull(); + } + } + + internal class FakeStream : Stream + { + public override void Flush() + { + //do nothing + //throw new System.NotImplementedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new System.NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new System.NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new System.NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + //do nothing + } + + public override bool CanRead { get; } + public override bool CanSeek { get; } + public override bool CanWrite => true; + public override long Length { get; } + public override long Position { get; set; } + } +} diff --git a/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs b/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs index 5b2950747..9ec3a7ccd 100644 --- a/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs @@ -1,16 +1,20 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; + using Ocelot.Infrastructure.RequestData; -using Ocelot.Responses; -using Shouldly; -using TestStack.BDDfy; +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Repository { public class ScopedRequestDataRepositoryTests { - private IRequestScopedDataRepository _requestScopedDataRepository; - private IHttpContextAccessor _httpContextAccesor; + private readonly IRequestScopedDataRepository _requestScopedDataRepository; + private readonly IHttpContextAccessor _httpContextAccesor; private string _key; private object _toAdd; private Response _result; diff --git a/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs b/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs index 32afcae91..21f17617e 100644 --- a/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs +++ b/test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs @@ -1,20 +1,25 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; + using Moq; + using Ocelot.Infrastructure; using Ocelot.Request.Creator; using Ocelot.Request.Middleware; + using Shouldly; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; + using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Request.Creator { public class DownstreamRequestCreatorTests { - private Mock _framework; - private DownstreamRequestCreator _downstreamRequestCreator; + private readonly Mock _framework; + private readonly DownstreamRequestCreator _downstreamRequestCreator; private HttpRequestMessage _request; private DownstreamRequest _result; @@ -31,7 +36,7 @@ public void should_create_downstream_request() var content = new StringContent("test"); request.Content = content; - this.Given(_ => GivenTheFrameworkIs("")) + this.Given(_ => GivenTheFrameworkIs(string.Empty)) .And(_ => GivenTheRequestIs(request)) .When(_ => WhenICreate()) .Then(_ => ThenTheDownstreamRequestHasABody()) diff --git a/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs b/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs index 93d963b68..160ee0ef5 100644 --- a/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs @@ -1,45 +1,43 @@ -using Ocelot.Middleware; +using System.Net.Http; -namespace Ocelot.UnitTests.Request -{ - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Infrastructure; - using Ocelot.Logging; - using Ocelot.Request.Creator; - using Ocelot.Request.Mapper; - using Ocelot.Request.Middleware; - using Ocelot.Responses; - using Shouldly; - using System.Net.Http; - using TestStack.BDDfy; - using Xunit; +using Microsoft.AspNetCore.Http; - public class DownstreamRequestInitialiserMiddlewareTests - { - private readonly DownstreamRequestInitialiserMiddleware _middleware; +using Moq; - private readonly Mock _httpContext; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Infrastructure; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.Request.Creator; +using Ocelot.Request.Mapper; +using Ocelot.Request.Middleware; - private readonly Mock _httpRequest; +using Ocelot.Responses; - private readonly Mock _next; +using Shouldly; - private readonly Mock _requestMapper; +using TestStack.BDDfy; - private readonly Mock _loggerFactory; +using Xunit; +namespace Ocelot.UnitTests.Request +{ + public class DownstreamRequestInitialiserMiddlewareTests + { + private readonly DownstreamRequestInitialiserMiddleware _middleware; + private readonly HttpContext _httpContext; + private readonly Mock _next; + private readonly Mock _requestMapper; + private readonly Mock _loggerFactory; private readonly Mock _logger; - private Response _mappedRequest; - private DownstreamContext _downstreamContext; public DownstreamRequestInitialiserMiddlewareTests() { - _httpContext = new Mock(); - _httpRequest = new Mock(); + _httpContext = new DefaultHttpContext(); _requestMapper = new Mock(); - _next = new Mock(); + _next = new Mock(); _logger = new Mock(); _loggerFactory = new Mock(); @@ -52,8 +50,6 @@ public DownstreamRequestInitialiserMiddlewareTests() _loggerFactory.Object, _requestMapper.Object, new DownstreamRequestCreator(new FrameworkDescription())); - - _downstreamContext = new DownstreamContext(_httpContext.Object); } [Fact] @@ -65,6 +61,20 @@ public void Should_handle_valid_httpRequest() .Then(_ => ThenTheContexRequestIsMappedToADownstreamRequest()) .And(_ => ThenTheDownstreamRequestIsStored()) .And(_ => ThenTheNextMiddlewareIsInvoked()) + .And(_ => ThenTheDownstreamRequestMethodIs("GET")) + .BDDfy(); + } + + [Fact] + public void Should_map_downstream_route_method_to_downstream_request() + { + this.Given(_ => GivenTheHttpContextContainsARequest()) + .And(_ => GivenTheMapperWillReturnAMappedRequest()) + .When(_ => WhenTheMiddlewareIsInvoked()) + .Then(_ => ThenTheContexRequestIsMappedToADownstreamRequest()) + .And(_ => ThenTheDownstreamRequestIsStored()) + .And(_ => ThenTheNextMiddlewareIsInvoked()) + .And(_ => ThenTheDownstreamRequestMethodIs("GET")) .BDDfy(); } @@ -80,11 +90,14 @@ public void Should_handle_mapping_failure() .BDDfy(); } + private void ThenTheDownstreamRequestMethodIs(string expected) + { + _httpContext.Items.DownstreamRequest().Method.ShouldBe(expected); + } + private void GivenTheHttpContextContainsARequest() { - _httpContext - .Setup(hc => hc.Request) - .Returns(_httpRequest.Object); + _httpContext.Items.UpsertDownstreamRoute(new DownstreamRouteBuilder().Build()); } private void GivenTheMapperWillReturnAMappedRequest() @@ -92,7 +105,7 @@ private void GivenTheMapperWillReturnAMappedRequest() _mappedRequest = new OkResponse(new HttpRequestMessage(HttpMethod.Get, "http://www.bbc.co.uk")); _requestMapper - .Setup(rm => rm.Map(It.IsAny())) + .Setup(rm => rm.Map(It.IsAny(), It.IsAny())) .ReturnsAsync(_mappedRequest); } @@ -101,44 +114,44 @@ private void GivenTheMapperWillReturnAnError() _mappedRequest = new ErrorResponse(new UnmappableRequestError(new System.Exception("boooom!"))); _requestMapper - .Setup(rm => rm.Map(It.IsAny())) + .Setup(rm => rm.Map(It.IsAny(), It.IsAny())) .ReturnsAsync(_mappedRequest); } private void WhenTheMiddlewareIsInvoked() { - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); } private void ThenTheContexRequestIsMappedToADownstreamRequest() { - _requestMapper.Verify(rm => rm.Map(_httpRequest.Object), Times.Once); + _requestMapper.Verify(rm => rm.Map(_httpContext.Request, _httpContext.Items.DownstreamRoute()), Times.Once); } private void ThenTheDownstreamRequestIsStored() { - _downstreamContext.DownstreamRequest.ShouldNotBeNull(); + _httpContext.Items.DownstreamRequest().ShouldNotBeNull(); } private void ThenTheDownstreamRequestIsNotStored() { - _downstreamContext.DownstreamRequest.ShouldBeNull(); + _httpContext.Items.DownstreamRequest().ShouldBeNull(); } private void ThenAPipelineErrorIsStored() { - _downstreamContext.IsError.ShouldBeTrue(); - _downstreamContext.Errors.ShouldBe(_mappedRequest.Errors); + _httpContext.Items.Errors().Count.ShouldBeGreaterThan(0); + _httpContext.Items.Errors().ShouldBe(_mappedRequest.Errors); } private void ThenTheNextMiddlewareIsInvoked() { - _next.Verify(n => n(_downstreamContext), Times.Once); + _next.Verify(n => n(_httpContext), Times.Once); } private void ThenTheNextMiddlewareIsNotInvoked() { - _next.Verify(n => n(It.IsAny()), Times.Never); + _next.Verify(n => n(It.IsAny()), Times.Never); } } } diff --git a/test/Ocelot.UnitTests/Request/DownstreamRequestTests.cs b/test/Ocelot.UnitTests/Request/DownstreamRequestTests.cs index d5882951e..e5e2a6eec 100644 --- a/test/Ocelot.UnitTests/Request/DownstreamRequestTests.cs +++ b/test/Ocelot.UnitTests/Request/DownstreamRequestTests.cs @@ -1,7 +1,10 @@ -using Ocelot.Request.Middleware; -using Shouldly; using System; using System.Net.Http; + +using Ocelot.Request.Middleware; + +using Shouldly; + using Xunit; namespace Ocelot.UnitTests.Request diff --git a/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs b/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs index ac81a4d47..55f4eeab1 100644 --- a/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs +++ b/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs @@ -1,441 +1,488 @@ -namespace Ocelot.UnitTests.Request.Mapper -{ - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.Primitives; - using Ocelot.Request.Mapper; - using Ocelot.Responses; - using Shouldly; - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Net.Http; - using System.Security.Cryptography; - using System.Text; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - - public class RequestMapperTests - { - private readonly HttpContext _httpContext; - private readonly HttpRequest _inputRequest; - - private readonly RequestMapper _requestMapper; - - private Response _mappedRequest; - - private List> _inputHeaders = null; - - public RequestMapperTests() - { - _httpContext = new DefaultHttpContext(); - _inputRequest = _httpContext.Request; - _requestMapper = new RequestMapper(); - } - - [Theory] - [InlineData("https", "my.url:123", "/abc/DEF", "?a=1&b=2", "https://my.url:123/abc/DEF?a=1&b=2")] - [InlineData("http", "blah.com", "/d ef", "?abc=123", "http://blah.com/d%20ef?abc=123")] // note! the input is encoded when building the input request - [InlineData("http", "myusername:mypassword@abc.co.uk", null, null, "http://myusername:mypassword@abc.co.uk/")] - [InlineData("http", "點看.com", null, null, "http://xn--c1yn36f.com/")] - [InlineData("http", "xn--c1yn36f.com", null, null, "http://xn--c1yn36f.com/")] - public void Should_map_valid_request_uri(string scheme, string host, string path, string queryString, string expectedUri) - { - this.Given(_ => GivenTheInputRequestHasMethod("GET")) - .And(_ => GivenTheInputRequestHasScheme(scheme)) - .And(_ => GivenTheInputRequestHasHost(host)) - .And(_ => GivenTheInputRequestHasPath(path)) - .And(_ => GivenTheInputRequestHasQueryString(queryString)) - .When(_ => WhenMapped()) - .Then(_ => ThenNoErrorIsReturned()) - .And(_ => ThenTheMappedRequestHasUri(expectedUri)) - .BDDfy(); - } - - [Theory] - [InlineData("ftp", "google.com", "/abc/DEF", "?a=1&b=2")] - public void Should_error_on_unsupported_request_uri(string scheme, string host, string path, string queryString) - { - this.Given(_ => GivenTheInputRequestHasMethod("GET")) - .And(_ => GivenTheInputRequestHasScheme(scheme)) - .And(_ => GivenTheInputRequestHasHost(host)) - .And(_ => GivenTheInputRequestHasPath(path)) - .And(_ => GivenTheInputRequestHasQueryString(queryString)) - .When(_ => WhenMapped()) - .Then(_ => ThenAnErrorIsReturned()) - .And(_ => ThenTheMappedRequestIsNull()) - .BDDfy(); - } - - [Theory] - [InlineData("GET")] - [InlineData("POST")] - [InlineData("WHATEVER")] - public void Should_map_method(string method) - { - this.Given(_ => GivenTheInputRequestHasMethod(method)) - .And(_ => GivenTheInputRequestHasAValidUri()) - .When(_ => WhenMapped()) - .Then(_ => ThenNoErrorIsReturned()) - .And(_ => ThenTheMappedRequestHasMethod(method)) - .BDDfy(); - } - - [Fact] - public void Should_map_all_headers() - { - this.Given(_ => GivenTheInputRequestHasHeaders()) - .And(_ => GivenTheInputRequestHasMethod("GET")) - .And(_ => GivenTheInputRequestHasAValidUri()) - .When(_ => WhenMapped()) - .Then(_ => ThenNoErrorIsReturned()) - .And(_ => ThenTheMappedRequestHasEachHeader()) - .BDDfy(); - } - - [Fact] - public void Should_handle_no_headers() - { - this.Given(_ => GivenTheInputRequestHasNoHeaders()) - .And(_ => GivenTheInputRequestHasMethod("GET")) - .And(_ => GivenTheInputRequestHasAValidUri()) - .When(_ => WhenMapped()) - .Then(_ => ThenNoErrorIsReturned()) - .And(_ => ThenTheMappedRequestHasNoHeaders()) - .BDDfy(); - } - - [Fact] - public void Should_map_content() - { - this.Given(_ => GivenTheInputRequestHasContent("This is my content")) - .And(_ => GivenTheInputRequestHasMethod("GET")) - .And(_ => GivenTheInputRequestHasAValidUri()) - .When(_ => WhenMapped()) - .Then(_ => ThenNoErrorIsReturned()) - .And(_ => ThenTheMappedRequestHasContent("This is my content")) - .BDDfy(); - } - - [Fact] - public void Should_handle_no_content() - { - this.Given(_ => GivenTheInputRequestHasNullContent()) - .And(_ => GivenTheInputRequestHasMethod("GET")) - .And(_ => GivenTheInputRequestHasAValidUri()) - .When(_ => WhenMapped()) - .Then(_ => ThenNoErrorIsReturned()) - .And(_ => ThenTheMappedRequestHasNoContent()) - .BDDfy(); - } - - [Fact] - public void Should_handle_no_content_type() - { - this.Given(_ => GivenTheInputRequestHasNoContentType()) - .And(_ => GivenTheInputRequestHasMethod("GET")) - .And(_ => GivenTheInputRequestHasAValidUri()) - .When(_ => WhenMapped()) - .Then(_ => ThenNoErrorIsReturned()) - .And(_ => ThenTheMappedRequestHasNoContent()) - .BDDfy(); - } - - [Fact] - public void Should_handle_no_content_length() - { - this.Given(_ => GivenTheInputRequestHasNoContentLength()) - .And(_ => GivenTheInputRequestHasMethod("GET")) - .And(_ => GivenTheInputRequestHasAValidUri()) - .When(_ => WhenMapped()) - .Then(_ => ThenNoErrorIsReturned()) - .And(_ => ThenTheMappedRequestHasNoContent()) - .BDDfy(); - } - - private void GivenTheInputRequestHasNoContentLength() - { - _inputRequest.ContentLength = null; - } - - private void GivenTheInputRequestHasNoContentType() - { - _inputRequest.ContentType = null; - } - - [Fact] - public void Should_map_content_headers() - { - byte[] md5bytes = new byte[0]; - using (var md5 = MD5.Create()) - { - md5bytes = md5.ComputeHash(Encoding.UTF8.GetBytes("some md5")); - } - - this.Given(_ => GivenTheInputRequestHasContent("This is my content")) - .And(_ => GivenTheContentTypeIs("application/json")) - .And(_ => GivenTheContentEncodingIs("gzip, compress")) - .And(_ => GivenTheContentLanguageIs("english")) - .And(_ => GivenTheContentLocationIs("/my-receipts/38")) - .And(_ => GivenTheContentRangeIs("bytes 1-2/*")) - .And(_ => GivenTheContentDispositionIs("inline")) - .And(_ => GivenTheContentMD5Is(md5bytes)) - .And(_ => GivenTheInputRequestHasMethod("GET")) - .And(_ => GivenTheInputRequestHasAValidUri()) - .When(_ => WhenMapped()) - .Then(_ => ThenNoErrorIsReturned()) - .And(_ => ThenTheMappedRequestHasContentTypeHeader("application/json")) - .And(_ => ThenTheMappedRequestHasContentEncodingHeader("gzip", "compress")) - .And(_ => ThenTheMappedRequestHasContentLanguageHeader("english")) - .And(_ => ThenTheMappedRequestHasContentLocationHeader("/my-receipts/38")) - .And(_ => ThenTheMappedRequestHasContentMD5Header(md5bytes)) - .And(_ => ThenTheMappedRequestHasContentRangeHeader()) - .And(_ => ThenTheMappedRequestHasContentDispositionHeader("inline")) - .And(_ => ThenTheMappedRequestHasContentSize("This is my content".Length)) - .And(_ => ThenTheContentHeadersAreNotAddedToNonContentHeaders()) - .BDDfy(); - } - - [Fact] - public void should_not_add_content_headers() - { - this.Given(_ => GivenTheInputRequestHasContent("This is my content")) - .And(_ => GivenTheContentTypeIs("application/json")) - .And(_ => GivenTheInputRequestHasMethod("POST")) - .And(_ => GivenTheInputRequestHasAValidUri()) - .When(_ => WhenMapped()) - .Then(_ => ThenNoErrorIsReturned()) - .And(_ => ThenTheMappedRequestHasContentTypeHeader("application/json")) - .And(_ => ThenTheMappedRequestHasContentSize("This is my content".Length)) - .And(_ => ThenTheOtherContentTypeHeadersAreNotMapped()) - .BDDfy(); - } - - private void ThenTheContentHeadersAreNotAddedToNonContentHeaders() - { - _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-Disposition"); - _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-ContentMD5"); - _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-ContentRange"); - _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-ContentLanguage"); - _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-ContentEncoding"); - _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-ContentLocation"); - _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-Length"); - _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-Type"); - } - - private void ThenTheOtherContentTypeHeadersAreNotMapped() - { - _mappedRequest.Data.Content.Headers.ContentDisposition.ShouldBeNull(); - _mappedRequest.Data.Content.Headers.ContentMD5.ShouldBeNull(); - _mappedRequest.Data.Content.Headers.ContentRange.ShouldBeNull(); - _mappedRequest.Data.Content.Headers.ContentLanguage.ShouldBeEmpty(); - _mappedRequest.Data.Content.Headers.ContentEncoding.ShouldBeEmpty(); - _mappedRequest.Data.Content.Headers.ContentLocation.ShouldBeNull(); - } - - private void ThenTheMappedRequestHasContentDispositionHeader(string expected) - { - _mappedRequest.Data.Content.Headers.ContentDisposition.DispositionType.ShouldBe(expected); - } - - private void GivenTheContentDispositionIs(string input) - { - _inputRequest.Headers.Add("Content-Disposition", input); - } - - private void ThenTheMappedRequestHasContentMD5Header(byte[] expected) - { - _mappedRequest.Data.Content.Headers.ContentMD5.ShouldBe(expected); - } - - private void GivenTheContentMD5Is(byte[] input) - { - var base64 = Convert.ToBase64String(input); - _inputRequest.Headers.Add("Content-MD5", base64); - } - - private void ThenTheMappedRequestHasContentRangeHeader() - { - _mappedRequest.Data.Content.Headers.ContentRange.From.ShouldBe(1); - _mappedRequest.Data.Content.Headers.ContentRange.To.ShouldBe(2); - } - - private void GivenTheContentRangeIs(string input) - { - _inputRequest.Headers.Add("Content-Range", input); - } - - private void ThenTheMappedRequestHasContentLocationHeader(string expected) - { - _mappedRequest.Data.Content.Headers.ContentLocation.OriginalString.ShouldBe(expected); - } - - private void GivenTheContentLocationIs(string input) - { - _inputRequest.Headers.Add("Content-Location", input); - } - - private void ThenTheMappedRequestHasContentLanguageHeader(string expected) - { - _mappedRequest.Data.Content.Headers.ContentLanguage.First().ShouldBe(expected); - } - - private void GivenTheContentLanguageIs(string input) - { - _inputRequest.Headers.Add("Content-Language", input); - } - - private void ThenTheMappedRequestHasContentEncodingHeader(string expected, string expectedTwo) - { - _mappedRequest.Data.Content.Headers.ContentEncoding.ToArray()[0].ShouldBe(expected); - _mappedRequest.Data.Content.Headers.ContentEncoding.ToArray()[1].ShouldBe(expectedTwo); - } - - private void GivenTheContentEncodingIs(string input) - { - _inputRequest.Headers.Add("Content-Encoding", input); - } - - private void GivenTheContentTypeIs(string contentType) - { - _inputRequest.ContentType = contentType; - } - - private void ThenTheMappedRequestHasContentTypeHeader(string expected) - { - _mappedRequest.Data.Content.Headers.ContentType.MediaType.ShouldBe(expected); - } - - private void ThenTheMappedRequestHasContentSize(long expected) - { - _mappedRequest.Data.Content.Headers.ContentLength.ShouldBe(expected); - } - - private void GivenTheInputRequestHasMethod(string method) - { - _inputRequest.Method = method; - } - - private void GivenTheInputRequestHasScheme(string scheme) - { - _inputRequest.Scheme = scheme; - } - - private void GivenTheInputRequestHasHost(string host) - { - _inputRequest.Host = new HostString(host); - } - - private void GivenTheInputRequestHasPath(string path) - { - if (path != null) - { - _inputRequest.Path = path; - } - } - - private void GivenTheInputRequestHasQueryString(string querystring) - { - if (querystring != null) - { - _inputRequest.QueryString = new QueryString(querystring); - } - } - - private void GivenTheInputRequestHasAValidUri() - { - GivenTheInputRequestHasScheme("http"); - GivenTheInputRequestHasHost("www.google.com"); - } - - private void GivenTheInputRequestHasHeaders() - { - _inputHeaders = new List>() - { - new KeyValuePair("abc", new StringValues(new string[]{"123","456" })), - new KeyValuePair("def", new StringValues(new string[]{"789","012" })), - }; - - foreach (var inputHeader in _inputHeaders) - { - _inputRequest.Headers.Add(inputHeader); - } - } - - private void GivenTheInputRequestHasNoHeaders() - { - _inputRequest.Headers.Clear(); - } - - private void GivenTheInputRequestHasContent(string content) - { - _inputRequest.Body = new MemoryStream(Encoding.UTF8.GetBytes(content)); - } - - private void GivenTheInputRequestHasNullContent() - { - _inputRequest.Body = null; - } - - private async Task WhenMapped() - { - _mappedRequest = await _requestMapper.Map(_inputRequest); - } - - private void ThenNoErrorIsReturned() - { - _mappedRequest.IsError.ShouldBeFalse(); - } - - private void ThenAnErrorIsReturned() - { - _mappedRequest.IsError.ShouldBeTrue(); - } - - private void ThenTheMappedRequestHasUri(string expectedUri) - { - _mappedRequest.Data.RequestUri.OriginalString.ShouldBe(expectedUri); - } - - private void ThenTheMappedRequestHasMethod(string expectedMethod) - { - _mappedRequest.Data.Method.ToString().ShouldBe(expectedMethod); - } - - private void ThenTheMappedRequestHasEachHeader() - { - _mappedRequest.Data.Headers.Count().ShouldBe(_inputHeaders.Count); - foreach (var header in _mappedRequest.Data.Headers) - { - var inputHeader = _inputHeaders.First(h => h.Key == header.Key); - inputHeader.ShouldNotBeNull(); - inputHeader.Value.Count().ShouldBe(header.Value.Count()); - foreach (var inputHeaderValue in inputHeader.Value) - { - header.Value.Any(v => v == inputHeaderValue); - } - } - } - - private void ThenTheMappedRequestHasNoHeaders() - { - _mappedRequest.Data.Headers.Count().ShouldBe(0); - } - - private void ThenTheMappedRequestHasContent(string expectedContent) - { - _mappedRequest.Data.Content.ReadAsStringAsync().GetAwaiter().GetResult().ShouldBe(expectedContent); - } - - private void ThenTheMappedRequestHasNoContent() - { - _mappedRequest.Data.Content.ShouldBeNull(); - } - - private void ThenTheMappedRequestIsNull() - { - _mappedRequest.Data.ShouldBeNull(); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Request.Mapper; +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Request.Mapper +{ + public class RequestMapperTests + { + private readonly HttpContext _httpContext; + private readonly HttpRequest _inputRequest; + + private readonly RequestMapper _requestMapper; + + private Response _mappedRequest; + + private List> _inputHeaders = null; + + private DownstreamRoute _downstreamRoute; + + public RequestMapperTests() + { + _httpContext = new DefaultHttpContext(); + _inputRequest = _httpContext.Request; + _requestMapper = new RequestMapper(); + } + + [Theory] + [InlineData("https", "my.url:123", "/abc/DEF", "?a=1&b=2", "https://my.url:123/abc/DEF?a=1&b=2")] + [InlineData("http", "blah.com", "/d ef", "?abc=123", "http://blah.com/d%20ef?abc=123")] // note! the input is encoded when building the input request + [InlineData("http", "myusername:mypassword@abc.co.uk", null, null, "http://myusername:mypassword@abc.co.uk/")] + [InlineData("http", "點看.com", null, null, "http://xn--c1yn36f.com/")] + [InlineData("http", "xn--c1yn36f.com", null, null, "http://xn--c1yn36f.com/")] + public void Should_map_valid_request_uri(string scheme, string host, string path, string queryString, string expectedUri) + { + this.Given(_ => GivenTheInputRequestHasMethod("GET")) + .And(_ => GivenTheInputRequestHasScheme(scheme)) + .And(_ => GivenTheInputRequestHasHost(host)) + .And(_ => GivenTheInputRequestHasPath(path)) + .And(_ => GivenTheInputRequestHasQueryString(queryString)) + .And(_ => GivenTheDownstreamRoute()) + .When(_ => WhenMapped()) + .Then(_ => ThenNoErrorIsReturned()) + .And(_ => ThenTheMappedRequestHasUri(expectedUri)) + .BDDfy(); + } + + [Theory] + [InlineData("ftp", "google.com", "/abc/DEF", "?a=1&b=2")] + public void Should_error_on_unsupported_request_uri(string scheme, string host, string path, string queryString) + { + this.Given(_ => GivenTheInputRequestHasMethod("GET")) + .And(_ => GivenTheInputRequestHasScheme(scheme)) + .And(_ => GivenTheInputRequestHasHost(host)) + .And(_ => GivenTheInputRequestHasPath(path)) + .And(_ => GivenTheInputRequestHasQueryString(queryString)) + .When(_ => WhenMapped()) + .Then(_ => ThenAnErrorIsReturned()) + .And(_ => ThenTheMappedRequestIsNull()) + .BDDfy(); + } + + [Theory] + [InlineData("GET")] + [InlineData("POST")] + [InlineData("WHATEVER")] + public void Should_map_method(string method) + { + this.Given(_ => GivenTheInputRequestHasMethod(method)) + .And(_ => GivenTheInputRequestHasAValidUri()) + .And(_ => GivenTheDownstreamRoute()) + .When(_ => WhenMapped()) + .Then(_ => ThenNoErrorIsReturned()) + .And(_ => ThenTheMappedRequestHasMethod(method)) + .BDDfy(); + } + + [Theory] + [InlineData("", "GET")] + [InlineData(null, "GET")] + [InlineData("POST", "POST")] + public void Should_use_downstream_route_method_if_set(string input, string expected) + { + this.Given(_ => GivenTheInputRequestHasMethod("GET")) + .And(_ => GivenTheDownstreamRouteMethodIs(input)) + .And(_ => GivenTheInputRequestHasAValidUri()) + .When(_ => WhenMapped()) + .Then(_ => ThenNoErrorIsReturned()) + .And(_ => ThenTheMappedRequestHasMethod(expected)) + .BDDfy(); + } + + [Fact] + public void Should_map_all_headers() + { + this.Given(_ => GivenTheInputRequestHasHeaders()) + .And(_ => GivenTheInputRequestHasMethod("GET")) + .And(_ => GivenTheInputRequestHasAValidUri()) + .And(_ => GivenTheDownstreamRoute()) + .When(_ => WhenMapped()) + .Then(_ => ThenNoErrorIsReturned()) + .And(_ => ThenTheMappedRequestHasEachHeader()) + .BDDfy(); + } + + [Fact] + public void Should_handle_no_headers() + { + this.Given(_ => GivenTheInputRequestHasNoHeaders()) + .And(_ => GivenTheInputRequestHasMethod("GET")) + .And(_ => GivenTheInputRequestHasAValidUri()) + .And(_ => GivenTheDownstreamRoute()) + .When(_ => WhenMapped()) + .Then(_ => ThenNoErrorIsReturned()) + .And(_ => ThenTheMappedRequestHasNoHeaders()) + .BDDfy(); + } + + [Fact] + public void Should_map_content() + { + this.Given(_ => GivenTheInputRequestHasContent("This is my content")) + .And(_ => GivenTheInputRequestHasMethod("GET")) + .And(_ => GivenTheInputRequestHasAValidUri()) + .And(_ => GivenTheDownstreamRoute()) + .When(_ => WhenMapped()) + .Then(_ => ThenNoErrorIsReturned()) + .And(_ => ThenTheMappedRequestHasContent("This is my content")) + .BDDfy(); + } + + [Fact] + public void Should_handle_no_content() + { + this.Given(_ => GivenTheInputRequestHasNullContent()) + .And(_ => GivenTheInputRequestHasMethod("GET")) + .And(_ => GivenTheInputRequestHasAValidUri()) + .And(_ => GivenTheDownstreamRoute()) + .When(_ => WhenMapped()) + .Then(_ => ThenNoErrorIsReturned()) + .And(_ => ThenTheMappedRequestHasNoContent()) + .BDDfy(); + } + + [Fact] + public void Should_handle_no_content_type() + { + this.Given(_ => GivenTheInputRequestHasNoContentType()) + .And(_ => GivenTheInputRequestHasMethod("GET")) + .And(_ => GivenTheInputRequestHasAValidUri()) + .And(_ => GivenTheDownstreamRoute()) + .When(_ => WhenMapped()) + .Then(_ => ThenNoErrorIsReturned()) + .And(_ => ThenTheMappedRequestHasNoContent()) + .BDDfy(); + } + + [Fact] + public void Should_handle_no_content_length() + { + this.Given(_ => GivenTheInputRequestHasNoContentLength()) + .And(_ => GivenTheInputRequestHasMethod("GET")) + .And(_ => GivenTheInputRequestHasAValidUri()) + .And(_ => GivenTheDownstreamRoute()) + .When(_ => WhenMapped()) + .Then(_ => ThenNoErrorIsReturned()) + .And(_ => ThenTheMappedRequestHasNoContent()) + .BDDfy(); + } + + [Fact] + public void Should_map_content_headers() + { + byte[] md5bytes = new byte[0]; + using (var md5 = MD5.Create()) + { + md5bytes = md5.ComputeHash(Encoding.UTF8.GetBytes("some md5")); + } + + this.Given(_ => GivenTheInputRequestHasContent("This is my content")) + .And(_ => GivenTheContentTypeIs("application/json")) + .And(_ => GivenTheContentEncodingIs("gzip, compress")) + .And(_ => GivenTheContentLanguageIs("english")) + .And(_ => GivenTheContentLocationIs("/my-receipts/38")) + .And(_ => GivenTheContentRangeIs("bytes 1-2/*")) + .And(_ => GivenTheContentDispositionIs("inline")) + .And(_ => GivenTheContentMD5Is(md5bytes)) + .And(_ => GivenTheInputRequestHasMethod("GET")) + .And(_ => GivenTheInputRequestHasAValidUri()) + .And(_ => GivenTheDownstreamRoute()) + .When(_ => WhenMapped()) + .Then(_ => ThenNoErrorIsReturned()) + .And(_ => ThenTheMappedRequestHasContentTypeHeader("application/json")) + .And(_ => ThenTheMappedRequestHasContentEncodingHeader("gzip", "compress")) + .And(_ => ThenTheMappedRequestHasContentLanguageHeader("english")) + .And(_ => ThenTheMappedRequestHasContentLocationHeader("/my-receipts/38")) + .And(_ => ThenTheMappedRequestHasContentMD5Header(md5bytes)) + .And(_ => ThenTheMappedRequestHasContentRangeHeader()) + .And(_ => ThenTheMappedRequestHasContentDispositionHeader("inline")) + .And(_ => ThenTheMappedRequestHasContentSize("This is my content".Length)) + .And(_ => ThenTheContentHeadersAreNotAddedToNonContentHeaders()) + .BDDfy(); + } + + [Fact] + public void should_not_add_content_headers() + { + this.Given(_ => GivenTheInputRequestHasContent("This is my content")) + .And(_ => GivenTheContentTypeIs("application/json")) + .And(_ => GivenTheInputRequestHasMethod("POST")) + .And(_ => GivenTheInputRequestHasAValidUri()) + .And(_ => GivenTheDownstreamRoute()) + .When(_ => WhenMapped()) + .Then(_ => ThenNoErrorIsReturned()) + .And(_ => ThenTheMappedRequestHasContentTypeHeader("application/json")) + .And(_ => ThenTheMappedRequestHasContentSize("This is my content".Length)) + .And(_ => ThenTheOtherContentTypeHeadersAreNotMapped()) + .BDDfy(); + } + + private void GivenTheDownstreamRouteMethodIs(string input) + { + _downstreamRoute = new DownstreamRouteBuilder() + .WithDownStreamHttpMethod(input) + .WithDownstreamHttpVersion(new Version("1.1")).Build(); + } + + private void GivenTheDownstreamRoute() + { + _downstreamRoute = new DownstreamRouteBuilder() + .WithDownstreamHttpVersion(new Version("1.1")).Build(); + } + + private void GivenTheInputRequestHasNoContentLength() + { + _inputRequest.ContentLength = null; + } + + private void GivenTheInputRequestHasNoContentType() + { + _inputRequest.ContentType = null; + } + + private void ThenTheContentHeadersAreNotAddedToNonContentHeaders() + { + _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-Disposition"); + _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-ContentMD5"); + _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-ContentRange"); + _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-ContentLanguage"); + _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-ContentEncoding"); + _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-ContentLocation"); + _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-Length"); + _mappedRequest.Data.Headers.ShouldNotContain(x => x.Key == "Content-Type"); + } + + private void ThenTheOtherContentTypeHeadersAreNotMapped() + { + _mappedRequest.Data.Content.Headers.ContentDisposition.ShouldBeNull(); + _mappedRequest.Data.Content.Headers.ContentMD5.ShouldBeNull(); + _mappedRequest.Data.Content.Headers.ContentRange.ShouldBeNull(); + _mappedRequest.Data.Content.Headers.ContentLanguage.ShouldBeEmpty(); + _mappedRequest.Data.Content.Headers.ContentEncoding.ShouldBeEmpty(); + _mappedRequest.Data.Content.Headers.ContentLocation.ShouldBeNull(); + } + + private void ThenTheMappedRequestHasContentDispositionHeader(string expected) + { + _mappedRequest.Data.Content.Headers.ContentDisposition.DispositionType.ShouldBe(expected); + } + + private void GivenTheContentDispositionIs(string input) + { + _inputRequest.Headers.Add("Content-Disposition", input); + } + + private void ThenTheMappedRequestHasContentMD5Header(byte[] expected) + { + _mappedRequest.Data.Content.Headers.ContentMD5.ShouldBe(expected); + } + + private void GivenTheContentMD5Is(byte[] input) + { + var base64 = Convert.ToBase64String(input); + _inputRequest.Headers.Add("Content-MD5", base64); + } + + private void ThenTheMappedRequestHasContentRangeHeader() + { + _mappedRequest.Data.Content.Headers.ContentRange.From.ShouldBe(1); + _mappedRequest.Data.Content.Headers.ContentRange.To.ShouldBe(2); + } + + private void GivenTheContentRangeIs(string input) + { + _inputRequest.Headers.Add("Content-Range", input); + } + + private void ThenTheMappedRequestHasContentLocationHeader(string expected) + { + _mappedRequest.Data.Content.Headers.ContentLocation.OriginalString.ShouldBe(expected); + } + + private void GivenTheContentLocationIs(string input) + { + _inputRequest.Headers.Add("Content-Location", input); + } + + private void ThenTheMappedRequestHasContentLanguageHeader(string expected) + { + _mappedRequest.Data.Content.Headers.ContentLanguage.First().ShouldBe(expected); + } + + private void GivenTheContentLanguageIs(string input) + { + _inputRequest.Headers.Add("Content-Language", input); + } + + private void ThenTheMappedRequestHasContentEncodingHeader(string expected, string expectedTwo) + { + _mappedRequest.Data.Content.Headers.ContentEncoding.ToArray()[0].ShouldBe(expected); + _mappedRequest.Data.Content.Headers.ContentEncoding.ToArray()[1].ShouldBe(expectedTwo); + } + + private void GivenTheContentEncodingIs(string input) + { + _inputRequest.Headers.Add("Content-Encoding", input); + } + + private void GivenTheContentTypeIs(string contentType) + { + _inputRequest.ContentType = contentType; + } + + private void ThenTheMappedRequestHasContentTypeHeader(string expected) + { + _mappedRequest.Data.Content.Headers.ContentType.MediaType.ShouldBe(expected); + } + + private void ThenTheMappedRequestHasContentSize(long expected) + { + _mappedRequest.Data.Content.Headers.ContentLength.ShouldBe(expected); + } + + private void GivenTheInputRequestHasMethod(string method) + { + _inputRequest.Method = method; + } + + private void GivenTheInputRequestHasScheme(string scheme) + { + _inputRequest.Scheme = scheme; + } + + private void GivenTheInputRequestHasHost(string host) + { + _inputRequest.Host = new HostString(host); + } + + private void GivenTheInputRequestHasPath(string path) + { + if (path != null) + { + _inputRequest.Path = path; + } + } + + private void GivenTheInputRequestHasQueryString(string querystring) + { + if (querystring != null) + { + _inputRequest.QueryString = new QueryString(querystring); + } + } + + private void GivenTheInputRequestHasAValidUri() + { + GivenTheInputRequestHasScheme("http"); + GivenTheInputRequestHasHost("www.google.com"); + } + + private void GivenTheInputRequestHasHeaders() + { + _inputHeaders = new List>() + { + new("abc", new StringValues(new string[]{"123","456" })), + new("def", new StringValues(new string[]{"789","012" })), + }; + + foreach (var inputHeader in _inputHeaders) + { + _inputRequest.Headers.Add(inputHeader); + } + } + + private void GivenTheInputRequestHasNoHeaders() + { + _inputRequest.Headers.Clear(); + } + + private void GivenTheInputRequestHasContent(string content) + { + _inputRequest.Body = new MemoryStream(Encoding.UTF8.GetBytes(content)); + } + + private void GivenTheInputRequestHasNullContent() + { + _inputRequest.Body = null; + } + + private async Task WhenMapped() + { + _mappedRequest = await _requestMapper.Map(_inputRequest, _downstreamRoute); + } + + private void ThenNoErrorIsReturned() + { + _mappedRequest.IsError.ShouldBeFalse(); + } + + private void ThenAnErrorIsReturned() + { + _mappedRequest.IsError.ShouldBeTrue(); + } + + private void ThenTheMappedRequestHasUri(string expectedUri) + { + _mappedRequest.Data.RequestUri.OriginalString.ShouldBe(expectedUri); + } + + private void ThenTheMappedRequestHasMethod(string expectedMethod) + { + _mappedRequest.Data.Method.ToString().ShouldBe(expectedMethod); + } + + private void ThenTheMappedRequestHasEachHeader() + { + _mappedRequest.Data.Headers.Count().ShouldBe(_inputHeaders.Count); + foreach (var header in _mappedRequest.Data.Headers) + { + var inputHeader = _inputHeaders.First(h => h.Key == header.Key); + inputHeader.ShouldNotBe(default(KeyValuePair)); + inputHeader.Value.Count().ShouldBe(header.Value.Count()); + foreach (var inputHeaderValue in inputHeader.Value) + { + header.Value.Any(v => v == inputHeaderValue); + } + } + } + + private void ThenTheMappedRequestHasNoHeaders() + { + _mappedRequest.Data.Headers.Count().ShouldBe(0); + } + + private void ThenTheMappedRequestHasContent(string expectedContent) + { + _mappedRequest.Data.Content.ReadAsStringAsync().GetAwaiter().GetResult().ShouldBe(expectedContent); + } + + private void ThenTheMappedRequestHasNoContent() + { + _mappedRequest.Data.Content.ShouldBeNull(); + } + + private void ThenTheMappedRequestIsNull() + { + _mappedRequest.Data.ShouldBeNull(); + } + } +} diff --git a/test/Ocelot.UnitTests/RequestId/ReRouteRequestIdMiddlewareTests.cs b/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs similarity index 65% rename from test/Ocelot.UnitTests/RequestId/ReRouteRequestIdMiddlewareTests.cs rename to test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs index 5aebd7e57..e69444490 100644 --- a/test/Ocelot.UnitTests/RequestId/ReRouteRequestIdMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs @@ -1,222 +1,229 @@ -namespace Ocelot.UnitTests.RequestId -{ - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Configuration.Builder; - using Ocelot.DownstreamRouteFinder; - using Ocelot.DownstreamRouteFinder.UrlMatcher; - using Ocelot.Infrastructure.RequestData; - using Ocelot.Logging; - using Ocelot.Middleware; - using Ocelot.Request.Middleware; - using Ocelot.RequestId.Middleware; - using Ocelot.Responses; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Net.Http; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - - public class ReRouteRequestIdMiddlewareTests - { - private readonly HttpRequestMessage _downstreamRequest; - private string _value; - private string _key; - private Mock _loggerFactory; - private Mock _logger; - private readonly ReRouteRequestIdMiddleware _middleware; - private readonly DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; - private readonly Mock _repo; - - public ReRouteRequestIdMiddlewareTests() - { - _downstreamRequest = new HttpRequestMessage(HttpMethod.Get, "http://test.com"); - _repo = new Mock(); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _next = context => - { - context.HttpContext.Response.Headers.Add("LSRequestId", context.HttpContext.TraceIdentifier); - return Task.CompletedTask; - }; - _middleware = new ReRouteRequestIdMiddleware(_next, _loggerFactory.Object, _repo.Object); - _downstreamContext.DownstreamRequest = new DownstreamRequest(_downstreamRequest); - } - - [Fact] - public void should_pass_down_request_id_from_upstream_request() - { - var downstreamRoute = new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("any old string") - .WithRequestIdKey("LSRequestId") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); - - var requestId = Guid.NewGuid().ToString(); - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) - .And(x => GivenThereIsNoGlobalRequestId()) - .And(x => x.GivenTheRequestIdIsAddedToTheRequest("LSRequestId", requestId)) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheTraceIdIs(requestId)) - .BDDfy(); - } - - [Fact] - public void should_add_request_id_when_not_on_upstream_request() - { - var downstreamRoute = new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("any old string") - .WithRequestIdKey("LSRequestId") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) - .And(x => GivenThereIsNoGlobalRequestId()) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheTraceIdIsAnything()) - .BDDfy(); - } - - [Fact] - public void should_add_request_id_scoped_repo_for_logging_later() - { - var downstreamRoute = new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("any old string") - .WithRequestIdKey("LSRequestId") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); - - var requestId = Guid.NewGuid().ToString(); - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) - .And(x => GivenThereIsNoGlobalRequestId()) - .And(x => x.GivenTheRequestIdIsAddedToTheRequest("LSRequestId", requestId)) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheTraceIdIs(requestId)) - .And(x => ThenTheRequestIdIsSaved()) - .BDDfy(); - } - - [Fact] - public void should_update_request_id_scoped_repo_for_logging_later() - { - var downstreamRoute = new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("any old string") - .WithRequestIdKey("LSRequestId") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); - - var requestId = Guid.NewGuid().ToString(); - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) - .And(x => GivenTheRequestIdWasSetGlobally()) - .And(x => x.GivenTheRequestIdIsAddedToTheRequest("LSRequestId", requestId)) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheTraceIdIs(requestId)) - .And(x => ThenTheRequestIdIsUpdated()) - .BDDfy(); - } - - [Fact] - public void should_not_update_if_global_request_id_is_same_as_re_route_request_id() - { - var downstreamRoute = new DownstreamRoute(new List(), - new ReRouteBuilder() - .WithDownstreamReRoute(new DownstreamReRouteBuilder() - .WithDownstreamPathTemplate("any old string") - .WithRequestIdKey("LSRequestId") - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()) - .WithUpstreamHttpMethod(new List { "Get" }) - .Build()); - - var requestId = "alreadyset"; - - this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) - .And(x => GivenTheRequestIdWasSetGlobally()) - .And(x => x.GivenTheRequestIdIsAddedToTheRequest("LSRequestId", requestId)) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheTraceIdIs(requestId)) - .And(x => ThenTheRequestIdIsNotUpdated()) - .BDDfy(); - } - - private void WhenICallTheMiddleware() - { - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); - } - - private void GivenThereIsNoGlobalRequestId() - { - _repo.Setup(x => x.Get("RequestId")).Returns(new OkResponse(null)); - } - - private void GivenTheRequestIdWasSetGlobally() - { - _repo.Setup(x => x.Get("RequestId")).Returns(new OkResponse("alreadyset")); - } - - private void ThenTheRequestIdIsSaved() - { - _repo.Verify(x => x.Add("RequestId", _value), Times.Once); - } - - private void ThenTheRequestIdIsUpdated() - { - _repo.Verify(x => x.Update("RequestId", _value), Times.Once); - } - - private void ThenTheRequestIdIsNotUpdated() - { - _repo.Verify(x => x.Update("RequestId", _value), Times.Never); - } - - private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) - { - _downstreamContext.TemplatePlaceholderNameAndValues = downstreamRoute.TemplatePlaceholderNameAndValues; - _downstreamContext.DownstreamReRoute = downstreamRoute.ReRoute.DownstreamReRoute[0]; - } - - private void GivenTheRequestIdIsAddedToTheRequest(string key, string value) - { - _key = key; - _value = value; - _downstreamContext.HttpContext.Request.Headers.TryAdd(_key, _value); - } - - private void ThenTheTraceIdIsAnything() - { - _downstreamContext.HttpContext.Response.Headers.TryGetValue("LSRequestId", out var value); - value.First().ShouldNotBeNullOrEmpty(); - } - - private void ThenTheTraceIdIs(string expected) - { - _downstreamContext.HttpContext.Response.Headers.TryGetValue("LSRequestId", out var value); - value.First().ShouldBe(expected); - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.Request.Middleware; +using Ocelot.RequestId.Middleware; + +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.RequestId +{ + public class RequestIdMiddlewareTests + { + private readonly HttpRequestMessage _downstreamRequest; + private string _value; + private string _key; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private readonly RequestIdMiddleware _middleware; + private readonly RequestDelegate _next; + private readonly Mock _repo; + private readonly HttpContext _httpContext; + public RequestIdMiddlewareTests() + { + _httpContext = new DefaultHttpContext(); + _downstreamRequest = new HttpRequestMessage(HttpMethod.Get, "http://test.com"); + _repo = new Mock(); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _next = context => + { + _httpContext.Response.Headers.Add("LSRequestId", _httpContext.TraceIdentifier); + return Task.CompletedTask; + }; + _middleware = new RequestIdMiddleware(_next, _loggerFactory.Object, _repo.Object); + _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(_downstreamRequest)); + } + + [Fact] + public void should_pass_down_request_id_from_upstream_request() + { + var downstreamRoute = new DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithRequestIdKey("LSRequestId") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()); + + var requestId = Guid.NewGuid().ToString(); + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => GivenThereIsNoGlobalRequestId()) + .And(x => x.GivenTheRequestIdIsAddedToTheRequest("LSRequestId", requestId)) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheTraceIdIs(requestId)) + .BDDfy(); + } + + [Fact] + public void should_add_request_id_when_not_on_upstream_request() + { + var downstreamRoute = new DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithRequestIdKey("LSRequestId") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()); + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => GivenThereIsNoGlobalRequestId()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheTraceIdIsAnything()) + .BDDfy(); + } + + [Fact] + public void should_add_request_id_scoped_repo_for_logging_later() + { + var downstreamRoute = new DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithRequestIdKey("LSRequestId") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()); + + var requestId = Guid.NewGuid().ToString(); + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => GivenThereIsNoGlobalRequestId()) + .And(x => x.GivenTheRequestIdIsAddedToTheRequest("LSRequestId", requestId)) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheTraceIdIs(requestId)) + .And(x => ThenTheRequestIdIsSaved()) + .BDDfy(); + } + + [Fact] + public void should_update_request_id_scoped_repo_for_logging_later() + { + var downstreamRoute = new DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithRequestIdKey("LSRequestId") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()); + + var requestId = Guid.NewGuid().ToString(); + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => GivenTheRequestIdWasSetGlobally()) + .And(x => x.GivenTheRequestIdIsAddedToTheRequest("LSRequestId", requestId)) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheTraceIdIs(requestId)) + .And(x => ThenTheRequestIdIsUpdated()) + .BDDfy(); + } + + [Fact] + public void should_not_update_if_global_request_id_is_same_as_re_route_request_id() + { + var downstreamRoute = new DownstreamRouteHolder(new List(), + new RouteBuilder() + .WithDownstreamRoute(new DownstreamRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithRequestIdKey("LSRequestId") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()); + + var requestId = "alreadyset"; + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => GivenTheRequestIdWasSetGlobally()) + .And(x => x.GivenTheRequestIdIsAddedToTheRequest("LSRequestId", requestId)) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheTraceIdIs(requestId)) + .And(x => ThenTheRequestIdIsNotUpdated()) + .BDDfy(); + } + + private void WhenICallTheMiddleware() + { + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); + } + + private void GivenThereIsNoGlobalRequestId() + { + _repo.Setup(x => x.Get("RequestId")).Returns(new OkResponse(null)); + } + + private void GivenTheRequestIdWasSetGlobally() + { + _repo.Setup(x => x.Get("RequestId")).Returns(new OkResponse("alreadyset")); + } + + private void ThenTheRequestIdIsSaved() + { + _repo.Verify(x => x.Add("RequestId", _value), Times.Once); + } + + private void ThenTheRequestIdIsUpdated() + { + _repo.Verify(x => x.Update("RequestId", _value), Times.Once); + } + + private void ThenTheRequestIdIsNotUpdated() + { + _repo.Verify(x => x.Update("RequestId", _value), Times.Never); + } + + private void GivenTheDownStreamRouteIs(DownstreamRouteHolder downstreamRoute) + { + _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); + + _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); + } + + private void GivenTheRequestIdIsAddedToTheRequest(string key, string value) + { + _key = key; + _value = value; + _httpContext.Request.Headers.TryAdd(_key, _value); + } + + private void ThenTheTraceIdIsAnything() + { + _httpContext.Response.Headers.TryGetValue("LSRequestId", out var value); + value.First().ShouldNotBeNullOrEmpty(); + } + + private void ThenTheTraceIdIs(string expected) + { + _httpContext.Response.Headers.TryGetValue("LSRequestId", out var value); + value.First().ShouldBe(expected); + } + } } diff --git a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs index 050b2da8e..cf73b9a2a 100644 --- a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs @@ -1,512 +1,520 @@ -namespace Ocelot.UnitTests.Requester -{ - using Microsoft.Extensions.DependencyInjection; - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Logging; - using Ocelot.Requester; - using Ocelot.Requester.QoS; - using Ocelot.Responses; - using Responder; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Net.Http; - using TestStack.BDDfy; - using Xunit; - - public class DelegatingHandlerHandlerProviderFactoryTests - { - private DelegatingHandlerHandlerFactory _factory; - private readonly Mock _loggerFactory; - private readonly Mock _logger; - private DownstreamReRoute _downstreamReRoute; - private Response>> _result; - private readonly Mock _qosFactory; - private readonly Mock _tracingFactory; - private IServiceProvider _serviceProvider; - private readonly IServiceCollection _services; - private readonly QosDelegatingHandlerDelegate _qosDelegate; - - public DelegatingHandlerHandlerProviderFactoryTests() - { - _qosDelegate = (a, b) => new FakeQoSHandler(); - _tracingFactory = new Mock(); - _qosFactory = new Mock(); - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _services = new ServiceCollection(); - _services.AddSingleton(_qosDelegate); - } - - [Fact] - public void should_follow_ordering_add_specifics() - { - var qosOptions = new QoSOptionsBuilder() - .WithTimeoutValue(1) - .WithDurationOfBreak(1) - .WithExceptionsAllowedBeforeBreaking(1) - .Build(); - - var reRoute = new DownstreamReRouteBuilder() - .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true, int.MaxValue)) - .WithDelegatingHandlers(new List - { - "FakeDelegatingHandler", - "FakeDelegatingHandlerTwo" - }) - .WithLoadBalancerKey("") - .Build(); - - this.Given(x => GivenTheFollowingRequest(reRoute)) - .And(x => GivenTheQosFactoryReturns(new FakeQoSHandler())) - .And(x => GivenTheTracingFactoryReturns()) - .And(x => GivenTheServiceProviderReturnsGlobalDelegatingHandlers()) - .And(x => GivenTheServiceProviderReturnsSpecificDelegatingHandlers()) - .When(x => WhenIGet()) - .Then(x => ThenThereIsDelegatesInProvider(6)) - .And(x => ThenHandlerAtPositionIs(0)) - .And(x => ThenHandlerAtPositionIs(1)) - .And(x => ThenHandlerAtPositionIs(2)) - .And(x => ThenHandlerAtPositionIs(3)) - .And(x => ThenHandlerAtPositionIs(4)) - .And(x => ThenHandlerAtPositionIs(5)) - .BDDfy(); - } - - [Fact] - public void should_follow_ordering_order_specifics_and_globals() - { - var qosOptions = new QoSOptionsBuilder() - .WithTimeoutValue(1) - .WithDurationOfBreak(1) - .WithExceptionsAllowedBeforeBreaking(1) - .Build(); - - var reRoute = new DownstreamReRouteBuilder() - .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true, int.MaxValue)) - .WithDelegatingHandlers(new List - { - "FakeDelegatingHandlerTwo", - "FakeDelegatingHandler", - "FakeDelegatingHandlerFour" - }) - .WithLoadBalancerKey("") - .Build(); - - this.Given(x => GivenTheFollowingRequest(reRoute)) - .And(x => GivenTheQosFactoryReturns(new FakeQoSHandler())) - .And(x => GivenTheTracingFactoryReturns()) - .And(x => GivenTheServiceProviderReturnsGlobalDelegatingHandlers()) - .And(x => GivenTheServiceProviderReturnsSpecificDelegatingHandlers()) - .When(x => WhenIGet()) - .Then(x => ThenThereIsDelegatesInProvider(6)) - .And(x => ThenHandlerAtPositionIs(0)) //first because global not in config - .And(x => ThenHandlerAtPositionIs(1)) //first from config - .And(x => ThenHandlerAtPositionIs(2)) //second from config - .And(x => ThenHandlerAtPositionIs(3)) //third from config (global) - .And(x => ThenHandlerAtPositionIs(4)) - .And(x => ThenHandlerAtPositionIs(5)) - .BDDfy(); - } - - [Fact] - public void should_follow_ordering_order_specifics() - { - var qosOptions = new QoSOptionsBuilder() - .WithTimeoutValue(1) - .WithDurationOfBreak(1) - .WithExceptionsAllowedBeforeBreaking(1) - .Build(); - - var reRoute = new DownstreamReRouteBuilder() - .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true, int.MaxValue)) - .WithDelegatingHandlers(new List - { - "FakeDelegatingHandlerTwo", - "FakeDelegatingHandler" - }) - .WithLoadBalancerKey("") - .Build(); - - this.Given(x => GivenTheFollowingRequest(reRoute)) - .And(x => GivenTheQosFactoryReturns(new FakeQoSHandler())) - .And(x => GivenTheTracingFactoryReturns()) - .And(x => GivenTheServiceProviderReturnsGlobalDelegatingHandlers()) - .And(x => GivenTheServiceProviderReturnsSpecificDelegatingHandlers()) - .When(x => WhenIGet()) - .Then(x => ThenThereIsDelegatesInProvider(6)) - .And(x => ThenHandlerAtPositionIs(0)) - .And(x => ThenHandlerAtPositionIs(1)) - .And(x => ThenHandlerAtPositionIs(2)) - .And(x => ThenHandlerAtPositionIs(3)) - .And(x => ThenHandlerAtPositionIs(4)) - .And(x => ThenHandlerAtPositionIs(5)) - .BDDfy(); - } - - [Fact] - public void should_follow_ordering_order_and_only_add_specifics_in_config() - { - var qosOptions = new QoSOptionsBuilder() - .WithTimeoutValue(1) - .WithDurationOfBreak(1) - .WithExceptionsAllowedBeforeBreaking(1) - .Build(); - - var reRoute = new DownstreamReRouteBuilder() - .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true, int.MaxValue)) - .WithDelegatingHandlers(new List - { - "FakeDelegatingHandler", - }) - .WithLoadBalancerKey("") - .Build(); - - this.Given(x => GivenTheFollowingRequest(reRoute)) - .And(x => GivenTheQosFactoryReturns(new FakeQoSHandler())) - .And(x => GivenTheTracingFactoryReturns()) - .And(x => GivenTheServiceProviderReturnsGlobalDelegatingHandlers()) - .And(x => GivenTheServiceProviderReturnsSpecificDelegatingHandlers()) - .When(x => WhenIGet()) - .Then(x => ThenThereIsDelegatesInProvider(5)) - .And(x => ThenHandlerAtPositionIs(0)) - .And(x => ThenHandlerAtPositionIs(1)) - .And(x => ThenHandlerAtPositionIs(2)) - .And(x => ThenHandlerAtPositionIs(3)) - .And(x => ThenHandlerAtPositionIs(4)) - .BDDfy(); - } - - [Fact] - public void should_follow_ordering_dont_add_specifics() - { - var qosOptions = new QoSOptionsBuilder() - .WithTimeoutValue(1) - .WithDurationOfBreak(1) - .WithExceptionsAllowedBeforeBreaking(1) - .Build(); - - var reRoute = new DownstreamReRouteBuilder() - .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true, int.MaxValue)) - .WithLoadBalancerKey("") - .Build(); - - this.Given(x => GivenTheFollowingRequest(reRoute)) - .And(x => GivenTheQosFactoryReturns(new FakeQoSHandler())) - .And(x => GivenTheTracingFactoryReturns()) - .And(x => GivenTheServiceProviderReturnsGlobalDelegatingHandlers()) - .And(x => GivenTheServiceProviderReturnsSpecificDelegatingHandlers()) - .When(x => WhenIGet()) - .Then(x => ThenThereIsDelegatesInProvider(4)) - .And(x => ThenHandlerAtPositionIs(0)) - .And(x => ThenHandlerAtPositionIs(1)) - .And(x => ThenHandlerAtPositionIs(2)) - .And(x => ThenHandlerAtPositionIs(3)) - .BDDfy(); - } - - [Fact] - public void should_apply_re_route_specific() - { - var qosOptions = new QoSOptionsBuilder() - .Build(); - - var reRoute = new DownstreamReRouteBuilder() - .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false, true, int.MaxValue)) - .WithDelegatingHandlers(new List - { - "FakeDelegatingHandler", - "FakeDelegatingHandlerTwo" - }) - .WithLoadBalancerKey("") - .Build(); - - this.Given(x => GivenTheFollowingRequest(reRoute)) - .And(x => GivenTheServiceProviderReturnsSpecificDelegatingHandlers()) - .When(x => WhenIGet()) - .Then(x => ThenThereIsDelegatesInProvider(2)) - .And(x => ThenTheDelegatesAreAddedCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_all_from_all_routes_provider_and_qos() - { - var qosOptions = new QoSOptionsBuilder() - .WithTimeoutValue(1) - .WithDurationOfBreak(1) - .WithExceptionsAllowedBeforeBreaking(1) - .Build(); - - var reRoute = new DownstreamReRouteBuilder() - .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false, true, int.MaxValue)).WithLoadBalancerKey("").Build(); - - this.Given(x => GivenTheFollowingRequest(reRoute)) - .And(x => GivenTheQosFactoryReturns(new FakeQoSHandler())) - .And(x => GivenTheServiceProviderReturnsGlobalDelegatingHandlers()) - .When(x => WhenIGet()) - .Then(x => ThenThereIsDelegatesInProvider(3)) - .And(x => ThenTheDelegatesAreAddedCorrectly()) - .And(x => ThenItIsQosHandler(2)) - .BDDfy(); - } - - [Fact] - public void should_return_provider_with_no_delegates() - { - var qosOptions = new QoSOptionsBuilder() - .Build(); - - var reRoute = new DownstreamReRouteBuilder() - .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false, true, int.MaxValue)).WithLoadBalancerKey("").Build(); - - this.Given(x => GivenTheFollowingRequest(reRoute)) - .And(x => GivenTheServiceProviderReturnsNothing()) - .When(x => WhenIGet()) - .Then(x => ThenNoDelegatesAreInTheProvider()) - .BDDfy(); - } - - [Fact] - public void should_return_provider_with_qos_delegate() - { - var qosOptions = new QoSOptionsBuilder() - .WithTimeoutValue(1) - .WithDurationOfBreak(1) - .WithExceptionsAllowedBeforeBreaking(1) - .Build(); - - var reRoute = new DownstreamReRouteBuilder() - .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false, true, int.MaxValue)).WithLoadBalancerKey("").Build(); - - this.Given(x => GivenTheFollowingRequest(reRoute)) - .And(x => GivenTheQosFactoryReturns(new FakeQoSHandler())) - .And(x => GivenTheServiceProviderReturnsNothing()) - .When(x => WhenIGet()) - .Then(x => ThenThereIsDelegatesInProvider(1)) - .And(x => ThenItIsQosHandler(0)) - .BDDfy(); - } - - [Fact] - public void should_return_provider_with_qos_delegate_when_timeout_value_set() - { - var qosOptions = new QoSOptionsBuilder() - .WithTimeoutValue(1) - .Build(); - - var reRoute = new DownstreamReRouteBuilder() - .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false, true, int.MaxValue)).WithLoadBalancerKey("").Build(); - - this.Given(x => GivenTheFollowingRequest(reRoute)) - .And(x => GivenTheQosFactoryReturns(new FakeQoSHandler())) - .And(x => GivenTheServiceProviderReturnsNothing()) - .When(x => WhenIGet()) - .Then(x => ThenThereIsDelegatesInProvider(1)) - .And(x => ThenItIsQosHandler(0)) - .BDDfy(); - } - - [Fact] - public void should_log_error_and_return_no_qos_provider_delegate_when_qos_factory_returns_error() - { - var qosOptions = new QoSOptionsBuilder() - .WithTimeoutValue(1) - .WithDurationOfBreak(1) - .WithExceptionsAllowedBeforeBreaking(1) - .Build(); - - var reRoute = new DownstreamReRouteBuilder() - .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true, int.MaxValue)) - .WithLoadBalancerKey("") - .Build(); - - this.Given(x => GivenTheFollowingRequest(reRoute)) - .And(x => GivenTheQosFactoryReturnsError()) - .And(x => GivenTheTracingFactoryReturns()) - .And(x => GivenTheServiceProviderReturnsGlobalDelegatingHandlers()) - .And(x => GivenTheServiceProviderReturnsSpecificDelegatingHandlers()) - .When(x => WhenIGet()) - .Then(x => ThenThereIsDelegatesInProvider(4)) - .And(x => ThenHandlerAtPositionIs(0)) - .And(x => ThenHandlerAtPositionIs(1)) - .And(x => ThenHandlerAtPositionIs(2)) - .And(x => ThenHandlerAtPositionIs(3)) - .And(_ => ThenTheWarningIsLogged()) - .BDDfy(); - } - - [Fact] - public void should_log_error_and_return_no_qos_provider_delegate_when_qos_factory_returns_null() - { - var qosOptions = new QoSOptionsBuilder() - .WithTimeoutValue(1) - .WithDurationOfBreak(1) - .WithExceptionsAllowedBeforeBreaking(1) - .Build(); - - var reRoute = new DownstreamReRouteBuilder() - .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true, int.MaxValue)) - .WithLoadBalancerKey("") - .Build(); - - this.Given(x => GivenTheFollowingRequest(reRoute)) - .And(x => GivenTheQosFactoryReturnsNull()) - .And(x => GivenTheTracingFactoryReturns()) - .And(x => GivenTheServiceProviderReturnsGlobalDelegatingHandlers()) - .And(x => GivenTheServiceProviderReturnsSpecificDelegatingHandlers()) - .When(x => WhenIGet()) - .Then(x => ThenThereIsDelegatesInProvider(4)) - .And(x => ThenHandlerAtPositionIs(0)) - .And(x => ThenHandlerAtPositionIs(1)) - .And(x => ThenHandlerAtPositionIs(2)) - .And(x => ThenHandlerAtPositionIs(3)) - .And(_ => ThenTheWarningIsLogged()) - .BDDfy(); - } - - private void ThenTheWarningIsLogged() - { - _logger.Verify(x => x.LogWarning($"ReRoute {_downstreamReRoute.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) - where T : DelegatingHandler - { - var delegates = _result.Data; - var del = delegates[pos].Invoke(); - del.ShouldBeOfType(); - } - - private void GivenTheTracingFactoryReturns() - { - _tracingFactory - .Setup(x => x.Get()) - .Returns(new FakeTracingHandler()); - } - - private void GivenTheServiceProviderReturnsGlobalDelegatingHandlers() - where TOne : DelegatingHandler - where TTwo : DelegatingHandler - { - _services.AddTransient(); - _services.AddTransient(s => - { - var service = s.GetService(); - return new GlobalDelegatingHandler(service); - }); - _services.AddTransient(); - _services.AddTransient(s => - { - var service = s.GetService(); - return new GlobalDelegatingHandler(service); - }); - } - - private void GivenTheServiceProviderReturnsSpecificDelegatingHandlers() - where TOne : DelegatingHandler - where TTwo : DelegatingHandler - { - _services.AddTransient(); - _services.AddTransient(); - } - - private void GivenTheServiceProviderReturnsNothing() - { - _serviceProvider = _services.BuildServiceProvider(); - } - - private void ThenAnErrorIsReturned() - { - _result.IsError.ShouldBeTrue(); - } - - private void ThenTheDelegatesAreAddedCorrectly() - { - var delegates = _result.Data; - - var del = delegates[0].Invoke(); - var handler = (FakeDelegatingHandler)del; - handler.Order.ShouldBe(1); - - del = delegates[1].Invoke(); - var handlerTwo = (FakeDelegatingHandlerTwo)del; - handlerTwo.Order.ShouldBe(2); - } - - private void GivenTheQosFactoryReturns(DelegatingHandler handler) - { - _qosFactory - .Setup(x => x.Get(It.IsAny())) - .Returns(new OkResponse(handler)); - } - - private void GivenTheQosFactoryReturnsError() - { - _qosFactory - .Setup(x => x.Get(It.IsAny())) - .Returns(new ErrorResponse(new AnyError())); - } - - private void GivenTheQosFactoryReturnsNull() - { - _qosFactory - .Setup(x => x.Get(It.IsAny())) - .Returns((ErrorResponse)null); - } - - private void ThenItIsQosHandler(int i) - { - var delegates = _result.Data; - var del = delegates[i].Invoke(); - del.ShouldBeOfType(); - } - - private void ThenThereIsDelegatesInProvider(int count) - { - _result.ShouldNotBeNull(); - _result.Data.Count.ShouldBe(count); - } - - private void GivenTheFollowingRequest(DownstreamReRoute request) - { - _downstreamReRoute = request; - } - - private void WhenIGet() - { - _serviceProvider = _services.BuildServiceProvider(); - _factory = new DelegatingHandlerHandlerFactory(_tracingFactory.Object, _qosFactory.Object, _serviceProvider, _loggerFactory.Object); - _result = _factory.Get(_downstreamReRoute); - } - - private void ThenNoDelegatesAreInTheProvider() - { - _result.ShouldNotBeNull(); - _result.Data.Count.ShouldBe(0); - } - } - - internal class FakeTracingHandler : DelegatingHandler, ITracingHandler - { - } - - internal class FakeQoSHandler : DelegatingHandler - { - } -} +using System; +using System.Collections.Generic; +using System.Net.Http; + +using Microsoft.Extensions.DependencyInjection; + +using Moq; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Requester; +using Ocelot.Requester.QoS; + +using Ocelot.UnitTests.Responder; + +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class DelegatingHandlerHandlerProviderFactoryTests + { + private DelegatingHandlerHandlerFactory _factory; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private DownstreamRoute _downstreamRoute; + private Response>> _result; + private readonly Mock _qosFactory; + private readonly Mock _tracingFactory; + private IServiceProvider _serviceProvider; + private readonly IServiceCollection _services; + private readonly QosDelegatingHandlerDelegate _qosDelegate; + + public DelegatingHandlerHandlerProviderFactoryTests() + { + _qosDelegate = (a, b) => new FakeQoSHandler(); + _tracingFactory = new Mock(); + _qosFactory = new Mock(); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _services = new ServiceCollection(); + _services.AddSingleton(_qosDelegate); + } + + [Fact] + public void should_follow_ordering_add_specifics() + { + var qosOptions = new QoSOptionsBuilder() + .WithTimeoutValue(1) + .WithDurationOfBreak(1) + .WithExceptionsAllowedBeforeBreaking(1) + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(qosOptions) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true, int.MaxValue)) + .WithDelegatingHandlers(new List + { + "FakeDelegatingHandler", + "FakeDelegatingHandlerTwo", + }) + .WithLoadBalancerKey(string.Empty) + .Build(); + + this.Given(x => GivenTheFollowingRequest(route)) + .And(x => GivenTheQosFactoryReturns(new FakeQoSHandler())) + .And(x => GivenTheTracingFactoryReturns()) + .And(x => GivenTheServiceProviderReturnsGlobalDelegatingHandlers()) + .And(x => GivenTheServiceProviderReturnsSpecificDelegatingHandlers()) + .When(x => WhenIGet()) + .Then(x => ThenThereIsDelegatesInProvider(6)) + .And(x => ThenHandlerAtPositionIs(0)) + .And(x => ThenHandlerAtPositionIs(1)) + .And(x => ThenHandlerAtPositionIs(2)) + .And(x => ThenHandlerAtPositionIs(3)) + .And(x => ThenHandlerAtPositionIs(4)) + .And(x => ThenHandlerAtPositionIs(5)) + .BDDfy(); + } + + [Fact] + public void should_follow_ordering_order_specifics_and_globals() + { + var qosOptions = new QoSOptionsBuilder() + .WithTimeoutValue(1) + .WithDurationOfBreak(1) + .WithExceptionsAllowedBeforeBreaking(1) + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(qosOptions) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true, int.MaxValue)) + .WithDelegatingHandlers(new List + { + "FakeDelegatingHandlerTwo", + "FakeDelegatingHandler", + "FakeDelegatingHandlerFour", + }) + .WithLoadBalancerKey(string.Empty) + .Build(); + + this.Given(x => GivenTheFollowingRequest(route)) + .And(x => GivenTheQosFactoryReturns(new FakeQoSHandler())) + .And(x => GivenTheTracingFactoryReturns()) + .And(x => GivenTheServiceProviderReturnsGlobalDelegatingHandlers()) + .And(x => GivenTheServiceProviderReturnsSpecificDelegatingHandlers()) + .When(x => WhenIGet()) + .Then(x => ThenThereIsDelegatesInProvider(6)) + .And(x => ThenHandlerAtPositionIs(0)) //first because global not in config + .And(x => ThenHandlerAtPositionIs(1)) //first from config + .And(x => ThenHandlerAtPositionIs(2)) //second from config + .And(x => ThenHandlerAtPositionIs(3)) //third from config (global) + .And(x => ThenHandlerAtPositionIs(4)) + .And(x => ThenHandlerAtPositionIs(5)) + .BDDfy(); + } + + [Fact] + public void should_follow_ordering_order_specifics() + { + var qosOptions = new QoSOptionsBuilder() + .WithTimeoutValue(1) + .WithDurationOfBreak(1) + .WithExceptionsAllowedBeforeBreaking(1) + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(qosOptions) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true, int.MaxValue)) + .WithDelegatingHandlers(new List + { + "FakeDelegatingHandlerTwo", + "FakeDelegatingHandler", + }) + .WithLoadBalancerKey(string.Empty) + .Build(); + + this.Given(x => GivenTheFollowingRequest(route)) + .And(x => GivenTheQosFactoryReturns(new FakeQoSHandler())) + .And(x => GivenTheTracingFactoryReturns()) + .And(x => GivenTheServiceProviderReturnsGlobalDelegatingHandlers()) + .And(x => GivenTheServiceProviderReturnsSpecificDelegatingHandlers()) + .When(x => WhenIGet()) + .Then(x => ThenThereIsDelegatesInProvider(6)) + .And(x => ThenHandlerAtPositionIs(0)) + .And(x => ThenHandlerAtPositionIs(1)) + .And(x => ThenHandlerAtPositionIs(2)) + .And(x => ThenHandlerAtPositionIs(3)) + .And(x => ThenHandlerAtPositionIs(4)) + .And(x => ThenHandlerAtPositionIs(5)) + .BDDfy(); + } + + [Fact] + public void should_follow_ordering_order_and_only_add_specifics_in_config() + { + var qosOptions = new QoSOptionsBuilder() + .WithTimeoutValue(1) + .WithDurationOfBreak(1) + .WithExceptionsAllowedBeforeBreaking(1) + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(qosOptions) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true, int.MaxValue)) + .WithDelegatingHandlers(new List + { + "FakeDelegatingHandler", + }) + .WithLoadBalancerKey(string.Empty) + .Build(); + + this.Given(x => GivenTheFollowingRequest(route)) + .And(x => GivenTheQosFactoryReturns(new FakeQoSHandler())) + .And(x => GivenTheTracingFactoryReturns()) + .And(x => GivenTheServiceProviderReturnsGlobalDelegatingHandlers()) + .And(x => GivenTheServiceProviderReturnsSpecificDelegatingHandlers()) + .When(x => WhenIGet()) + .Then(x => ThenThereIsDelegatesInProvider(5)) + .And(x => ThenHandlerAtPositionIs(0)) + .And(x => ThenHandlerAtPositionIs(1)) + .And(x => ThenHandlerAtPositionIs(2)) + .And(x => ThenHandlerAtPositionIs(3)) + .And(x => ThenHandlerAtPositionIs(4)) + .BDDfy(); + } + + [Fact] + public void should_follow_ordering_dont_add_specifics() + { + var qosOptions = new QoSOptionsBuilder() + .WithTimeoutValue(1) + .WithDurationOfBreak(1) + .WithExceptionsAllowedBeforeBreaking(1) + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(qosOptions) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true, int.MaxValue)) + .WithLoadBalancerKey(string.Empty) + .Build(); + + this.Given(x => GivenTheFollowingRequest(route)) + .And(x => GivenTheQosFactoryReturns(new FakeQoSHandler())) + .And(x => GivenTheTracingFactoryReturns()) + .And(x => GivenTheServiceProviderReturnsGlobalDelegatingHandlers()) + .And(x => GivenTheServiceProviderReturnsSpecificDelegatingHandlers()) + .When(x => WhenIGet()) + .Then(x => ThenThereIsDelegatesInProvider(4)) + .And(x => ThenHandlerAtPositionIs(0)) + .And(x => ThenHandlerAtPositionIs(1)) + .And(x => ThenHandlerAtPositionIs(2)) + .And(x => ThenHandlerAtPositionIs(3)) + .BDDfy(); + } + + [Fact] + public void should_apply_re_route_specific() + { + var qosOptions = new QoSOptionsBuilder() + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(qosOptions) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false, true, int.MaxValue)) + .WithDelegatingHandlers(new List + { + "FakeDelegatingHandler", + "FakeDelegatingHandlerTwo", + }) + .WithLoadBalancerKey(string.Empty) + .Build(); + + this.Given(x => GivenTheFollowingRequest(route)) + .And(x => GivenTheServiceProviderReturnsSpecificDelegatingHandlers()) + .When(x => WhenIGet()) + .Then(x => ThenThereIsDelegatesInProvider(2)) + .And(x => ThenTheDelegatesAreAddedCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_all_from_all_routes_provider_and_qos() + { + var qosOptions = new QoSOptionsBuilder() + .WithTimeoutValue(1) + .WithDurationOfBreak(1) + .WithExceptionsAllowedBeforeBreaking(1) + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(qosOptions) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false, true, int.MaxValue)).WithLoadBalancerKey(string.Empty).Build(); + + this.Given(x => GivenTheFollowingRequest(route)) + .And(x => GivenTheQosFactoryReturns(new FakeQoSHandler())) + .And(x => GivenTheServiceProviderReturnsGlobalDelegatingHandlers()) + .When(x => WhenIGet()) + .Then(x => ThenThereIsDelegatesInProvider(3)) + .And(x => ThenTheDelegatesAreAddedCorrectly()) + .And(x => ThenItIsQosHandler(2)) + .BDDfy(); + } + + [Fact] + public void should_return_provider_with_no_delegates() + { + var qosOptions = new QoSOptionsBuilder() + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(qosOptions) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false, true, int.MaxValue)).WithLoadBalancerKey(string.Empty).Build(); + + this.Given(x => GivenTheFollowingRequest(route)) + .And(x => GivenTheServiceProviderReturnsNothing()) + .When(x => WhenIGet()) + .Then(x => ThenNoDelegatesAreInTheProvider()) + .BDDfy(); + } + + [Fact] + public void should_return_provider_with_qos_delegate() + { + var qosOptions = new QoSOptionsBuilder() + .WithTimeoutValue(1) + .WithDurationOfBreak(1) + .WithExceptionsAllowedBeforeBreaking(1) + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(qosOptions) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false, true, int.MaxValue)).WithLoadBalancerKey(string.Empty).Build(); + + this.Given(x => GivenTheFollowingRequest(route)) + .And(x => GivenTheQosFactoryReturns(new FakeQoSHandler())) + .And(x => GivenTheServiceProviderReturnsNothing()) + .When(x => WhenIGet()) + .Then(x => ThenThereIsDelegatesInProvider(1)) + .And(x => ThenItIsQosHandler(0)) + .BDDfy(); + } + + [Fact] + public void should_return_provider_with_qos_delegate_when_timeout_value_set() + { + var qosOptions = new QoSOptionsBuilder() + .WithTimeoutValue(1) + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(qosOptions) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, false, true, int.MaxValue)).WithLoadBalancerKey(string.Empty).Build(); + + this.Given(x => GivenTheFollowingRequest(route)) + .And(x => GivenTheQosFactoryReturns(new FakeQoSHandler())) + .And(x => GivenTheServiceProviderReturnsNothing()) + .When(x => WhenIGet()) + .Then(x => ThenThereIsDelegatesInProvider(1)) + .And(x => ThenItIsQosHandler(0)) + .BDDfy(); + } + + [Fact] + public void should_log_error_and_return_no_qos_provider_delegate_when_qos_factory_returns_error() + { + var qosOptions = new QoSOptionsBuilder() + .WithTimeoutValue(1) + .WithDurationOfBreak(1) + .WithExceptionsAllowedBeforeBreaking(1) + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(qosOptions) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true, int.MaxValue)) + .WithLoadBalancerKey(string.Empty) + .Build(); + + this.Given(x => GivenTheFollowingRequest(route)) + .And(x => GivenTheQosFactoryReturnsError()) + .And(x => GivenTheTracingFactoryReturns()) + .And(x => GivenTheServiceProviderReturnsGlobalDelegatingHandlers()) + .And(x => GivenTheServiceProviderReturnsSpecificDelegatingHandlers()) + .When(x => WhenIGet()) + .Then(x => ThenThereIsDelegatesInProvider(4)) + .And(x => ThenHandlerAtPositionIs(0)) + .And(x => ThenHandlerAtPositionIs(1)) + .And(x => ThenHandlerAtPositionIs(2)) + .And(x => ThenHandlerAtPositionIs(3)) + .And(_ => ThenTheWarningIsLogged()) + .BDDfy(); + } + + [Fact] + public void should_log_error_and_return_no_qos_provider_delegate_when_qos_factory_returns_null() + { + var qosOptions = new QoSOptionsBuilder() + .WithTimeoutValue(1) + .WithDurationOfBreak(1) + .WithExceptionsAllowedBeforeBreaking(1) + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(qosOptions) + .WithHttpHandlerOptions(new HttpHandlerOptions(true, true, true, true, int.MaxValue)) + .WithLoadBalancerKey(string.Empty) + .Build(); + + this.Given(x => GivenTheFollowingRequest(route)) + .And(x => GivenTheQosFactoryReturnsNull()) + .And(x => GivenTheTracingFactoryReturns()) + .And(x => GivenTheServiceProviderReturnsGlobalDelegatingHandlers()) + .And(x => GivenTheServiceProviderReturnsSpecificDelegatingHandlers()) + .When(x => WhenIGet()) + .Then(x => ThenThereIsDelegatesInProvider(4)) + .And(x => ThenHandlerAtPositionIs(0)) + .And(x => ThenHandlerAtPositionIs(1)) + .And(x => ThenHandlerAtPositionIs(2)) + .And(x => ThenHandlerAtPositionIs(3)) + .And(_ => ThenTheWarningIsLogged()) + .BDDfy(); + } + + 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); + } + + private void ThenHandlerAtPositionIs(int pos) + where T : DelegatingHandler + { + var delegates = _result.Data; + var del = delegates[pos].Invoke(); + del.ShouldBeOfType(); + } + + private void GivenTheTracingFactoryReturns() + { + _tracingFactory + .Setup(x => x.Get()) + .Returns(new FakeTracingHandler()); + } + + private void GivenTheServiceProviderReturnsGlobalDelegatingHandlers() + where TOne : DelegatingHandler + where TTwo : DelegatingHandler + { + _services.AddTransient(); + _services.AddTransient(s => + { + var service = s.GetService(); + return new GlobalDelegatingHandler(service); + }); + _services.AddTransient(); + _services.AddTransient(s => + { + var service = s.GetService(); + return new GlobalDelegatingHandler(service); + }); + } + + private void GivenTheServiceProviderReturnsSpecificDelegatingHandlers() + where TOne : DelegatingHandler + where TTwo : DelegatingHandler + { + _services.AddTransient(); + _services.AddTransient(); + } + + private void GivenTheServiceProviderReturnsNothing() + { + _serviceProvider = _services.BuildServiceProvider(); + } + + private void ThenAnErrorIsReturned() + { + _result.IsError.ShouldBeTrue(); + } + + private void ThenTheDelegatesAreAddedCorrectly() + { + var delegates = _result.Data; + + var del = delegates[0].Invoke(); + var handler = (FakeDelegatingHandler)del; + handler.Order.ShouldBe(1); + + del = delegates[1].Invoke(); + var handlerTwo = (FakeDelegatingHandlerTwo)del; + handlerTwo.Order.ShouldBe(2); + } + + private void GivenTheQosFactoryReturns(DelegatingHandler handler) + { + _qosFactory + .Setup(x => x.Get(It.IsAny())) + .Returns(new OkResponse(handler)); + } + + private void GivenTheQosFactoryReturnsError() + { + _qosFactory + .Setup(x => x.Get(It.IsAny())) + .Returns(new ErrorResponse(new AnyError())); + } + + private void GivenTheQosFactoryReturnsNull() + { + _qosFactory + .Setup(x => x.Get(It.IsAny())) + .Returns((ErrorResponse)null); + } + + private void ThenItIsQosHandler(int i) + { + var delegates = _result.Data; + var del = delegates[i].Invoke(); + del.ShouldBeOfType(); + } + + private void ThenThereIsDelegatesInProvider(int count) + { + _result.ShouldNotBeNull(); + _result.Data.Count.ShouldBe(count); + } + + private void GivenTheFollowingRequest(DownstreamRoute request) + { + _downstreamRoute = request; + } + + private void WhenIGet() + { + _serviceProvider = _services.BuildServiceProvider(); + _factory = new DelegatingHandlerHandlerFactory(_tracingFactory.Object, _qosFactory.Object, _serviceProvider, _loggerFactory.Object); + _result = _factory.Get(_downstreamRoute); + } + + private void ThenNoDelegatesAreInTheProvider() + { + _result.ShouldNotBeNull(); + _result.Data.Count.ShouldBe(0); + } + } + + internal class FakeTracingHandler : DelegatingHandler, ITracingHandler + { + } + + internal class FakeQoSHandler : DelegatingHandler + { + } +} diff --git a/test/Ocelot.UnitTests/Requester/FakeDelegatingHandler.cs b/test/Ocelot.UnitTests/Requester/FakeDelegatingHandler.cs index e78fac8c7..ad1e62afe 100644 --- a/test/Ocelot.UnitTests/Requester/FakeDelegatingHandler.cs +++ b/test/Ocelot.UnitTests/Requester/FakeDelegatingHandler.cs @@ -17,7 +17,7 @@ public FakeDelegatingHandler(int order) Order = order; } - public int Order { get; private set; } + public int Order { get; } public DateTime TimeCalled { get; private set; } @@ -35,7 +35,7 @@ public FakeDelegatingHandlerThree() Order = 3; } - public int Order { get; private set; } + public int Order { get; } public DateTime TimeCalled { get; private set; } @@ -53,7 +53,7 @@ public FakeDelegatingHandlerFour() Order = 4; } - public int Order { get; private set; } + public int Order { get; } public DateTime TimeCalled { get; private set; } @@ -71,7 +71,7 @@ public FakeDelegatingHandlerTwo() Order = 2; } - public int Order { get; private set; } + public int Order { get; } public DateTime TimeCalled { get; private set; } diff --git a/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs b/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs index c8dbd4a2b..51b82a7a8 100644 --- a/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs @@ -1,7 +1,16 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; + using Moq; + using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Logging; @@ -9,14 +18,11 @@ using Ocelot.Request.Middleware; using Ocelot.Requester; using Ocelot.Responses; + using Shouldly; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; + using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Requester @@ -27,7 +33,7 @@ public class HttpClientBuilderTests : IDisposable private readonly Mock _factory; private IHttpClient _httpClient; private HttpResponseMessage _response; - private DownstreamContext _context; + private HttpContext _context; private readonly Mock _cacheHandlers; private readonly Mock _logger; private int _count; @@ -50,16 +56,16 @@ public void should_build_http_client() var qosOptions = new QoSOptionsBuilder() .Build(); - var reRoute = new DownstreamReRouteBuilder() + var route = new DownstreamRouteBuilder() .WithQosOptions(qosOptions) .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true, int.MaxValue)) - .WithLoadBalancerKey("") - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("").Build()) + .WithLoadBalancerKey(string.Empty) + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build()) .WithQosOptions(new QoSOptionsBuilder().Build()) .Build(); this.Given(x => GivenTheFactoryReturns()) - .And(x => GivenARequest(reRoute)) + .And(x => GivenARequest(route)) .When(x => WhenIBuild()) .Then(x => ThenTheHttpClientShouldNotBeNull()) .BDDfy(); @@ -71,17 +77,17 @@ public void should_get_from_cache() var qosOptions = new QoSOptionsBuilder() .Build(); - var reRoute = new DownstreamReRouteBuilder() + var route = new DownstreamRouteBuilder() .WithQosOptions(qosOptions) .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true, int.MaxValue)) - .WithLoadBalancerKey("") - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("").Build()) + .WithLoadBalancerKey(string.Empty) + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build()) .WithQosOptions(new QoSOptionsBuilder().Build()) .Build(); this.Given(x => GivenARealCache()) .And(x => GivenTheFactoryReturns()) - .And(x => GivenARequest(reRoute)) + .And(x => GivenARequest(route)) .And(x => WhenIBuildTheFirstTime()) .And(x => WhenISave()) .And(x => WhenIBuildAgain()) @@ -97,21 +103,21 @@ public void should_get_from_cache_with_different_query_string() var qosOptions = new QoSOptionsBuilder() .Build(); - var reRoute = new DownstreamReRouteBuilder() + var route = new DownstreamRouteBuilder() .WithQosOptions(qosOptions) .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true, int.MaxValue)) - .WithLoadBalancerKey("") - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("").Build()) + .WithLoadBalancerKey(string.Empty) + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build()) .WithQosOptions(new QoSOptionsBuilder().Build()) .Build(); this.Given(x => GivenARealCache()) .And(x => GivenTheFactoryReturns()) - .And(x => GivenARequest(reRoute, "http://wwww.someawesomewebsite.com/woot?badman=1")) + .And(x => GivenARequest(route, "http://wwww.someawesomewebsite.com/woot?badman=1")) .And(x => WhenIBuildTheFirstTime()) .And(x => WhenISave()) .And(x => WhenIBuildAgain()) - .And(x => GivenARequest(reRoute, "http://wwww.someawesomewebsite.com/woot?badman=2")) + .And(x => GivenARequest(route, "http://wwww.someawesomewebsite.com/woot?badman=2")) .And(x => WhenISave()) .When(x => WhenIBuildAgain()) .Then(x => ThenTheHttpClientIsFromTheCache()) @@ -124,29 +130,29 @@ public void should_not_get_from_cache_with_different_query_string() var qosOptions = new QoSOptionsBuilder() .Build(); - var reRouteA = new DownstreamReRouteBuilder() + var routeA = new DownstreamRouteBuilder() .WithQosOptions(qosOptions) .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true, int.MaxValue)) - .WithLoadBalancerKey("") - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithContainsQueryString(true).WithOriginalValue("").Build()) + .WithLoadBalancerKey(string.Empty) + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithContainsQueryString(true).WithOriginalValue(string.Empty).Build()) .WithQosOptions(new QoSOptionsBuilder().Build()) .Build(); - var reRouteB = new DownstreamReRouteBuilder() + var routeB = new DownstreamRouteBuilder() .WithQosOptions(qosOptions) .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true, int.MaxValue)) - .WithLoadBalancerKey("") - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithContainsQueryString(true).WithOriginalValue("").Build()) + .WithLoadBalancerKey(string.Empty) + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithContainsQueryString(true).WithOriginalValue(string.Empty).Build()) .WithQosOptions(new QoSOptionsBuilder().Build()) .Build(); this.Given(x => GivenARealCache()) .And(x => GivenTheFactoryReturns()) - .And(x => GivenARequest(reRouteA, "http://wwww.someawesomewebsite.com/woot?badman=1")) + .And(x => GivenARequest(routeA, "http://wwww.someawesomewebsite.com/woot?badman=1")) .And(x => WhenIBuildTheFirstTime()) .And(x => WhenISave()) .And(x => WhenIBuildAgain()) - .And(x => GivenARequest(reRouteB, "http://wwww.someawesomewebsite.com/woot?badman=2")) + .And(x => GivenARequest(routeB, "http://wwww.someawesomewebsite.com/woot?badman=2")) .And(x => WhenISave()) .When(x => WhenIBuildAgain()) .Then(x => ThenTheHttpClientIsNotFromTheCache()) @@ -159,17 +165,17 @@ public void should_log_if_ignoring_ssl_errors() var qosOptions = new QoSOptionsBuilder() .Build(); - var reRoute = new DownstreamReRouteBuilder() + var route = new DownstreamRouteBuilder() .WithQosOptions(qosOptions) .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true, int.MaxValue)) - .WithLoadBalancerKey("") - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("").Build()) + .WithLoadBalancerKey(string.Empty) + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build()) .WithQosOptions(new QoSOptionsBuilder().Build()) .WithDangerousAcceptAnyServerCertificateValidator(true) .Build(); this.Given(x => GivenTheFactoryReturns()) - .And(x => GivenARequest(reRoute)) + .And(x => GivenARequest(route)) .When(x => WhenIBuild()) .Then(x => ThenTheHttpClientShouldNotBeNull()) .Then(x => ThenTheDangerousAcceptAnyServerCertificateValidatorWarningIsLogged()) @@ -182,25 +188,25 @@ public void should_call_delegating_handlers_in_order() var qosOptions = new QoSOptionsBuilder() .Build(); - var reRoute = new DownstreamReRouteBuilder() + var route = new DownstreamRouteBuilder() .WithQosOptions(qosOptions) .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true, int.MaxValue)) - .WithLoadBalancerKey("") - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("").Build()) + .WithLoadBalancerKey(string.Empty) + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build()) .WithQosOptions(new QoSOptionsBuilder().Build()) .Build(); var fakeOne = new FakeDelegatingHandler(); var fakeTwo = new FakeDelegatingHandler(); - var handlers = new List>() + var handlers = new List> { () => fakeOne, - () => fakeTwo + () => fakeTwo, }; this.Given(x => GivenTheFactoryReturns(handlers)) - .And(x => GivenARequest(reRoute)) + .And(x => GivenARequest(route)) .And(x => WhenIBuild()) .When(x => WhenICallTheClient()) .Then(x => ThenTheFakeAreHandledInOrder(fakeOne, fakeTwo)) @@ -214,16 +220,16 @@ public void should_re_use_cookies_from_container() var qosOptions = new QoSOptionsBuilder() .Build(); - var reRoute = new DownstreamReRouteBuilder() + var route = new DownstreamRouteBuilder() .WithQosOptions(qosOptions) .WithHttpHandlerOptions(new HttpHandlerOptions(false, true, false, true, int.MaxValue)) - .WithLoadBalancerKey("") - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("").Build()) + .WithLoadBalancerKey(string.Empty) + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build()) .WithQosOptions(new QoSOptionsBuilder().Build()) .Build(); this.Given(_ => GivenADownstreamService()) - .And(_ => GivenARequest(reRoute)) + .And(_ => GivenARequest(route)) .And(_ => GivenTheFactoryReturnsNothing()) .And(_ => WhenIBuild()) .And(_ => WhenICallTheClient("http://localhost:5003")) @@ -250,19 +256,19 @@ public void should_add_verb_to_cache_key(string verb) var qosOptions = new QoSOptionsBuilder() .Build(); - var reRoute = new DownstreamReRouteBuilder() + var route = new DownstreamRouteBuilder() .WithQosOptions(qosOptions) .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true, int.MaxValue)) - .WithLoadBalancerKey("") - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("").Build()) + .WithLoadBalancerKey(string.Empty) + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build()) .WithQosOptions(new QoSOptionsBuilder().Build()) .Build(); this.Given(_ => GivenADownstreamService()) - .And(_ => GivenARequestWithAUrlAndMethod(reRoute, downstreamUrl, method)) + .And(_ => GivenARequestWithAUrlAndMethod(route, downstreamUrl, method)) .And(_ => GivenTheFactoryReturnsNothing()) .And(_ => WhenIBuild()) - .And(_ => GivenCacheIsCalledWithExpectedKey($"{method.ToString()}:{downstreamUrl}")) + .And(_ => GivenCacheIsCalledWithExpectedKey($"{method}:{downstreamUrl}")) .BDDfy(); } @@ -289,17 +295,17 @@ private void WhenISave() private void GivenCacheIsCalledWithExpectedKey(string expectedKey) { - _cacheHandlers.Verify(x => x.Get(It.IsAny()), Times.Once); + _cacheHandlers.Verify(x => x.Get(It.IsAny()), Times.Once); } private void ThenTheDangerousAcceptAnyServerCertificateValidatorWarningIsLogged() { - _logger.Verify(x => x.LogWarning($"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamReRoute, UpstreamPathTemplate: {_context.DownstreamReRoute.UpstreamPathTemplate}, DownstreamPathTemplate: {_context.DownstreamReRoute.DownstreamPathTemplate}"), Times.Once); + _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); } private void GivenTheClientIsCached() { - _cacheHandlers.Setup(x => x.Get(It.IsAny())).Returns(_httpClient); + _cacheHandlers.Setup(x => x.Get(It.IsAny())).Returns(_httpClient); } private void ThenTheCookieIsSet() @@ -358,25 +364,21 @@ private void GivenADownstreamService() _host.Start(); } - private void GivenARequest(DownstreamReRoute downstream) + private void GivenARequest(DownstreamRoute downstream) { GivenARequest(downstream, "http://localhost:5003"); } - private void GivenARequest(DownstreamReRoute downstream, string downstreamUrl) + private void GivenARequest(DownstreamRoute downstream, string downstreamUrl) { GivenARequestWithAUrlAndMethod(downstream, downstreamUrl, HttpMethod.Get); } - private void GivenARequestWithAUrlAndMethod(DownstreamReRoute downstream, string url, HttpMethod method) + private void GivenARequestWithAUrlAndMethod(DownstreamRoute downstream, string url, HttpMethod method) { - var context = new DownstreamContext(new DefaultHttpContext()) - { - DownstreamReRoute = downstream, - DownstreamRequest = new DownstreamRequest(new HttpRequestMessage() { RequestUri = new Uri(url), Method = method }), - }; - - _context = context; + _context = new DefaultHttpContext(); + _context.Items.UpsertDownstreamRoute(downstream); + _context.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage { RequestUri = new Uri(url), Method = method })); } private void ThenSomethingIsReturned() @@ -389,17 +391,17 @@ private void WhenICallTheClient() _response = _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "http://test.com")).GetAwaiter().GetResult(); } - private void ThenTheFakeAreHandledInOrder(FakeDelegatingHandler fakeOne, FakeDelegatingHandler fakeTwo) + private static void ThenTheFakeAreHandledInOrder(FakeDelegatingHandler fakeOne, FakeDelegatingHandler fakeTwo) { fakeOne.TimeCalled.ShouldBeGreaterThan(fakeTwo.TimeCalled); } private void GivenTheFactoryReturns() { - var handlers = new List>() { () => new FakeDelegatingHandler() }; + var handlers = new List> { () => new FakeDelegatingHandler() }; _factory - .Setup(x => x.Get(It.IsAny())) + .Setup(x => x.Get(It.IsAny())) .Returns(new OkResponse>>(handlers)); } @@ -408,31 +410,31 @@ private void GivenTheFactoryReturnsNothing() var handlers = new List>(); _factory - .Setup(x => x.Get(It.IsAny())) + .Setup(x => x.Get(It.IsAny())) .Returns(new OkResponse>>(handlers)); } private void GivenTheFactoryReturns(List> handlers) { _factory - .Setup(x => x.Get(It.IsAny())) + .Setup(x => x.Get(It.IsAny())) .Returns(new OkResponse>>(handlers)); } private void WhenIBuild() { - _httpClient = _builder.Create(_context); + _httpClient = _builder.Create(_context.Items.DownstreamRoute()); } private void WhenIBuildTheFirstTime() { - _firstHttpClient = _builder.Create(_context); + _firstHttpClient = _builder.Create(_context.Items.DownstreamRoute()); } private void WhenIBuildAgain() { _builder = new HttpClientBuilder(_factory.Object, _realCache, _logger.Object); - _againHttpClient = _builder.Create(_context); + _againHttpClient = _builder.Create(_context.Items.DownstreamRoute()); } private void ThenTheHttpClientShouldNotBeNull() diff --git a/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs b/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs index 43bfa5d5a..3ad6a9cec 100644 --- a/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs +++ b/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs @@ -1,202 +1,203 @@ -using Microsoft.AspNetCore.Http; -using Moq; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Logging; -using Ocelot.Middleware; -using Ocelot.Request.Middleware; -using Ocelot.Requester; -using Ocelot.Responses; -using Shouldly; -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; using System.Threading.Tasks; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Requester -{ - public class HttpClientHttpRequesterTest - { - private readonly Mock _cacheHandlers; - private readonly Mock _factory; - private Response _response; - private readonly HttpClientHttpRequester _httpClientRequester; - private DownstreamContext _request; - private Mock _loggerFactory; - private Mock _logger; - private Mock _mapper; - - public HttpClientHttpRequesterTest() - { - _factory = new Mock(); - _factory.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse>>(new List>())); - _logger = new Mock(); - _loggerFactory = new Mock(); - _loggerFactory - .Setup(x => x.CreateLogger()) - .Returns(_logger.Object); - _cacheHandlers = new Mock(); - _mapper = new Mock(); - _httpClientRequester = new HttpClientHttpRequester( - _loggerFactory.Object, - _cacheHandlers.Object, - _factory.Object, - _mapper.Object); - } - - [Fact] - public void should_call_request_correctly() - { - var upstreamTemplate = new UpstreamPathTemplateBuilder().WithOriginalValue("").Build(); - - var qosOptions = new QoSOptionsBuilder() - .Build(); - - var reRoute = new DownstreamReRouteBuilder() - .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true, int.MaxValue)) - .WithLoadBalancerKey("") - .WithUpstreamPathTemplate(upstreamTemplate) - .WithQosOptions(new QoSOptionsBuilder().Build()) - .Build(); - - var context = new DownstreamContext(new DefaultHttpContext()) - { - DownstreamReRoute = reRoute, - DownstreamRequest = new DownstreamRequest(new HttpRequestMessage() { RequestUri = new Uri("http://www.bbc.co.uk") }), - }; - - this.Given(x => x.GivenTheRequestIs(context)) - .And(x => GivenTheHouseReturnsOkHandler()) - .When(x => x.WhenIGetResponse()) - .Then(x => x.ThenTheResponseIsCalledCorrectly()) - .BDDfy(); - } - - [Fact] - public void should_call_request_unable_to_complete_request() - { - var upstreamTemplate = new UpstreamPathTemplateBuilder().WithOriginalValue("").Build(); - - var qosOptions = new QoSOptionsBuilder() - .Build(); - - var reRoute = new DownstreamReRouteBuilder() - .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true, int.MaxValue)) - .WithLoadBalancerKey("") - .WithUpstreamPathTemplate(upstreamTemplate) - .WithQosOptions(new QoSOptionsBuilder().Build()) - .Build(); - - var context = new DownstreamContext(new DefaultHttpContext()) - { - DownstreamReRoute = reRoute, - DownstreamRequest = new DownstreamRequest(new HttpRequestMessage() { RequestUri = new Uri("http://localhost:60080") }), - }; - - this.Given(x => x.GivenTheRequestIs(context)) - .When(x => x.WhenIGetResponse()) - .Then(x => x.ThenTheResponseIsCalledError()) - .BDDfy(); - } - [Fact] - public void http_client_request_times_out() - { - var upstreamTemplate = new UpstreamPathTemplateBuilder().WithOriginalValue("").Build(); +using Microsoft.AspNetCore.Http; - var qosOptions = new QoSOptionsBuilder() - .Build(); - - var reRoute = new DownstreamReRouteBuilder() - .WithQosOptions(qosOptions) - .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true, int.MaxValue)) - .WithLoadBalancerKey("") - .WithUpstreamPathTemplate(upstreamTemplate) - .WithQosOptions(new QoSOptionsBuilder().WithTimeoutValue(1).Build()) - .Build(); - - var context = new DownstreamContext(new DefaultHttpContext()) - { - DownstreamReRoute = reRoute, - DownstreamRequest = new DownstreamRequest(new HttpRequestMessage() { RequestUri = new Uri("http://localhost:60080") }), - }; - - this.Given(_ => GivenTheRequestIs(context)) - .And(_ => GivenTheHouseReturnsTimeoutHandler()) - .When(_ => WhenIGetResponse()) - .Then(_ => ThenTheResponseIsCalledError()) - .And(_ => ThenTheErrorIsTimeout()) - .BDDfy(); - } - - private void GivenTheRequestIs(DownstreamContext request) - { - _request = request; - } - - private void WhenIGetResponse() - { - _response = _httpClientRequester.GetResponse(_request).GetAwaiter().GetResult(); - } - - private void ThenTheResponseIsCalledCorrectly() - { - _response.IsError.ShouldBeFalse(); - } - - private void ThenTheResponseIsCalledError() - { - _response.IsError.ShouldBeTrue(); - } - - private void ThenTheErrorIsTimeout() - { - _mapper.Verify(x => x.Map(It.IsAny()), Times.Once); - _response.Errors[0].ShouldBeOfType(); - } - - private void GivenTheHouseReturnsOkHandler() - { - var handlers = new List> - { - () => new OkDelegatingHandler() - }; - - _factory.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse>>(handlers)); - } - - private void GivenTheHouseReturnsTimeoutHandler() - { - var handlers = new List> - { - () => new TimeoutDelegatingHandler() - }; +using Moq; - _factory.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse>>(handlers)); +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.Request.Middleware; +using Ocelot.Requester; +using Ocelot.Responses; - _mapper.Setup(x => x.Map(It.IsAny())).Returns(new UnableToCompleteRequestError(new Exception())); - } +using Shouldly; - private class OkDelegatingHandler : DelegatingHandler - { - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - return Task.FromResult(new HttpResponseMessage()); - } - } +using TestStack.BDDfy; - private class TimeoutDelegatingHandler : DelegatingHandler - { - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - await Task.Delay(100000, cancellationToken); - return new HttpResponseMessage(); - } - } - } -} +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class HttpClientHttpRequesterTest + { + private readonly Mock _cacheHandlers; + private readonly Mock _factory; + private Response _response; + private readonly HttpClientHttpRequester _httpClientRequester; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private readonly Mock _mapper; + private HttpContext _httpContext; + + public HttpClientHttpRequesterTest() + { + _httpContext = new DefaultHttpContext(); + _factory = new Mock(); + _factory.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse>>(new List>())); + _logger = new Mock(); + _loggerFactory = new Mock(); + _loggerFactory + .Setup(x => x.CreateLogger()) + .Returns(_logger.Object); + _cacheHandlers = new Mock(); + _mapper = new Mock(); + _httpClientRequester = new HttpClientHttpRequester( + _loggerFactory.Object, + _cacheHandlers.Object, + _factory.Object, + _mapper.Object); + } + + [Fact] + public void should_call_request_correctly() + { + var upstreamTemplate = new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build(); + + var qosOptions = new QoSOptionsBuilder() + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(qosOptions) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true, int.MaxValue)) + .WithLoadBalancerKey(string.Empty) + .WithUpstreamPathTemplate(upstreamTemplate) + .WithQosOptions(new QoSOptionsBuilder().Build()) + .Build(); + + var httpContext = new DefaultHttpContext(); + httpContext.Items.UpsertDownstreamRoute(route); + httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage { RequestUri = new Uri("http://www.bbc.co.uk") })); + + this.Given(x => x.GivenTheRequestIs(httpContext)) + .And(x => GivenTheHouseReturnsOkHandler()) + .When(x => x.WhenIGetResponse()) + .Then(x => x.ThenTheResponseIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_call_request_unable_to_complete_request() + { + var upstreamTemplate = new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build(); + + var qosOptions = new QoSOptionsBuilder() + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(qosOptions) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true, int.MaxValue)) + .WithLoadBalancerKey(string.Empty) + .WithUpstreamPathTemplate(upstreamTemplate) + .WithQosOptions(new QoSOptionsBuilder().Build()) + .Build(); + + var httpContext = new DefaultHttpContext(); + httpContext.Items.UpsertDownstreamRoute(route); + httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage { RequestUri = new Uri("http://localhost:60080") })); + + this.Given(x => x.GivenTheRequestIs(httpContext)) + .When(x => x.WhenIGetResponse()) + .Then(x => x.ThenTheResponseIsCalledError()) + .BDDfy(); + } + + [Fact] + public void http_client_request_times_out() + { + var upstreamTemplate = new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build(); + + var qosOptions = new QoSOptionsBuilder() + .Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(qosOptions) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false, true, int.MaxValue)) + .WithLoadBalancerKey(string.Empty) + .WithUpstreamPathTemplate(upstreamTemplate) + .WithQosOptions(new QoSOptionsBuilder().WithTimeoutValue(1).Build()) + .Build(); + + var httpContext = new DefaultHttpContext(); + httpContext.Items.UpsertDownstreamRoute(route); + httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage { RequestUri = new Uri("http://localhost:60080") })); + + this.Given(_ => GivenTheRequestIs(httpContext)) + .And(_ => GivenTheHouseReturnsTimeoutHandler()) + .When(_ => WhenIGetResponse()) + .Then(_ => ThenTheResponseIsCalledError()) + .And(_ => ThenTheErrorIsTimeout()) + .BDDfy(); + } + + private void GivenTheRequestIs(HttpContext httpContext) + { + _httpContext = httpContext; + } + + private void WhenIGetResponse() + { + _response = _httpClientRequester.GetResponse(_httpContext).GetAwaiter().GetResult(); + } + + private void ThenTheResponseIsCalledCorrectly() + { + _response.IsError.ShouldBeFalse(); + } + + private void ThenTheResponseIsCalledError() + { + _response.IsError.ShouldBeTrue(); + } + + private void ThenTheErrorIsTimeout() + { + _mapper.Verify(x => x.Map(It.IsAny()), Times.Once); + _response.Errors[0].ShouldBeOfType(); + } + + private void GivenTheHouseReturnsOkHandler() + { + var handlers = new List> + { + () => new OkDelegatingHandler(), + }; + + _factory.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse>>(handlers)); + } + + private void GivenTheHouseReturnsTimeoutHandler() + { + var handlers = new List> + { + () => new TimeoutDelegatingHandler(), + }; + + _factory.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse>>(handlers)); + + _mapper.Setup(x => x.Map(It.IsAny())).Returns(new UnableToCompleteRequestError(new Exception())); + } + + private class OkDelegatingHandler : DelegatingHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage()); + } + } + + private class TimeoutDelegatingHandler : DelegatingHandler + { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + await Task.Delay(100000, cancellationToken); + return new HttpResponseMessage(); + } + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/HttpExeptionToErrorMapperTests.cs b/test/Ocelot.UnitTests/Requester/HttpExeptionToErrorMapperTests.cs index 14413aad0..d53d96697 100644 --- a/test/Ocelot.UnitTests/Requester/HttpExeptionToErrorMapperTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpExeptionToErrorMapperTests.cs @@ -1,15 +1,21 @@ -namespace Ocelot.UnitTests.Requester -{ - using Microsoft.Extensions.DependencyInjection; - using Ocelot.Errors; - using Ocelot.Requester; - using Responder; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Threading.Tasks; - using Xunit; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; + +using Microsoft.Extensions.DependencyInjection; + +using Ocelot.Errors; +using Ocelot.Requester; + +using Ocelot.UnitTests.Responder; + +using Shouldly; + +using Xunit; +namespace Ocelot.UnitTests.Requester +{ public class HttpExeptionToErrorMapperTests { private HttpExeptionToErrorMapper _mapper; @@ -38,6 +44,14 @@ public void should_return_request_canceled() error.ShouldBeOfType(); } + [Fact] + public void should_return_ConnectionToDownstreamServiceError() + { + var error = _mapper.Map(new HttpRequestException()); + + error.ShouldBeOfType(); + } + [Fact] public void should_return_request_canceled_for_subtype() { diff --git a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs index 8a7b1fc88..2b44f2878 100644 --- a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs @@ -1,132 +1,137 @@ -namespace Ocelot.UnitTests.Requester -{ - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.Configuration.Builder; - using Ocelot.Logging; - using Ocelot.Middleware; - using Ocelot.Requester; - using Ocelot.Requester.Middleware; - using Ocelot.Responses; - using Ocelot.UnitTests.Responder; - using Shouldly; - using System; - using System.Linq; - using System.Net.Http; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; - public class HttpRequesterMiddlewareTests - { - private readonly Mock _requester; - private Response _response; - private Mock _loggerFactory; - private Mock _logger; - private readonly HttpRequesterMiddleware _middleware; - private DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; +using Microsoft.AspNetCore.Http; - public HttpRequesterMiddlewareTests() - { - _requester = new Mock(); - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _next = context => Task.CompletedTask; - _middleware = new HttpRequesterMiddleware(_next, _loggerFactory.Object, _requester.Object); - } +using Moq; - [Fact] - public void should_call_services_correctly() - { - this.Given(x => x.GivenTheRequestIs()) - .And(x => x.GivenTheRequesterReturns(new OkResponse(new HttpResponseMessage(System.Net.HttpStatusCode.OK)))) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamResponseIsSet()) - .Then(x => InformationIsLogged()) - .BDDfy(); - } +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.Requester; +using Ocelot.Requester.Middleware; - [Fact] - public void should_set_error() - { - this.Given(x => x.GivenTheRequestIs()) - .And(x => x.GivenTheRequesterReturns(new ErrorResponse(new AnyError()))) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheErrorIsSet()) - .BDDfy(); - } +using Ocelot.UnitTests.Responder; - [Fact] - public void should_log_downstream_internal_server_error() - { - this.Given(x => x.GivenTheRequestIs()) - .And(x => x.GivenTheRequesterReturns( - new OkResponse(new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError)))) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.WarningIsLogged()) - .BDDfy(); - } +using Ocelot.Responses; - private void ThenTheErrorIsSet() - { - _downstreamContext.IsError.ShouldBeTrue(); - } +using Shouldly; - private void WhenICallTheMiddleware() - { - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); - } +using TestStack.BDDfy; - private void GivenTheRequestIs() - { - _downstreamContext = - new DownstreamContext(new DefaultHttpContext()) - { - DownstreamReRoute = new DownstreamReRouteBuilder().Build() - }; - } - - private void GivenTheRequesterReturns(Response response) - { - _response = response; - - _requester - .Setup(x => x.GetResponse(It.IsAny())) - .ReturnsAsync(_response); - } - - private void ThenTheDownstreamResponseIsSet() - { - foreach (var httpResponseHeader in _response.Data.Headers) - { - if (_downstreamContext.DownstreamResponse.Headers.Any(x => x.Key == httpResponseHeader.Key)) - { - throw new Exception("Header in response not in downstreamresponse headers"); - } - } - - _downstreamContext.DownstreamResponse.Content.ShouldBe(_response.Data.Content); - _downstreamContext.DownstreamResponse.StatusCode.ShouldBe(_response.Data.StatusCode); - } - - private void WarningIsLogged() - { - _logger.Verify( - x => x.LogWarning( - It.IsAny() - ), - Times.Once); - } - - private void InformationIsLogged() - { - _logger.Verify( - x => x.LogInformation( - It.IsAny() - ), - Times.Once); - } - } -} +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class HttpRequesterMiddlewareTests + { + private readonly Mock _requester; + private Response _response; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private readonly HttpRequesterMiddleware _middleware; + private readonly RequestDelegate _next; + private readonly HttpContext _httpContext; + + public HttpRequesterMiddlewareTests() + { + _httpContext = new DefaultHttpContext(); + _requester = new Mock(); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _next = context => Task.CompletedTask; + _middleware = new HttpRequesterMiddleware(_next, _loggerFactory.Object, _requester.Object); + } + + [Fact] + public void should_call_services_correctly() + { + this.Given(x => x.GivenTheRequestIs()) + .And(x => x.GivenTheRequesterReturns(new OkResponse(new HttpResponseMessage(System.Net.HttpStatusCode.OK)))) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheDownstreamResponseIsSet()) + .Then(x => InformationIsLogged()) + .BDDfy(); + } + + [Fact] + public void should_set_error() + { + this.Given(x => x.GivenTheRequestIs()) + .And(x => x.GivenTheRequesterReturns(new ErrorResponse(new AnyError()))) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheErrorIsSet()) + .BDDfy(); + } + + [Fact] + public void should_log_downstream_internal_server_error() + { + this.Given(x => x.GivenTheRequestIs()) + .And(x => x.GivenTheRequesterReturns( + new OkResponse(new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError)))) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.WarningIsLogged()) + .BDDfy(); + } + + private void ThenTheErrorIsSet() + { + _httpContext.Items.Errors().Count.ShouldBeGreaterThan(0); + } + + private void WhenICallTheMiddleware() + { + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); + } + + private void GivenTheRequestIs() + { + _httpContext.Items.UpsertDownstreamRoute(new DownstreamRouteBuilder().Build()); + } + + private void GivenTheRequesterReturns(Response response) + { + _response = response; + + _requester + .Setup(x => x.GetResponse(It.IsAny())) + .ReturnsAsync(_response); + } + + private void ThenTheDownstreamResponseIsSet() + { + foreach (var httpResponseHeader in _response.Data.Headers) + { + if (_httpContext.Items.DownstreamResponse().Headers.Any(x => x.Key == httpResponseHeader.Key)) + { + throw new Exception("Header in response not in downstreamresponse headers"); + } + } + + _httpContext.Items.DownstreamResponse().Content.ShouldBe(_response.Data.Content); + _httpContext.Items.DownstreamResponse().StatusCode.ShouldBe(_response.Data.StatusCode); + } + + private void WarningIsLogged() + { + _logger.Verify( + x => x.LogWarning( + It.IsAny() + ), + Times.Once); + } + + private void InformationIsLogged() + { + _logger.Verify( + x => x.LogInformation( + It.IsAny() + ), + Times.Once); + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/QoSFactoryTests.cs b/test/Ocelot.UnitTests/Requester/QoSFactoryTests.cs index 93c1c58ea..b3843a3b2 100644 --- a/test/Ocelot.UnitTests/Requester/QoSFactoryTests.cs +++ b/test/Ocelot.UnitTests/Requester/QoSFactoryTests.cs @@ -1,55 +1,60 @@ -namespace Ocelot.UnitTests.Requester -{ - using Microsoft.Extensions.DependencyInjection; - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Logging; - using Ocelot.Requester; - using Ocelot.Requester.QoS; - using Shouldly; - using System.Net.Http; - using Xunit; +using System.Net.Http; - public class QoSFactoryTests - { - private QoSFactory _factory; - private ServiceCollection _services; - private readonly Mock _loggerFactory; +using Microsoft.Extensions.DependencyInjection; - public QoSFactoryTests() - { - _services = new ServiceCollection(); - _loggerFactory = new Mock(); - var provider = _services.BuildServiceProvider(); - _factory = new QoSFactory(provider, _loggerFactory.Object); - } +using Moq; - [Fact] - public void should_return_error() - { - var downstreamReRoute = new DownstreamReRouteBuilder().Build(); - var handler = _factory.Get(downstreamReRoute); - handler.IsError.ShouldBeTrue(); - handler.Errors[0].ShouldBeOfType(); - } +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Requester; +using Ocelot.Requester.QoS; - [Fact] - public void should_return_handler() - { - _services = new ServiceCollection(); - DelegatingHandler QosDelegatingHandlerDelegate(DownstreamReRoute a, IOcelotLoggerFactory b) => new FakeDelegatingHandler(); - _services.AddSingleton(QosDelegatingHandlerDelegate); - var provider = _services.BuildServiceProvider(); - _factory = new QoSFactory(provider, _loggerFactory.Object); - var downstreamReRoute = new DownstreamReRouteBuilder().Build(); - var handler = _factory.Get(downstreamReRoute); - handler.IsError.ShouldBeFalse(); - handler.Data.ShouldBeOfType(); - } +using Shouldly; - private class FakeDelegatingHandler : DelegatingHandler - { - } - } -} +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class QoSFactoryTests + { + private QoSFactory _factory; + private ServiceCollection _services; + private readonly Mock _loggerFactory; + + public QoSFactoryTests() + { + _services = new ServiceCollection(); + _loggerFactory = new Mock(); + var provider = _services.BuildServiceProvider(); + _factory = new QoSFactory(provider, _loggerFactory.Object); + } + + [Fact] + public void should_return_error() + { + var downstreamRoute = new DownstreamRouteBuilder().Build(); + var handler = _factory.Get(downstreamRoute); + handler.IsError.ShouldBeTrue(); + handler.Errors[0].ShouldBeOfType(); + } + + [Fact] + public void should_return_handler() + { + _services = new ServiceCollection(); + DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute a, IOcelotLoggerFactory b) => new FakeDelegatingHandler(); + _services.AddSingleton(QosDelegatingHandlerDelegate); + var provider = _services.BuildServiceProvider(); + _factory = new QoSFactory(provider, _loggerFactory.Object); + var downstreamRoute = new DownstreamRouteBuilder().Build(); + var handler = _factory.Get(downstreamRoute); + handler.IsError.ShouldBeFalse(); + handler.Data.ShouldBeOfType(); + } + + private class FakeDelegatingHandler : DelegatingHandler + { + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/TracingHandlerFactoryTests.cs b/test/Ocelot.UnitTests/Requester/TracingHandlerFactoryTests.cs index 6e40a895b..80c49599e 100644 --- a/test/Ocelot.UnitTests/Requester/TracingHandlerFactoryTests.cs +++ b/test/Ocelot.UnitTests/Requester/TracingHandlerFactoryTests.cs @@ -1,27 +1,32 @@ +using System; + +using Microsoft.Extensions.DependencyInjection; + +using Moq; + +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Requester; + +using Shouldly; + +using Xunit; + namespace Ocelot.UnitTests.Requester { - using Microsoft.Extensions.DependencyInjection; - using Moq; - using Ocelot.Infrastructure.RequestData; - using Ocelot.Logging; - using Ocelot.Requester; - using Shouldly; - using System; - using Xunit; - public class TracingHandlerFactoryTests { private readonly TracingHandlerFactory _factory; - private Mock _tracer; - private IServiceCollection _serviceCollection; - private IServiceProvider _serviceProvider; - private Mock _repo; + private readonly Mock _tracer; + private readonly IServiceCollection _serviceCollection; + private readonly IServiceProvider _serviceProvider; + private readonly Mock _repo; public TracingHandlerFactoryTests() { _tracer = new Mock(); _serviceCollection = new ServiceCollection(); - _serviceCollection.AddSingleton(_tracer.Object); + _serviceCollection.AddSingleton(_tracer.Object); _serviceProvider = _serviceCollection.BuildServiceProvider(); _repo = new Mock(); _factory = new TracingHandlerFactory(_serviceProvider, _repo.Object); diff --git a/test/Ocelot.UnitTests/Responder/AnyError.cs b/test/Ocelot.UnitTests/Responder/AnyError.cs index 9d7b2cd49..a684ce17f 100644 --- a/test/Ocelot.UnitTests/Responder/AnyError.cs +++ b/test/Ocelot.UnitTests/Responder/AnyError.cs @@ -1,15 +1,15 @@ -using Ocelot.Errors; - -namespace Ocelot.UnitTests.Responder -{ - internal class AnyError : Error - { - public AnyError() : base("blahh", OcelotErrorCode.UnknownError) - { - } - - public AnyError(OcelotErrorCode errorCode) : base("blah", errorCode) - { - } - } +using Ocelot.Errors; + +namespace Ocelot.UnitTests.Responder +{ + internal class AnyError : Error + { + public AnyError() : base("blahh", OcelotErrorCode.UnknownError, 404) + { + } + + public AnyError(OcelotErrorCode errorCode) : base("blah", errorCode, 404) + { + } + } } diff --git a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs index 8b550a05c..46d967da7 100644 --- a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs +++ b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs @@ -1,10 +1,14 @@ -using Ocelot.Errors; -using Ocelot.Responder; -using Shouldly; -using System; +using System; using System.Collections.Generic; -using System.Net; -using TestStack.BDDfy; +using System.Net; + +using Ocelot.Errors; +using Ocelot.Responder; + +using Shouldly; + +using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.Responder @@ -29,10 +33,10 @@ public void should_return_unauthorized(OcelotErrorCode errorCode) [Theory] [InlineData(OcelotErrorCode.CannotFindClaimError)] - [InlineData(OcelotErrorCode.ClaimValueNotAuthorisedError)] - [InlineData(OcelotErrorCode.ScopeNotAuthorisedError)] + [InlineData(OcelotErrorCode.ClaimValueNotAuthorizedError)] + [InlineData(OcelotErrorCode.ScopeNotAuthorizedError)] [InlineData(OcelotErrorCode.UnauthorizedError)] - [InlineData(OcelotErrorCode.UserDoesNotHaveClaimError)] + [InlineData(OcelotErrorCode.UserDoesNotHaveClaimError)] public void should_return_forbidden(OcelotErrorCode errorCode) { ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.Forbidden); @@ -47,11 +51,20 @@ public void should_return_service_unavailable(OcelotErrorCode errorCode) [Theory] [InlineData(OcelotErrorCode.UnableToCompleteRequestError)] + [InlineData(OcelotErrorCode.CouldNotFindLoadBalancerCreator)] + [InlineData(OcelotErrorCode.ErrorInvokingLoadBalancerCreator)] public void should_return_internal_server_error(OcelotErrorCode errorCode) { ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.InternalServerError); } + [Theory] + [InlineData(OcelotErrorCode.ConnectionToDownstreamServiceError)] + public void should_return_bad_gateway_error(OcelotErrorCode errorCode) + { + ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.BadGateway); + } + [Theory] [InlineData(OcelotErrorCode.CannotAddDataError)] [InlineData(OcelotErrorCode.CannotFindDataError)] @@ -95,13 +108,13 @@ public void AuthenticationErrorsHaveHighestPriority() } [Fact] - public void AuthorisationErrorsHaveSecondHighestPriority() + public void AuthorizationErrorsHaveSecondHighestPriority() { var errors = new List { OcelotErrorCode.CannotAddDataError, OcelotErrorCode.CannotFindClaimError, - OcelotErrorCode.RequestTimedOutError + OcelotErrorCode.RequestTimedOutError, }; ShouldMapErrorsToStatusCode(errors, HttpStatusCode.Forbidden); @@ -113,7 +126,7 @@ public void ServiceUnavailableErrorsHaveThirdHighestPriority() var errors = new List { OcelotErrorCode.CannotAddDataError, - OcelotErrorCode.RequestTimedOutError + OcelotErrorCode.RequestTimedOutError, }; ShouldMapErrorsToStatusCode(errors, HttpStatusCode.ServiceUnavailable); @@ -125,7 +138,7 @@ public void check_we_have_considered_all_errors_in_these_tests() // If this test fails then it's because the number of error codes has changed. // You should make the appropriate changes to the test cases here to ensure // they cover all the error codes, and then modify this assertion. - Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(38, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); + Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(41, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); } private void ShouldMapErrorToStatusCode(OcelotErrorCode errorCode, HttpStatusCode expectedHttpStatusCode) @@ -168,4 +181,4 @@ private void ThenTheResponseIsStatusCodeIs(HttpStatusCode expectedCode) _result.ShouldBe((int)expectedCode); } } -} +} diff --git a/test/Ocelot.UnitTests/Responder/HttpContextResponderTests.cs b/test/Ocelot.UnitTests/Responder/HttpContextResponderTests.cs index bd485c032..bbda223ee 100644 --- a/test/Ocelot.UnitTests/Responder/HttpContextResponderTests.cs +++ b/test/Ocelot.UnitTests/Responder/HttpContextResponderTests.cs @@ -1,13 +1,17 @@ -using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; + +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; + using Ocelot.Headers; using Ocelot.Middleware; using Ocelot.Responder; + using Shouldly; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; + using Xunit; namespace Ocelot.UnitTests.Responder @@ -15,7 +19,7 @@ namespace Ocelot.UnitTests.Responder public class HttpContextResponderTests { private readonly HttpContextResponder _responder; - private RemoveOutputHeaders _removeOutputHeaders; + private readonly RemoveOutputHeaders _removeOutputHeaders; public HttpContextResponderTests() { @@ -27,10 +31,10 @@ public HttpContextResponderTests() public void should_remove_transfer_encoding_header() { var httpContext = new DefaultHttpContext(); - var response = new DownstreamResponse(new StringContent(""), HttpStatusCode.OK, + var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.OK, new List>> { - new KeyValuePair>("Transfer-Encoding", new List {"woop"}) + new("Transfer-Encoding", new List {"woop"}), }, "some reason"); _responder.SetResponseOnHttpContext(httpContext, response).GetAwaiter().GetResult(); @@ -71,10 +75,10 @@ public void should_have_content_length() public void should_add_header() { var httpContext = new DefaultHttpContext(); - var response = new DownstreamResponse(new StringContent(""), HttpStatusCode.OK, + var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.OK, new List>> { - new KeyValuePair>("test", new List {"test"}) + new("test", new List {"test"}), }, "some reason"); _responder.SetResponseOnHttpContext(httpContext, response).GetAwaiter().GetResult(); @@ -86,10 +90,10 @@ public void should_add_header() public void should_add_reason_phrase() { var httpContext = new DefaultHttpContext(); - var response = new DownstreamResponse(new StringContent(""), HttpStatusCode.OK, + var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.OK, new List>> { - new KeyValuePair>("test", new List {"test"}) + new("test", new List {"test"}), }, "some reason"); _responder.SetResponseOnHttpContext(httpContext, response).GetAwaiter().GetResult(); diff --git a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs index de7ab9d35..b4083dad8 100644 --- a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs @@ -1,78 +1,82 @@ -using Ocelot.Middleware; - -namespace Ocelot.UnitTests.Responder -{ - using Microsoft.AspNetCore.Http; - using Moq; - using Ocelot.DownstreamRouteFinder.Finder; - using Ocelot.Errors; - using Ocelot.Logging; - using Ocelot.Responder; - using Ocelot.Responder.Middleware; - using System.Net.Http; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Xunit; - - public class ResponderMiddlewareTests - { - private readonly Mock _responder; - private readonly Mock _codeMapper; - private Mock _loggerFactory; - private Mock _logger; - private readonly ResponderMiddleware _middleware; - private readonly DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; - - public ResponderMiddlewareTests() - { - _responder = new Mock(); - _codeMapper = new Mock(); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _next = context => Task.CompletedTask; - _middleware = new ResponderMiddleware(_next, _responder.Object, _loggerFactory.Object, _codeMapper.Object); - } - - [Fact] - public void should_not_return_any_errors() - { - this.Given(x => x.GivenTheHttpResponseMessageIs(new DownstreamResponse(new HttpResponseMessage()))) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenThereAreNoErrors()) - .BDDfy(); - } - - [Fact] - public void should_return_any_errors() - { - this.Given(x => x.GivenTheHttpResponseMessageIs(new DownstreamResponse(new HttpResponseMessage()))) - .And(x => x.GivenThereArePipelineErrors(new UnableToFindDownstreamRouteError("/path", "GET"))) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenThereAreNoErrors()) - .BDDfy(); - } - - private void WhenICallTheMiddleware() - { - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); - } - - private void GivenTheHttpResponseMessageIs(DownstreamResponse response) - { - _downstreamContext.DownstreamResponse = response; - } - - private void ThenThereAreNoErrors() - { - //todo a better assert? - } - - private void GivenThereArePipelineErrors(Error error) - { - _downstreamContext.Errors.Add(error); +using Ocelot.Middleware; +using System.Net.Http; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Http; + +using Moq; + +using Ocelot.DownstreamRouteFinder.Finder; +using Ocelot.Errors; +using Ocelot.Logging; +using Ocelot.Responder; +using Ocelot.Responder.Middleware; + +using TestStack.BDDfy; + +using Xunit; + +namespace Ocelot.UnitTests.Responder +{ + public class ResponderMiddlewareTests + { + private readonly Mock _responder; + private readonly Mock _codeMapper; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private readonly ResponderMiddleware _middleware; + private readonly RequestDelegate _next; + private readonly HttpContext _httpContext; + + public ResponderMiddlewareTests() + { + _httpContext = new DefaultHttpContext(); + _responder = new Mock(); + _codeMapper = new Mock(); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _next = context => Task.CompletedTask; + _middleware = new ResponderMiddleware(_next, _responder.Object, _loggerFactory.Object, _codeMapper.Object); } - } + + [Fact] + public void should_not_return_any_errors() + { + this.Given(x => x.GivenTheHttpResponseMessageIs(new DownstreamResponse(new HttpResponseMessage()))) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenThereAreNoErrors()) + .BDDfy(); + } + + [Fact] + public void should_return_any_errors() + { + this.Given(x => x.GivenTheHttpResponseMessageIs(new DownstreamResponse(new HttpResponseMessage()))) + .And(x => x.GivenThereArePipelineErrors(new UnableToFindDownstreamRouteError("/path", "GET"))) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenThereAreNoErrors()) + .BDDfy(); + } + + private void WhenICallTheMiddleware() + { + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); + } + + private void GivenTheHttpResponseMessageIs(DownstreamResponse response) + { + _httpContext.Items.UpsertDownstreamResponse(response); + } + + private void ThenThereAreNoErrors() + { + //todo a better assert? + } + + private void GivenThereArePipelineErrors(Error error) + { + _httpContext.Items.SetError(error); + } + } } diff --git a/test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs b/test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs index 637048f59..cb9de4a84 100644 --- a/test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs +++ b/test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs @@ -15,68 +15,68 @@ namespace Ocelot.UnitTests.Security { public class IPSecurityPolicyTests { - private readonly DownstreamContext _downstreamContext; - private readonly DownstreamReRouteBuilder _downstreamReRouteBuilder; + private readonly DownstreamRouteBuilder _downstreamRouteBuilder; private readonly IPSecurityPolicy _ipSecurityPolicy; private Response response; + private readonly HttpContext _httpContext; public IPSecurityPolicyTests() { - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - _downstreamContext.DownstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); - _downstreamContext.HttpContext.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.1")[0]; - _downstreamReRouteBuilder = new DownstreamReRouteBuilder(); + _httpContext = new DefaultHttpContext(); + _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com"))); + _httpContext.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.1")[0]; + _downstreamRouteBuilder = new DownstreamRouteBuilder(); _ipSecurityPolicy = new IPSecurityPolicy(); } [Fact] - private void should_No_blocked_Ip_and_allowed_Ip() + public void should_No_blocked_Ip_and_allowed_Ip() { - this.Given(x => x.GivenSetDownstreamReRoute()) + this.Given(x => x.GivenSetDownstreamRoute()) .When(x => x.WhenTheSecurityPolicy()) .Then(x => x.ThenSecurityPassing()) .BDDfy(); } [Fact] - private void should_blockedIp_clientIp_block() + public void should_blockedIp_clientIp_block() { - _downstreamContext.HttpContext.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.1")[0]; + _httpContext.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.1")[0]; this.Given(x => x.GivenSetBlockedIP()) - .Given(x => x.GivenSetDownstreamReRoute()) + .Given(x => x.GivenSetDownstreamRoute()) .When(x => x.WhenTheSecurityPolicy()) .Then(x => x.ThenNotSecurityPassing()) .BDDfy(); } [Fact] - private void should_blockedIp_clientIp_Not_block() + public void should_blockedIp_clientIp_Not_block() { - _downstreamContext.HttpContext.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.2")[0]; + _httpContext.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.2")[0]; this.Given(x => x.GivenSetBlockedIP()) - .Given(x => x.GivenSetDownstreamReRoute()) + .Given(x => x.GivenSetDownstreamRoute()) .When(x => x.WhenTheSecurityPolicy()) .Then(x => x.ThenSecurityPassing()) .BDDfy(); } [Fact] - private void should_allowedIp_clientIp_block() + public void should_allowedIp_clientIp_block() { - _downstreamContext.HttpContext.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.1")[0]; + _httpContext.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.1")[0]; this.Given(x => x.GivenSetAllowedIP()) - .Given(x => x.GivenSetDownstreamReRoute()) + .Given(x => x.GivenSetDownstreamRoute()) .When(x => x.WhenTheSecurityPolicy()) .Then(x => x.ThenSecurityPassing()) .BDDfy(); } [Fact] - private void should_allowedIp_clientIp_Not_block() + public void should_allowedIp_clientIp_Not_block() { - _downstreamContext.HttpContext.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.2")[0]; + _httpContext.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.2")[0]; this.Given(x => x.GivenSetAllowedIP()) - .Given(x => x.GivenSetDownstreamReRoute()) + .Given(x => x.GivenSetDownstreamRoute()) .When(x => x.WhenTheSecurityPolicy()) .Then(x => x.ThenNotSecurityPassing()) .BDDfy(); @@ -84,22 +84,22 @@ private void should_allowedIp_clientIp_Not_block() private void GivenSetAllowedIP() { - _downstreamReRouteBuilder.WithSecurityOptions(new SecurityOptions(new List { "192.168.1.1" }, new List())); + _downstreamRouteBuilder.WithSecurityOptions(new SecurityOptions(new List { "192.168.1.1" }, new List())); } private void GivenSetBlockedIP() { - _downstreamReRouteBuilder.WithSecurityOptions(new SecurityOptions(new List(), new List { "192.168.1.1" })); + _downstreamRouteBuilder.WithSecurityOptions(new SecurityOptions(new List(), new List { "192.168.1.1" })); } - private void GivenSetDownstreamReRoute() + private void GivenSetDownstreamRoute() { - _downstreamContext.DownstreamReRoute = _downstreamReRouteBuilder.Build(); + _httpContext.Items.UpsertDownstreamRoute(_downstreamRouteBuilder.Build()); } private void WhenTheSecurityPolicy() { - response = this._ipSecurityPolicy.Security(_downstreamContext).GetAwaiter().GetResult(); + response = _ipSecurityPolicy.Security(_httpContext.Items.DownstreamRoute(), _httpContext).GetAwaiter().GetResult(); } private void ThenSecurityPassing() diff --git a/test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs b/test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs index 7d2b3d892..5ce4d0e2d 100644 --- a/test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs @@ -1,106 +1,111 @@ -using Microsoft.AspNetCore.Http; -using Moq; -using Ocelot.Errors; -using Ocelot.Logging; -using Ocelot.Middleware; -using Ocelot.Request.Middleware; -using Ocelot.Responses; -using Ocelot.Security; -using Ocelot.Security.Middleware; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; using System.Threading.Tasks; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Security -{ - public class SecurityMiddlewareTests - { - private List> _securityPolicyList; - private Mock _loggerFactory; - private Mock _logger; - private readonly SecurityMiddleware _middleware; - private readonly DownstreamContext _downstreamContext; - private readonly OcelotRequestDelegate _next; - - public SecurityMiddlewareTests() - { - _loggerFactory = new Mock(); - _logger = new Mock(); - _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - _securityPolicyList = new List>(); - _securityPolicyList.Add(new Mock()); - _securityPolicyList.Add(new Mock()); - _next = context => - { - return Task.CompletedTask; - }; - _middleware = new SecurityMiddleware(_loggerFactory.Object, _securityPolicyList.Select(f => f.Object).ToList(), _next); - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - _downstreamContext.DownstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); - } - [Fact] - public void should_legal_request() - { - this.Given(x => x.GivenPassingSecurityVerification()) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheRequestIsPassingSecurity()) - .BDDfy(); - } +using Microsoft.AspNetCore.Http; - [Fact] - public void should_verification_failed_request() - { - this.Given(x => x.GivenNotPassingSecurityVerification()) - .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheRequestIsNotPassingSecurity()) - .BDDfy(); - } - - private void GivenPassingSecurityVerification() - { - foreach (var item in _securityPolicyList) - { - Response response = new OkResponse(); - item.Setup(x => x.Security(_downstreamContext)).Returns(Task.FromResult(response)); - } - } - - private void GivenNotPassingSecurityVerification() - { - for (int i = 0; i < _securityPolicyList.Count; i++) - { - Mock item = _securityPolicyList[i]; - if (i == 0) - { - Error error = new UnauthenticatedError($"Not passing security verification"); - Response response = new ErrorResponse(error); - item.Setup(x => x.Security(_downstreamContext)).Returns(Task.FromResult(response)); - } - else - { - Response response = new OkResponse(); - item.Setup(x => x.Security(_downstreamContext)).Returns(Task.FromResult(response)); - } - } - } +using Moq; - private void WhenICallTheMiddleware() - { - _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); - } +using Ocelot.Errors; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.Request.Middleware; +using Ocelot.Responses; +using Ocelot.Security; +using Ocelot.Security.Middleware; - private void ThenTheRequestIsPassingSecurity() - { - Assert.False(_downstreamContext.IsError); - } +using TestStack.BDDfy; - private void ThenTheRequestIsNotPassingSecurity() - { - Assert.True(_downstreamContext.IsError); - } - } -} +using Xunit; +using Shouldly; + +namespace Ocelot.UnitTests.Security +{ + public class SecurityMiddlewareTests + { + private readonly List> _securityPolicyList; + private readonly Mock _loggerFactory; + private readonly Mock _logger; + private readonly SecurityMiddleware _middleware; + private readonly RequestDelegate _next; + private readonly HttpContext _httpContext; + + public SecurityMiddlewareTests() + { + _httpContext = new DefaultHttpContext(); + _loggerFactory = new Mock(); + _logger = new Mock(); + _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _securityPolicyList = new List> + { + new(), + new(), + }; + _next = context => Task.CompletedTask; + _middleware = new SecurityMiddleware(_next, _loggerFactory.Object, _securityPolicyList.Select(f => f.Object).ToList()); + _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com"))); + } + + [Fact] + public void should_legal_request() + { + this.Given(x => x.GivenPassingSecurityVerification()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheRequestIsPassingSecurity()) + .BDDfy(); + } + + [Fact] + public void should_verification_failed_request() + { + this.Given(x => x.GivenNotPassingSecurityVerification()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheRequestIsNotPassingSecurity()) + .BDDfy(); + } + + private void GivenPassingSecurityVerification() + { + foreach (var item in _securityPolicyList) + { + Response response = new OkResponse(); + item.Setup(x => x.Security(_httpContext.Items.DownstreamRoute(), _httpContext)).Returns(Task.FromResult(response)); + } + } + + private void GivenNotPassingSecurityVerification() + { + for (var i = 0; i < _securityPolicyList.Count; i++) + { + var item = _securityPolicyList[i]; + if (i == 0) + { + Error error = new UnauthenticatedError("Not passing security verification"); + Response response = new ErrorResponse(error); + item.Setup(x => x.Security(_httpContext.Items.DownstreamRoute(), _httpContext)).Returns(Task.FromResult(response)); + } + else + { + Response response = new OkResponse(); + item.Setup(x => x.Security(_httpContext.Items.DownstreamRoute(), _httpContext)).Returns(Task.FromResult(response)); + } + } + } + + private void WhenICallTheMiddleware() + { + _middleware.Invoke(_httpContext).GetAwaiter().GetResult(); + } + + private void ThenTheRequestIsPassingSecurity() + { + _httpContext.Items.Errors().Count.ShouldBe(0); + } + + private void ThenTheRequestIsNotPassingSecurity() + { + _httpContext.Items.Errors().Count.ShouldBeGreaterThan(0); + } + } +} diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs index 1faed24a5..c1efcad59 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs @@ -1,8 +1,13 @@ +using System; +using System.Collections.Generic; + using Ocelot.ServiceDiscovery.Providers; -using Ocelot.Values; -using Shouldly; -using System.Collections.Generic; -using TestStack.BDDfy; +using Ocelot.Values; + +using Shouldly; + +using TestStack.BDDfy; + using Xunit; namespace Ocelot.UnitTests.ServiceDiscovery @@ -20,7 +25,7 @@ public void should_return_services() var services = new List { - new Service("product", hostAndPort, string.Empty, string.Empty, new string[0]) + new("product", hostAndPort, string.Empty, string.Empty, Array.Empty()), }; this.Given(x => x.GivenServices(services)) diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs index 0b5362ed7..e414019f6 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs @@ -1,187 +1,195 @@ -namespace Ocelot.UnitTests.ServiceDiscovery -{ - using Microsoft.Extensions.DependencyInjection; - using Moq; - using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using Ocelot.Logging; - using Ocelot.Responses; - using Ocelot.ServiceDiscovery; - using Ocelot.ServiceDiscovery.Providers; - using Shouldly; - using System; - using System.Collections.Generic; - using System.Threading.Tasks; - using TestStack.BDDfy; - using Values; - using Xunit; - - public class ServiceDiscoveryProviderFactoryTests - { - private ServiceProviderConfiguration _serviceConfig; - private Response _result; - private ServiceDiscoveryProviderFactory _factory; - private DownstreamReRoute _reRoute; - private readonly Mock _loggerFactory; - private Mock _logger; - private IServiceProvider _provider; - private readonly IServiceCollection _collection; - - public ServiceDiscoveryProviderFactoryTests() - { - _loggerFactory = new Mock(); - _logger = new Mock(); - _collection = new ServiceCollection(); - _provider = _collection.BuildServiceProvider(); - _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, _provider); - } - - [Fact] - public void should_return_no_service_provider() - { - var serviceConfig = new ServiceProviderConfigurationBuilder() - .Build(); - - var reRoute = new DownstreamReRouteBuilder().Build(); - - this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) - .When(x => x.WhenIGetTheServiceProvider()) - .Then(x => x.ThenTheServiceProviderIs()) - .BDDfy(); - } - - [Fact] - public void should_return_list_of_configuration_services() - { - var serviceConfig = new ServiceProviderConfigurationBuilder() - .Build(); - - var downstreamAddresses = new List() - { - new DownstreamHostAndPort("asdf.com", 80), - new DownstreamHostAndPort("abc.com", 80) - }; - - var reRoute = new DownstreamReRouteBuilder().WithDownstreamAddresses(downstreamAddresses).Build(); - - this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) - .When(x => x.WhenIGetTheServiceProvider()) - .Then(x => x.ThenTheServiceProviderIs()) - .Then(x => ThenTheFollowingServicesAreReturned(downstreamAddresses)) - .BDDfy(); - } - - [Fact] - public void should_return_provider_because_type_matches_reflected_type_from_delegate() - { - var reRoute = new DownstreamReRouteBuilder() - .WithServiceName("product") - .WithUseServiceDiscovery(true) - .Build(); - - var serviceConfig = new ServiceProviderConfigurationBuilder() - .WithType(nameof(Fake)) - .Build(); - - this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) - .And(x => GivenAFakeDelegate()) - .When(x => x.WhenIGetTheServiceProvider()) - .Then(x => x.ThenTheDelegateIsCalled()) - .BDDfy(); - } - - [Fact] - public void should_not_return_provider_because_type_doesnt_match_reflected_type_from_delegate() - { - var reRoute = new DownstreamReRouteBuilder() - .WithServiceName("product") - .WithUseServiceDiscovery(true) - .Build(); - - var serviceConfig = new ServiceProviderConfigurationBuilder() - .WithType("Wookie") - .Build(); - - this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) - .And(x => GivenAFakeDelegate()) - .When(x => x.WhenIGetTheServiceProvider()) - .Then(x => x.ThenTheResultIsError()) - .BDDfy(); - } - - [Fact] - public void should_return_service_fabric_provider() - { - var reRoute = new DownstreamReRouteBuilder() - .WithServiceName("product") - .WithUseServiceDiscovery(true) - .Build(); - - var serviceConfig = new ServiceProviderConfigurationBuilder() - .WithType("ServiceFabric") - .Build(); - - this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) - .When(x => x.WhenIGetTheServiceProvider()) - .Then(x => x.ThenTheServiceProviderIs()) - .BDDfy(); - } - - private void GivenAFakeDelegate() - { - ServiceDiscoveryFinderDelegate fake = (provider, config, name) => new Fake(); - _collection.AddSingleton(fake); - _provider = _collection.BuildServiceProvider(); - _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, _provider); - } - - private class Fake : IServiceDiscoveryProvider - { - public Task> Get() - { - return null; - } - } - - private void ThenTheDelegateIsCalled() - { - _result.Data.GetType().Name.ShouldBe("Fake"); - } - - private void ThenTheResultIsError() - { - _result.IsError.ShouldBeTrue(); - } - - private void ThenTheFollowingServicesAreReturned(List downstreamAddresses) - { - var result = (ConfigurationServiceProvider)_result.Data; - var services = result.Get().Result; - - for (int i = 0; i < services.Count; i++) - { - var service = services[i]; - var downstreamAddress = downstreamAddresses[i]; - - service.HostAndPort.DownstreamHost.ShouldBe(downstreamAddress.Host); - service.HostAndPort.DownstreamPort.ShouldBe(downstreamAddress.Port); - } - } - - private void GivenTheReRoute(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute) - { - _serviceConfig = serviceConfig; - _reRoute = reRoute; - } - - private void WhenIGetTheServiceProvider() - { - _result = _factory.Get(_serviceConfig, _reRoute); - } - - private void ThenTheServiceProviderIs() - { - _result.Data.ShouldBeOfType(); - } - } -} +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Microsoft.Extensions.DependencyInjection; + +using Moq; + +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.ServiceDiscovery; +using Ocelot.ServiceDiscovery.Providers; + +using Ocelot.Responses; + +using Shouldly; + +using TestStack.BDDfy; + +using Ocelot.Values; + +using Xunit; + +namespace Ocelot.UnitTests.ServiceDiscovery +{ + public class ServiceDiscoveryProviderFactoryTests + { + private ServiceProviderConfiguration _serviceConfig; + private Response _result; + private ServiceDiscoveryProviderFactory _factory; + private DownstreamRoute _route; + private readonly Mock _loggerFactory; + private Mock _logger; + private IServiceProvider _provider; + private readonly IServiceCollection _collection; + + public ServiceDiscoveryProviderFactoryTests() + { + _loggerFactory = new Mock(); + _logger = new Mock(); + _collection = new ServiceCollection(); + _provider = _collection.BuildServiceProvider(); + _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, _provider); + } + + [Fact] + public void should_return_no_service_provider() + { + var serviceConfig = new ServiceProviderConfigurationBuilder() + .Build(); + + var route = new DownstreamRouteBuilder().Build(); + + this.Given(x => x.GivenTheRoute(serviceConfig, route)) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheServiceProviderIs()) + .BDDfy(); + } + + [Fact] + public void should_return_list_of_configuration_services() + { + var serviceConfig = new ServiceProviderConfigurationBuilder() + .Build(); + + var downstreamAddresses = new List + { + new("asdf.com", 80), + new("abc.com", 80), + }; + + var route = new DownstreamRouteBuilder().WithDownstreamAddresses(downstreamAddresses).Build(); + + this.Given(x => x.GivenTheRoute(serviceConfig, route)) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheServiceProviderIs()) + .Then(x => ThenTheFollowingServicesAreReturned(downstreamAddresses)) + .BDDfy(); + } + + [Fact] + public void should_return_provider_because_type_matches_reflected_type_from_delegate() + { + var route = new DownstreamRouteBuilder() + .WithServiceName("product") + .WithUseServiceDiscovery(true) + .Build(); + + var serviceConfig = new ServiceProviderConfigurationBuilder() + .WithType(nameof(Fake)) + .Build(); + + this.Given(x => x.GivenTheRoute(serviceConfig, route)) + .And(x => GivenAFakeDelegate()) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheDelegateIsCalled()) + .BDDfy(); + } + + [Fact] + public void should_not_return_provider_because_type_doesnt_match_reflected_type_from_delegate() + { + var route = new DownstreamRouteBuilder() + .WithServiceName("product") + .WithUseServiceDiscovery(true) + .Build(); + + var serviceConfig = new ServiceProviderConfigurationBuilder() + .WithType("Wookie") + .Build(); + + this.Given(x => x.GivenTheRoute(serviceConfig, route)) + .And(x => GivenAFakeDelegate()) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheResultIsError()) + .BDDfy(); + } + + [Fact] + public void should_return_service_fabric_provider() + { + var route = new DownstreamRouteBuilder() + .WithServiceName("product") + .WithUseServiceDiscovery(true) + .Build(); + + var serviceConfig = new ServiceProviderConfigurationBuilder() + .WithType("ServiceFabric") + .Build(); + + this.Given(x => x.GivenTheRoute(serviceConfig, route)) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheServiceProviderIs()) + .BDDfy(); + } + + private void GivenAFakeDelegate() + { + ServiceDiscoveryFinderDelegate fake = (provider, config, name) => new Fake(); + _collection.AddSingleton(fake); + _provider = _collection.BuildServiceProvider(); + _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, _provider); + } + + private class Fake : IServiceDiscoveryProvider + { + public Task> Get() + { + return null; + } + } + + private void ThenTheDelegateIsCalled() + { + _result.Data.GetType().Name.ShouldBe("Fake"); + } + + private void ThenTheResultIsError() + { + _result.IsError.ShouldBeTrue(); + } + + private void ThenTheFollowingServicesAreReturned(List downstreamAddresses) + { + var result = (ConfigurationServiceProvider)_result.Data; + var services = result.Get().Result; + + for (var i = 0; i < services.Count; i++) + { + var service = services[i]; + var downstreamAddress = downstreamAddresses[i]; + + service.HostAndPort.DownstreamHost.ShouldBe(downstreamAddress.Host); + service.HostAndPort.DownstreamPort.ShouldBe(downstreamAddress.Port); + } + } + + private void GivenTheRoute(ServiceProviderConfiguration serviceConfig, DownstreamRoute route) + { + _serviceConfig = serviceConfig; + _route = route; + } + + private void WhenIGetTheServiceProvider() + { + _result = _factory.Get(_serviceConfig, _route); + } + + private void ThenTheServiceProviderIs() + { + _result.Data.ShouldBeOfType(); + } + } +} diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs index 7056556ef..a104297d4 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs @@ -1,13 +1,18 @@ -namespace Ocelot.UnitTests.ServiceDiscovery -{ - using Ocelot.ServiceDiscovery.Configuration; - using Ocelot.ServiceDiscovery.Providers; - using Ocelot.Values; - using Shouldly; - using System.Collections.Generic; - using TestStack.BDDfy; - using Xunit; +using System.Collections.Generic; + +using Ocelot.ServiceDiscovery.Configuration; +using Ocelot.ServiceDiscovery.Providers; + +using Shouldly; + +using TestStack.BDDfy; +using Ocelot.Values; + +using Xunit; + +namespace Ocelot.UnitTests.ServiceDiscovery +{ public class ServiceFabricServiceDiscoveryProviderTests { private ServiceFabricServiceDiscoveryProvider _provider; diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs index e27d5e782..00aa73705 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs @@ -1,7 +1,12 @@ -using Ocelot.Values; -using Shouldly; -using System.Collections.Generic; -using TestStack.BDDfy; +using System; +using System.Collections.Generic; + +using Ocelot.Values; + +using Shouldly; + +using TestStack.BDDfy; + using Xunit; // nothing in use @@ -11,8 +16,8 @@ public class ServiceRegistryTests { private Service _service; private List _services; - private ServiceRegistry _serviceRegistry; - private ServiceRepository _serviceRepository; + private readonly ServiceRegistry _serviceRegistry; + private readonly ServiceRepository _serviceRepository; public ServiceRegistryTests() { @@ -52,13 +57,13 @@ private void WhenILookupTheService(string name) private void GivenAServiceIsRegistered(string name, string address, int port) { - _service = new Service(name, new ServiceHostAndPort(address, port), string.Empty, string.Empty, new string[0]); + _service = new Service(name, new ServiceHostAndPort(address, port), string.Empty, string.Empty, Array.Empty()); _serviceRepository.Set(_service); } private void GivenAServiceToRegister(string name, string address, int port) { - _service = new Service(name, new ServiceHostAndPort(address, port), string.Empty, string.Empty, new string[0]); + _service = new Service(name, new ServiceHostAndPort(address, port), string.Empty, string.Empty, Array.Empty()); } private void WhenIRegisterTheService() @@ -111,7 +116,7 @@ public interface IServiceRepository public class ServiceRepository : IServiceRepository { - private Dictionary> _registeredServices; + private readonly Dictionary> _registeredServices; public ServiceRepository() { @@ -125,15 +130,14 @@ public List Get(string serviceName) public void Set(Service serviceNameAndAddress) { - List services; - if (_registeredServices.TryGetValue(serviceNameAndAddress.Name, out services)) + if (_registeredServices.TryGetValue(serviceNameAndAddress.Name, out var services)) { services.Add(serviceNameAndAddress); _registeredServices[serviceNameAndAddress.Name] = services; } else { - _registeredServices[serviceNameAndAddress.Name] = new List() { serviceNameAndAddress }; + _registeredServices[serviceNameAndAddress.Name] = new List { serviceNameAndAddress }; } } } diff --git a/test/Ocelot.UnitTests/UnitTests.runsettings b/test/Ocelot.UnitTests/UnitTests.runsettings index 1cba21cc9..80c9b34f5 100644 --- a/test/Ocelot.UnitTests/UnitTests.runsettings +++ b/test/Ocelot.UnitTests/UnitTests.runsettings @@ -1,23 +1,23 @@ - - - - - - - opencover - false - true - - - - - - - - - - + + + + + + + opencover + false + true + + + + + + + + + + \ No newline at end of file diff --git a/test/Ocelot.UnitTests/idsrv3test.pfx b/test/Ocelot.UnitTests/idsrv3test.pfx deleted file mode 100644 index 0247dea03..000000000 Binary files a/test/Ocelot.UnitTests/idsrv3test.pfx and /dev/null differ diff --git a/test/Ocelot.UnitTests/mycert.pfx b/test/Ocelot.UnitTests/mycert.pfx new file mode 100644 index 000000000..ead06a05d Binary files /dev/null and b/test/Ocelot.UnitTests/mycert.pfx differ diff --git a/tools/packages.config b/tools/packages.config deleted file mode 100644 index 238f21ca0..000000000 --- a/tools/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - -