diff --git a/build.cake b/build.cake index 619df853c..ef8cb5c69 100644 --- a/build.cake +++ b/build.cake @@ -1,514 +1,514 @@ -#tool "nuget:?package=GitVersion.CommandLine" -#tool "nuget:?package=GitReleaseNotes" -#addin nuget:?package=Cake.Json -#addin nuget:?package=Newtonsoft.Json&version=9.0.1 -#tool "nuget:?package=OpenCover" -#tool "nuget:?package=ReportGenerator" -#tool "nuget:?package=coveralls.net&version=0.7.0" -#addin Cake.Coveralls - -// 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 = 82d; -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"); - -// unstable releases -var nugetFeedUnstableKey = EnvironmentVariable("nuget-apikey-unstable"); -var nugetFeedUnstableUploadUrl = "https://www.nuget.org/api/v2/package"; -var nugetFeedUnstableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package"; - -// stable releases -var tagsUrl = "https://api.github.com/repos/tompallister/ocelot/releases/tags/"; -var nugetFeedStableKey = EnvironmentVariable("nuget-apikey-stable"); -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. -var releaseTag = ""; -string committedVersion = "0.0.0-dev"; -var buildVersion = committedVersion; -GitVersion versioning = null; -var nugetFeedUnstableBranchFilter = "^(develop)$|^(PullRequest/)"; - -var target = Argument("target", "Default"); - - -Information("target is " +target); -Information("Build configuration is " + compileConfig); - -Task("Default") - .IsDependentOn("Build"); - -Task("Build") - .IsDependentOn("RunTests") - .IsDependentOn("CreatePackages"); - -Task("BuildAndReleaseUnstable") - .IsDependentOn("Build") - .IsDependentOn("ReleasePackagesToUnstableFeed"); - -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 (AppVeyor.IsRunningOnAppVeyor) - { - Information("Persisting version number..."); - PersistVersion(committedVersion, nugetVersion); - buildVersion = nugetVersion; - } - else - { - Information("We are not running on build server, so we won't persist the version number."); - } - }); - -Task("Compile") - .IsDependentOn("Clean") - .IsDependentOn("Version") - .Does(() => - { - var settings = new DotNetCoreBuildSettings - { - Configuration = compileConfig, - }; - - DotNetCoreBuild(slnFile, settings); - }); - -Task("RunUnitTests") - .IsDependentOn("Compile") - .Does(() => - { - if (IsRunningOnWindows()) - { - var coverageSummaryFile = artifactsForUnitTestsDir + File("coverage.xml"); - - EnsureDirectoryExists(artifactsForUnitTestsDir); - - OpenCover(tool => - { - tool.DotNetCoreTest(unitTestAssemblies); - }, - new FilePath(coverageSummaryFile), - new OpenCoverSettings() - { - Register="user", - ArgumentCustomization=args=>args.Append(@"-oldstyle -returntargetcode -excludebyattribute:*.ExcludeFromCoverage*") - } - .WithFilter("+[Ocelot*]*") - .WithFilter("-[xunit*]*") - .WithFilter("-[Ocelot*Tests]*") - ); - - ReportGenerator(coverageSummaryFile, artifactsForUnitTestsDir); - - if (AppVeyor.IsRunningOnAppVeyor) - { - 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 = !AppVeyor.IsRunningOnAppVeyor ? 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)); - }; - - } - else - { - var settings = new DotNetCoreTestSettings - { - Configuration = compileConfig, - }; - - EnsureDirectoryExists(artifactsForUnitTestsDir); - DotNetCoreTest(unitTestAssemblies, settings); - } - }); - -Task("RunAcceptanceTests") - .IsDependentOn("Compile") - .Does(() => - { - if(TravisCI.IsRunningOnTravisCI) - { - Information( - @"Job: - JobId: {0} - JobNumber: {1} - OSName: {2}", - BuildSystem.TravisCI.Environment.Job.JobId, - BuildSystem.TravisCI.Environment.Job.JobNumber, - BuildSystem.TravisCI.Environment.Job.OSName - ); - - if(TravisCI.Environment.Job.OSName.ToLower() == "osx") - { - return; - } - } - - 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(() => - { - if(TravisCI.IsRunningOnTravisCI) - { - Information( - @"Job: - JobId: {0} - JobNumber: {1} - OSName: {2}", - BuildSystem.TravisCI.Environment.Job.JobId, - BuildSystem.TravisCI.Environment.Job.JobNumber, - BuildSystem.TravisCI.Environment.Job.OSName - ); - - if(TravisCI.Environment.Job.OSName.ToLower() == "osx") - { - return; - } - } - - var settings = new DotNetCoreTestSettings - { - Configuration = compileConfig, - ArgumentCustomization = args => args - .Append("--no-restore") - .Append("--no-build") - }; - - EnsureDirectoryExists(artifactsForIntegrationTestsDir); - DotNetCoreTest(integrationTestAssemblies, settings); - }); - -Task("RunTests") - .IsDependentOn("RunUnitTests") - .IsDependentOn("RunAcceptanceTests") - .IsDependentOn("RunIntegrationTests"); - -Task("CreatePackages") - .IsDependentOn("Compile") - .Does(() => - { - EnsureDirectoryExists(packagesDir); - CopyFiles("./src/**/Ocelot.*.nupkg", packagesDir); - - //GenerateReleaseNotes(releaseNotesFile); - - System.IO.File.WriteAllLines(artifactsFile, new[]{ - "nuget:Ocelot." + buildVersion + ".nupkg", - //"releaseNotes:releasenotes.md" - }); - - if (AppVeyor.IsRunningOnAppVeyor) - { - var path = packagesDir.ToString() + @"/**/*"; - - foreach (var file in GetFiles(path)) - { - AppVeyor.UploadArtifact(file.FullPath); - } - } - }); - -Task("ReleasePackagesToUnstableFeed") - .IsDependentOn("CreatePackages") - .Does(() => - { - if (ShouldPublishToUnstableFeed(nugetFeedUnstableBranchFilter, versioning.BranchName)) - { - PublishPackages(packagesDir, artifactsFile, nugetFeedUnstableKey, nugetFeedUnstableUploadUrl, nugetFeedUnstableSymbolsUploadUrl); - } - }); - -Task("EnsureStableReleaseRequirements") - .Does(() => - { - Information("Check if stable release..."); - - if (!AppVeyor.IsRunningOnAppVeyor) - { - throw new Exception("Stable release should happen via appveyor"); - } - - Information("Running on AppVeyor..."); - - Information("IsTag = " + AppVeyor.Environment.Repository.Tag.IsTag); - - Information("Name = " + AppVeyor.Environment.Repository.Tag.Name); - - var isTag = - AppVeyor.Environment.Repository.Tag.IsTag && - !string.IsNullOrWhiteSpace(AppVeyor.Environment.Repository.Tag.Name); - - if (!isTag) - { - throw new Exception("Stable release should happen from a published GitHub release"); - } - - Information("Release is stable..."); - }); - -Task("UpdateVersionInfo") - .IsDependentOn("EnsureStableReleaseRequirements") - .Does(() => - { - releaseTag = AppVeyor.Environment.Repository.Tag.Name; - AppVeyor.UpdateBuildVersion(releaseTag); - }); - -Task("DownloadGitHubReleaseArtifacts") - .IsDependentOn("UpdateVersionInfo") - .Does(() => - { - try - { - Information("DownloadGitHubReleaseArtifacts"); - - EnsureDirectoryExists(packagesDir); - - Information("Directory exists..."); - - var releaseUrl = tagsUrl + releaseTag; - - Information("Release url " + releaseUrl); - - //var releaseJson = Newtonsoft.Json.Linq.JObject.Parse(GetResource(releaseUrl)); - - var assets_url = Newtonsoft.Json.Linq.JObject.Parse(GetResource(releaseUrl)) - .GetValue("assets_url") - .Value(); - - Information("Assets url " + assets_url); - - var assets = GetResource(assets_url); - - Information("Assets " + assets_url); - - foreach(var asset in Newtonsoft.Json.JsonConvert.DeserializeObject(assets)) - { - Information("In the loop.."); - - var file = packagesDir + File(asset.Value("name")); - - Information("Downloading " + file); - - DownloadFile(asset.Value("browser_download_url"), file); - } - - Information("Out of the loop..."); - } - catch(Exception exception) - { - Information("There was an exception " + exception); - throw; - } - }); - -Task("ReleasePackagesToStableFeed") - .IsDependentOn("DownloadGitHubReleaseArtifacts") - .Does(() => - { - PublishPackages(packagesDir, artifactsFile, nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl); - }); - -Task("Release") - .IsDependentOn("ReleasePackagesToStableFeed"); - -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) -{ - var artifacts = System.IO.File - .ReadAllLines(artifactsFile) - .Select(l => l.Split(':')) - .ToDictionary(v => v[0], v => v[1]); - - var codePackage = packagesDir + File(artifacts["nuget"]); - - Information("Pushing package " + codePackage); - - Information("Calling NuGetPush"); - - NuGetPush( - codePackage, - new NuGetPushSettings { - ApiKey = feedApiKey, - Source = codeFeedUrl - }); -} - -/// 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 ShouldPublishToUnstableFeed(string filter, string branchName) -{ - var regex = new System.Text.RegularExpressions.Regex(filter); - var publish = regex.IsMatch(branchName); - if (publish) - { - Information("Branch " + branchName + " will be published to the unstable feed"); - } - else - { - Information("Branch " + branchName + " will not be published to the unstable feed"); - } - return publish; -} +#tool "nuget:?package=GitVersion.CommandLine" +#tool "nuget:?package=GitReleaseNotes" +#addin nuget:?package=Cake.Json +#addin nuget:?package=Newtonsoft.Json&version=9.0.1 +#tool "nuget:?package=OpenCover" +#tool "nuget:?package=ReportGenerator" +#tool "nuget:?package=coveralls.net&version=0.7.0" +#addin Cake.Coveralls&version=0.7.0 + +// compile +var compileConfig = Argument("configuration", "Release"); +var slnFile = "./Ocelot.sln"; + +// build artifacts +var artifactsDir = Directory("artifacts"); + +// unit testing +var artifactsForUnitTestsDir = artifactsDir + Directory("UnitTests"); +var unitTestAssemblies = @"./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj"; +var minCodeCoverage = 82d; +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"); + +// unstable releases +var nugetFeedUnstableKey = EnvironmentVariable("nuget-apikey-unstable"); +var nugetFeedUnstableUploadUrl = "https://www.nuget.org/api/v2/package"; +var nugetFeedUnstableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package"; + +// stable releases +var tagsUrl = "https://api.github.com/repos/tompallister/ocelot/releases/tags/"; +var nugetFeedStableKey = EnvironmentVariable("nuget-apikey-stable"); +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. +var releaseTag = ""; +string committedVersion = "0.0.0-dev"; +var buildVersion = committedVersion; +GitVersion versioning = null; +var nugetFeedUnstableBranchFilter = "^(develop)$|^(PullRequest/)"; + +var target = Argument("target", "Default"); + + +Information("target is " +target); +Information("Build configuration is " + compileConfig); + +Task("Default") + .IsDependentOn("Build"); + +Task("Build") + .IsDependentOn("RunTests") + .IsDependentOn("CreatePackages"); + +Task("BuildAndReleaseUnstable") + .IsDependentOn("Build") + .IsDependentOn("ReleasePackagesToUnstableFeed"); + +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 (AppVeyor.IsRunningOnAppVeyor) + { + Information("Persisting version number..."); + PersistVersion(committedVersion, nugetVersion); + buildVersion = nugetVersion; + } + else + { + Information("We are not running on build server, so we won't persist the version number."); + } + }); + +Task("Compile") + .IsDependentOn("Clean") + .IsDependentOn("Version") + .Does(() => + { + var settings = new DotNetCoreBuildSettings + { + Configuration = compileConfig, + }; + + DotNetCoreBuild(slnFile, settings); + }); + +Task("RunUnitTests") + .IsDependentOn("Compile") + .Does(() => + { + if (IsRunningOnWindows()) + { + var coverageSummaryFile = artifactsForUnitTestsDir + File("coverage.xml"); + + EnsureDirectoryExists(artifactsForUnitTestsDir); + + OpenCover(tool => + { + tool.DotNetCoreTest(unitTestAssemblies); + }, + new FilePath(coverageSummaryFile), + new OpenCoverSettings() + { + Register="user", + ArgumentCustomization=args=>args.Append(@"-oldstyle -returntargetcode -excludebyattribute:*.ExcludeFromCoverage*") + } + .WithFilter("+[Ocelot*]*") + .WithFilter("-[xunit*]*") + .WithFilter("-[Ocelot*Tests]*") + ); + + ReportGenerator(coverageSummaryFile, artifactsForUnitTestsDir); + + if (AppVeyor.IsRunningOnAppVeyor) + { + 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 = !AppVeyor.IsRunningOnAppVeyor ? 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)); + }; + + } + else + { + var settings = new DotNetCoreTestSettings + { + Configuration = compileConfig, + }; + + EnsureDirectoryExists(artifactsForUnitTestsDir); + DotNetCoreTest(unitTestAssemblies, settings); + } + }); + +Task("RunAcceptanceTests") + .IsDependentOn("Compile") + .Does(() => + { + if(TravisCI.IsRunningOnTravisCI) + { + Information( + @"Job: + JobId: {0} + JobNumber: {1} + OSName: {2}", + BuildSystem.TravisCI.Environment.Job.JobId, + BuildSystem.TravisCI.Environment.Job.JobNumber, + BuildSystem.TravisCI.Environment.Job.OSName + ); + + if(TravisCI.Environment.Job.OSName.ToLower() == "osx") + { + return; + } + } + + 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(() => + { + if(TravisCI.IsRunningOnTravisCI) + { + Information( + @"Job: + JobId: {0} + JobNumber: {1} + OSName: {2}", + BuildSystem.TravisCI.Environment.Job.JobId, + BuildSystem.TravisCI.Environment.Job.JobNumber, + BuildSystem.TravisCI.Environment.Job.OSName + ); + + if(TravisCI.Environment.Job.OSName.ToLower() == "osx") + { + return; + } + } + + var settings = new DotNetCoreTestSettings + { + Configuration = compileConfig, + ArgumentCustomization = args => args + .Append("--no-restore") + .Append("--no-build") + }; + + EnsureDirectoryExists(artifactsForIntegrationTestsDir); + DotNetCoreTest(integrationTestAssemblies, settings); + }); + +Task("RunTests") + .IsDependentOn("RunUnitTests") + .IsDependentOn("RunAcceptanceTests") + .IsDependentOn("RunIntegrationTests"); + +Task("CreatePackages") + .IsDependentOn("Compile") + .Does(() => + { + EnsureDirectoryExists(packagesDir); + CopyFiles("./src/**/Ocelot.*.nupkg", packagesDir); + + //GenerateReleaseNotes(releaseNotesFile); + + System.IO.File.WriteAllLines(artifactsFile, new[]{ + "nuget:Ocelot." + buildVersion + ".nupkg", + //"releaseNotes:releasenotes.md" + }); + + if (AppVeyor.IsRunningOnAppVeyor) + { + var path = packagesDir.ToString() + @"/**/*"; + + foreach (var file in GetFiles(path)) + { + AppVeyor.UploadArtifact(file.FullPath); + } + } + }); + +Task("ReleasePackagesToUnstableFeed") + .IsDependentOn("CreatePackages") + .Does(() => + { + if (ShouldPublishToUnstableFeed(nugetFeedUnstableBranchFilter, versioning.BranchName)) + { + PublishPackages(packagesDir, artifactsFile, nugetFeedUnstableKey, nugetFeedUnstableUploadUrl, nugetFeedUnstableSymbolsUploadUrl); + } + }); + +Task("EnsureStableReleaseRequirements") + .Does(() => + { + Information("Check if stable release..."); + + if (!AppVeyor.IsRunningOnAppVeyor) + { + throw new Exception("Stable release should happen via appveyor"); + } + + Information("Running on AppVeyor..."); + + Information("IsTag = " + AppVeyor.Environment.Repository.Tag.IsTag); + + Information("Name = " + AppVeyor.Environment.Repository.Tag.Name); + + var isTag = + AppVeyor.Environment.Repository.Tag.IsTag && + !string.IsNullOrWhiteSpace(AppVeyor.Environment.Repository.Tag.Name); + + if (!isTag) + { + throw new Exception("Stable release should happen from a published GitHub release"); + } + + Information("Release is stable..."); + }); + +Task("UpdateVersionInfo") + .IsDependentOn("EnsureStableReleaseRequirements") + .Does(() => + { + releaseTag = AppVeyor.Environment.Repository.Tag.Name; + AppVeyor.UpdateBuildVersion(releaseTag); + }); + +Task("DownloadGitHubReleaseArtifacts") + .IsDependentOn("UpdateVersionInfo") + .Does(() => + { + try + { + Information("DownloadGitHubReleaseArtifacts"); + + EnsureDirectoryExists(packagesDir); + + Information("Directory exists..."); + + var releaseUrl = tagsUrl + releaseTag; + + Information("Release url " + releaseUrl); + + //var releaseJson = Newtonsoft.Json.Linq.JObject.Parse(GetResource(releaseUrl)); + + var assets_url = Newtonsoft.Json.Linq.JObject.Parse(GetResource(releaseUrl)) + .GetValue("assets_url") + .Value(); + + Information("Assets url " + assets_url); + + var assets = GetResource(assets_url); + + Information("Assets " + assets_url); + + foreach(var asset in Newtonsoft.Json.JsonConvert.DeserializeObject(assets)) + { + Information("In the loop.."); + + var file = packagesDir + File(asset.Value("name")); + + Information("Downloading " + file); + + DownloadFile(asset.Value("browser_download_url"), file); + } + + Information("Out of the loop..."); + } + catch(Exception exception) + { + Information("There was an exception " + exception); + throw; + } + }); + +Task("ReleasePackagesToStableFeed") + .IsDependentOn("DownloadGitHubReleaseArtifacts") + .Does(() => + { + PublishPackages(packagesDir, artifactsFile, nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl); + }); + +Task("Release") + .IsDependentOn("ReleasePackagesToStableFeed"); + +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) +{ + var artifacts = System.IO.File + .ReadAllLines(artifactsFile) + .Select(l => l.Split(':')) + .ToDictionary(v => v[0], v => v[1]); + + var codePackage = packagesDir + File(artifacts["nuget"]); + + Information("Pushing package " + codePackage); + + Information("Calling NuGetPush"); + + NuGetPush( + codePackage, + new NuGetPushSettings { + ApiKey = feedApiKey, + Source = codeFeedUrl + }); +} + +/// 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 ShouldPublishToUnstableFeed(string filter, string branchName) +{ + var regex = new System.Text.RegularExpressions.Regex(filter); + var publish = regex.IsMatch(branchName); + if (publish) + { + Information("Branch " + branchName + " will be published to the unstable feed"); + } + else + { + Information("Branch " + branchName + " will not be published to the unstable feed"); + } + return publish; +} diff --git a/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs index 97045bc8b..769344344 100644 --- a/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs @@ -252,7 +252,7 @@ private string CreateReRouteKey(FileReRoute fileReRoute) return $"{nameof(CookieStickySessions)}:{fileReRoute.LoadBalancerOptions.Key}"; } - return $"{fileReRoute.UpstreamPathTemplate}|{string.Join(",", fileReRoute.UpstreamHttpMethod)}"; + return $"{fileReRoute.UpstreamPathTemplate}|{string.Join(",", fileReRoute.UpstreamHttpMethod)}|{string.Join(",", fileReRoute.DownstreamHostAndPorts.Select(x => $"{x.Host}:{x.Port}"))}"; } } } diff --git a/src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs b/src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs index 21fcfcda0..28fec5059 100644 --- a/src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs +++ b/src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs @@ -1,11 +1,11 @@ -using System.Threading.Tasks; -using Ocelot.Configuration.File; -using Ocelot.Responses; - -namespace Ocelot.Configuration.Setter -{ - public interface IFileConfigurationSetter - { - Task Set(FileConfiguration config); - } -} \ No newline at end of file +using System.Threading.Tasks; +using Ocelot.Configuration.File; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Setter +{ + public interface IFileConfigurationSetter + { + Task Set(FileConfiguration config); + } +} diff --git a/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs index 2bc43cfee..d1fb295ff 100644 --- a/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs +++ b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs @@ -1,55 +1,55 @@ -using System.Collections.Generic; -using System.Linq; -using Ocelot.Configuration; -using Ocelot.Infrastructure; -using Ocelot.Infrastructure.Extensions; -using Ocelot.Middleware; -using Ocelot.Middleware.Multiplexer; -using Ocelot.Request.Middleware; -using Ocelot.Responses; - -namespace Ocelot.Headers -{ - public class HttpResponseHeaderReplacer : IHttpResponseHeaderReplacer - { - private readonly IPlaceholders _placeholders; - - public HttpResponseHeaderReplacer(IPlaceholders placeholders) - { - _placeholders = placeholders; - } - - public Response Replace(DownstreamResponse response, List fAndRs, DownstreamRequest request) - { - foreach (var f in fAndRs) - { - var dict = response.Headers.ToDictionary(x => x.Key); - - //if the response headers contain a matching find and replace - if(dict.TryGetValue(f.Key, out var values)) - { - //check to see if it is a placeholder in the find... - var placeholderValue = _placeholders.Get(f.Find, request); - - if(!placeholderValue.IsError) - { - //if it is we need to get the value of the placeholder - var replaced = values.Values.ToList()[f.Index].Replace(placeholderValue.Data, f.Replace.LastCharAsForwardSlash()); - - response.Headers.Remove(response.Headers.First(item => item.Key == f.Key)); - response.Headers.Add(new Header(f.Key, new List { replaced })); - } - else - { - var replaced = values.Values.ToList()[f.Index].Replace(f.Find, f.Replace); - - response.Headers.Remove(response.Headers.First(item => item.Key == f.Key)); - response.Headers.Add(new Header(f.Key, new List { replaced })); - } - } - } - - return new OkResponse(); - } - } -} +using System.Collections.Generic; +using System.Linq; +using Ocelot.Configuration; +using Ocelot.Infrastructure; +using Ocelot.Infrastructure.Extensions; +using Ocelot.Middleware; +using Ocelot.Middleware.Multiplexer; +using Ocelot.Request.Middleware; +using Ocelot.Responses; + +namespace Ocelot.Headers +{ + public class HttpResponseHeaderReplacer : IHttpResponseHeaderReplacer + { + private readonly IPlaceholders _placeholders; + + public HttpResponseHeaderReplacer(IPlaceholders placeholders) + { + _placeholders = placeholders; + } + + public Response Replace(DownstreamResponse response, List fAndRs, DownstreamRequest request) + { + foreach (var f in fAndRs) + { + var dict = response.Headers.ToDictionary(x => x.Key); + + //if the response headers contain a matching find and replace + if(dict.TryGetValue(f.Key, out var values)) + { + //check to see if it is a placeholder in the find... + var placeholderValue = _placeholders.Get(f.Find, request); + + if(!placeholderValue.IsError) + { + //if it is we need to get the value of the placeholder + var replaced = values.Values.ToList()[f.Index].Replace(placeholderValue.Data, f.Replace.LastCharAsForwardSlash()); + + response.Headers.Remove(response.Headers.First(item => item.Key == f.Key)); + response.Headers.Add(new Header(f.Key, new List { replaced })); + } + else + { + var replaced = values.Values.ToList()[f.Index].Replace(f.Find, f.Replace); + + response.Headers.Remove(response.Headers.First(item => item.Key == f.Key)); + response.Headers.Add(new Header(f.Key, new List { replaced })); + } + } + } + + return new OkResponse(); + } + } +} diff --git a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs index 9dfe3d641..a870cc0f6 100644 --- a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs +++ b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs @@ -1,48 +1,48 @@ -using System.Threading.Tasks; -using Ocelot.Logging; -using Ocelot.Middleware; - -namespace Ocelot.Headers.Middleware -{ - public class HttpHeadersTransformationMiddleware : OcelotMiddleware - { - private readonly OcelotRequestDelegate _next; - private readonly IHttpContextRequestHeaderReplacer _preReplacer; - private readonly IHttpResponseHeaderReplacer _postReplacer; - private readonly IAddHeadersToResponse _addHeadersToResponse; - private readonly IAddHeadersToRequest _addHeadersToRequest; - - public HttpHeadersTransformationMiddleware(OcelotRequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IHttpContextRequestHeaderReplacer preReplacer, - IHttpResponseHeaderReplacer postReplacer, - IAddHeadersToResponse addHeadersToResponse, - IAddHeadersToRequest addHeadersToRequest) - :base(loggerFactory.CreateLogger()) - { - _addHeadersToResponse = addHeadersToResponse; - _addHeadersToRequest = addHeadersToRequest; - _next = next; - _postReplacer = postReplacer; - _preReplacer = preReplacer; - } - - public async Task Invoke(DownstreamContext context) - { - var preFAndRs = context.DownstreamReRoute.UpstreamHeadersFindAndReplace; - - //todo - this should be on httprequestmessage not httpcontext? - _preReplacer.Replace(context.HttpContext, preFAndRs); - - _addHeadersToRequest.SetHeadersOnDownstreamRequest(context.DownstreamReRoute.AddHeadersToUpstream, context.HttpContext); - - await _next.Invoke(context); - - var postFAndRs = context.DownstreamReRoute.DownstreamHeadersFindAndReplace; - - _postReplacer.Replace(context.DownstreamResponse, postFAndRs, context.DownstreamRequest); - - _addHeadersToResponse.Add(context.DownstreamReRoute.AddHeadersToDownstream, context.DownstreamResponse); - } - } -} +using System.Threading.Tasks; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.Headers.Middleware +{ + public class HttpHeadersTransformationMiddleware : OcelotMiddleware + { + private readonly OcelotRequestDelegate _next; + private readonly IHttpContextRequestHeaderReplacer _preReplacer; + private readonly IHttpResponseHeaderReplacer _postReplacer; + private readonly IAddHeadersToResponse _addHeadersToResponse; + private readonly IAddHeadersToRequest _addHeadersToRequest; + + public HttpHeadersTransformationMiddleware(OcelotRequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IHttpContextRequestHeaderReplacer preReplacer, + IHttpResponseHeaderReplacer postReplacer, + IAddHeadersToResponse addHeadersToResponse, + IAddHeadersToRequest addHeadersToRequest) + :base(loggerFactory.CreateLogger()) + { + _addHeadersToResponse = addHeadersToResponse; + _addHeadersToRequest = addHeadersToRequest; + _next = next; + _postReplacer = postReplacer; + _preReplacer = preReplacer; + } + + public async Task Invoke(DownstreamContext context) + { + var preFAndRs = context.DownstreamReRoute.UpstreamHeadersFindAndReplace; + + //todo - this should be on httprequestmessage not httpcontext? + _preReplacer.Replace(context.HttpContext, preFAndRs); + + _addHeadersToRequest.SetHeadersOnDownstreamRequest(context.DownstreamReRoute.AddHeadersToUpstream, context.HttpContext); + + await _next.Invoke(context); + + var postFAndRs = context.DownstreamReRoute.DownstreamHeadersFindAndReplace; + + _postReplacer.Replace(context.DownstreamResponse, postFAndRs, context.DownstreamRequest); + + _addHeadersToResponse.Add(context.DownstreamReRoute.AddHeadersToDownstream, context.DownstreamResponse); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/HeaderTests.cs b/test/Ocelot.AcceptanceTests/HeaderTests.cs index b964161a2..fd8b27678 100644 --- a/test/Ocelot.AcceptanceTests/HeaderTests.cs +++ b/test/Ocelot.AcceptanceTests/HeaderTests.cs @@ -1,365 +1,412 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.File; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.AcceptanceTests -{ - public class HeaderTests : IDisposable - { - private IWebHost _builder; - private int _count; - private readonly Steps _steps; - - public HeaderTests() - { - _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 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(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode) - { - _builder = new WebHostBuilder() - .UseUrls(baseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .Configure(app => - { - app.UsePathBase(basePath); - app.Run(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; - }); - }) - .Build(); - - _builder.Start(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey) - { - _builder = new WebHostBuilder() - .UseUrls(baseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .Configure(app => - { - app.UsePathBase(basePath); - app.Run(async context => - { - if(context.Request.Headers.TryGetValue(headerKey, out var values)) - { - var result = values.First(); - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(result); - } - }); - }) - .Build(); - - _builder.Start(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey, string headerValue) - { - _builder = new WebHostBuilder() - .UseUrls(baseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .Configure(app => - { - app.UsePathBase(basePath); - app.Run(context => - { - context.Response.OnStarting(() => { - context.Response.Headers.Add(headerKey, headerValue); - context.Response.StatusCode = statusCode; - return Task.CompletedTask; - }); - - return Task.CompletedTask; - }); - }) - .Build(); - - _builder.Start(); - } - - public void Dispose() - { - _builder?.Dispose(); - _steps.Dispose(); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class HeaderTests : IDisposable + { + private IWebHost _builder; + private int _count; + private readonly Steps _steps; + + public HeaderTests() + { + _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(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(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; + }); + }) + .Build(); + + _builder.Start(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + if (context.Request.Headers.TryGetValue(headerKey, out var values)) + { + var result = values.First(); + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(result); + } + }); + }) + .Build(); + + _builder.Start(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey, string headerValue) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(context => + { + context.Response.OnStarting(() => + { + context.Response.Headers.Add(headerKey, headerValue); + context.Response.StatusCode = statusCode; + return Task.CompletedTask; + }); + + return Task.CompletedTask; + }); + }) + .Build(); + + _builder.Start(); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs index 84b0c0bb3..44a89929d 100644 --- a/test/Ocelot.IntegrationTests/AdministrationTests.cs +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -1,727 +1,852 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Security.Claims; -using CacheManager.Core; -using IdentityServer4.AccessTokenValidation; -using IdentityServer4.Models; -using IdentityServer4.Test; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Ocelot.Cache; -using Ocelot.Configuration.File; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -[assembly: CollectionBehavior(DisableTestParallelization = true)] -namespace Ocelot.IntegrationTests -{ - public class AdministrationTests : IDisposable - { - private HttpClient _httpClient; - private readonly HttpClient _httpClientTwo; - private HttpResponseMessage _response; - private IWebHost _builder; - private IWebHostBuilder _webHostBuilder; - private string _ocelotBaseUrl; - private BearerToken _token; - private IWebHostBuilder _webHostBuilderTwo; - private IWebHost _builderTwo; - private IWebHost _identityServerBuilder; - - public AdministrationTests() - { - _httpClient = new HttpClient(); - _httpClientTwo = new HttpClient(); - _ocelotBaseUrl = "http://localhost:5000"; - _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); - } - - [Fact] - public void should_return_response_401_with_call_re_routes_controller() - { - var configuration = new FileConfiguration(); - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunning()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_call_re_routes_controller() - { - var configuration = new FileConfiguration(); - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunning()) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_call_re_routes_controller_using_base_url_added_in_file_config() - { - _httpClient = new HttpClient(); - _ocelotBaseUrl = "http://localhost:5011"; - _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); - - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - BaseUrl = _ocelotBaseUrl - } - }; - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunningWithNoWebHostBuilder(_ocelotBaseUrl)) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void should_be_able_to_use_token_from_ocelot_a_on_ocelot_b() - { - var configuration = new FileConfiguration(); - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenIdentityServerSigningEnvironmentalVariablesAreSet()) - .And(x => GivenOcelotIsRunning()) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenAnotherOcelotIsRunning("http://localhost:5007")) - .When(x => WhenIGetUrlOnTheSecondOcelot("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void should_return_file_configuration() - { - var configuration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - RequestIdKey = "RequestId", - ServiceDiscoveryProvider = new FileServiceDiscoveryProvider - { - Host = "127.0.0.1", - } - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/", - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 10, - Region = "Geoff" - } - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/test", - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 10, - Region = "Dave" - } - } - } - }; - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenOcelotIsRunning()) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseShouldBe(configuration)) - .BDDfy(); - } - - [Fact] - public void should_get_file_configuration_edit_and_post_updated_version() - { - var initialConfiguration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/" - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/test" - } - } - }; - - var updatedConfiguration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "http", - DownstreamPathTemplate = "/geoffrey", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/" - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "123.123.123", - Port = 443, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/blooper/{productId}", - UpstreamHttpMethod = new List { "post" }, - UpstreamPathTemplate = "/test" - } - } - }; - - this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) - .And(x => GivenOcelotIsRunning()) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .When(x => WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration)) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => ThenTheResponseShouldBe(updatedConfiguration)) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .And(x => ThenTheResponseShouldBe(updatedConfiguration)) - .BDDfy(); - } - - [Fact] - public void should_clear_region() - { - var initialConfiguration = new FileConfiguration - { - GlobalConfiguration = new FileGlobalConfiguration - { - }, - ReRoutes = new List() - { - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/", - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 10 - } - }, - new FileReRoute() - { - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 80, - } - }, - DownstreamScheme = "https", - DownstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "get" }, - UpstreamPathTemplate = "/test", - FileCacheOptions = new FileCacheOptions - { - TtlSeconds = 10 - } - } - } - }; - - var regionToClear = "gettest"; - - this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) - .And(x => GivenOcelotIsRunning()) - .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIDeleteOnTheApiGateway($"/administration/outputcache/{regionToClear}")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NoContent)) - .BDDfy(); - } - - [Fact] - public void should_return_response_200_with_call_re_routes_controller_when_using_own_identity_server_to_secure_admin_area() - { - var configuration = new FileConfiguration(); - - var identityServerRootUrl = "http://localhost:5123"; - - Action options = o => { - o.Authority = identityServerRootUrl; - o.ApiName = "api"; - o.RequireHttpsMetadata = false; - o.SupportedTokens = SupportedTokens.Both; - o.ApiSecret = "secret"; - }; - - this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenThereIsAnIdentityServerOn(identityServerRootUrl, "api")) - .And(x => GivenOcelotIsRunningWithIdentityServerSettings(options)) - .And(x => GivenIHaveAToken(identityServerRootUrl)) - .And(x => GivenIHaveAddedATokenToMyRequest()) - .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) - .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - private void GivenIHaveAToken(string url) - { - var formData = new List> - { - new KeyValuePair("client_id", "api"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "api"), - new KeyValuePair("username", "test"), - new KeyValuePair("password", "test"), - new KeyValuePair("grant_type", "password") - }; - var content = new FormUrlEncodedContent(formData); - - using (var httpClient = new HttpClient()) - { - var response = httpClient.PostAsync($"{url}/connect/token", content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } - } - - private void GivenThereIsAnIdentityServerOn(string url, string apiName) - { - _identityServerBuilder = new WebHostBuilder() - .UseUrls(url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureServices(services => - { - services.AddLogging(); - services.AddIdentityServer() - .AddDeveloperSigningCredential() - .AddInMemoryApiResources(new List - { - new ApiResource - { - Name = apiName, - Description = apiName, - Enabled = true, - DisplayName = apiName, - Scopes = new List() - { - new Scope(apiName) - } - } - }) - .AddInMemoryClients(new List - { - new Client - { - ClientId = apiName, - AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, - ClientSecrets = new List {new Secret("secret".Sha256())}, - AllowedScopes = new List { apiName }, - AccessTokenType = AccessTokenType.Jwt, - Enabled = true - } - }) - .AddTestUsers(new List - { - new TestUser - { - Username = "test", - Password = "test", - SubjectId = "1231231" - } - }); - }) - .Configure(app => - { - app.UseIdentityServer(); - }) - .Build(); - - _identityServerBuilder.Start(); - - using (var httpClient = new HttpClient()) - { - var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result; - response.EnsureSuccessStatusCode(); - } - } - - private void GivenAnotherOcelotIsRunning(string baseUrl) - { - _httpClientTwo.BaseAddress = new Uri(baseUrl); - - _webHostBuilderTwo = new WebHostBuilder() - .UseUrls(baseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(x => - { - Action settings = (s) => - { - s.WithMicrosoftLogging(log => - { - log.AddConsole(LogLevel.Debug); - }) - .WithDictionaryHandle(); - }; - - x.AddOcelot() - .AddCacheManager(settings) - .AddAdministration("/administration", "secret"); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - _builderTwo = _webHostBuilderTwo.Build(); - - _builderTwo.Start(); - } - - private void GivenIdentityServerSigningEnvironmentalVariablesAreSet() - { - Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", "idsrv3test.pfx"); - Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", "idsrv3test"); - } - - private void WhenIGetUrlOnTheSecondOcelot(string url) - { - _httpClientTwo.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - _response = _httpClientTwo.GetAsync(url).Result; - } - - private void WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration) - { - var json = JsonConvert.SerializeObject(updatedConfiguration); - var content = new StringContent(json); - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - _response = _httpClient.PostAsync(url, content).Result; - } - - private void ThenTheResponseShouldBe(List expected) - { - var content = _response.Content.ReadAsStringAsync().Result; - var result = JsonConvert.DeserializeObject(content); - result.Value.ShouldBe(expected); - } - - private void ThenTheResponseShouldBe(FileConfiguration expecteds) - { - var response = JsonConvert.DeserializeObject(_response.Content.ReadAsStringAsync().Result); - - response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey); - response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host); - response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port); - - for (var i = 0; i < response.ReRoutes.Count; i++) - { - for (var j = 0; j < response.ReRoutes[i].DownstreamHostAndPorts.Count; j++) - { - var result = response.ReRoutes[i].DownstreamHostAndPorts[j]; - var expected = expecteds.ReRoutes[i].DownstreamHostAndPorts[j]; - result.Host.ShouldBe(expected.Host); - result.Port.ShouldBe(expected.Port); - } - - response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].DownstreamPathTemplate); - response.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.ReRoutes[i].DownstreamScheme); - response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].UpstreamPathTemplate); - response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expecteds.ReRoutes[i].UpstreamHttpMethod); - } - } - - private void GivenIHaveAddedATokenToMyRequest() - { - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - } - - private void GivenIHaveAnOcelotToken(string adminPath) - { - var tokenUrl = $"{adminPath}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", "admin"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "admin"), - new KeyValuePair("grant_type", "client_credentials") - }; - var content = new FormUrlEncodedContent(formData); - - var response = _httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - var configPath = $"{adminPath}/.well-known/openid-configuration"; - response = _httpClient.GetAsync(configPath).Result; - response.EnsureSuccessStatusCode(); - } - - private void GivenOcelotIsRunningWithIdentityServerSettings(Action configOptions) - { - _webHostBuilder = 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: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(x => { - x.AddSingleton(_webHostBuilder); - x.AddOcelot() - .AddCacheManager(c => - { - c.WithDictionaryHandle(); - }) - .AddAdministration("/administration", configOptions); - }) - .Configure(app => { - app.UseOcelot().Wait(); - }); - - _builder = _webHostBuilder.Build(); - - _builder.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: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(x => - { - Action settings = (s) => - { - s.WithMicrosoftLogging(log => - { - log.AddConsole(LogLevel.Debug); - }) - .WithDictionaryHandle(); - }; - - x.AddOcelot() - .AddCacheManager(settings) - .AddAdministration("/administration", "secret"); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - _builder = _webHostBuilder.Build(); - - _builder.Start(); - } - - private void GivenOcelotIsRunningWithNoWebHostBuilder(string baseUrl) - { - _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: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("ocelot.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(x => { - x.AddSingleton(_webHostBuilder); - x.AddOcelot() - .AddCacheManager(c => - { - c.WithDictionaryHandle(); - }) - .AddAdministration("/administration", "secret"); - }) - .Configure(app => { - app.UseOcelot().Wait(); - }); - - _builder = _webHostBuilder.Build(); - - _builder.Start(); - } - - private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) - { - var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; - - var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - - var text = File.ReadAllText(configurationPath); - - configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - - text = File.ReadAllText(configurationPath); - } - - private void WhenIGetUrlOnTheApiGateway(string url) - { - _response = _httpClient.GetAsync(url).Result; - } - - private void WhenIDeleteOnTheApiGateway(string url) - { - _response = _httpClient.DeleteAsync(url).Result; - } - - private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) - { - _response.StatusCode.ShouldBe(expectedHttpStatusCode); - } - - public void Dispose() - { - Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", ""); - Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", ""); - _builder?.Dispose(); - _httpClient?.Dispose(); - _identityServerBuilder?.Dispose(); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using CacheManager.Core; +using IdentityServer4.AccessTokenValidation; +using IdentityServer4.Models; +using IdentityServer4.Test; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ocelot.Cache; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] +namespace Ocelot.IntegrationTests +{ + public class AdministrationTests : IDisposable + { + private HttpClient _httpClient; + private readonly HttpClient _httpClientTwo; + private HttpResponseMessage _response; + private IWebHost _builder; + private IWebHostBuilder _webHostBuilder; + private string _ocelotBaseUrl; + private BearerToken _token; + private IWebHostBuilder _webHostBuilderTwo; + private IWebHost _builderTwo; + private IWebHost _identityServerBuilder; + private IWebHost _fooServiceBuilder; + private IWebHost _barServiceBuilder; + + public AdministrationTests() + { + _httpClient = new HttpClient(); + _httpClientTwo = new HttpClient(); + _ocelotBaseUrl = "http://localhost:5000"; + _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); + } + + [Fact] + public void should_return_response_401_with_call_re_routes_controller() + { + var configuration = new FileConfiguration(); + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_call_re_routes_controller() + { + var configuration = new FileConfiguration(); + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_call_re_routes_controller_using_base_url_added_in_file_config() + { + _httpClient = new HttpClient(); + _ocelotBaseUrl = "http://localhost:5011"; + _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); + + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + BaseUrl = _ocelotBaseUrl + } + }; + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunningWithNoWebHostBuilder(_ocelotBaseUrl)) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_be_able_to_use_token_from_ocelot_a_on_ocelot_b() + { + var configuration = new FileConfiguration(); + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenIdentityServerSigningEnvironmentalVariablesAreSet()) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenAnotherOcelotIsRunning("http://localhost:5007")) + .When(x => WhenIGetUrlOnTheSecondOcelot("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_return_file_configuration() + { + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + RequestIdKey = "RequestId", + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "127.0.0.1", + } + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10, + Region = "Geoff" + } + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/test", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10, + Region = "Dave" + } + } + } + }; + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseShouldBe(configuration)) + .BDDfy(); + } + + [Fact] + public void should_get_file_configuration_edit_and_post_updated_version() + { + var initialConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/test" + } + } + }; + + var updatedConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/geoffrey", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "123.123.123", + Port = 443, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/blooper/{productId}", + UpstreamHttpMethod = new List { "post" }, + UpstreamPathTemplate = "/test" + } + } + }; + + this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .When(x => WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseShouldBe(updatedConfiguration)) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .And(x => ThenTheResponseShouldBe(updatedConfiguration)) + .BDDfy(); + } + + [Fact] + public void should_get_file_configuration_edit_and_post_updated_version_redirecting_reroute() + { + var fooPort = 47689; + var barPort = 47690; + + var initialConfiguration = new FileConfiguration + { + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = fooPort, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/foo", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/foo" + } + } + }; + + var updatedConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = barPort, + } + }, + DownstreamScheme = "http", + DownstreamPathTemplate = "/bar", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/foo" + } + } + }; + + this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) + .And(x => GivenThereIsAFooServiceRunningOn($"http://localhost:{fooPort}")) + .And(x => GivenThereIsABarServiceRunningOn($"http://localhost:{barPort}")) + .And(x => GivenOcelotIsRunning()) + .And(x => WhenIGetUrlOnTheApiGateway("/foo")) + .Then(x => ThenTheResponseBodyShouldBe("foo")) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseShouldBe(updatedConfiguration)) + .And(x => WhenIGetUrlOnTheApiGateway("/foo")) + .Then(x => ThenTheResponseBodyShouldBe("bar")) + .When(x => WhenIPostOnTheApiGateway("/administration/configuration", initialConfiguration)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseShouldBe(initialConfiguration)) + .And(x => WhenIGetUrlOnTheApiGateway("/foo")) + .Then(x => ThenTheResponseBodyShouldBe("foo")) + .BDDfy(); + } + + [Fact] + public void should_clear_region() + { + var initialConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10 + } + }, + new FileReRoute() + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "get" }, + UpstreamPathTemplate = "/test", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 10 + } + } + } + }; + + var regionToClear = "gettest"; + + this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIDeleteOnTheApiGateway($"/administration/outputcache/{regionToClear}")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NoContent)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_call_re_routes_controller_when_using_own_identity_server_to_secure_admin_area() + { + var configuration = new FileConfiguration(); + + var identityServerRootUrl = "http://localhost:5123"; + + Action options = o => { + o.Authority = identityServerRootUrl; + o.ApiName = "api"; + o.RequireHttpsMetadata = false; + o.SupportedTokens = SupportedTokens.Both; + o.ApiSecret = "secret"; + }; + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenThereIsAnIdentityServerOn(identityServerRootUrl, "api")) + .And(x => GivenOcelotIsRunningWithIdentityServerSettings(options)) + .And(x => GivenIHaveAToken(identityServerRootUrl)) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + private void GivenIHaveAToken(string url) + { + var formData = new List> + { + new KeyValuePair("client_id", "api"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "api"), + new KeyValuePair("username", "test"), + new KeyValuePair("password", "test"), + new KeyValuePair("grant_type", "password") + }; + var content = new FormUrlEncodedContent(formData); + + using (var httpClient = new HttpClient()) + { + var response = httpClient.PostAsync($"{url}/connect/token", content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + } + } + + private void GivenThereIsAnIdentityServerOn(string url, string apiName) + { + _identityServerBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureServices(services => + { + services.AddLogging(); + services.AddIdentityServer() + .AddDeveloperSigningCredential() + .AddInMemoryApiResources(new List + { + new ApiResource + { + Name = apiName, + Description = apiName, + Enabled = true, + DisplayName = apiName, + Scopes = new List() + { + new Scope(apiName) + } + } + }) + .AddInMemoryClients(new List + { + new Client + { + ClientId = apiName, + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List {new Secret("secret".Sha256())}, + AllowedScopes = new List { apiName }, + AccessTokenType = AccessTokenType.Jwt, + Enabled = true + } + }) + .AddTestUsers(new List + { + new TestUser + { + Username = "test", + Password = "test", + SubjectId = "1231231" + } + }); + }) + .Configure(app => + { + app.UseIdentityServer(); + }) + .Build(); + + _identityServerBuilder.Start(); + + using (var httpClient = new HttpClient()) + { + var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result; + response.EnsureSuccessStatusCode(); + } + } + + private void GivenAnotherOcelotIsRunning(string baseUrl) + { + _httpClientTwo.BaseAddress = new Uri(baseUrl); + + _webHostBuilderTwo = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddJsonFile("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(x => + { + Action settings = (s) => + { + s.WithMicrosoftLogging(log => + { + log.AddConsole(LogLevel.Debug); + }) + .WithDictionaryHandle(); + }; + + x.AddOcelot() + .AddCacheManager(settings) + .AddAdministration("/administration", "secret"); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + + _builderTwo = _webHostBuilderTwo.Build(); + + _builderTwo.Start(); + } + + private void GivenIdentityServerSigningEnvironmentalVariablesAreSet() + { + Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", "idsrv3test.pfx"); + Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", "idsrv3test"); + } + + private void WhenIGetUrlOnTheSecondOcelot(string url) + { + _httpClientTwo.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + _response = _httpClientTwo.GetAsync(url).Result; + } + + private void WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration) + { + var json = JsonConvert.SerializeObject(updatedConfiguration); + var content = new StringContent(json); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + _response = _httpClient.PostAsync(url, content).Result; + } + + private void ThenTheResponseShouldBe(List expected) + { + var content = _response.Content.ReadAsStringAsync().Result; + var result = JsonConvert.DeserializeObject(content); + result.Value.ShouldBe(expected); + } + + private void ThenTheResponseBodyShouldBe(string expected) + { + var content = _response.Content.ReadAsStringAsync().Result; + content.ShouldBe(expected); + } + + private void ThenTheResponseShouldBe(FileConfiguration expecteds) + { + var response = JsonConvert.DeserializeObject(_response.Content.ReadAsStringAsync().Result); + + response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey); + response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host); + response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port); + + for (var i = 0; i < response.ReRoutes.Count; i++) + { + for (var j = 0; j < response.ReRoutes[i].DownstreamHostAndPorts.Count; j++) + { + var result = response.ReRoutes[i].DownstreamHostAndPorts[j]; + var expected = expecteds.ReRoutes[i].DownstreamHostAndPorts[j]; + result.Host.ShouldBe(expected.Host); + result.Port.ShouldBe(expected.Port); + } + + response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].DownstreamPathTemplate); + response.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.ReRoutes[i].DownstreamScheme); + response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].UpstreamPathTemplate); + response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expecteds.ReRoutes[i].UpstreamHttpMethod); + } + } + + private void GivenIHaveAddedATokenToMyRequest() + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + } + + private void GivenIHaveAnOcelotToken(string adminPath) + { + var tokenUrl = $"{adminPath}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", "admin"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "admin"), + new KeyValuePair("grant_type", "client_credentials") + }; + var content = new FormUrlEncodedContent(formData); + + var response = _httpClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + var configPath = $"{adminPath}/.well-known/openid-configuration"; + response = _httpClient.GetAsync(configPath).Result; + response.EnsureSuccessStatusCode(); + } + + private void GivenOcelotIsRunningWithIdentityServerSettings(Action configOptions) + { + _webHostBuilder = 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: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddJsonFile("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(x => { + x.AddSingleton(_webHostBuilder); + x.AddOcelot() + .AddCacheManager(c => + { + c.WithDictionaryHandle(); + }) + .AddAdministration("/administration", configOptions); + }) + .Configure(app => { + app.UseOcelot().Wait(); + }); + + _builder = _webHostBuilder.Build(); + + _builder.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: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddJsonFile("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(x => + { + Action settings = (s) => + { + s.WithMicrosoftLogging(log => + { + log.AddConsole(LogLevel.Debug); + }) + .WithDictionaryHandle(); + }; + + x.AddOcelot() + .AddCacheManager(settings) + .AddAdministration("/administration", "secret"); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + + _builder = _webHostBuilder.Build(); + + _builder.Start(); + } + + private void GivenOcelotIsRunningWithNoWebHostBuilder(string baseUrl) + { + _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: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddJsonFile("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(x => { + x.AddSingleton(_webHostBuilder); + x.AddOcelot() + .AddCacheManager(c => + { + c.WithDictionaryHandle(); + }) + .AddAdministration("/administration", "secret"); + }) + .Configure(app => { + app.UseOcelot().Wait(); + }); + + _builder = _webHostBuilder.Build(); + + _builder.Start(); + } + + private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + var text = File.ReadAllText(configurationPath); + + configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + text = File.ReadAllText(configurationPath); + } + + private void WhenIGetUrlOnTheApiGateway(string url) + { + _response = _httpClient.GetAsync(url).Result; + } + + private void WhenIDeleteOnTheApiGateway(string url) + { + _response = _httpClient.DeleteAsync(url).Result; + } + + private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) + { + _response.StatusCode.ShouldBe(expectedHttpStatusCode); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", ""); + Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", ""); + _builder?.Dispose(); + _httpClient?.Dispose(); + _identityServerBuilder?.Dispose(); + } + + private void GivenThereIsAFooServiceRunningOn(string baseUrl) + { + _fooServiceBuilder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase("/foo"); + app.Run(async context => + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync("foo"); + }); + }) + .Build(); + + _fooServiceBuilder.Start(); + } + + private void GivenThereIsABarServiceRunningOn(string baseUrl) + { + _barServiceBuilder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase("/bar"); + app.Run(async context => + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync("bar"); + }); + }) + .Build(); + + _barServiceBuilder.Start(); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs index 459976f34..79111e2e5 100644 --- a/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs @@ -199,7 +199,7 @@ public void should_set_up_aggregate_re_route() .WithDownstreamScheme("http") .WithUpstreamHttpMethod(new List() {"Get"}) .WithDownstreamAddresses(new List() {new DownstreamHostAndPort("localhost", 51878)}) - .WithLoadBalancerKey("/laura|Get") + .WithLoadBalancerKey("/laura|Get|localhost:51878") .Build(); var lauraReRoute = new ReRouteBuilder() @@ -218,7 +218,7 @@ public void should_set_up_aggregate_re_route() .WithDownstreamScheme("http") .WithUpstreamHttpMethod(new List() { "Get" }) .WithDownstreamAddresses(new List() { new DownstreamHostAndPort("localhost", 51878) }) - .WithLoadBalancerKey("/tom|Get") + .WithLoadBalancerKey("/tom|Get|localhost:51880") .Build(); var tomReRoute = new ReRouteBuilder() @@ -409,7 +409,7 @@ public void should_use_downstream_host() .WithDownstreamPathTemplate("/products/{productId}") .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod(new List {"Get"}) - .WithLoadBalancerKey("/api/products/{productId}|Get") + .WithLoadBalancerKey("/api/products/{productId}|Get|127.0.0.1:0") .Build(); this.Given(x => x.GivenTheConfigIs(new FileConfiguration @@ -461,7 +461,7 @@ public void should_use_downstream_scheme() .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod(new List {"Get"}) .WithDelegatingHandlers(handlers) - .WithLoadBalancerKey("/api/products/{productId}|Get") + .WithLoadBalancerKey("/api/products/{productId}|Get|") .Build(); this.Given(x => x.GivenTheConfigIs(new FileConfiguration @@ -506,7 +506,7 @@ public void should_use_service_discovery_for_downstream_service_host() .WithUpstreamHttpMethod(new List {"Get"}) .WithUseServiceDiscovery(true) .WithServiceName("ProductService") - .WithLoadBalancerKey("/api/products/{productId}|Get") + .WithLoadBalancerKey("/api/products/{productId}|Get|") .Build(); this.Given(x => x.GivenTheConfigIs(new FileConfiguration @@ -557,7 +557,7 @@ public void should_not_use_service_discovery_for_downstream_host_url_when_no_ser .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod(new List {"Get"}) .WithUseServiceDiscovery(false) - .WithLoadBalancerKey("/api/products/{productId}|Get") + .WithLoadBalancerKey("/api/products/{productId}|Get|") .Build(); this.Given(x => x.GivenTheConfigIs(new FileConfiguration @@ -600,7 +600,7 @@ public void should_call_template_pattern_creator_correctly() .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod(new List {"Get"}) .WithUpstreamTemplatePattern(new UpstreamPathTemplate("(?i)/api/products/.*/$", 1)) - .WithLoadBalancerKey("/api/products/{productId}|Get") + .WithLoadBalancerKey("/api/products/{productId}|Get|") .Build(); this.Given(x => x.GivenTheConfigIs(new FileConfiguration @@ -645,7 +645,7 @@ public void should_call_request_id_creator() .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod(new List {"Get"}) .WithRequestIdKey("blahhhh") - .WithLoadBalancerKey("/api/products/{productId}|Get") + .WithLoadBalancerKey("/api/products/{productId}|Get|") .Build(); this.Given(x => x.GivenTheConfigIs(new FileConfiguration @@ -740,7 +740,7 @@ public void should_create_with_headers_to_extract(FileConfiguration fileConfig) { new ClaimToThing("CustomerId", "CustomerId", "", 0), }) - .WithLoadBalancerKey("/api/products/{productId}|Get") + .WithLoadBalancerKey("/api/products/{productId}|Get|") .Build(); var expected = new List @@ -783,7 +783,7 @@ public void should_create_with_authentication_properties(FileConfiguration fileC .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod(new List {"Get"}) .WithAuthenticationOptions(authenticationOptions) - .WithLoadBalancerKey("/api/products/{productId}|Get") + .WithLoadBalancerKey("/api/products/{productId}|Get|") .Build(); var expected = new List