diff --git a/HttpClientInterception.sln b/HttpClientInterception.sln deleted file mode 100644 index e0c1e175..00000000 --- a/HttpClientInterception.sln +++ /dev/null @@ -1,125 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31423.177 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{00F2B78C-D33E-4090-9888-C33435B02F1F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1A2A0990-94DE-4F14-850B-39BA7183BC63}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - .gitattributes = .gitattributes - .gitignore = .gitignore - .vsconfig = .vsconfig - build.ps1 = build.ps1 - Directory.Build.props = Directory.Build.props - Directory.Build.targets = Directory.Build.targets - Directory.Packages.props = Directory.Packages.props - global.json = global.json - HttpClientInterception.ruleset = HttpClientInterception.ruleset - LICENSE = LICENSE - NuGet.config = NuGet.config - README.md = README.md - SECURITY.md = SECURITY.md - stylecop.json = stylecop.json - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{6DC2A1B9-1D37-4062-A75D-6A03C7367A46}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JustEat.HttpClientInterception", "src\HttpClientInterception\JustEat.HttpClientInterception.csproj", "{ED8F9445-5C18-48C1-B946-CBF8AC4A2C47}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JustEat.HttpClientInterception.Tests", "tests\HttpClientInterception.Tests\JustEat.HttpClientInterception.Tests.csproj", "{F9CCB17C-1E8E-4197-BC6A-ABB350F41F07}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{58FB0A27-4361-45ED-BB97-FD836F5FEF24}" - ProjectSection(SolutionItems) = preProject - samples\README.md = samples\README.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApp", "samples\SampleApp\SampleApp.csproj", "{A9B2AF56-BAD5-4099-AE88-37B71E72C20D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApp.Tests", "samples\SampleApp.Tests\SampleApp.Tests.csproj", "{F02ABFCA-0CAB-4A99-A9F5-B9AC8FADE8C3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JustEat.HttpClientInterception.Benchmarks", "tests\HttpClientInterception.Benchmarks\JustEat.HttpClientInterception.Benchmarks.csproj", "{E2EC0ECB-DA6E-4A03-85D2-1625475B4EA8}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{F258F815-A7C5-44F7-AF55-E12C0F1FB023}" - ProjectSection(SolutionItems) = preProject - .github\CODEOWNERS = .github\CODEOWNERS - .github\CONTRIBUTING.md = .github\CONTRIBUTING.md - .github\dependabot.yml = .github\dependabot.yml - .github\ISSUE_TEMPLATE.md = .github\ISSUE_TEMPLATE.md - .github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md - .github\stale.yml = .github\stale.yml - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEMPLATE", "{9071A217-7882-4C3D-A7F6-3D7A56D6177D}" - ProjectSection(SolutionItems) = preProject - .github\ISSUE_TEMPLATE\bug_report.md = .github\ISSUE_TEMPLATE\bug_report.md - .github\ISSUE_TEMPLATE\feature_request.md = .github\ISSUE_TEMPLATE\feature_request.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".vscode", ".vscode", "{D8A60EDB-E535-4DE2-9284-E758F1FDA350}" - ProjectSection(SolutionItems) = preProject - .vscode\launch.json = .vscode\launch.json - .vscode\tasks.json = .vscode\tasks.json - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{8127648B-FBA5-487E-8583-0E4A374C65DC}" - ProjectSection(SolutionItems) = preProject - .github\workflows\approve-and-merge.yml = .github\workflows\approve-and-merge.yml - .github\workflows\build.yml = .github\workflows\build.yml - .github\workflows\bump-version.yml = .github\workflows\bump-version.yml - .github\workflows\codeql.yml = .github\workflows\codeql.yml - .github\workflows\dependabot-approve.yml = .github\workflows\dependabot-approve.yml - .github\workflows\dependency-review.yml = .github\workflows\dependency-review.yml - .github\workflows\lint.yml = .github\workflows\lint.yml - .github\workflows\release.yml = .github\workflows\release.yml - .github\workflows\scorecard.yml = .github\workflows\scorecard.yml - .github\workflows\update-docs.yml = .github\workflows\update-docs.yml - .github\workflows\update-dotnet-sdk.yml = .github\workflows\update-dotnet-sdk.yml - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {ED8F9445-5C18-48C1-B946-CBF8AC4A2C47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ED8F9445-5C18-48C1-B946-CBF8AC4A2C47}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ED8F9445-5C18-48C1-B946-CBF8AC4A2C47}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ED8F9445-5C18-48C1-B946-CBF8AC4A2C47}.Release|Any CPU.Build.0 = Release|Any CPU - {F9CCB17C-1E8E-4197-BC6A-ABB350F41F07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F9CCB17C-1E8E-4197-BC6A-ABB350F41F07}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F9CCB17C-1E8E-4197-BC6A-ABB350F41F07}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F9CCB17C-1E8E-4197-BC6A-ABB350F41F07}.Release|Any CPU.Build.0 = Release|Any CPU - {A9B2AF56-BAD5-4099-AE88-37B71E72C20D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A9B2AF56-BAD5-4099-AE88-37B71E72C20D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A9B2AF56-BAD5-4099-AE88-37B71E72C20D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A9B2AF56-BAD5-4099-AE88-37B71E72C20D}.Release|Any CPU.Build.0 = Release|Any CPU - {F02ABFCA-0CAB-4A99-A9F5-B9AC8FADE8C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F02ABFCA-0CAB-4A99-A9F5-B9AC8FADE8C3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F02ABFCA-0CAB-4A99-A9F5-B9AC8FADE8C3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F02ABFCA-0CAB-4A99-A9F5-B9AC8FADE8C3}.Release|Any CPU.Build.0 = Release|Any CPU - {E2EC0ECB-DA6E-4A03-85D2-1625475B4EA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E2EC0ECB-DA6E-4A03-85D2-1625475B4EA8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E2EC0ECB-DA6E-4A03-85D2-1625475B4EA8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E2EC0ECB-DA6E-4A03-85D2-1625475B4EA8}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {ED8F9445-5C18-48C1-B946-CBF8AC4A2C47} = {00F2B78C-D33E-4090-9888-C33435B02F1F} - {F9CCB17C-1E8E-4197-BC6A-ABB350F41F07} = {6DC2A1B9-1D37-4062-A75D-6A03C7367A46} - {A9B2AF56-BAD5-4099-AE88-37B71E72C20D} = {58FB0A27-4361-45ED-BB97-FD836F5FEF24} - {F02ABFCA-0CAB-4A99-A9F5-B9AC8FADE8C3} = {58FB0A27-4361-45ED-BB97-FD836F5FEF24} - {E2EC0ECB-DA6E-4A03-85D2-1625475B4EA8} = {6DC2A1B9-1D37-4062-A75D-6A03C7367A46} - {F258F815-A7C5-44F7-AF55-E12C0F1FB023} = {1A2A0990-94DE-4F14-850B-39BA7183BC63} - {9071A217-7882-4C3D-A7F6-3D7A56D6177D} = {F258F815-A7C5-44F7-AF55-E12C0F1FB023} - {D8A60EDB-E535-4DE2-9284-E758F1FDA350} = {1A2A0990-94DE-4F14-850B-39BA7183BC63} - {8127648B-FBA5-487E-8583-0E4A374C65DC} = {F258F815-A7C5-44F7-AF55-E12C0F1FB023} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {E4BC3B24-A96F-46C5-9405-CBE87A972881} - EndGlobalSection -EndGlobal diff --git a/HttpClientInterception.slnx b/HttpClientInterception.slnx new file mode 100644 index 00000000..e5a989bc --- /dev/null +++ b/HttpClientInterception.slnx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/HttpClientInterception/HttpClientInterceptorOptions.cs b/src/HttpClientInterception/HttpClientInterceptorOptions.cs index 1bc4f464..b286cde6 100644 --- a/src/HttpClientInterception/HttpClientInterceptorOptions.cs +++ b/src/HttpClientInterception/HttpClientInterceptorOptions.cs @@ -575,7 +575,17 @@ private static async Task BuildResponseAsync(HttpRequestMes { var responses = _mappings.Values .OrderByDescending((p) => p.Priority.HasValue) - .ThenBy((p) => p.Priority); + .ThenBy((p) => p.Priority) + .ThenByDescending((p) => + { + // Sort by the number of request headers, so that those with more headers expected are matched first. + // Count() is not used to avoid side-effects of enumerating the headers if they are dynamically generated. +#if NET8_0_OR_GREATER + return p.RequestHeaders?.TryGetNonEnumeratedCount(out var count) is true ? count : 0; +#else + return p.RequestHeaders is ICollection>> collection ? collection.Count : 0; +#endif + }); foreach (var response in responses) { diff --git a/tests/HttpClientInterception.Tests/Bundles/BundleExtensionsTests.cs b/tests/HttpClientInterception.Tests/Bundles/BundleExtensionsTests.cs index 3d1aaa67..da38bfc2 100644 --- a/tests/HttpClientInterception.Tests/Bundles/BundleExtensionsTests.cs +++ b/tests/HttpClientInterception.Tests/Bundles/BundleExtensionsTests.cs @@ -601,4 +601,34 @@ public static void Can_Register_Bundle_With_Null_Header_Values_In_Bundle() // Act options.RegisterBundle(Path.Join("Bundles", "templated-bundle-null-headers.json"), headers); } + + [Fact] + public static async Task Can_Intercept_Http_Requests_With_Correct_Precedence_For_Http_Request_Headers() + { + // Arrange + var options = new HttpClientInterceptorOptions().ThrowsOnMissingRegistration(); + + var requestUrl = "https://registry.hub.docker.com/v2/user/image/manifests/latest"; + + var unauthorized = new Dictionary() + { + ["Accept"] = "application/vnd.oci.image.index.v1+json", + }; + + var authorized = new Dictionary() + { + ["Accept"] = "application/vnd.oci.image.index.v1+json", + ["Authorization"] = "Bearer not-a-real-docker-hub-token", + }; + + options.RegisterBundle(Path.Join("Bundles", "header-matching.json")); + + // Act + var first = await HttpAssert.GetAsync(options, requestUrl, headers: unauthorized); + var second = await HttpAssert.GetAsync(options, requestUrl, headers: authorized); + + // Assert + first.ShouldBe("unauthorized"); + second.ShouldBe("authorized"); + } } diff --git a/tests/HttpClientInterception.Tests/Bundles/header-matching.json b/tests/HttpClientInterception.Tests/Bundles/header-matching.json new file mode 100644 index 00000000..2eab7114 --- /dev/null +++ b/tests/HttpClientInterception.Tests/Bundles/header-matching.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://raw.githubusercontent.com/justeattakeaway/httpclient-interception/main/src/HttpClientInterception/Bundles/http-request-bundle-schema.json", + "id": "test-http-request-bundle", + "comment": "An HTTP request bundle for testing HTTP request header matching for the same URL.", + "version": 1, + "items": [ + { + "id": "authorized", + "comment": "An HTTP request with an Authorization header.", + "uri": "https://registry.hub.docker.com/v2/user/image/manifests/latest", + "requestHeaders": { + "Accept": [ + "application/vnd.oci.image.index.v1+json" + ], + "Authorization": [ + "Bearer not-a-real-docker-hub-token" + ] + }, + "contentString": "authorized" + }, + { + "id": "unauthorized", + "comment": "An HTTP request without an Authorization header.", + "uri": "https://registry.hub.docker.com/v2/user/image/manifests/latest", + "requestHeaders": { + "Accept": [ + "application/vnd.oci.image.index.v1+json" + ] + }, + "contentHeaders": { + "Content-Type": [ "application/json" ] + }, + "contentString": "unauthorized" + } + ] +}