diff --git a/.circleci/config.yml b/.circleci/config.yml index 536914b2..0f904362 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,30 +1,30 @@ -version: 2.1 -jobs: - build: - docker: - - image: mijitt0m/ocelot-build:0.0.1 - steps: - - checkout - - run: make build - release: - docker: - - image: mijitt0m/ocelot-build:0.0.1 - steps: - - checkout - - run: make release -workflows: - version: 2 - master: - jobs: - - release: - filters: - branches: - only: master - pr: - jobs: - - build: - filters: - branches: - ignore: - - master - - develop +version: 2.1 +jobs: + build: + docker: + - image: mijitt0m/ocelot-build:0.0.1 + steps: + - checkout + - run: make build + release: + docker: + - image: mijitt0m/ocelot-build:0.0.1 + steps: + - checkout + - run: make release +workflows: + version: 2 + master: + jobs: + - release: + filters: + branches: + only: master + pr: + jobs: + - build: + filters: + branches: + ignore: + - master + - develop diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 49a79976..ebf3d11d 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -1,46 +1,46 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at tom@threemammals.co.uk. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at tom@threemammals.co.uk. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5174ce46..f722c978 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,9 +1,9 @@ -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 :) - -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 :) +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 :) + +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 :) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index ddb30a7c..e4d3765a 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,17 +1,17 @@ -## Expected Behavior / New Feature - - -## Actual Behavior / Motivation for New Feature - - -## Steps to Reproduce the Problem - - 1. - 1. - 1. - -## Specifications - - - Version: - - Platform: - - Subsystem: +## Expected Behavior / New Feature + + +## Actual Behavior / Motivation for New Feature + + +## Steps to Reproduce the Problem + + 1. + 1. + 1. + +## Specifications + + - Version: + - Platform: + - Subsystem: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 72a3b6fe..8f6923ea 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,7 @@ -Fixes / New Feature # - -## Proposed Changes - - - - - - - +Fixes / New Feature # + +## Proposed Changes + + - + - + - diff --git a/README.md b/README.md index 24cb7b9d..b578a992 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,98 @@ -[](https://threemammals.com/ocelot) - -[![CircleCI](https://circleci.com/gh/ThreeMammals/Ocelot.svg?style=svg)](https://circleci.com/gh/ThreeMammals/Ocelot) - -[![Coverage Status](https://coveralls.io/repos/github/ThreeMammals/Ocelot/badge.svg?branch=master)](https://coveralls.io/github/ThreeMammals/Ocelot?branch=master) - - - -# Ocelot - -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. - -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! - -## Features - -A quick list of Ocelot's capabilities for more information see the [documentation](https://ocelot.readthedocs.io/en/latest/). - -* Routing -* Request Aggregation -* Service Discovery with Consul & Eureka -* Service Fabric -* Kubernetes -* WebSockets -* Authentication -* Authorisation -* Rate Limiting -* Caching -* Retry policies / QoS -* Load Balancing -* Logging / Tracing / Correlation -* Headers / 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 2.1` and `.NET Framework 4.7.2` and up. [This](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) documentation may prove helpful when working out if Ocelot would be suitable for you. - -Install Ocelot and it's dependencies using NuGet. - -`Install-Package Ocelot` - -Or via the .NET Core CLI: - -`dotnet add package ocelot` - -All versions can be found [here](https://www.nuget.org/packages/Ocelot/) - -## Documentation - -Please click [here](https://ocelot.readthedocs.io/en/latest/) for the Ocelot documentation. This includes lots of information and will be helpful if you want to understand the features Ocelot currently offers. - -## Coming up - -You can see what we are working on [here](https://github.com/ThreeMammals/Ocelot/issues). - -## Contributing - -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 :) - -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! - -[![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://threemammals.com/ocelot) + +[![CircleCI](https://circleci.com/gh/ThreeMammals/Ocelot.svg?style=svg)](https://circleci.com/gh/ThreeMammals/Ocelot) + +[![Coverage Status](https://coveralls.io/repos/github/ThreeMammals/Ocelot/badge.svg?branch=master)](https://coveralls.io/github/ThreeMammals/Ocelot?branch=master) + + + +# Ocelot + +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. + +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! + +## Features + +A quick list of Ocelot's capabilities for more information see the [documentation](https://ocelot.readthedocs.io/en/latest/). + +* Routing +* Request Aggregation +* Service Discovery with Consul & Eureka +* Service Fabric +* Kubernetes +* WebSockets +* Authentication +* Authorisation +* Rate Limiting +* Caching +* Retry policies / QoS +* Load Balancing +* Logging / Tracing / Correlation +* Headers / 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 2.1` and `.NET Framework 4.7.2` and up. [This](https://docs.microsoft.com/en-us/dotnet/standard/net-standard) documentation may prove helpful when working out if Ocelot would be suitable for you. + +Install Ocelot and it's dependencies using NuGet. + +`Install-Package Ocelot` + +Or via the .NET Core CLI: + +`dotnet add package ocelot` + +All versions can be found [here](https://www.nuget.org/packages/Ocelot/) + +## Documentation + +Please click [here](https://ocelot.readthedocs.io/en/latest/) for the Ocelot documentation. This includes lots of information and will be helpful if you want to understand the features Ocelot currently offers. + +## Coming up + +You can see what we are working on [here](https://github.com/ThreeMammals/Ocelot/issues). + +## Contributing + +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 :) + +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! + +[![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) diff --git a/build.cake b/build.cake index c6b1e79e..6865fc7d 100644 --- a/build.cake +++ b/build.cake @@ -1,497 +1,497 @@ -#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 = "coveralls-repo-token-ocelot"; -var coverallsRepo = "https://coveralls.io/github/TomPallister/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/tompallister/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 - .Append("--settings test/Ocelot.UnitTests/UnitTests.runsettings") - }; - - EnsureDirectoryExists(artifactsForUnitTestsDir); - DotNetCoreTest(unitTestAssemblies, testSettings); - - if (IsRunningOnWindows()) - { - var coverageSummaryFile = GetSubDirectories(artifactsForUnitTestsDir).First().CombineWithFilePath(File("coverage.opencover.xml")); - ReportGenerator(coverageSummaryFile, artifactsForUnitTestsDir); - - 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 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.PatchAsync($"https://api.github.com/repos/ThreeMammals/Ocelot/releases/{releaseId}", content).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 "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 = "coveralls-repo-token-ocelot"; +var coverallsRepo = "https://coveralls.io/github/TomPallister/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/tompallister/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 + .Append("--settings test/Ocelot.UnitTests/UnitTests.runsettings") + }; + + EnsureDirectoryExists(artifactsForUnitTestsDir); + DotNetCoreTest(unitTestAssemblies, testSettings); + + if (IsRunningOnWindows()) + { + var coverageSummaryFile = GetSubDirectories(artifactsForUnitTestsDir).First().CombineWithFilePath(File("coverage.opencover.xml")); + ReportGenerator(coverageSummaryFile, artifactsForUnitTestsDir); + + 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 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.PatchAsync($"https://api.github.com/repos/ThreeMammals/Ocelot/releases/{releaseId}", content).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")); } \ No newline at end of file diff --git a/docker/Dockerfile.base b/docker/Dockerfile.base index 6d0adee4..02d91f53 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 +# 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 diff --git a/docker/Dockerfile.build b/docker/Dockerfile.build index ceaffab4..5dea2a42 100644 --- a/docker/Dockerfile.build +++ b/docker/Dockerfile.build @@ -1,12 +1,12 @@ -# call from ocelot repo root with -# docker build -f ./docker/Dockerfile.build . - -FROM mijitt0m/ocelot-build:0.0.1 - -WORKDIR /src - -COPY ./. . - -RUN chmod u+x build.sh - +# call from ocelot repo root with +# docker build -f ./docker/Dockerfile.build . + +FROM mijitt0m/ocelot-build:0.0.1 + +WORKDIR /src + +COPY ./. . + +RUN chmod u+x build.sh + RUN make build \ No newline at end of file diff --git a/docker/Dockerfile.release b/docker/Dockerfile.release index dadb4099..30863a8e 100644 --- a/docker/Dockerfile.release +++ b/docker/Dockerfile.release @@ -1,18 +1,18 @@ -# call from ocelot repo root with -# docker build --build-arg OCELOT_NUTGET_API_KEY=$OCELOT_NUTGET_API_KEY --build-arg OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY -f ./docker/Dockerfile.release . - -FROM mijitt0m/ocelot-build:0.0.1 - -ARG OCELOT_GITHUB_API_KEY -ARG OCELOT_NUTGET_API_KEY - -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 - +# call from ocelot repo root with +# docker build --build-arg OCELOT_NUTGET_API_KEY=$OCELOT_NUTGET_API_KEY --build-arg OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY -f ./docker/Dockerfile.release . + +FROM mijitt0m/ocelot-build:0.0.1 + +ARG OCELOT_GITHUB_API_KEY +ARG OCELOT_NUTGET_API_KEY + +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 diff --git a/docker/README.md b/docker/README.md index dafbd7f1..3eb46aa4 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,3 +1,3 @@ -# docker build - +# docker build + This folder contains the dockerfile and script to create the ocelot build container. \ No newline at end of file diff --git a/docker/docker-build.sh b/docker/docker-build.sh index b3ac62da..8bb4e206 100755 --- a/docker/docker-build.sh +++ b/docker/docker-build.sh @@ -1,6 +1,6 @@ -# 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 +# 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/releaseprocess.rst b/docs/building/releaseprocess.rst index 7cb0d37b..43cb5f61 100644 --- a/docs/building/releaseprocess.rst +++ b/docs/building/releaseprocess.rst @@ -1,35 +1,35 @@ -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. - -Ocelot uses the following process to accept work into the NuGet packages. - -1. User creates an issue or picks up an existing issue in GitHub. - -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. - -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? - - Have I updated any documentation that my changes may have affected? - - Does my feature make sense, have I checked all of Ocelot's other features to make sure it doesn't already exist? -In order for a PR to be merged the following must have occured. - - All new code is covered by unit tests. - - All new code has at least 1 acceptance test covering the happy path. - - Tests must have passed. - - 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. - -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 `_. - -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. +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. + +Ocelot uses the following process to accept work into the NuGet packages. + +1. User creates an issue or picks up an existing issue in GitHub. + +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. + +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? + - Have I updated any documentation that my changes may have affected? + - Does my feature make sense, have I checked all of Ocelot's other features to make sure it doesn't already exist? +In order for a PR to be merged the following must have occured. + - All new code is covered by unit tests. + - All new code has at least 1 acceptance test covering the happy path. + - Tests must have passed. + - 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. + +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 `_. + +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. diff --git a/docs/features/caching.rst b/docs/features/caching.rst index c2a8be45..c4be74f4 100644 --- a/docs/features/caching.rst +++ b/docs/features/caching.rst @@ -1,54 +1,54 @@ -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. - -The following example shows how to add CacheManager to Ocelot so that you can do output caching. - -First of all add the following NuGet package. - - ``Install-Package Ocelot.Cache.CacheManager`` - -This will give you access to the Ocelot cache manager extension methods. - -The second thing you need to do something like the following to your ConfigureServices.. - -.. code-block:: csharp - - s.AddOcelot() - .AddCacheManager(x => - { - x.WithDictionaryHandle(); - }) - -Finally in order to use caching on a route in your ReRoute configuration add this setting. - -.. code-block:: json - - "FileCacheOptions": { "TtlSeconds": 15, "Region": "somename" } - -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. - -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>()`` - -``IOcelotCache`` this is for output caching. - -``IOcelotCache`` this is for caching the file configuration if you are calling something remote to get your config such as Consul. - -Please dig into the Ocelot source code to find more. I would really appreciate it if anyone wants to implement Redis, memcache etc.. - +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. + +The following example shows how to add CacheManager to Ocelot so that you can do output caching. + +First of all add the following NuGet package. + + ``Install-Package Ocelot.Cache.CacheManager`` + +This will give you access to the Ocelot cache manager extension methods. + +The second thing you need to do something like the following to your ConfigureServices.. + +.. code-block:: csharp + + s.AddOcelot() + .AddCacheManager(x => + { + x.WithDictionaryHandle(); + }) + +Finally in order to use caching on a route in your ReRoute configuration add this setting. + +.. code-block:: json + + "FileCacheOptions": { "TtlSeconds": 15, "Region": "somename" } + +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. + +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>()`` + +``IOcelotCache`` this is for output caching. + +``IOcelotCache`` this is for caching the file configuration if you are calling something remote to get your config such as Consul. + +Please dig into the Ocelot source code to find more. I would really appreciate it if anyone wants to implement Redis, memcache etc.. + diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 7f04acdb..38ecf808 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -1,224 +1,224 @@ -Configuration -============ - -An example configuration can be found `here `_. -There are two sections to the configuration. An array of ReRoutes and a GlobalConfiguration. -The ReRoutes are the objects that tell Ocelot how to treat an upstream request. The Global -configuration is a bit hacky and allows overrides of ReRoute specific settings. It's useful -if you don't want to manage lots of ReRoute specific settings. - -.. code-block:: json - - { - "ReRoutes": [], - "GlobalConfiguration": {} - } - -Here is an example ReRoute configuration, You don't need to set all of these things but this is everything that is available at the moment: - -.. code-block:: json - - { - "DownstreamPathTemplate": "/", - "UpstreamPathTemplate": "/", - "UpstreamHttpMethod": [ - "Get" - ], - "AddHeadersToRequest": {}, - "AddClaimsToRequest": {}, - "RouteClaimsRequirement": {}, - "AddQueriesToRequest": {}, - "RequestIdKey": "", - "FileCacheOptions": { - "TtlSeconds": 0, - "Region": "" - }, - "ReRouteIsCaseSensitive": false, - "ServiceName": "", - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 51876, - } - ], - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 0, - "DurationOfBreak": 0, - "TimeoutValue": 0 - }, - "LoadBalancer": "", - "RateLimitOptions": { - "ClientWhitelist": [], - "EnableRateLimiting": false, - "Period": "", - "PeriodTimespan": 0, - "Limit": 0 - }, - "AuthenticationOptions": { - "AuthenticationProviderKey": "", - "AllowedScopes": [] - }, - "HttpHandlerOptions": { - "AllowAutoRedirect": true, - "UseCookieContainer": true, - "UseTracing": true - }, - "DangerousAcceptAnyServerCertificateValidator": false - } - -More information on how to use these options is below.. - -Multiple environments -^^^^^^^^^^^^^^^^^^^^^ - -Like any other asp.net core project Ocelot supports configuration file names such as configuration.dev.json, configuration.test.json etc. In order to implement this add the following -to you - -.. code-block:: csharp - - .ConfigureAppConfiguration((hostingContext, config) => - { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("ocelot.json") - .AddJsonFile($"configuration.{hostingContext.HostingEnvironment.EnvironmentName}.json") - .AddEnvironmentVariables(); - }) - -Ocelot will now use the environment specific configuration and fall back to ocelot.json if there isn't one. - -You also need to set the corresponding environment variable which is ASPNETCORE_ENVIRONMENT. More info on this can be found in the `asp.net core docs `_. - -Merging configuration files -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This feature was requested in `Issue 296 `_ and allows users to have multiple configuration files to make managing large configurations easier. - -Instead of adding the configuration directly e.g. AddJsonFile("ocelot.json") you can call AddOcelot() like below. - -.. code-block:: csharp - - .ConfigureAppConfiguration((hostingContext, config) => - { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddOcelot(hostingContext.HostingEnvironment) - .AddEnvironmentVariables(); - }) - -In this scenario Ocelot will look for any files that match the pattern (?i)ocelot.([a-zA-Z0-9]*).json and then merge these together. If you want to set the GlobalConfiguration property you must have a file called ocelot.global.json. - -The way Ocelot merges the files is basically load them, loop over them, add any ReRoutes, add any AggregateReRoutes and if the file is called ocelot.global.json add the GlobalConfiguration aswell as any ReRoutes or AggregateReRoutes. Ocelot will then save the merged configuration to a file called ocelot.json and this will be used as the source of truth while ocelot is running. - -At the moment there is no validation at this stage it only happens when Ocelot validates the final merged configuration. This is something to be aware of when you are investigating problems. I would advise always checking what is in ocelot.json if you have any problems. - -You can also give Ocelot a specific path to look in for the configuration files like below. - -.. code-block:: csharp - - .ConfigureAppConfiguration((hostingContext, config) => - { - config - .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", true, true) - .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddOcelot("/foo/bar", hostingContext.HostingEnvironment) - .AddEnvironmentVariables(); - }) - -Ocelot needs the HostingEnvironment so it knows to exclude anything environment specific from the algorithm. - -Store configuration in 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 you add the following when you register your services Ocelot will attempt to store and retrieve its configuration in consul KV store. - -.. code-block:: csharp - - services - .AddOcelot() - .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. - -.. code-block:: json - - "GlobalConfiguration": { - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 9500 - } - } - -I decided to create this feature after working on the Raft consensus algorithm and finding out its super hard. Why not take advantage of the fact Consul already gives you this! -I guess it means if you want to use Ocelot to its fullest you take on Consul as a dependency for now. - -This feature has a 3 second ttl cache before making a new request to your local consul agent. - -Reload JSON config on change -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Ocelot supports reloading the json configuration file on change. e.g. the following will recreate Ocelots internal configuration when the ocelot.json file is updated -manually. - -.. code-block:: json - - config.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true); - -Configuration Key ------------------ - -If you are using Consul for configuration (or other providers in the future) you might want to key your configurations so you can have multiple configurations :) This feature was requested in `issue 346 `_! In order to specify the key you need to set the ConfigurationKey property in the ServiceDiscoveryProvider section of the configuration json file e.g. - -.. code-block:: json - - "GlobalConfiguration": { - "ServiceDiscoveryProvider": { - "Host": "localhost", - "Port": 9500, - "ConfigurationKey": "Oceolot_A" - } - } - -In this example Ocelot will use Oceolot_A as the key for your configuration when looking it up in Consul. - -If you do not set the ConfigurationKey Ocelot will use the string InternalConfiguration as the key. - -Follow Redirects / Use CookieContainer -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Use HttpHandlerOptions in ReRoute configuration to set up HttpHandler behavior: - -1. AllowAutoRedirect is a value that indicates whether the request should follow redirection responses. Set it true if the request should automatically -follow redirection responses from the Downstream resource; otherwise false. The default value is false. - -2. UseCookieContainer is a value that indicates whether the handler uses the CookieContainer -property to store server cookies and uses these cookies when sending requests. The default value is false. Please note -that if you are using the CookieContainer Ocelot caches the HttpClient for each downstream service. This means that all requests -to that DownstreamService will share the same cookies. `Issue 274 `_ was created because a user -noticed that the cookies were being shared. I tried to think of a nice way to handle this but I think it is impossible. If you don't cache the clients -that means each request gets a new client and therefore a new cookie container. If you clear the cookies from the cached client container you get race conditions due to inflight -requests. This would also mean that subsequent requests 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. - -.. code-block:: json - - "DangerousAcceptAnyServerCertificateValidator": true - -I don't recommend doing this, I suggest creating your own certificate and then getting it trusted by your local / remote machine if you can. +Configuration +============ + +An example configuration can be found `here `_. +There are two sections to the configuration. An array of ReRoutes and a GlobalConfiguration. +The ReRoutes are the objects that tell Ocelot how to treat an upstream request. The Global +configuration is a bit hacky and allows overrides of ReRoute specific settings. It's useful +if you don't want to manage lots of ReRoute specific settings. + +.. code-block:: json + + { + "ReRoutes": [], + "GlobalConfiguration": {} + } + +Here is an example ReRoute configuration, You don't need to set all of these things but this is everything that is available at the moment: + +.. code-block:: json + + { + "DownstreamPathTemplate": "/", + "UpstreamPathTemplate": "/", + "UpstreamHttpMethod": [ + "Get" + ], + "AddHeadersToRequest": {}, + "AddClaimsToRequest": {}, + "RouteClaimsRequirement": {}, + "AddQueriesToRequest": {}, + "RequestIdKey": "", + "FileCacheOptions": { + "TtlSeconds": 0, + "Region": "" + }, + "ReRouteIsCaseSensitive": false, + "ServiceName": "", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 51876, + } + ], + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 0, + "DurationOfBreak": 0, + "TimeoutValue": 0 + }, + "LoadBalancer": "", + "RateLimitOptions": { + "ClientWhitelist": [], + "EnableRateLimiting": false, + "Period": "", + "PeriodTimespan": 0, + "Limit": 0 + }, + "AuthenticationOptions": { + "AuthenticationProviderKey": "", + "AllowedScopes": [] + }, + "HttpHandlerOptions": { + "AllowAutoRedirect": true, + "UseCookieContainer": true, + "UseTracing": true + }, + "DangerousAcceptAnyServerCertificateValidator": false + } + +More information on how to use these options is below.. + +Multiple environments +^^^^^^^^^^^^^^^^^^^^^ + +Like any other asp.net core project Ocelot supports configuration file names such as configuration.dev.json, configuration.test.json etc. In order to implement this add the following +to you + +.. code-block:: csharp + + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json") + .AddJsonFile($"configuration.{hostingContext.HostingEnvironment.EnvironmentName}.json") + .AddEnvironmentVariables(); + }) + +Ocelot will now use the environment specific configuration and fall back to ocelot.json if there isn't one. + +You also need to set the corresponding environment variable which is ASPNETCORE_ENVIRONMENT. More info on this can be found in the `asp.net core docs `_. + +Merging configuration files +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This feature was requested in `Issue 296 `_ and allows users to have multiple configuration files to make managing large configurations easier. + +Instead of adding the configuration directly e.g. AddJsonFile("ocelot.json") you can call AddOcelot() like below. + +.. code-block:: csharp + + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddOcelot(hostingContext.HostingEnvironment) + .AddEnvironmentVariables(); + }) + +In this scenario Ocelot will look for any files that match the pattern (?i)ocelot.([a-zA-Z0-9]*).json and then merge these together. If you want to set the GlobalConfiguration property you must have a file called ocelot.global.json. + +The way Ocelot merges the files is basically load them, loop over them, add any ReRoutes, add any AggregateReRoutes and if the file is called ocelot.global.json add the GlobalConfiguration aswell as any ReRoutes or AggregateReRoutes. Ocelot will then save the merged configuration to a file called ocelot.json and this will be used as the source of truth while ocelot is running. + +At the moment there is no validation at this stage it only happens when Ocelot validates the final merged configuration. This is something to be aware of when you are investigating problems. I would advise always checking what is in ocelot.json if you have any problems. + +You can also give Ocelot a specific path to look in for the configuration files like below. + +.. code-block:: csharp + + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddOcelot("/foo/bar", hostingContext.HostingEnvironment) + .AddEnvironmentVariables(); + }) + +Ocelot needs the HostingEnvironment so it knows to exclude anything environment specific from the algorithm. + +Store configuration in 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 you add the following when you register your services Ocelot will attempt to store and retrieve its configuration in consul KV store. + +.. code-block:: csharp + + services + .AddOcelot() + .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. + +.. code-block:: json + + "GlobalConfiguration": { + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 9500 + } + } + +I decided to create this feature after working on the Raft consensus algorithm and finding out its super hard. Why not take advantage of the fact Consul already gives you this! +I guess it means if you want to use Ocelot to its fullest you take on Consul as a dependency for now. + +This feature has a 3 second ttl cache before making a new request to your local consul agent. + +Reload JSON config on change +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Ocelot supports reloading the json configuration file on change. e.g. the following will recreate Ocelots internal configuration when the ocelot.json file is updated +manually. + +.. code-block:: json + + config.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true); + +Configuration Key +----------------- + +If you are using Consul for configuration (or other providers in the future) you might want to key your configurations so you can have multiple configurations :) This feature was requested in `issue 346 `_! In order to specify the key you need to set the ConfigurationKey property in the ServiceDiscoveryProvider section of the configuration json file e.g. + +.. code-block:: json + + "GlobalConfiguration": { + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 9500, + "ConfigurationKey": "Oceolot_A" + } + } + +In this example Ocelot will use Oceolot_A as the key for your configuration when looking it up in Consul. + +If you do not set the ConfigurationKey Ocelot will use the string InternalConfiguration as the key. + +Follow Redirects / Use CookieContainer +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use HttpHandlerOptions in ReRoute configuration to set up HttpHandler behavior: + +1. AllowAutoRedirect is a value that indicates whether the request should follow redirection responses. Set it true if the request should automatically +follow redirection responses from the Downstream resource; otherwise false. The default value is false. + +2. UseCookieContainer is a value that indicates whether the handler uses the CookieContainer +property to store server cookies and uses these cookies when sending requests. The default value is false. Please note +that if you are using the CookieContainer Ocelot caches the HttpClient for each downstream service. This means that all requests +to that DownstreamService will share the same cookies. `Issue 274 `_ was created because a user +noticed that the cookies were being shared. I tried to think of a nice way to handle this but I think it is impossible. If you don't cache the clients +that means each request gets a new client and therefore a new cookie container. If you clear the cookies from the cached client container you get race conditions due to inflight +requests. This would also mean that subsequent requests 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. + +.. code-block:: json + + "DangerousAcceptAnyServerCertificateValidator": true + +I don't recommend doing this, I suggest creating your own certificate and then getting it trusted by your local / remote machine if you can. diff --git a/docs/features/graphql.rst b/docs/features/graphql.rst index 31b59bdc..36006fae 100644 --- a/docs/features/graphql.rst +++ b/docs/features/graphql.rst @@ -1,15 +1,15 @@ -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. - - -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 :> - - +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. + + +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/samples/Docker-Compose/docker-compose.yaml b/samples/Docker-Compose/docker-compose.yaml index 83136844..5236202f 100644 --- a/samples/Docker-Compose/docker-compose.yaml +++ b/samples/Docker-Compose/docker-compose.yaml @@ -1,24 +1,24 @@ -version: "3.4" -services: - - tests: - build: - context: . - target: builder - volumes: - - type: bind - source: . - target: /results - command: test --logger:trx -r /results - - benchmarks: - build: - context: . - target: builder - args: - build_configuration: Release - command: run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj 0 1 2 3 4 - - manual-test: - build: . - ports: [ "5000:80" ] +version: "3.4" +services: + + tests: + build: + context: . + target: builder + volumes: + - type: bind + source: . + target: /results + command: test --logger:trx -r /results + + benchmarks: + build: + context: . + target: builder + args: + build_configuration: Release + command: run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj 0 1 2 3 4 + + manual-test: + build: . + ports: [ "5000:80" ] diff --git a/samples/Docker/Dockerfile b/samples/Docker/Dockerfile index ebbd1eab..86b12ab0 100644 --- a/samples/Docker/Dockerfile +++ b/samples/Docker/Dockerfile @@ -1,48 +1,48 @@ -#This is the base image used for any ran images -FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-stretch-slim AS base -WORKDIR /app -EXPOSE 80 - -#This image is used to build the source for the runnable app -#It can also be used to run other CLI commands on the project, such as packing/deploying nuget packages. Some examples: -#Run tests: docker build --target builder -t ocelot-build . && docker run ocelot-build test --logger:trx;LogFileName=results.trx -#Run benchmarks: docker build --target builder --build-arg build_configuration=Release -t ocelot-build . && docker run ocelot-build run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj -FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS builder -WORKDIR /build -#First we add only the project files so that we can cache nuget packages with dotnet restore -COPY Ocelot.sln Ocelot.sln -COPY src/Ocelot/Ocelot.csproj src/Ocelot/Ocelot.csproj -COPY src/Ocelot.Administration/Ocelot.Administration.csproj src/Ocelot.Administration/Ocelot.Administration.csproj -COPY src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj -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 -COPY test/Ocelot.ManualTest/Ocelot.ManualTest.csproj test/Ocelot.ManualTest/Ocelot.ManualTest.csproj -COPY test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj -COPY test/Ocelot.UnitTests/Ocelot.UnitTests.csproj test/Ocelot.UnitTests/Ocelot.UnitTests.csproj -COPY test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj - -RUN dotnet restore -#Now we add the rest of the source and run a complete build... --no-restore is used because nuget should be resolved at this point -COPY codeanalysis.ruleset codeanalysis.ruleset -COPY src src -COPY test test -ARG build_configuration=Debug -RUN dotnet build --no-restore -c ${build_configuration} -ENTRYPOINT ["dotnet"] - -#This is just for holding the published manual tests... -FROM builder AS manual-test-publish -ARG build_configuration=Debug -RUN dotnet publish --no-build -c ${build_configuration} -o /app test/Ocelot.ManualTest - -#Run manual tests! This is the default run option. -#docker build -t ocelot-manual-test . && docker run --net host ocelot-manual-test -FROM base AS manual-test -ENV ASPNETCORE_ENVIRONMENT=Development -COPY --from=manual-test-publish /app . -ENTRYPOINT ["dotnet", "Ocelot.ManualTest.dll"] +#This is the base image used for any ran images +FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-stretch-slim AS base +WORKDIR /app +EXPOSE 80 + +#This image is used to build the source for the runnable app +#It can also be used to run other CLI commands on the project, such as packing/deploying nuget packages. Some examples: +#Run tests: docker build --target builder -t ocelot-build . && docker run ocelot-build test --logger:trx;LogFileName=results.trx +#Run benchmarks: docker build --target builder --build-arg build_configuration=Release -t ocelot-build . && docker run ocelot-build run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj +FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS builder +WORKDIR /build +#First we add only the project files so that we can cache nuget packages with dotnet restore +COPY Ocelot.sln Ocelot.sln +COPY src/Ocelot/Ocelot.csproj src/Ocelot/Ocelot.csproj +COPY src/Ocelot.Administration/Ocelot.Administration.csproj src/Ocelot.Administration/Ocelot.Administration.csproj +COPY src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj +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 +COPY test/Ocelot.ManualTest/Ocelot.ManualTest.csproj test/Ocelot.ManualTest/Ocelot.ManualTest.csproj +COPY test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj +COPY test/Ocelot.UnitTests/Ocelot.UnitTests.csproj test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +COPY test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj + +RUN dotnet restore +#Now we add the rest of the source and run a complete build... --no-restore is used because nuget should be resolved at this point +COPY codeanalysis.ruleset codeanalysis.ruleset +COPY src src +COPY test test +ARG build_configuration=Debug +RUN dotnet build --no-restore -c ${build_configuration} +ENTRYPOINT ["dotnet"] + +#This is just for holding the published manual tests... +FROM builder AS manual-test-publish +ARG build_configuration=Debug +RUN dotnet publish --no-build -c ${build_configuration} -o /app test/Ocelot.ManualTest + +#Run manual tests! This is the default run option. +#docker build -t ocelot-manual-test . && docker run --net host ocelot-manual-test +FROM base AS manual-test +ENV ASPNETCORE_ENVIRONMENT=Development +COPY --from=manual-test-publish /app . +ENTRYPOINT ["dotnet", "Ocelot.ManualTest.dll"] diff --git a/test/Ocelot.AcceptanceTests/AggregateTests.cs b/test/Ocelot.AcceptanceTests/AggregateTests.cs index 70a01ef1..fe702a32 100644 --- a/test/Ocelot.AcceptanceTests/AggregateTests.cs +++ b/test/Ocelot.AcceptanceTests/AggregateTests.cs @@ -1,656 +1,656 @@ -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 = 51886, - } - }, - 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:51886", "/", 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 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 = 51886, + } + }, + 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:51886", "/", 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"); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/CachingTests.cs b/test/Ocelot.AcceptanceTests/CachingTests.cs index 332ef35e..d8b1a05b 100644 --- a/test/Ocelot.AcceptanceTests/CachingTests.cs +++ b/test/Ocelot.AcceptanceTests/CachingTests.cs @@ -1,225 +1,225 @@ -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; - private readonly ServiceHandler _serviceHandler; - - public CachingTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - - [Fact] - public void should_return_cached_response() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 57899, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 100 - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:57899", 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")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => _steps.ThenTheContentLengthIs(16)) - .BDDfy(); - } - - [Fact] - public void should_return_cached_response_with_expires_header() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 52839, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 100 - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:52839", 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")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => _steps.ThenTheContentLengthIs(16)) - .And(x => _steps.ThenTheResponseBodyHeaderIs("Expires", "-1")) - .BDDfy(); - } - - [Fact] - public void should_return_cached_response_when_using_jsonserialized_cache() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 57879, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 100 - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:57879", 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")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void should_not_return_cached_response_as_ttl_expires() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 57873, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 1 - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:57873", 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()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) - .BDDfy(); - } - - private void GivenTheCacheExpires() - { - Thread.Sleep(1000); - } - - private void GivenTheServiceNowReturns(string url, int statusCode, string responseBody) - { - _serviceHandler.Dispose(); - GivenThereIsAServiceRunningOn(url, statusCode, responseBody, null, null); - } - - private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, string key, string value) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(key)) - { - context.Response.Headers.Add(key, value); - } - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } -} +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; + private readonly ServiceHandler _serviceHandler; + + public CachingTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_return_cached_response() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 57899, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 100 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:57899", 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")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.ThenTheContentLengthIs(16)) + .BDDfy(); + } + + [Fact] + public void should_return_cached_response_with_expires_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 52839, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 100 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:52839", 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")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.ThenTheContentLengthIs(16)) + .And(x => _steps.ThenTheResponseBodyHeaderIs("Expires", "-1")) + .BDDfy(); + } + + [Fact] + public void should_return_cached_response_when_using_jsonserialized_cache() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 57879, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 100 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:57879", 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")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_not_return_cached_response_as_ttl_expires() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 57873, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 1 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:57873", 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()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) + .BDDfy(); + } + + private void GivenTheCacheExpires() + { + Thread.Sleep(1000); + } + + private void GivenTheServiceNowReturns(string url, int statusCode, string responseBody) + { + _serviceHandler.Dispose(); + GivenThereIsAServiceRunningOn(url, statusCode, responseBody, null, null); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, string key, string value) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(key)) + { + context.Response.Headers.Add(key, value); + } + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/HeaderTests.cs b/test/Ocelot.AcceptanceTests/HeaderTests.cs index b139b4d3..e7c0c2ca 100644 --- a/test/Ocelot.AcceptanceTests/HeaderTests.cs +++ b/test/Ocelot.AcceptanceTests/HeaderTests.cs @@ -1,446 +1,446 @@ -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; - private readonly Steps _steps; - private readonly ServiceHandler _serviceHandler; - - public HeaderTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - - [Fact] - public void should_transform_upstream_header() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51871, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - UpstreamHeaderTransform = new Dictionary - { - {"Laz", "D, GP"} - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51871", "/", 200, "Laz")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .And(x => _steps.GivenIAddAHeader("Laz", "D")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("GP")) - .BDDfy(); - } - - [Fact] - public void should_transform_downstream_header() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51871, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - DownstreamHeaderTransform = new Dictionary - { - {"Location", "http://www.bbc.co.uk/, http://ocelot.com/"} - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51871", "/", 200, "Location", "http://www.bbc.co.uk/")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://ocelot.com/")) - .BDDfy(); - } - - [Fact] - public void should_fix_issue_190() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 6773, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - DownstreamHeaderTransform = new Dictionary - { - {"Location", "http://localhost:6773, {BaseUrl}"} - }, - HttpHandlerOptions = new FileHttpHandlerOptions - { - AllowAutoRedirect = false - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6773", "/", 302, "Location", "http://localhost:6773/pay/Receive")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect)) - .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://localhost:5000/pay/Receive")) - .BDDfy(); - } - - [Fact] - public void should_fix_issue_205() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 6773, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - DownstreamHeaderTransform = new Dictionary - { - {"Location", "{DownstreamBaseUrl}, {BaseUrl}"} - }, - HttpHandlerOptions = new FileHttpHandlerOptions - { - AllowAutoRedirect = false - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6773", "/", 302, "Location", "http://localhost:6773/pay/Receive")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect)) - .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://localhost:5000/pay/Receive")) - .BDDfy(); - } - - [Fact] - public void should_fix_issue_417() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 6773, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - DownstreamHeaderTransform = new Dictionary - { - {"Location", "{DownstreamBaseUrl}, {BaseUrl}"} - }, - HttpHandlerOptions = new FileHttpHandlerOptions - { - AllowAutoRedirect = false - } - } - }, - GlobalConfiguration = new FileGlobalConfiguration - { - BaseUrl = "http://anotherapp.azurewebsites.net" - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6773", "/", 302, "Location", "http://localhost:6773/pay/Receive")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect)) - .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://anotherapp.azurewebsites.net/pay/Receive")) - .BDDfy(); - } - - [Fact] - public void request_should_reuse_cookies_with_cookie_container() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/sso/{everything}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 6774, - } - }, - UpstreamPathTemplate = "/sso/{everything}", - UpstreamHttpMethod = new List { "Get", "Post", "Options" }, - HttpHandlerOptions = new FileHttpHandlerOptions - { - UseCookieContainer = true - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6774", "/sso/test", 200)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseHeaderIs("Set-Cookie", "test=0; path=/")) - .And(x => _steps.GivenIAddCookieToMyRequest("test=1; path=/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void request_should_have_own_cookies_no_cookie_container() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/sso/{everything}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 6775, - } - }, - UpstreamPathTemplate = "/sso/{everything}", - UpstreamHttpMethod = new List { "Get", "Post", "Options" }, - HttpHandlerOptions = new FileHttpHandlerOptions - { - UseCookieContainer = false - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6775", "/sso/test", 200)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseHeaderIs("Set-Cookie", "test=0; path=/")) - .And(x => _steps.GivenIAddCookieToMyRequest("test=1; path=/")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void issue_474_should_not_put_spaces_in_header() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 52866, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:52866", "/", 200, "Accept")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .And(x => _steps.GivenIAddAHeader("Accept", "text/html,application/xhtml+xml,application/xml;")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("text/html,application/xhtml+xml,application/xml;")) - .BDDfy(); - } - - [Fact] - public void issue_474_should_put_spaces_in_header() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51874, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51874", "/", 200, "Accept")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .And(x => _steps.GivenIAddAHeader("Accept", "text/html")) - .And(x => _steps.GivenIAddAHeader("Accept", "application/xhtml+xml")) - .And(x => _steps.GivenIAddAHeader("Accept", "application/xml")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("text/html, application/xhtml+xml, application/xml")) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, context => - { - if (_count == 0) - { - context.Response.Cookies.Append("test", "0"); - _count++; - context.Response.StatusCode = statusCode; - return Task.CompletedTask; - } - - if (context.Request.Cookies.TryGetValue("test", out var cookieValue) || context.Request.Headers.TryGetValue("Set-Cookie", out var headerValue)) - { - if (cookieValue == "0" || headerValue == "test=1; path=/") - { - context.Response.StatusCode = statusCode; - return Task.CompletedTask; - } - } - - context.Response.StatusCode = 500; - return Task.CompletedTask; - }); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - if (context.Request.Headers.TryGetValue(headerKey, out var values)) - { - var result = values.First(); - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(result); - } - }); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey, string headerValue) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, context => - { - context.Response.OnStarting(() => - { - context.Response.Headers.Add(headerKey, headerValue); - context.Response.StatusCode = statusCode; - return Task.CompletedTask; - }); - - return Task.CompletedTask; - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } -} +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; + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public HeaderTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_transform_upstream_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51871, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + UpstreamHeaderTransform = new Dictionary + { + {"Laz", "D, GP"} + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51871", "/", 200, "Laz")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenIAddAHeader("Laz", "D")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("GP")) + .BDDfy(); + } + + [Fact] + public void should_transform_downstream_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51871, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHeaderTransform = new Dictionary + { + {"Location", "http://www.bbc.co.uk/, http://ocelot.com/"} + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51871", "/", 200, "Location", "http://www.bbc.co.uk/")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://ocelot.com/")) + .BDDfy(); + } + + [Fact] + public void should_fix_issue_190() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 6773, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHeaderTransform = new Dictionary + { + {"Location", "http://localhost:6773, {BaseUrl}"} + }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + AllowAutoRedirect = false + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6773", "/", 302, "Location", "http://localhost:6773/pay/Receive")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect)) + .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://localhost:5000/pay/Receive")) + .BDDfy(); + } + + [Fact] + public void should_fix_issue_205() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 6773, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHeaderTransform = new Dictionary + { + {"Location", "{DownstreamBaseUrl}, {BaseUrl}"} + }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + AllowAutoRedirect = false + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6773", "/", 302, "Location", "http://localhost:6773/pay/Receive")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect)) + .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://localhost:5000/pay/Receive")) + .BDDfy(); + } + + [Fact] + public void should_fix_issue_417() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 6773, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHeaderTransform = new Dictionary + { + {"Location", "{DownstreamBaseUrl}, {BaseUrl}"} + }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + AllowAutoRedirect = false + } + } + }, + GlobalConfiguration = new FileGlobalConfiguration + { + BaseUrl = "http://anotherapp.azurewebsites.net" + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6773", "/", 302, "Location", "http://localhost:6773/pay/Receive")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect)) + .And(x => _steps.ThenTheResponseHeaderIs("Location", "http://anotherapp.azurewebsites.net/pay/Receive")) + .BDDfy(); + } + + [Fact] + public void request_should_reuse_cookies_with_cookie_container() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/sso/{everything}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 6774, + } + }, + UpstreamPathTemplate = "/sso/{everything}", + UpstreamHttpMethod = new List { "Get", "Post", "Options" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseCookieContainer = true + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6774", "/sso/test", 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseHeaderIs("Set-Cookie", "test=0; path=/")) + .And(x => _steps.GivenIAddCookieToMyRequest("test=1; path=/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void request_should_have_own_cookies_no_cookie_container() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/sso/{everything}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 6775, + } + }, + UpstreamPathTemplate = "/sso/{everything}", + UpstreamHttpMethod = new List { "Get", "Post", "Options" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseCookieContainer = false + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6775", "/sso/test", 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseHeaderIs("Set-Cookie", "test=0; path=/")) + .And(x => _steps.GivenIAddCookieToMyRequest("test=1; path=/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void issue_474_should_not_put_spaces_in_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 52866, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:52866", "/", 200, "Accept")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenIAddAHeader("Accept", "text/html,application/xhtml+xml,application/xml;")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("text/html,application/xhtml+xml,application/xml;")) + .BDDfy(); + } + + [Fact] + public void issue_474_should_put_spaces_in_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51874, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51874", "/", 200, "Accept")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenIAddAHeader("Accept", "text/html")) + .And(x => _steps.GivenIAddAHeader("Accept", "application/xhtml+xml")) + .And(x => _steps.GivenIAddAHeader("Accept", "application/xml")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("text/html, application/xhtml+xml, application/xml")) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, context => + { + if (_count == 0) + { + context.Response.Cookies.Append("test", "0"); + _count++; + context.Response.StatusCode = statusCode; + return Task.CompletedTask; + } + + if (context.Request.Cookies.TryGetValue("test", out var cookieValue) || context.Request.Headers.TryGetValue("Set-Cookie", out var headerValue)) + { + if (cookieValue == "0" || headerValue == "test=1; path=/") + { + context.Response.StatusCode = statusCode; + return Task.CompletedTask; + } + } + + context.Response.StatusCode = 500; + return Task.CompletedTask; + }); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + if (context.Request.Headers.TryGetValue(headerKey, out var values)) + { + var result = values.First(); + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(result); + } + }); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey, string headerValue) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, context => + { + context.Response.OnStarting(() => + { + context.Response.Headers.Add(headerKey, headerValue); + context.Response.StatusCode = statusCode; + return Task.CompletedTask; + }); + + return Task.CompletedTask; + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/HttpClientCachingTests.cs b/test/Ocelot.AcceptanceTests/HttpClientCachingTests.cs index 2a5116b7..a3b8f49e 100644 --- a/test/Ocelot.AcceptanceTests/HttpClientCachingTests.cs +++ b/test/Ocelot.AcceptanceTests/HttpClientCachingTests.cs @@ -1,163 +1,163 @@ -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; - } - } -} +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; + } + } +} diff --git a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs index a25c6116..d7cf7c2d 100644 --- a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs +++ b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs @@ -1,274 +1,274 @@ -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; - - public class PollyQoSTests : IDisposable - { - private readonly Steps _steps; - private int _requestCount; - private readonly ServiceHandler _serviceHandler; - - public PollyQoSTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - - [Fact] - public void should_not_timeout() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51569, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Post" }, - QoSOptions = new FileQoSOptions - { - TimeoutValue = 1000, - ExceptionsAllowedBeforeBreaking = 10 - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51569", 200, string.Empty, 10)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void should_timeout() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51579, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Post" }, - QoSOptions = new FileQoSOptions - { - TimeoutValue = 10, - ExceptionsAllowedBeforeBreaking = 10 - } - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51579", 201, string.Empty, 1000)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .BDDfy(); - } - - [Fact] - public void should_open_circuit_breaker_then_close() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51892, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - QoSOptions = new FileQoSOptions - { - ExceptionsAllowedBeforeBreaking = 1, - TimeoutValue = 500, - DurationOfBreak = 1000 - }, - } - } - }; - - this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51892", "Hello from Laura")) - .Given(x => _steps.GivenThereIsAConfiguration(configuration)) - .Given(x => _steps.GivenOcelotIsRunningWithPolly()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => x.GivenIWaitMilliseconds(3000)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void open_circuit_should_not_effect_different_reRoute() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51870, - } - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - QoSOptions = new FileQoSOptions - { - ExceptionsAllowedBeforeBreaking = 1, - TimeoutValue = 500, - DurationOfBreak = 1000 - } - }, - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51880, - } - }, - 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)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/working")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => 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) - { - Thread.Sleep(ms); - } - - private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - //circuit starts closed - if (_requestCount == 0) - { - _requestCount++; - context.Response.StatusCode = 200; - await context.Response.WriteAsync(responseBody); - return; - } - - //request one times out and polly throws exception, circuit opens - if (_requestCount == 1) - { - _requestCount++; - await Task.Delay(1000); - context.Response.StatusCode = 200; - return; - } - - //after break closes we return 200 OK - if (_requestCount == 2) - { - context.Response.StatusCode = 200; - await context.Response.WriteAsync(responseBody); - } - }); - } - - private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, int timeout) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - Thread.Sleep(timeout); - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } -} +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; + + public class PollyQoSTests : IDisposable + { + private readonly Steps _steps; + private int _requestCount; + private readonly ServiceHandler _serviceHandler; + + public PollyQoSTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_not_timeout() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51569, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Post" }, + QoSOptions = new FileQoSOptions + { + TimeoutValue = 1000, + ExceptionsAllowedBeforeBreaking = 10 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51569", 200, string.Empty, 10)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithPolly()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_timeout() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51579, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Post" }, + QoSOptions = new FileQoSOptions + { + TimeoutValue = 10, + ExceptionsAllowedBeforeBreaking = 10 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51579", 201, string.Empty, 1000)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithPolly()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .BDDfy(); + } + + [Fact] + public void should_open_circuit_breaker_then_close() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51892, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + QoSOptions = new FileQoSOptions + { + ExceptionsAllowedBeforeBreaking = 1, + TimeoutValue = 500, + DurationOfBreak = 1000 + }, + } + } + }; + + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51892", "Hello from Laura")) + .Given(x => _steps.GivenThereIsAConfiguration(configuration)) + .Given(x => _steps.GivenOcelotIsRunningWithPolly()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => x.GivenIWaitMilliseconds(3000)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void open_circuit_should_not_effect_different_reRoute() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51870, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + QoSOptions = new FileQoSOptions + { + ExceptionsAllowedBeforeBreaking = 1, + TimeoutValue = 500, + DurationOfBreak = 1000 + } + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51880, + } + }, + 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)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithPolly()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/working")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => 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) + { + Thread.Sleep(ms); + } + + private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string responseBody) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + //circuit starts closed + if (_requestCount == 0) + { + _requestCount++; + context.Response.StatusCode = 200; + await context.Response.WriteAsync(responseBody); + return; + } + + //request one times out and polly throws exception, circuit opens + if (_requestCount == 1) + { + _requestCount++; + await Task.Delay(1000); + context.Response.StatusCode = 200; + return; + } + + //after break closes we return 200 OK + if (_requestCount == 2) + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync(responseBody); + } + }); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, int timeout) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + Thread.Sleep(timeout); + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs b/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs index a352efea..828a3a90 100644 --- a/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs +++ b/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs @@ -1,68 +1,68 @@ -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; - private readonly ServiceHandler _serviceHandler; - - public ResponseCodeTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - - [Fact] - public void should_return_response_304_when_service_returns_304() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/{everything}", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 59892, - } - }, - UpstreamPathTemplate = "/{everything}", - UpstreamHttpMethod = new List { "Get" }, - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:59892", "/inline.132.bundle.js", 304)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/inline.132.bundle.js")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotModified)) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode) - { - _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => - { - context.Response.StatusCode = statusCode; - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - } - } -} +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; + private readonly ServiceHandler _serviceHandler; + + public ResponseCodeTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + [Fact] + public void should_return_response_304_when_service_returns_304() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/{everything}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 59892, + } + }, + UpstreamPathTemplate = "/{everything}", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:59892", "/inline.132.bundle.js", 304)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/inline.132.bundle.js")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotModified)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => + { + context.Response.StatusCode = statusCode; + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/RoutingTests.cs index a6015ebe..cce30751 100644 --- a/test/Ocelot.AcceptanceTests/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingTests.cs @@ -1,1057 +1,1057 @@ -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; - - 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 = 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 - } - } - }; - - 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 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 = 50187, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List(), - } - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:50187", "/", 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(); - } - } -} +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; + + 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 = 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 + } + } + }; + + 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 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 = 50187, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List(), + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:50187", "/", 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(); + } + } +} diff --git a/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs b/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs index 104099a3..d99eb2e0 100644 --- a/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs +++ b/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs @@ -1,204 +1,204 @@ -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 Xunit; - -namespace Ocelot.IntegrationTests -{ - public class ThreadSafeHeadersTests : IDisposable - { - private readonly HttpClient _httpClient; - private IWebHost _builder; - private IWebHostBuilder _webHostBuilder; - private readonly string _ocelotBaseUrl; - private IWebHost _downstreamBuilder; - private readonly Random _random; - private readonly ConcurrentBag _results; - - public ThreadSafeHeadersTests() - { - _results = new ConcurrentBag(); - _random = new Random(); - _httpClient = new HttpClient(); - _ocelotBaseUrl = "http://localhost:5001"; - _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); - } - - [Fact] - public void should_return_same_response_for_each_different_header_under_load_to_downsteam_service() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51611, - }, - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenThereIsAServiceRunningOn("http://localhost:51611")) - .And(x => GivenOcelotIsRunning()) - .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesWithDifferentHeaderValues("/", 300)) - .Then(x => ThenTheSameHeaderValuesAreReturnedByTheDownstreamService()) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string url) - { - _downstreamBuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(url) - .Configure(app => - { - app.Run(async context => - { - var header = context.Request.Headers["ThreadSafeHeadersTest"]; - - context.Response.StatusCode = 200; - await context.Response.WriteAsync(header[0]); - }); - }) - .Build(); - - _downstreamBuilder.Start(); - } - - private void GivenOcelotIsRunning() - { - _webHostBuilder = new WebHostBuilder() - .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.AddOcelot(); - }) - .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 WhenIGetUrlOnTheApiGatewayMultipleTimesWithDifferentHeaderValues(string url, int times) - { - var tasks = new Task[times]; - - for (int i = 0; i < times; i++) - { - var urlCopy = url; - var random = _random.Next(0, 50); - tasks[i] = GetForThreadSafeHeadersTest(urlCopy, random); - } - - Task.WaitAll(tasks); - } - - private async Task GetForThreadSafeHeadersTest(string url, int random) - { - var request = new HttpRequestMessage(HttpMethod.Get, url); - 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 tshtr = new ThreadSafeHeadersTestResult(result, random); - _results.Add(tshtr); - } - - private void ThenTheSameHeaderValuesAreReturnedByTheDownstreamService() - { - foreach (var result in _results) - { - result.Result.ShouldBe(result.Random); - } - } - - public void Dispose() - { - _builder?.Dispose(); - _httpClient?.Dispose(); - _downstreamBuilder?.Dispose(); - } - - private class ThreadSafeHeadersTestResult - { - public ThreadSafeHeadersTestResult(int result, int random) - { - Result = result; - Random = random; - } - - public int Result { get; private set; } - public int Random { get; private set; } - } - } -} +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 Xunit; + +namespace Ocelot.IntegrationTests +{ + public class ThreadSafeHeadersTests : IDisposable + { + private readonly HttpClient _httpClient; + private IWebHost _builder; + private IWebHostBuilder _webHostBuilder; + private readonly string _ocelotBaseUrl; + private IWebHost _downstreamBuilder; + private readonly Random _random; + private readonly ConcurrentBag _results; + + public ThreadSafeHeadersTests() + { + _results = new ConcurrentBag(); + _random = new Random(); + _httpClient = new HttpClient(); + _ocelotBaseUrl = "http://localhost:5001"; + _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); + } + + [Fact] + public void should_return_same_response_for_each_different_header_under_load_to_downsteam_service() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51611, + }, + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenThereIsAServiceRunningOn("http://localhost:51611")) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesWithDifferentHeaderValues("/", 300)) + .Then(x => ThenTheSameHeaderValuesAreReturnedByTheDownstreamService()) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url) + { + _downstreamBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + var header = context.Request.Headers["ThreadSafeHeadersTest"]; + + context.Response.StatusCode = 200; + await context.Response.WriteAsync(header[0]); + }); + }) + .Build(); + + _downstreamBuilder.Start(); + } + + private void GivenOcelotIsRunning() + { + _webHostBuilder = new WebHostBuilder() + .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.AddOcelot(); + }) + .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 WhenIGetUrlOnTheApiGatewayMultipleTimesWithDifferentHeaderValues(string url, int times) + { + var tasks = new Task[times]; + + for (int i = 0; i < times; i++) + { + var urlCopy = url; + var random = _random.Next(0, 50); + tasks[i] = GetForThreadSafeHeadersTest(urlCopy, random); + } + + Task.WaitAll(tasks); + } + + private async Task GetForThreadSafeHeadersTest(string url, int random) + { + var request = new HttpRequestMessage(HttpMethod.Get, url); + 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 tshtr = new ThreadSafeHeadersTestResult(result, random); + _results.Add(tshtr); + } + + private void ThenTheSameHeaderValuesAreReturnedByTheDownstreamService() + { + foreach (var result in _results) + { + result.Result.ShouldBe(result.Random); + } + } + + public void Dispose() + { + _builder?.Dispose(); + _httpClient?.Dispose(); + _downstreamBuilder?.Dispose(); + } + + private class ThreadSafeHeadersTestResult + { + public ThreadSafeHeadersTestResult(int result, int random) + { + Result = result; + Random = random; + } + + public int Result { get; private set; } + public int Random { get; private set; } + } + } +}