├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG ├── LICENSE ├── Package.props ├── README.md ├── Release.proj ├── RichardSzalay.MockHttp.Tests ├── Infrastructure │ └── HttpHelpers.cs ├── Issues │ ├── Issue116Tests.cs │ ├── Issue149Tests.cs │ ├── Issue29Tests.cs │ └── Issue33Tests.cs ├── Matchers │ ├── AnyMatcherTests.cs │ ├── ContentMatcherTests.cs │ ├── CustomMatcherTests.cs │ ├── FormDataMatcherTests.cs │ ├── HeadersMatcherTests.cs │ ├── JsonContentMatcherTests.cs │ ├── MethodMatcherTests.cs │ ├── PartialContentMatcherTests.cs │ ├── QueryStringMatcherTests.cs │ ├── UrlMatcherTests.cs │ └── XmlContentMatcherTests.cs ├── MockHttpMessageHandlerTests.cs ├── MockedRequestExtentionsRespondTests.cs ├── MockedRequestExtentionsWithTests.cs └── RichardSzalay.MockHttp.Tests.csproj ├── RichardSzalay.MockHttp.nuspec ├── RichardSzalay.MockHttp.sln ├── RichardSzalay.MockHttp ├── BackendDefinitionBehavior.cs ├── CodeAnalysis │ └── Attributes.cs ├── Formatters │ ├── RequestHandlerResultFormatter.cs │ ├── Resources.Designer.cs │ └── Resources.resx ├── IMockedRequest.cs ├── IMockedRequestMatcher.cs ├── Matchers │ ├── AnyMatcher.cs │ ├── ContentMatcher.cs │ ├── CustomMatcher.cs │ ├── FormDataMatcher.cs │ ├── HeadersMatcher.cs │ ├── JsonContentMatcher.cs │ ├── MethodMatcher.cs │ ├── PartialContentMatcher.cs │ ├── QueryStringMatcher.cs │ ├── UrlMatcher.cs │ └── XmlContentMatcher.cs ├── MockHttpMatchException.cs ├── MockHttpMessageHandler.cs ├── MockHttpMessageHandlerExtensions.cs ├── MockedRequest.cs ├── MockedRequestExtensions.cs ├── MockedRequestJsonExtensions.cs ├── MockedRequestXmlExtensions.cs ├── RequestHandlerResult.cs ├── RichardSzalay.MockHttp.csproj ├── StringUtil.cs ├── TaskEx.cs └── UriUtil.cs └── mockhttp.snk /.editorconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardszalay/mockhttp/7b0d72f5a5d243193d8b9f1142ee59e78de98b3a/.editorconfig -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: MockHttp Build 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | name: "Build" 11 | runs-on: ${{ matrix.os }} 12 | env: 13 | DOTNET_NOLOGO: true 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: [windows-latest, ubuntu-latest, macOS-latest] 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Setup .NET 21 | uses: actions/setup-dotnet@v3 22 | with: 23 | dotnet-version: 6.0.x 24 | - name: Restore dependencies 25 | run: dotnet restore ./RichardSzalay.MockHttp.sln 26 | - name: Build 27 | run: dotnet build -c Release --no-restore ./RichardSzalay.MockHttp.sln 28 | - name: Test 29 | run: dotnet test -c Release --no-build --verbosity normal ./RichardSzalay.MockHttp.sln 30 | - name: Package 31 | run: dotnet pack -c Release --no-build ./RichardSzalay.MockHttp/RichardSzalay.MockHttp.csproj 32 | if: github.event_name != 'pull_request' && matrix.os == 'ubuntu-latest' 33 | 34 | - name: "Upload artifact: RichardSzalay.MockHttp.nupkg" 35 | uses: actions/upload-artifact@v3.1.1 36 | with: 37 | name: RichardSzalay.MockHttp.nupkg 38 | path: RichardSzalay.MockHttp/bin/Release/*.nupkg 39 | if: github.event_name != 'pull_request' && matrix.os == 'ubuntu-latest' 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build generated files 2 | AssemblyVersionInfo.cs 3 | 4 | ## Ignore Visual Studio temporary files, build results, and 5 | ## files generated by popular Visual Studio add-ons. 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.sln.docstates 11 | 12 | # Build results 13 | [Dd]ebug/ 14 | [Dd]ebugPublic/ 15 | [Rr]elease/ 16 | x64/ 17 | build/ 18 | bld/ 19 | [Bb]in/ 20 | [Oo]bj/ 21 | *.nupkg 22 | 23 | # MSTest test Results 24 | [Tt]est[Rr]esult*/ 25 | [Bb]uild[Ll]og.* 26 | 27 | #NUNIT 28 | *.VisualState.xml 29 | TestResult.xml 30 | 31 | # Build Results of an ATL Project 32 | [Dd]ebugPS/ 33 | [Rr]eleasePS/ 34 | dlldata.c 35 | 36 | *_i.c 37 | *_p.c 38 | *_i.h 39 | *.ilk 40 | *.meta 41 | *.obj 42 | *.pch 43 | *.pdb 44 | *.pgc 45 | *.pgd 46 | *.rsp 47 | *.sbr 48 | *.tlb 49 | *.tli 50 | *.tlh 51 | *.tmp 52 | *.tmp_proj 53 | *.log 54 | *.vspscc 55 | *.vssscc 56 | .builds 57 | *.pidb 58 | *.svclog 59 | *.scc 60 | 61 | # Chutzpah Test files 62 | _Chutzpah* 63 | 64 | # Visual C++ cache files 65 | ipch/ 66 | *.aps 67 | *.ncb 68 | *.opensdf 69 | *.sdf 70 | *.cachefile 71 | 72 | # Visual Studio profiler 73 | *.psess 74 | *.vsp 75 | *.vspx 76 | 77 | # TFS 2012 Local Workspace 78 | $tf/ 79 | 80 | # Guidance Automation Toolkit 81 | *.gpState 82 | 83 | # ReSharper is a .NET coding add-in 84 | _ReSharper*/ 85 | *.[Rr]e[Ss]harper 86 | *.DotSettings.user 87 | 88 | # JustCode is a .NET coding addin-in 89 | .JustCode 90 | 91 | # TeamCity is a build add-in 92 | _TeamCity* 93 | 94 | # DotCover is a Code Coverage Tool 95 | *.dotCover 96 | 97 | # NCrunch 98 | *.ncrunch* 99 | _NCrunch_* 100 | .*crunch*.local.xml 101 | 102 | # MightyMoose 103 | *.mm.* 104 | AutoTest.Net/ 105 | 106 | # Web workbench (sass) 107 | .sass-cache/ 108 | 109 | # Installshield output folder 110 | [Ee]xpress/ 111 | 112 | # DocProject is a documentation generator add-in 113 | DocProject/buildhelp/ 114 | DocProject/Help/*.HxT 115 | DocProject/Help/*.HxC 116 | DocProject/Help/*.hhc 117 | DocProject/Help/*.hhk 118 | DocProject/Help/*.hhp 119 | DocProject/Help/Html2 120 | DocProject/Help/html 121 | 122 | # Click-Once directory 123 | publish/ 124 | 125 | # Publish Web Output 126 | *.[Pp]ublish.xml 127 | *.azurePubxml 128 | 129 | # NuGet Packages Directory 130 | packages/ 131 | ## TODO: If the tool you use requires repositories.config uncomment the next line 132 | #!packages/repositories.config 133 | 134 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 135 | # This line needs to be after the ignore of the build folder (and the packages folder if the line above has been uncommented) 136 | !packages/build/ 137 | 138 | # Windows Azure Build Output 139 | csx/ 140 | *.build.csdef 141 | 142 | # Windows Store app package directory 143 | AppPackages/ 144 | 145 | # Others 146 | sql/ 147 | *.Cache 148 | ClientBin/ 149 | [Ss]tyle[Cc]op.* 150 | ~$* 151 | *~ 152 | *.dbmdl 153 | *.dbproj.schemaview 154 | *.pfx 155 | *.publishsettings 156 | node_modules/ 157 | 158 | # RIA/Silverlight projects 159 | Generated_Code/ 160 | 161 | # Backup & report files from converting an old project file to a newer 162 | # Visual Studio version. Backup files are not needed, because we have git ;-) 163 | _UpgradeReport_Files/ 164 | Backup*/ 165 | UpgradeLog*.XML 166 | UpgradeLog*.htm 167 | 168 | # SQL Server files 169 | *.mdf 170 | *.ldf 171 | 172 | # Business Intelligence projects 173 | *.rdl.data 174 | *.bim.layout 175 | *.bim_*.settings 176 | 177 | # Microsoft Fakes 178 | FakesAssemblies/ 179 | .dotnetcli/ 180 | .vs/ 181 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 7.0.0 - Change target profiles to netstandard1.1, netstandard2.0, net5.0, net6.0 (BREAKING) 2 | - Change default fallback behaviour to throw an exception with a report of the match attempts 3 | - Add JSON and XML matchers 4 | - Add support for synchronous HttpClient.Send #104 5 | - Modernize source #41 and add SourceLink support #66 6 | - Fix matching of encoded URL paths #116 7 | - Throw a descriptive error when matching on a mocked request with no response #87 (thanks perfectsquircle!) 8 | - Fix race condition on outstanding requests exception message #96 (thanks jr01!) 9 | 10 | 6.0.0 - Assemblies are now strong named (binary BREAKING) #1 11 | 12 | 5.0.0 - Align with official recommendations on multi-targetting HttpClient: 13 | - Add netstandard2.0 target #61 14 | - Change .NET 4.5 target to use in-band System.Net.Http reference (BREAKING) #61 15 | - Remove PCL profile 111 (BREAKING) #18 16 | 17 | 4.0.0 - Default Fallback message now includes request method and URL (BREAKING) 18 | - Deprecated FallbackMessage property removed (BREAKING) 19 | 20 | 3.3.0 - Added overloads for including custom headers in the response (thanks Sascha Kiefer!) 21 | 22 | 3.2.1 - XML documentation is now included in the NuGet package. Fixes #52 23 | 24 | 3.2.0 - MockHttpMessageHandler now tracks successful matches. Fixes #35 25 | - Added WithExactQueryString / WithExactFormData overloads. Fixes #37 26 | - Added BackendDefinitionBehavior to allow matching Backend Definitions when Request Expectations exist, but don't match. Fixes #45 27 | - Fixed typo in Response(HttpResponseMessage) obsolete message. Fixes #44 28 | 29 | 3.1.0 - Bump major version. Fixes #50 30 | 31 | 1.5.1 - Respond(HttpClient) now works as expected. Fixes #39 32 | - HttpResponseMessage can be disposed without breaking future requests. Fixes #33 33 | 34 | 1.5.0 - WithHeaders now also matches against Content-* headers (thanks Cory Lucas!) 35 | 36 | 1.4.0 - Cancellations and HttpClient timeouts are now supported. Fixes #29 37 | - Added a .ToHttpClient() convenience method to HttpClientHandler 38 | 39 | 1.3.1 - Multiple requests to the same mocked handler now return unique response streams. Fixes #21 40 | 41 | 1.3.0 - Added support for .NET Core via the .NET Standard Library (1.1) 42 | - Relative URLs now match correctly on Xamarin Android 43 | 1.2.2 - Root absolute URLs defined with no trailing flash now match those with a slash (and vice versa) 44 | 45 | 1.2.1 - HttpResponseMessage.RequestMessage is now assigned correctly 46 | - Form/Query data matching now works with both + and %20 space encodings (thanks Jozef Izso!) 47 | 48 | 1.2.0 - Changed PCL profile to support WP8.1 49 | 50 | 1.1.0 - Added MockHttpMessageHandler.Fallback and HttpClient passthrough support -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Richard Szalay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Package.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RichardSzalay.MockHttp 7 | 7.0.0 8 | Richard Szalay 9 | 10 | Testing layer for Microsoft's HttpClient library 11 | MIT 12 | Copyright 2023 Richard Szalay 13 | 14 | 15 | 16 | 17 | $(Version) 18 | $(Version) 19 | $(Version) 20 | 21 | 22 | 23 | 24 | true 25 | true 26 | embedded 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | @(ReleaseNoteLines, '%0a') 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NuGet](http://img.shields.io/nuget/v/RichardSzalay.MockHttp.svg?style=flat-square)](https://www.nuget.org/packages/RichardSzalay.MockHttp/)[![NuGet](https://img.shields.io/nuget/dt/RichardSzalay.MockHttp.svg?style=flat-square)](https://www.nuget.org/packages/RichardSzalay.MockHttp/) 2 | 3 | MockHttp for HttpClient 4 | ===================== 5 | 6 | MockHttp is a testing layer for Microsoft's HttpClient library. It allows stubbed responses to be configured for matched HTTP requests and can be used to test your application's service layer. 7 | 8 | ## NuGet 9 | 10 | PM> Install-Package RichardSzalay.MockHttp 11 | 12 | ## How? 13 | 14 | MockHttp defines a replacement `HttpMessageHandler`, the engine that drives HttpClient, that provides a fluent configuration API and provides a canned response. The caller (eg. your application's service layer) remains unaware of its presence. 15 | 16 | ## Usage 17 | 18 | ```csharp 19 | var mockHttp = new MockHttpMessageHandler(); 20 | 21 | // Setup a respond for the user api (including a wildcard in the URL) 22 | mockHttp.When("http://localhost/api/user/*") 23 | .Respond("application/json", "{'name' : 'Test McGee'}"); // Respond with JSON 24 | 25 | // Inject the handler or client into your application code 26 | var client = mockHttp.ToHttpClient(); 27 | 28 | var response = await client.GetAsync("http://localhost/api/user/1234"); 29 | // or without async: var response = client.GetAsync("http://localhost/api/user/1234").Result; 30 | 31 | var json = await response.Content.ReadAsStringAsync(); 32 | 33 | // No network connection required 34 | Console.Write(json); // {'name' : 'Test McGee'} 35 | ``` 36 | 37 | ### When (Backend Definitions) vs Expect (Request Expectations) 38 | 39 | `MockHttpMessageHandler` defines both `When` and `Expect`, which can be used to define responses. They both expose the same fluent API, but each works in a slightly different way. 40 | 41 | Using `When` specifies a "Backend Definition". Backend Definitions can be matched against multiple times and in any order, but they won't match if there are any outstanding Request Expectations present (unless `BackendDefinitionBehavior.Always` is specified). If no Request Expectations match, `Fallback` will be used. 42 | 43 | Using `Expect` specifies a "Request Expectation". Request Expectations match only once and in the order they were added in. Only once all expectations have been satisfied will Backend Definitions be evaluated. Calling `mockHttp.VerifyNoOutstandingExpectation()` will assert that there are no expectations that have yet to be called. Calling `ResetExpectations` clears the the queue of expectations. 44 | 45 | This pattern is heavily inspired by [AngularJS's $httpBackend](https://docs.angularjs.org/api/ngMock/service/$httpBackend) 46 | 47 | ### Matchers (With*) 48 | 49 | The `With` and `Expect` methods return a `MockedRequest`, which can have additional constraints (called matchers) placed on them before specifying a response with `Respond`. 50 | 51 | Passing an HTTP method and URL to `When` or `Expect` is equivalent to applying a Method and Url matcher respectively. The following chart breaks down additional built in matchers and their usage: 52 | 53 | | Method | Description | 54 | | ------ | ----------- | 55 | |
WithQueryString("key", "value")

WithQueryString("key=value&other=value")

WithQueryString(new Dictionary<string,string>
{
{ "key", "value" },
{ "other", "value" }
}
| Matches on one or more querystring values, ignoring additional values | 56 | |
WithExactQueryString("key=value&other=value")

WithExactQueryString(new Dictionary<string,string>
{
{ "key", "value" },
{ "other", "value" }
}
| Matches on one or more querystring values, rejecting additional values | 57 | |
WithFormData("key", "value")

WithFormData("key=value&other=value")

WithFormData(new Dictionary<string,string>
{
{ "key", "value" },
{ "other", "value" }
})
| Matches on one or more form data values, ignoring additional values | 58 | |
WithExactFormData("key=value&other=value")

WithExactFormData(new Dictionary<string,string>
{
{ "key", "value" },
{ "other", "value" }
})
| Matches on one or more form data values, rejecting additional values | 59 | |
WithContent("{'name':'McGee'}")
| Matches on the (post) content of the request | 60 | |
WithPartialContent("McGee")
| Matches on the partial (post) content of the request | 61 | |
WithHeaders("Authorization", "Basic abcdef")

WithHeaders(@"Authorization: Basic abcdef
Accept: application/json")

WithHeaders(new Dictionary<string,string>
{
{ "Authorization", "Basic abcdef" },
{ "Accept", "application/json" }
})
| Matches on one or more HTTP header values | 62 | |
WithJsonContent<T>(new MyTypedRequest() [, jsonSerializerSettings])

WithJsonContent<T>(t => t.SomeProperty == 5 [, jsonSerializerSettings])
| Matches on requests that have matching JSON content | 63 | |
With(request => request.Content.Length > 50)
| Applies custom matcher logic against an HttpRequestMessage | 64 | 65 | These methods are chainable, making complex requirements easy to descirbe. 66 | 67 | ### Verifying Matches 68 | 69 | When using Request Expectations via `Expect`, `MockHttpMessageHandler.VerifyNoOutstandingExpectation()` can be used to assert that there are no unmatched requests. 70 | 71 | For other use cases, `GetMatchCount` will return the number of times a mocked request (returned by When / Expect) was called. This even works with `Fallback`, so you 72 | can check how many unmatched requests there were. 73 | 74 | ```csharp 75 | var mockHttp = new MockHttpMessageHandler(); 76 | 77 | var request = mockHttp.When("http://localhost/api/user/*") 78 | .Respond("application/json", "{'name' : 'Test McGee'}"); 79 | 80 | var client = mockHttp.ToHttpClient(); 81 | 82 | await client.GetAsync("http://localhost/api/user/1234"); 83 | await client.GetAsync("http://localhost/api/user/2345"); 84 | await client.GetAsync("http://localhost/api/user/3456"); 85 | 86 | Console.Write(mockHttp.GetMatchCount(request)); // 3 87 | ``` 88 | 89 | ### Match Behavior 90 | 91 | Each request is evaluated using the following process: 92 | 93 | 1. If Request Expectations exist and the request matches the next expectation in the queue, the expectation is used to process the response and is then removed from the queue 94 | 2. If no Request Expectations exist, or the handler was constructed with `BackendDefinitionBehavior.Always`, the first matching Backend Definition processes the response 95 | 3. `MockHttpMessageHandler.Fallback` handles the request 96 | 97 | ### Fallback 98 | 99 | The `Fallback` property handles all requests that weren't handled by the match behavior. Since it is also a mocked request, any of the `Respond` overloads can be applied. 100 | 101 | ``` 102 | // Unhandled requests should throw an exception 103 | mockHttp.Fallback.Throw(new InvalidOperationException("No matching mock handler")); 104 | 105 | // Unhandled requests should be executed against the network 106 | mockHttp.Fallback.Respond(new HttpClient()); 107 | ``` 108 | 109 | The default fallback behavior is to throw an exception that summarises why reach mocked request failed to match. 110 | 111 | ### Examples 112 | 113 | This example uses Expect to test an OAuth ticket recycle process: 114 | 115 | ```csharp 116 | // Simulate an expired token 117 | mockHttp.Expect("/users/me") 118 | .WithQueryString("access_token", "old_token") 119 | .Respond(HttpStatusCode.Unauthorized); 120 | 121 | // Expect the request to refresh the token and supply a new one 122 | mockHttp.Expect("/tokens/refresh") 123 | .WithFormData("refresh_token", "refresh_token") 124 | .Respond("application/json", "{'access_token' : 'new_token', 'refresh_token' : 'new_refresh'}"); 125 | 126 | // Expect the original call to be retried with the new token 127 | mockHttp.Expect("/users/me") 128 | .WithQueryString("access_token", "new_token") 129 | .Respond("application/json", "{'name' : 'Test McGee'}"); 130 | 131 | var httpClient = mockHttp.ToHttpClient(); 132 | 133 | var userService = new UserService(httpClient); 134 | 135 | var user = await userService.GetUserDetails(); 136 | 137 | Assert.Equals("Test McGee", user.Name); 138 | mockHttp.VerifyNoOutstandingExpectation(); 139 | ``` 140 | 141 | ## Platform Support 142 | 143 | MockHttp 7.0.0 and later are compiled for .NET 6, .NET 5, .NET Standard 2.0, .NET Standard 1.1 144 | 145 | [MockHttp 6.0.0](https://github.com/richardszalay/mockhttp/tree/v6.0.0#platform-support) has increased legacy platform support and can still be used, but is no longer updated with new features. 146 | 147 | ## Build / Release 148 | 149 | Clone the repository and build `RichardSzalay.MockHttp.sln` using MSBuild. NuGet package restore must be enabled. 150 | 151 | To release, build: 152 | 153 | ``` 154 | dotnet pack -c Release --no-build ./RichardSzalay.MockHttp/RichardSzalay.MockHttp.csproj 155 | ``` 156 | 157 | If you fork the project, simply rename the `nuspec` file accordingly and it will be picked up by the release script. 158 | 159 | ## Contributors 160 | 161 | Many thanks to all the members of the community that have contributed PRs to this project: 162 | 163 | * [jozefizso](https://github.com/jozefizso) 164 | * [camiller2](https://github.com/camiller2) 165 | * [wislon](https://github.com/wislon) 166 | * [coryflucas](https://github.com/coryflucas) 167 | * [esskar](https://github.com/esskar) 168 | * [jericho](https://github.com/jericho) 169 | * [perfectsquircle](https://github.com/perfectsquircle) 170 | * [jr01](https://github.com/jr01) 171 | 172 | ## License 173 | 174 | The MIT License (MIT) 175 | 176 | Copyright (c) 2023 Richard Szalay 177 | 178 | Permission is hereby granted, free of charge, to any person obtaining a copy 179 | of this software and associated documentation files (the "Software"), to deal 180 | in the Software without restriction, including without limitation the rights 181 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 182 | copies of the Software, and to permit persons to whom the Software is 183 | furnished to do so, subject to the following conditions: 184 | 185 | The above copyright notice and this permission notice shall be included in all 186 | copies or substantial portions of the Software. 187 | 188 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 189 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 190 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 191 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 192 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 193 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 194 | SOFTWARE. 195 | -------------------------------------------------------------------------------- /Release.proj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | $(APPVEYOR_BUILD_VERSION) 7 | pre 8 | 9 | 10 | 11 | $(PackageVersion) 12 | $(FullPackageVersion)-$(PackageVersionSuffix) 13 | 14 | 15 | 16 | 18 | 19 | 20 | 22 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/Infrastructure/HttpHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace RichardSzalay.MockHttp.Tests.Infrastructure; 6 | 7 | public static class HttpHelpers 8 | { 9 | internal static IEnumerable> ParseQueryString(string input) 10 | { 11 | return input.TrimStart('?').Split('&') 12 | .Select(pair => pair.Split(new[] { '=' }, 2)) 13 | .Select(pair => new KeyValuePair( 14 | Uri.UnescapeDataString(pair[0]), 15 | pair.Length == 2 ? Uri.UnescapeDataString(pair[1]) : null 16 | )) 17 | .ToList(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/Issues/Issue116Tests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace RichardSzalay.MockHttp.Tests.Issues; 8 | 9 | public class Issue116Tests 10 | { 11 | // https://github.com/richardszalay/mockhttp/issues/116 12 | [Fact] 13 | public async Task Can_simulate_timeout() 14 | { 15 | var handler = new MockHttpMessageHandler(); 16 | 17 | handler.When("/topics/$aws%2Fthings%2Ftest-host%2Fshadow%2Fname%2Ftest-shadow%2Fupdate") 18 | .Respond(HttpStatusCode.OK); 19 | 20 | var client = new HttpClient(handler); 21 | 22 | var result = await client.GetAsync("http://localhost/topics/$aws%2Fthings%2Ftest-host%2Fshadow%2Fname%2Ftest-shadow%2Fupdate"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/Issues/Issue149Tests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http; 3 | using System.Text; 4 | using System.Text.Json; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace RichardSzalay.MockHttp.Tests.Issues; 9 | 10 | public class Issue149Tests 11 | { 12 | private HttpClient client; 13 | 14 | //https://github.com/richardszalay/mockhttp/issues/149 15 | [Fact] 16 | public async Task MultipleJsonContentMatchersToSameUri() 17 | { 18 | var mockHttp = new MockHttpMessageHandler(); 19 | mockHttp 20 | .When("https://someUrl.com") 21 | .WithJsonContent(a => a.Key == "SomeApiKey") 22 | .Respond("application/json", "{\"some_result\" : \"Error\"}"); 23 | mockHttp 24 | .When("https://someUrl.com") 25 | .WithJsonContent(a => a.Content == "SomeContent") 26 | .Respond("application/json", "{\"some_result\" : \"Result\"}"); 27 | 28 | client = mockHttp.ToHttpClient(); 29 | 30 | var parameters = JsonSerializer.Serialize(new SomeJsonParameters("SomeApiKey", "OtherContent")); 31 | var response = await client.PostAsync( 32 | "https://someUrl.com", 33 | new StringContent(parameters, Encoding.UTF8, "application/json")); 34 | var content = await response.Content.ReadAsStringAsync(); 35 | Assert.True(!string.IsNullOrEmpty(content)); 36 | 37 | var parameters1 = JsonSerializer.Serialize(new SomeJsonParameters("OtherApiKey", "SomeContent")); 38 | var action1 = async ()=> await client.PostAsync( 39 | "https://someUrl.com", 40 | new StringContent(parameters1, Encoding.UTF8, "application/json")); 41 | var exception = await Record.ExceptionAsync(action1); 42 | Assert.Null(exception); 43 | } 44 | 45 | private record SomeJsonParameters(string Key, string Content); 46 | } -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/Issues/Issue29Tests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace RichardSzalay.MockHttp.Tests.Issues; 8 | 9 | public class Issue29Tests 10 | { 11 | // https://github.com/richardszalay/mockhttp/issues/29 12 | [Fact] 13 | public async Task Can_simulate_timeout() 14 | { 15 | var handler = new MockHttpMessageHandler(); 16 | 17 | handler.Fallback.Respond(async () => 18 | { 19 | await Task.Delay(10000); 20 | 21 | return new HttpResponseMessage(HttpStatusCode.OK); 22 | }); 23 | 24 | var client = new HttpClient(handler); 25 | client.Timeout = TimeSpan.FromMilliseconds(1000); 26 | 27 | try 28 | { 29 | var result = await client.GetAsync("http://localhost"); 30 | 31 | throw new InvalidOperationException("Expected timeout exception"); 32 | } 33 | catch (OperationCanceledException) 34 | { 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/Issues/Issue33Tests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace RichardSzalay.MockHttp.Tests.Issues; 7 | 8 | public class Issue33Tests 9 | { 10 | // https://github.com/richardszalay/mockhttp/issues/29 11 | [Fact] 12 | public async Task Disposing_response_does_not_fail_future_requests() 13 | { 14 | var handler = new MockHttpMessageHandler(); 15 | 16 | handler.When("*").Respond(HttpStatusCode.OK); 17 | 18 | var client = new HttpClient(handler); 19 | 20 | var firstResponse = await client.GetAsync("http://localhost"); 21 | firstResponse.Dispose(); 22 | 23 | var secondResponse = await client.GetAsync("http://localhost"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/Matchers/AnyMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Matchers; 2 | using System; 3 | using System.Linq; 4 | using Xunit; 5 | 6 | namespace RichardSzalay.MockHttp.Tests.Matchers; 7 | 8 | public class AnyMatcherTests 9 | { 10 | [Fact] 11 | public void Succeeds_if_first_matcher_succeeds() 12 | { 13 | var result = Test(true, false); 14 | 15 | Assert.True(result); 16 | } 17 | 18 | [Fact] 19 | public void Succeeds_if_last_matcher_succeeds() 20 | { 21 | var result = Test(false, true); 22 | 23 | Assert.True(result); 24 | } 25 | 26 | [Fact] 27 | public void Fails_if_all_matchers_fail() 28 | { 29 | var result = Test(false, false); 30 | 31 | Assert.False(result); 32 | } 33 | 34 | 35 | [Fact] 36 | public void ToString_describes_matcher() 37 | { 38 | var sut = new AnyMatcher(new[] 39 | { 40 | new FakeMatcher(true), 41 | new FakeMatcher(false) 42 | }); 43 | 44 | var result = sut.ToString(); 45 | var expected = $"matches any one of matches fake True{Environment.NewLine}" + 46 | $" OR matches fake False{Environment.NewLine}"; 47 | 48 | Assert.Equal(expected, result); 49 | } 50 | 51 | public bool Test(params bool[] matcherResults) 52 | { 53 | var matchers = matcherResults 54 | .Select(result => new FakeMatcher(result)); 55 | 56 | return new AnyMatcher(matchers).Matches(new System.Net.Http.HttpRequestMessage()); 57 | } 58 | 59 | private class FakeMatcher : IMockedRequestMatcher 60 | { 61 | private bool _result; 62 | public FakeMatcher(bool result) 63 | { 64 | _result = result; 65 | } 66 | 67 | public bool Matches(System.Net.Http.HttpRequestMessage message) 68 | { 69 | return _result; 70 | } 71 | 72 | public override string ToString() 73 | => $"matches fake {_result}"; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/Matchers/ContentMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Matchers; 2 | using System; 3 | using System.Net.Http; 4 | using Xunit; 5 | 6 | namespace RichardSzalay.MockHttp.Tests.Matchers; 7 | 8 | public class ContentMatcherTests 9 | { 10 | [Fact] 11 | public void Succeeds_on_matched_content() 12 | { 13 | var result = Test( 14 | expected: "Custom data", 15 | actual: "Custom data" 16 | ); 17 | 18 | Assert.True(result); 19 | } 20 | 21 | [Fact] 22 | public void Fails_on_unmatched_content() 23 | { 24 | var result = Test( 25 | expected: "Custom data", 26 | actual: "Custom data!" 27 | ); 28 | 29 | Assert.False(result); 30 | } 31 | 32 | [Fact] 33 | public void ToString_describes_matcher() 34 | { 35 | var sut = new ContentMatcher("test"); 36 | 37 | var result = sut.ToString(); 38 | 39 | Assert.Equal("request body matches test", result); 40 | } 41 | 42 | private bool Test(string expected, string actual) 43 | { 44 | var request = new HttpRequestMessage(HttpMethod.Get, 45 | "http://tempuri.org/home"); 46 | 47 | request.Content = new StringContent(actual); 48 | 49 | return new ContentMatcher(expected).Matches(request); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/Matchers/CustomMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Matchers; 2 | using System; 3 | using System.Net.Http; 4 | using Xunit; 5 | 6 | namespace RichardSzalay.MockHttp.Tests.Matchers; 7 | 8 | public class CustomMatcherTests 9 | { 10 | [Fact] 11 | public void Should_succeed_when_handler_succeeds() 12 | { 13 | bool result = Test(r => true); 14 | 15 | Assert.True(result); 16 | } 17 | 18 | [Fact] 19 | public void Should_fail_when_handler_fails() 20 | { 21 | bool result = Test(r => false); 22 | 23 | Assert.False(result); 24 | } 25 | 26 | [Fact] 27 | public void ToString_describes_matcher() 28 | { 29 | var sut = new CustomMatcher(_ => true); 30 | 31 | var result = sut.ToString(); 32 | 33 | Assert.Equal("request matches a custom predicate", result); 34 | } 35 | 36 | private bool Test(Func handler) 37 | { 38 | var sut = new CustomMatcher(handler); 39 | 40 | return sut.Matches(new HttpRequestMessage(HttpMethod.Get, 41 | "http://tempuri.org/home")); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/Matchers/FormDataMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Matchers; 2 | using RichardSzalay.MockHttp.Tests.Infrastructure; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using Xunit; 8 | 9 | namespace RichardSzalay.MockHttp.Tests.Matchers; 10 | 11 | public class FormDataMatcherTests 12 | { 13 | [Fact] 14 | public void Should_match_in_order() 15 | { 16 | bool result = Test( 17 | expected: "key1=value1&key2=value2", 18 | actual: "key1=value1&key2=value2" 19 | ); 20 | 21 | Assert.True(result); 22 | } 23 | 24 | [Fact] 25 | public void Should_match_out_of_order() 26 | { 27 | bool result = Test( 28 | expected: "key2=value2&key1=value1", 29 | actual: "key1=value1&key2=value2" 30 | ); 31 | 32 | Assert.True(result); 33 | } 34 | 35 | [Fact] 36 | public void Should_match_multiple_values() 37 | { 38 | bool result = Test( 39 | expected: "key1=value1&key1=value2", 40 | actual: "key1=value2&key1=value1" 41 | ); 42 | 43 | Assert.True(result); 44 | } 45 | 46 | [Fact] 47 | public void Should_support_matching_empty_values() 48 | { 49 | bool result = Test( 50 | expected: "key2=value2&key1", 51 | actual: "key1&key2=value2" 52 | ); 53 | 54 | Assert.True(result); 55 | } 56 | 57 | [Fact] 58 | public void Should_fail_for_incorrect_values() 59 | { 60 | bool result = Test( 61 | expected: "key1=value1&key2=value3", 62 | actual: "key1=value1&key2=value2" 63 | ); 64 | 65 | Assert.False(result); 66 | } 67 | 68 | [Fact] 69 | public void Should_fail_for_missing_keys() 70 | { 71 | bool result = Test( 72 | expected: "key2=value2&key1=value1", 73 | actual: "key1=value1&key3=value3" 74 | ); 75 | 76 | Assert.False(result); 77 | } 78 | 79 | [Fact] 80 | public void Should_not_fail_for_additional_keys_when_exact_is_false() 81 | { 82 | bool result = Test( 83 | expected: "key1=value1&key2=value2", 84 | actual: "key1=value1&key2=value2&key3=value3", 85 | exact: false 86 | ); 87 | 88 | Assert.True(result); 89 | } 90 | 91 | [Fact] 92 | public void Should_fail_for_additional_keys_when_exact_is_true() 93 | { 94 | bool result = Test( 95 | expected: "key1=value1&key2=value2", 96 | actual: "key1=value1&key2=value2&key3=value3", 97 | exact: true 98 | ); 99 | 100 | Assert.False(result); 101 | } 102 | 103 | private bool Test(string expected, string actual, bool exact = false) 104 | { 105 | var sut = new FormDataMatcher(expected, exact); 106 | 107 | FormUrlEncodedContent content = new FormUrlEncodedContent( 108 | HttpHelpers.ParseQueryString(actual) 109 | ); 110 | 111 | return sut.Matches(new HttpRequestMessage(HttpMethod.Get, 112 | "http://tempuri.org/home") 113 | { Content = content }); 114 | } 115 | 116 | [Fact] 117 | public void Should_support_matching_dictionary_data_with_url_encoded_values1() 118 | { 119 | var data = new Dictionary(); 120 | data.Add("key", "Value with spaces"); 121 | 122 | var content = new FormUrlEncodedContent(data); 123 | 124 | var sut = new FormDataMatcher(data); 125 | 126 | var actualMatch = sut.Matches(new HttpRequestMessage(HttpMethod.Get, "http://tempuri.org/home") { Content = content }); 127 | 128 | Assert.True(actualMatch, "FormDataMatcher.Matches() should match dictionary data with URL encoded query string values."); 129 | } 130 | 131 | [Fact] 132 | public void Should_support_matching_dictionary_data_with_url_encoded_values2() 133 | { 134 | var data = new Dictionary(); 135 | data.Add("key", "Value with spaces"); 136 | 137 | var content = new FormUrlEncodedContent(data); 138 | 139 | var sut = new FormDataMatcher("key=Value+with%20spaces"); 140 | 141 | var actualMatch = sut.Matches(new HttpRequestMessage(HttpMethod.Get, "http://tempuri.org/home") { Content = content }); 142 | 143 | Assert.True(actualMatch, "FormDataMatcher.Matches() should match dictionary data with URL encoded query string values."); 144 | } 145 | 146 | [Fact] 147 | public void Should_fail_for_non_form_data() 148 | { 149 | var content = new FormUrlEncodedContent(HttpHelpers.ParseQueryString("key=value")); 150 | content.Headers.ContentType = new MediaTypeHeaderValue("text/plain"); 151 | 152 | var result = Test( 153 | expected: "key=value", 154 | actual: content 155 | ); 156 | 157 | Assert.False(result); 158 | } 159 | 160 | [Fact] 161 | public void Supports_multipart_formdata_content() 162 | { 163 | var content = new MultipartFormDataContent 164 | { 165 | new FormUrlEncodedContent(HttpHelpers.ParseQueryString("key=value")) 166 | }; 167 | 168 | var result = Test( 169 | expected: "key=value", 170 | actual: content 171 | ); 172 | 173 | Assert.True(result); 174 | } 175 | 176 | [Fact] 177 | public void Matches_form_data_across_multipart_entries() 178 | { 179 | var content = new MultipartFormDataContent 180 | { 181 | new FormUrlEncodedContent(HttpHelpers.ParseQueryString("key1=value1")), 182 | new FormUrlEncodedContent(HttpHelpers.ParseQueryString("key2=value2")) 183 | }; 184 | 185 | var result = Test( 186 | expected: "key1=value1&key2=value2", 187 | actual: content 188 | ); 189 | 190 | Assert.True(result); 191 | } 192 | 193 | [Fact] 194 | public void Does_not_match_form_data_on_non_form_data_multipart_entries() 195 | { 196 | var content = new MultipartFormDataContent 197 | { 198 | new FormUrlEncodedContent(HttpHelpers.ParseQueryString("key1=value1")), 199 | new FormUrlEncodedContent(HttpHelpers.ParseQueryString("key2=value2")) 200 | }; 201 | 202 | content.First().Headers.ContentType = new MediaTypeHeaderValue("text/plain"); 203 | 204 | var result = Test( 205 | expected: "key1=value1&key2=value2", 206 | actual: content 207 | ); 208 | 209 | Assert.False(result); 210 | } 211 | 212 | private bool Test(string expected, HttpContent actual) 213 | { 214 | var sut = new FormDataMatcher(expected); 215 | 216 | return sut.Matches(new HttpRequestMessage(HttpMethod.Get, 217 | "http://tempuri.org/home") 218 | { Content = actual }); 219 | } 220 | 221 | [Fact] 222 | public void ToString_describes_partial_matcher() 223 | { 224 | var sut = new FormDataMatcher("key=Value+with%20spaces&b=2", exact: false); 225 | 226 | var result = sut.ToString(); 227 | 228 | Assert.Equal("form data matches key=Value%20with%20spaces&b=2", result); 229 | } 230 | 231 | [Fact] 232 | public void ToString_describes_exact_matcher() 233 | { 234 | var sut = new FormDataMatcher("key=Value+with%20spaces&b=2", exact: true); 235 | 236 | var result = sut.ToString(); 237 | 238 | Assert.Equal("form data exacly matches (no additional keys allowed) key=Value%20with%20spaces&b=2", result); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/Matchers/HeadersMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Matchers; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Text; 7 | using Xunit; 8 | 9 | namespace RichardSzalay.MockHttp.Tests.Matchers; 10 | 11 | public class HeadersMatcherTests 12 | { 13 | [Fact] 14 | public void Should_succeed_on_all_matched() 15 | { 16 | bool result = Test( 17 | expected: new HeadersMatcher(new Dictionary 18 | { 19 | { "Authorization", "Basic abcdef" }, 20 | { "Accept", "application/json" }, 21 | { "Content-Type", "text/plain; charset=utf-8" } 22 | }), 23 | actual: req => 24 | { 25 | req.Headers.Authorization = new AuthenticationHeaderValue("Basic", "abcdef"); 26 | req.Headers.Accept.Clear(); 27 | req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 28 | req.Content = new StringContent("test", Encoding.UTF8, "text/plain"); 29 | } 30 | ); 31 | 32 | Assert.True(result); 33 | } 34 | 35 | [Fact] 36 | public void Should_parse_string_headers() 37 | { 38 | bool result = Test( 39 | expected: new HeadersMatcher(@"Accept: application/json 40 | Authorization: Basic abcdef"), 41 | actual: req => 42 | { 43 | req.Headers.Authorization = new AuthenticationHeaderValue("Basic", "abcdef"); 44 | req.Headers.Accept.Clear(); 45 | req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 46 | } 47 | ); 48 | 49 | Assert.True(result); 50 | } 51 | 52 | private bool Test(HeadersMatcher expected, Action actual) 53 | { 54 | var request = new HttpRequestMessage(HttpMethod.Get, 55 | "http://tempuri.org/home"); 56 | 57 | actual(request); 58 | 59 | return expected.Matches(request); 60 | } 61 | 62 | [Fact] 63 | public void ToString_describes_matcher() 64 | { 65 | var sut = new HeadersMatcher(@"Accept: application/json 66 | Authorization: Basic abcdef"); 67 | 68 | var result = sut.ToString(); 69 | 70 | Assert.Equal($"headers match {Environment.NewLine}" + 71 | $" Accept: application/json{Environment.NewLine}" + 72 | $" Authorization: Basic abcdef{Environment.NewLine}", result); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/Matchers/JsonContentMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Matchers; 2 | using System; 3 | using System.Net.Http; 4 | using System.Text.Json; 5 | using Xunit; 6 | 7 | namespace RichardSzalay.MockHttp.Tests.Matchers; 8 | 9 | public class JsonContentMatcherTests 10 | { 11 | [Fact] 12 | public void Should_succeed_when_predicate_returns_true() 13 | { 14 | var result = Test( 15 | expected: c => c.Value == true, 16 | actual: new JsonContent(true) 17 | ); 18 | 19 | Assert.True(result); 20 | } 21 | 22 | [Fact] 23 | public void Should_fail_when_predicate_returns_false() 24 | { 25 | var result = Test( 26 | expected: c => c.Value == false, 27 | actual: new JsonContent(true) 28 | ); 29 | 30 | Assert.False(result); 31 | } 32 | 33 | private bool Test(Func expected, JsonContent actual) 34 | { 35 | var options = new JsonSerializerOptions(); 36 | 37 | var sut = new JsonContentMatcher(expected, options); 38 | 39 | StringContent content = new StringContent( 40 | JsonSerializer.Serialize(actual, options) 41 | ); 42 | 43 | return sut.Matches(new HttpRequestMessage(HttpMethod.Get, 44 | "http://tempuri.org/home") 45 | { Content = content }); 46 | } 47 | 48 | public record JsonContent(bool Value); 49 | 50 | [Fact] 51 | public void ToString_describes_matcher() 52 | { 53 | var sut = new JsonContentMatcher(x => true); 54 | 55 | var result = sut.ToString(); 56 | 57 | Assert.Equal("JSON request body matches custom JsonContent predicate", result); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/Matchers/MethodMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Matchers; 2 | using System.Net.Http; 3 | using Xunit; 4 | 5 | namespace RichardSzalay.MockHttp.Tests.Matchers; 6 | 7 | public class MethodMatcherTests 8 | { 9 | [Fact] 10 | public void Should_succeed_on_matched_method() 11 | { 12 | bool result = Test( 13 | expected: HttpMethod.Get, 14 | actual: HttpMethod.Get 15 | ); 16 | 17 | Assert.True(result); 18 | } 19 | 20 | [Fact] 21 | public void Should_fail_on_mismatched_method() 22 | { 23 | bool result = Test( 24 | expected: HttpMethod.Get, 25 | actual: HttpMethod.Post 26 | ); 27 | 28 | Assert.False(result); 29 | } 30 | 31 | private bool Test(HttpMethod expected, HttpMethod actual) 32 | { 33 | var sut = new MethodMatcher(expected); 34 | 35 | return sut.Matches(new HttpRequestMessage(actual, 36 | "http://tempuri.org/home")); 37 | } 38 | 39 | [Fact] 40 | public void ToString_describes_matcher() 41 | { 42 | var sut = new MethodMatcher(HttpMethod.Get); 43 | 44 | var result = sut.ToString(); 45 | 46 | Assert.Equal("method matches GET", result); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/Matchers/PartialContentMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Matchers; 2 | using System.Net.Http; 3 | using Xunit; 4 | 5 | namespace RichardSzalay.MockHttp.Tests.Matchers; 6 | 7 | public class PartialContentMatcherTests 8 | { 9 | [Fact] 10 | public void Succeeds_on_partially_matched_content() 11 | { 12 | var result = Test( 13 | expected: "Custom data", 14 | actual: "Custom data!" 15 | ); 16 | 17 | Assert.True(result); 18 | } 19 | 20 | [Fact] 21 | public void Fails_on_unmatched_content() 22 | { 23 | var result = Test( 24 | expected: "Custom data!", 25 | actual: "Custom data" 26 | ); 27 | 28 | Assert.False(result); 29 | } 30 | 31 | private bool Test(string expected, string actual) 32 | { 33 | var request = new HttpRequestMessage(HttpMethod.Get, 34 | "http://tempuri.org/home"); 35 | 36 | request.Content = new StringContent(actual); 37 | 38 | return new PartialContentMatcher(expected).Matches(request); 39 | } 40 | 41 | [Fact] 42 | public void ToString_describes_matcher() 43 | { 44 | var sut = new PartialContentMatcher("test"); 45 | 46 | var result = sut.ToString(); 47 | 48 | Assert.Equal("request body partially matches test", result); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/Matchers/QueryStringMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Matchers; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using Xunit; 5 | 6 | namespace RichardSzalay.MockHttp.Tests.Matchers; 7 | 8 | public class QueryStringMatcherTests 9 | { 10 | [Fact] 11 | public void Should_match_in_order() 12 | { 13 | bool result = Test( 14 | expected: "key1=value1&key2=value2", 15 | actual: "key1=value1&key2=value2" 16 | ); 17 | 18 | Assert.True(result); 19 | } 20 | 21 | [Fact] 22 | public void Should_match_out_of_order() 23 | { 24 | bool result = Test( 25 | expected: "key2=value2&key1=value1", 26 | actual: "key1=value1&key2=value2" 27 | ); 28 | 29 | Assert.True(result); 30 | } 31 | 32 | [Fact] 33 | public void Should_match_multiple_values() 34 | { 35 | bool result = Test( 36 | expected: "key1=value1&key1=value2", 37 | actual: "key1=value2&key1=value1" 38 | ); 39 | 40 | Assert.True(result); 41 | } 42 | 43 | [Fact] 44 | public void Should_support_matching_empty_values() 45 | { 46 | bool result = Test( 47 | expected: "key2=value2&key1", 48 | actual: "key1&key2=value2" 49 | ); 50 | 51 | Assert.True(result); 52 | } 53 | 54 | [Fact] 55 | public void Should_fail_for_incorrect_values() 56 | { 57 | bool result = Test( 58 | expected: "key1=value1&key2=value3", 59 | actual: "key1=value1&key2=value2" 60 | ); 61 | 62 | Assert.False(result); 63 | } 64 | 65 | [Fact] 66 | public void Should_fail_for_missing_keys() 67 | { 68 | bool result = Test( 69 | expected: "key2=value2&key1=value1", 70 | actual: "key1=value1&key3=value3" 71 | ); 72 | 73 | Assert.False(result); 74 | } 75 | 76 | [Fact] 77 | public void Should_not_fail_for_additional_keys_when_exact_is_false() 78 | { 79 | bool result = Test( 80 | expected: "key1=value1&key2=value2", 81 | actual: "key1=value1&key2=value2&key3=value3" 82 | ); 83 | 84 | Assert.True(result); 85 | } 86 | 87 | [Fact] 88 | public void Should_fail_for_additional_keys_when_exact_is_true() 89 | { 90 | bool result = Test( 91 | expected: "key1=value1&key2=value2", 92 | actual: "key1=value1&key2=value2&key3=value3", 93 | exact: true 94 | ); 95 | 96 | Assert.False(result); 97 | } 98 | 99 | [Fact] 100 | public void Should_support_matching_dictionary_data_with_url_encoded_values() 101 | { 102 | var data = new Dictionary(); 103 | data.Add("key", "Value with spaces"); 104 | 105 | var qs = "key=Value+with%20spaces"; 106 | 107 | var sut = new QueryStringMatcher(data); 108 | 109 | var actualMatch = sut.Matches(new HttpRequestMessage(HttpMethod.Get, "http://tempuri.org/home?" + qs)); 110 | 111 | Assert.True(actualMatch, "QueryStringMatcher.Matches() should match dictionary data with URL encoded query string values."); 112 | } 113 | 114 | private bool Test(string expected, string actual, bool exact = false) 115 | { 116 | var sut = new QueryStringMatcher(expected, exact); 117 | 118 | return sut.Matches(new HttpRequestMessage(HttpMethod.Get, 119 | "http://tempuri.org/home?" + actual)); 120 | } 121 | 122 | [Fact] 123 | public void ToString_describes_partial_matcher() 124 | { 125 | var sut = new QueryStringMatcher("key=Value+with%20spaces&b=2", exact: false); 126 | 127 | var result = sut.ToString(); 128 | 129 | Assert.Equal("query string matches key=Value%20with%20spaces&b=2", result); 130 | } 131 | 132 | [Fact] 133 | public void ToString_describes_exact_matcher() 134 | { 135 | var sut = new QueryStringMatcher("key=Value+with%20spaces&b=2", exact: true); 136 | 137 | var result = sut.ToString(); 138 | 139 | Assert.Equal("query string exacly matches (no additional keys allowed) key=Value%20with%20spaces&b=2", result); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/Matchers/UrlMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Matchers; 2 | using System.Net.Http; 3 | using Xunit; 4 | 5 | namespace RichardSzalay.MockHttp.Tests.Matchers; 6 | 7 | public class UrlMatcherTests 8 | { 9 | [Fact] 10 | public void Should_match_paths() 11 | { 12 | var result = Test( 13 | expected: "/test", 14 | actual: "http://tempuri.org/test" 15 | ); 16 | 17 | Assert.True(result); 18 | } 19 | 20 | [Fact] 21 | public void Should_not_match_substrings() 22 | { 23 | var result = Test( 24 | expected: "/test", 25 | actual: "http://tempuri.org/test2" 26 | ); 27 | 28 | Assert.False(result); 29 | } 30 | 31 | [Fact] 32 | public void Should_fail_on_unmatched_paths() 33 | { 34 | var result = Test( 35 | expected: "/apple", 36 | actual: "http://tempuri.org/test" 37 | ); 38 | 39 | Assert.False(result); 40 | } 41 | 42 | [Fact] 43 | public void Should_match_full_urls() 44 | { 45 | var result = Test( 46 | expected: "http://tempuri.org/test", 47 | actual: "http://tempuri.org/test" 48 | ); 49 | 50 | Assert.True(result); 51 | } 52 | 53 | [Fact] 54 | public void Should_ignore_query_strings_from_actual() 55 | { 56 | var result = Test( 57 | expected: "http://tempuri.org/test", 58 | actual: "http://tempuri.org/test?query=value" 59 | ); 60 | 61 | Assert.True(result); 62 | } 63 | 64 | [Fact] 65 | public void Should_fail_on_mismatched_urls() 66 | { 67 | var result = Test( 68 | expected: "http://orange.org/orange", 69 | actual: "http://apple.org/red" 70 | ); 71 | 72 | Assert.False(result); 73 | } 74 | 75 | [Fact] 76 | public void Should_match_wildcards_in_paths() 77 | { 78 | var result = Test( 79 | expected: "http://tempuri.org/test1/*/test2", 80 | actual: "http://tempuri.org/test1/test3/test2" 81 | ); 82 | 83 | Assert.True(result); 84 | } 85 | 86 | [Fact] 87 | public void Should_fail_on_mismatched_values_in_wildcards() 88 | { 89 | var result = Test( 90 | expected: "http://tempuri.org/test1/*/test2/*/test3", 91 | actual: "http://tempuri.org/test1/apple/test3/orange/test2" 92 | ); 93 | 94 | Assert.False(result); 95 | } 96 | 97 | [Fact] 98 | public void Should_require_nonblank_vlaues_for_path_wildcards() 99 | { 100 | var result = Test( 101 | expected: "http://tempuri.org/test1/*/test2", 102 | actual: "http://tempuri.org/test1//atest2" 103 | ); 104 | 105 | Assert.False(result); 106 | } 107 | 108 | [Fact] 109 | public void Pathless_absolute_url_matches_pathless_absolute_uri() 110 | { 111 | var result = Test( 112 | expected: "http://tempuri.org", 113 | actual: "http://tempuri.org" 114 | ); 115 | 116 | Assert.True(result); 117 | } 118 | 119 | [Fact] 120 | public void Pathless_absolute_url_matches_root_absolute_uri() 121 | { 122 | var result = Test( 123 | expected: "http://tempuri.org", 124 | actual: "http://tempuri.org/" 125 | ); 126 | 127 | Assert.True(result); 128 | } 129 | 130 | [Fact] 131 | public void Root_absolute_url_matches_pathless_absolute_uri() 132 | { 133 | var result = Test( 134 | expected: "http://tempuri.org/", 135 | actual: "http://tempuri.org" 136 | ); 137 | 138 | Assert.True(result); 139 | } 140 | 141 | private bool Test(string expected, string actual) 142 | { 143 | return new UrlMatcher(expected) 144 | .Matches(Request(actual)); 145 | } 146 | 147 | private HttpRequestMessage Request(string url) 148 | { 149 | return new HttpRequestMessage(HttpMethod.Get, url); 150 | } 151 | 152 | [Fact] 153 | public void ToString_describes_matcher() 154 | { 155 | var sut = new UrlMatcher("http://tempuri.org/"); 156 | 157 | var result = sut.ToString(); 158 | 159 | Assert.Equal("URL matches http://tempuri.org/", result); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/Matchers/XmlContentMatcherTests.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Matchers; 2 | using System; 3 | using System.IO; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Xml.Serialization; 7 | using Xunit; 8 | 9 | namespace RichardSzalay.MockHttp.Tests.Matchers; 10 | 11 | public class XmlContentMatcherTests 12 | { 13 | private static XmlSerializer xmlSerializer = new XmlSerializer(typeof(XmlContent)); 14 | 15 | [Fact] 16 | public void Should_succeed_when_predicate_returns_true() 17 | { 18 | var result = Test( 19 | expected: c => c.Value == true, 20 | actual: new XmlContent { Value = true } 21 | ); 22 | 23 | Assert.True(result); 24 | } 25 | 26 | [Fact] 27 | public void Should_fail_when_predicate_returns_false() 28 | { 29 | var result = Test( 30 | expected: c => c.Value == false, 31 | actual: new XmlContent { Value = true } 32 | ); 33 | 34 | Assert.False(result); 35 | } 36 | 37 | private bool Test(Func expected, XmlContent actual) 38 | { 39 | var sut = new XmlContentMatcher(expected); 40 | 41 | var ms = new MemoryStream(); 42 | var sw = new StreamWriter(ms); 43 | xmlSerializer.Serialize(sw, actual); 44 | 45 | StringContent content = new StringContent(Encoding.UTF8.GetString(ms.ToArray())); 46 | 47 | return sut.Matches(new HttpRequestMessage(HttpMethod.Get, 48 | "http://tempuri.org/home") 49 | { Content = content }); 50 | } 51 | 52 | public class XmlContent 53 | { 54 | public bool Value { get; set; } 55 | } 56 | 57 | [Fact] 58 | public void ToString_describes_matcher() 59 | { 60 | var sut = new XmlContentMatcher(x => true); 61 | 62 | var result = sut.ToString(); 63 | 64 | Assert.Equal("XML request body matches custom XmlContent predicate", result); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/MockHttpMessageHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace RichardSzalay.MockHttp.Tests; 8 | 9 | public class MockHttpMessageHandlerTests 10 | { 11 | [Fact] 12 | public void Should_respond_with_basic_requests() 13 | { 14 | var mockHandler = new MockHttpMessageHandler(); 15 | 16 | mockHandler 17 | .When("/test") 18 | .Respond("application/json", "{'status' : 'OK'}"); 19 | 20 | var result = new HttpClient(mockHandler).GetAsync("http://invalid/test").Result; 21 | 22 | Assert.Equal(System.Net.HttpStatusCode.OK, result.StatusCode); 23 | Assert.Equal("application/json", result.Content?.Headers.ContentType?.MediaType); 24 | Assert.Equal("{'status' : 'OK'}", result.Content?.ReadAsStringAsync().Result); 25 | } 26 | 27 | [Fact] 28 | public void Should_fall_through_to_next_handler() 29 | { 30 | var mockHandler = new MockHttpMessageHandler(); 31 | var client = new HttpClient(mockHandler); 32 | 33 | mockHandler 34 | .When(HttpMethod.Get, "/test") 35 | .Respond(System.Net.HttpStatusCode.OK, "application/json", "{'status' : 'OK'}"); 36 | 37 | mockHandler 38 | .When(HttpMethod.Post, "/test") 39 | .Respond(System.Net.HttpStatusCode.OK, "application/json", "{'status' : 'Boo'}"); 40 | 41 | var result = client.PostAsync("http://invalid/test", null).Result; 42 | 43 | Assert.Equal("{'status' : 'Boo'}", result.Content.ReadAsStringAsync().Result); 44 | } 45 | 46 | [Fact] 47 | public void Should_assign_request_to_response_object() 48 | { 49 | var mockHandler = new MockHttpMessageHandler(); 50 | var client = new HttpClient(mockHandler); 51 | 52 | mockHandler 53 | .When(HttpMethod.Get, "/test") 54 | .Respond(System.Net.HttpStatusCode.OK, "application/json", "{'status' : 'OK'}"); 55 | 56 | mockHandler 57 | .When(HttpMethod.Post, "/test") 58 | .Respond(System.Net.HttpStatusCode.OK, "application/json", "{'status' : 'Boo'}"); 59 | 60 | var request = new HttpRequestMessage(HttpMethod.Post, "http://invalid/test"); 61 | 62 | var result = client.SendAsync(request).Result; 63 | 64 | Assert.Same(request, result.RequestMessage); 65 | } 66 | 67 | [Fact] 68 | public void Manual_flush_should_wait_for_flush() 69 | { 70 | var mockHandler = new MockHttpMessageHandler(); 71 | var client = new HttpClient(mockHandler); 72 | 73 | mockHandler 74 | .When("/test") 75 | .Respond(System.Net.HttpStatusCode.OK, "application/json", "{'status' : 'OK'}"); 76 | 77 | mockHandler.AutoFlush = false; 78 | 79 | bool completed = false; 80 | 81 | var task = client.GetAsync("http://invalid/test") 82 | .ContinueWith(_ => completed = true); 83 | 84 | Assert.False(completed); 85 | 86 | mockHandler.Flush(); 87 | 88 | task.Wait(); 89 | 90 | Assert.True(completed); 91 | } 92 | 93 | [Fact] 94 | public void VerifyNoOutstandingRequest_should_throw_for_outstanding_requests() 95 | { 96 | var mockHandler = new MockHttpMessageHandler(); 97 | var client = new HttpClient(mockHandler); 98 | 99 | mockHandler 100 | .When("/test") 101 | .Respond(System.Net.HttpStatusCode.OK, "application/json", "{'status' : 'OK'}"); 102 | 103 | mockHandler.AutoFlush = false; 104 | 105 | var task = client.GetAsync("http://invalid/test"); 106 | 107 | Assert.Throws(() => mockHandler.VerifyNoOutstandingRequest()); 108 | } 109 | 110 | [Fact] 111 | public void VerifyNoOutstandingRequest_should_not_throw_when_outstanding_requests() 112 | { 113 | var mockHandler = new MockHttpMessageHandler(); 114 | var client = new HttpClient(mockHandler); 115 | 116 | mockHandler 117 | .When("/test") 118 | .Respond(System.Net.HttpStatusCode.OK, "application/json", "{'status' : 'OK'}"); 119 | 120 | mockHandler.AutoFlush = false; 121 | 122 | bool completed = false; 123 | 124 | var task = client.GetAsync("http://invalid/test") 125 | .ContinueWith(_ => completed = true); 126 | 127 | Assert.False(completed); 128 | 129 | mockHandler.Flush(); 130 | 131 | task.Wait(); 132 | 133 | mockHandler.VerifyNoOutstandingRequest(); 134 | } 135 | 136 | [Fact] 137 | [Obsolete] 138 | public void Should_return_fallback_for_unmatched_requests() 139 | { 140 | var mockHandler = new MockHttpMessageHandler(); 141 | var client = new HttpClient(mockHandler); 142 | 143 | mockHandler 144 | .When("/test") 145 | .Respond(System.Net.HttpStatusCode.OK, "application/json", "{'status' : 'OK'}"); 146 | 147 | mockHandler.Fallback.Respond(new HttpResponseMessage(System.Net.HttpStatusCode.OK) 148 | { 149 | ReasonPhrase = "Awesome" 150 | }); 151 | 152 | var result = client.GetAsync("http://invalid/test2").Result; 153 | 154 | Assert.Equal("Awesome", result.ReasonPhrase); 155 | } 156 | 157 | [Fact] 158 | public async Task Default_fallback_throws_MockHttpMatchException() 159 | { 160 | var mockHandler = new MockHttpMessageHandler(); 161 | var client = new HttpClient(mockHandler); 162 | 163 | mockHandler 164 | .When("/test") 165 | .Respond(System.Net.HttpStatusCode.OK, "application/json", "{'status' : 'OK'}"); 166 | 167 | var result = await Assert.ThrowsAsync(() => client.GetAsync("http://invalid/test2")); 168 | 169 | Assert.StartsWith("Failed to match a mocked request for GET http://invalid/test2", result.Message); 170 | } 171 | 172 | [Fact] 173 | public void Should_match_expect_before_when() 174 | { 175 | var mockHandler = new MockHttpMessageHandler(); 176 | var client = new HttpClient(mockHandler); 177 | 178 | mockHandler 179 | .When("/test") 180 | .Respond(System.Net.HttpStatusCode.OK, "application/json", "{'status' : 'OK'}"); 181 | 182 | mockHandler 183 | .Expect("/test") 184 | .Respond(System.Net.HttpStatusCode.OK, "application/json", "{'status' : 'Test'}"); 185 | 186 | var result1 = client.GetAsync("http://invalid/test").Result; 187 | var result2 = client.GetAsync("http://invalid/test").Result; 188 | 189 | Assert.Equal("{'status' : 'Test'}", result1.Content.ReadAsStringAsync().Result); 190 | Assert.Equal("{'status' : 'OK'}", result2.Content.ReadAsStringAsync().Result); 191 | } 192 | 193 | [Fact] 194 | public void Should_match_expect_in_order() 195 | { 196 | var mockHandler = new MockHttpMessageHandler(); 197 | var client = new HttpClient(mockHandler); 198 | 199 | mockHandler 200 | .Expect("/test") 201 | .Respond("application/json", "{'status' : 'First'}"); 202 | 203 | mockHandler 204 | .Expect("/test") 205 | .Respond("application/json", "{'status' : 'Second'}"); 206 | 207 | var result1 = client.GetAsync("http://invalid/test").Result; 208 | var result2 = client.GetAsync("http://invalid/test").Result; 209 | 210 | Assert.Equal("{'status' : 'First'}", result1.Content.ReadAsStringAsync().Result); 211 | Assert.Equal("{'status' : 'Second'}", result2.Content.ReadAsStringAsync().Result); 212 | } 213 | 214 | [Fact] 215 | public void Should_not_match_expect_out_of_order() 216 | { 217 | var mockHandler = new MockHttpMessageHandler(); 218 | var client = new HttpClient(mockHandler); 219 | 220 | mockHandler 221 | .Expect("/test1") 222 | .Respond("application/json", "{'status' : 'First'}"); 223 | 224 | mockHandler 225 | .Expect("/test2") 226 | .Respond("application/json", "{'status' : 'Second'}"); 227 | 228 | mockHandler.Fallback.Respond(HttpStatusCode.NotFound); 229 | 230 | var result = client.GetAsync("http://invalid/test2").Result; 231 | 232 | Assert.Equal(HttpStatusCode.NotFound, result.StatusCode); 233 | } 234 | 235 | [Fact] 236 | public void VerifyNoOutstandingExpectation_should_fail_if_outstanding_expectation() 237 | { 238 | var mockHandler = new MockHttpMessageHandler(); 239 | var client = new HttpClient(mockHandler); 240 | 241 | mockHandler 242 | .Expect("/test") 243 | .Respond("application/json", "{'status' : 'First'}"); 244 | 245 | Assert.Throws(() => mockHandler.VerifyNoOutstandingExpectation()); 246 | } 247 | 248 | [Fact] 249 | public void VerifyNoOutstandingExpectation_should_succeed_if_no_outstanding_expectation() 250 | { 251 | var mockHandler = new MockHttpMessageHandler(); 252 | var client = new HttpClient(mockHandler); 253 | 254 | mockHandler 255 | .Expect("/test") 256 | .Respond("application/json", "{'status' : 'First'}"); 257 | 258 | var result = client.GetAsync("http://invalid/test").Result; 259 | 260 | mockHandler.VerifyNoOutstandingExpectation(); 261 | } 262 | 263 | [Fact] 264 | public void ResetExpectations_should_clear_expectations() 265 | { 266 | var mockHandler = new MockHttpMessageHandler(); 267 | var client = new HttpClient(mockHandler); 268 | 269 | mockHandler 270 | .When("/test") 271 | .Respond("application/json", "{'status' : 'OK'}"); 272 | 273 | mockHandler 274 | .Expect("/test") 275 | .Respond("application/json", "{'status' : 'Test'}"); 276 | 277 | mockHandler.ResetExpectations(); 278 | 279 | var result = client.GetAsync("http://invalid/test").Result; 280 | 281 | Assert.Equal("{'status' : 'OK'}", result.Content.ReadAsStringAsync().Result); 282 | } 283 | 284 | [Fact] 285 | public void Should_not_match_when_if_expectations_exist_and_behavhior_is_NoExpectations() 286 | { 287 | var mockHandler = new MockHttpMessageHandler(); 288 | var client = new HttpClient(mockHandler); 289 | 290 | mockHandler 291 | .When("/test") 292 | .Respond(System.Net.HttpStatusCode.OK, "application/json", "{'status' : 'OK'}"); 293 | 294 | mockHandler 295 | .Expect("/testA") 296 | .Respond(System.Net.HttpStatusCode.OK, "application/json", "{'status' : 'Test'}"); 297 | 298 | mockHandler.Fallback.RespondMatchSummary(); 299 | 300 | var result = client.GetAsync("http://invalid/test").Result; 301 | 302 | Assert.Equal(HttpStatusCode.NotFound, result.StatusCode); 303 | } 304 | 305 | [Fact] 306 | public void Should_match_when_if_expectations_exist_and_behavhior_is_Always() 307 | { 308 | var mockHandler = new MockHttpMessageHandler(BackendDefinitionBehavior.Always); 309 | var client = new HttpClient(mockHandler); 310 | 311 | mockHandler 312 | .When("/test") 313 | .Respond(System.Net.HttpStatusCode.OK, "application/json", "{'status' : 'OK'}"); 314 | 315 | mockHandler 316 | .Expect("/testA") 317 | .Respond(System.Net.HttpStatusCode.OK, "application/json", "{'status' : 'Test'}"); 318 | 319 | var result = client.GetAsync("http://invalid/test").Result; 320 | 321 | Assert.Equal("{'status' : 'OK'}", result.Content.ReadAsStringAsync().Result); 322 | } 323 | 324 | [Fact] 325 | public void GetMatchCount_returns_zero_when_never_called() 326 | { 327 | var mockHandler = new MockHttpMessageHandler(); 328 | var client = new HttpClient(mockHandler); 329 | 330 | var testRequest = mockHandler 331 | .When("/test") 332 | .Respond("application/json", "{'status' : 'OK'}"); 333 | 334 | Assert.Equal(0, mockHandler.GetMatchCount(testRequest)); 335 | } 336 | 337 | [Fact] 338 | public async Task GetMatchCount_returns_call_count() 339 | { 340 | var mockHandler = new MockHttpMessageHandler(); 341 | var client = new HttpClient(mockHandler); 342 | 343 | var testARequest = mockHandler 344 | .When("/testA") 345 | .Respond("application/json", "{'status' : 'OK'}"); 346 | 347 | var testBRequest = mockHandler 348 | .When("/testB") 349 | .Respond("application/json", "{'status' : 'OK'}"); 350 | 351 | await client.GetAsync("http://invalid/testA"); 352 | await client.GetAsync("http://invalid/testA"); 353 | await client.GetAsync("http://invalid/testB"); 354 | 355 | Assert.Equal(2, mockHandler.GetMatchCount(testARequest)); 356 | Assert.Equal(1, mockHandler.GetMatchCount(testBRequest)); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/MockedRequestExtentionsRespondTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Xunit; 11 | 12 | namespace RichardSzalay.MockHttp.Tests; 13 | 14 | /// 15 | /// Sanity check tests for MockedRequest.Respond() extension methods. 16 | /// 17 | public class MockedRequestExtentionsRespondTests 18 | { 19 | [Fact] 20 | public void Respond_HttpContent() 21 | { 22 | var response = Test(r => r.Respond(new StringContent("test", Encoding.UTF8, "text/plain"))); 23 | 24 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 25 | Assert.Equal("text/plain", response.Content?.Headers.ContentType?.MediaType); 26 | Assert.Equal("test", response.Content?.ReadAsStringAsync().Result); 27 | } 28 | 29 | [Fact] 30 | public void Respond_HttpContent_Unique() 31 | { 32 | var mockHttp = new MockHttpMessageHandler(); 33 | 34 | var mockedRequest = mockHttp.When("/path"); 35 | mockedRequest.Respond(req => new StringContent("test", Encoding.UTF8, "text/plain")); 36 | 37 | var contentA = mockedRequest.SendAsync(request, CancellationToken.None).Result.Content; 38 | var contentB = mockedRequest.SendAsync(request, CancellationToken.None).Result.Content; 39 | 40 | Assert.NotSame(contentA, contentB); 41 | } 42 | 43 | [Fact] 44 | public void Respond_String_Unique() 45 | { 46 | var mockHttp = new MockHttpMessageHandler(); 47 | 48 | var mockedRequest = mockHttp.When("/path"); 49 | mockedRequest.Respond("application/json", "{\"test\":\"value\"}"); 50 | 51 | var contentA = mockedRequest.SendAsync(request, CancellationToken.None).Result.Content; 52 | var contentB = mockedRequest.SendAsync(request, CancellationToken.None).Result.Content; 53 | 54 | Assert.NotSame(contentA, contentB); 55 | } 56 | 57 | [Fact] 58 | public void Respond_Stream_Reread_If_Possible() 59 | { 60 | var mockHttp = new MockHttpMessageHandler(); 61 | 62 | var mockedRequest = mockHttp.When("/path"); 63 | mockedRequest.Respond("text/plain", new MemoryStream(Encoding.UTF8.GetBytes("test"))); 64 | 65 | var contentA = mockedRequest.SendAsync(request, CancellationToken.None).Result.Content.ReadAsStringAsync().Result; 66 | var contentB = mockedRequest.SendAsync(request, CancellationToken.None).Result.Content.ReadAsStringAsync().Result; 67 | 68 | Assert.NotSame(contentA, contentB); 69 | } 70 | 71 | [Fact] 72 | public void Respond_Stream_Handler() 73 | { 74 | var mockHttp = new MockHttpMessageHandler(); 75 | 76 | var mockedRequest = mockHttp.When("/path"); 77 | mockedRequest.Respond("text/plain", req => new MemoryStream(Encoding.UTF8.GetBytes("test"))); 78 | 79 | var contentA = mockedRequest.SendAsync(request, CancellationToken.None).Result.Content.ReadAsStringAsync().Result; 80 | var contentB = mockedRequest.SendAsync(request, CancellationToken.None).Result.Content.ReadAsStringAsync().Result; 81 | 82 | Assert.NotSame(contentA, contentB); 83 | } 84 | 85 | [Fact] 86 | [Obsolete] 87 | public void Respond_HttpMessage() 88 | { 89 | var expected = new HttpResponseMessage(); 90 | var response = Test(r => r.Respond(expected)); 91 | 92 | Assert.Same(expected, response); 93 | } 94 | 95 | [Fact] 96 | public void Respond_HttpStatusCode() 97 | { 98 | var response = Test(r => r.Respond(HttpStatusCode.NoContent)); 99 | 100 | Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); 101 | } 102 | 103 | [Fact] 104 | public void Respond_FuncRequestResponse() 105 | { 106 | var expected = new HttpResponseMessage(); 107 | var response = Test(r => r.Respond(req => expected)); 108 | 109 | Assert.Same(expected, response); 110 | } 111 | 112 | [Fact] 113 | public void Respond_FuncRequestAsyncResponse() 114 | { 115 | var expected = new HttpResponseMessage(); 116 | var response = Test(r => r.Respond(req => Task.FromResult(expected))); 117 | 118 | Assert.Same(expected, response); 119 | } 120 | 121 | [Fact] 122 | public void Respond_HttpStatus_HttpContent() 123 | { 124 | var response = Test(r => r.Respond(HttpStatusCode.Found, new StringContent("test", Encoding.UTF8, "text/plain"))); 125 | 126 | Assert.Equal(HttpStatusCode.Found, response.StatusCode); 127 | Assert.Equal("text/plain", response.Content?.Headers.ContentType?.MediaType); 128 | Assert.Equal("test", response.Content?.ReadAsStringAsync().Result); 129 | } 130 | 131 | [Fact] 132 | public void Respond_HttpStatus_headers_HttpContent() 133 | { 134 | var response = Test(r => r.Respond(HttpStatusCode.Found, new Dictionary { 135 | { "connection", "keep-alive" }, 136 | { "x-hello", "mockhttp" } 137 | }, new StringContent("test", Encoding.UTF8, "text/plain"))); 138 | 139 | Assert.Equal(HttpStatusCode.Found, response.StatusCode); 140 | Assert.Equal("text/plain", response.Content?.Headers.ContentType?.MediaType); 141 | Assert.Equal("test", response.Content?.ReadAsStringAsync().Result); 142 | Assert.Equal("keep-alive", response.Headers.Connection.First()); 143 | Assert.Equal("mockhttp", response.Headers.GetValues("x-hello").First()); 144 | } 145 | 146 | [Fact] 147 | public void Respond_mediaTypeString_contentStream() 148 | { 149 | MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes("test")); 150 | 151 | var response = Test(r => r.Respond("text/plain", ms)); 152 | 153 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 154 | Assert.Equal("text/plain", response.Content?.Headers.ContentType?.MediaType); 155 | Assert.Equal("test", response.Content?.ReadAsStringAsync().Result); 156 | } 157 | 158 | [Fact] 159 | public void Respond_headers_mediaTypeString_contentStream() 160 | { 161 | MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes("test")); 162 | 163 | var response = Test(r => r.Respond(new Dictionary { 164 | { "connection", "keep-alive" }, 165 | { "x-hello", "mockhttp" } 166 | }, "text/plain", ms)); 167 | 168 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 169 | Assert.Equal("text/plain", response.Content?.Headers.ContentType?.MediaType); 170 | Assert.Equal("test", response.Content?.ReadAsStringAsync().Result); 171 | Assert.Equal("keep-alive", response.Headers.Connection.First()); 172 | Assert.Equal("mockhttp", response.Headers.GetValues("x-hello").First()); 173 | } 174 | 175 | [Fact] 176 | public void Respond_mediaTypeString_contentString() 177 | { 178 | var response = Test(r => r.Respond("text/plain", "test")); 179 | 180 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 181 | Assert.Equal("text/plain", response.Content?.Headers.ContentType?.MediaType); 182 | Assert.Equal("test", response.Content?.ReadAsStringAsync().Result); 183 | } 184 | 185 | [Fact] 186 | public void Respond_headers_mediaTypeString_contentString() 187 | { 188 | var response = Test(r => r.Respond(new Dictionary { 189 | { "connection", "keep-alive" }, 190 | { "x-hello", "mockhttp" } 191 | }, "text/plain", "test")); 192 | 193 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 194 | Assert.Equal("text/plain", response.Content?.Headers.ContentType?.MediaType); 195 | Assert.Equal("test", response.Content?.ReadAsStringAsync().Result); 196 | Assert.Equal("keep-alive", response.Headers.Connection.First()); 197 | Assert.Equal("mockhttp", response.Headers.GetValues("x-hello").First()); 198 | } 199 | 200 | [Fact] 201 | public void Respond_HttpStatusCode_mediaTypeString_contentStream() 202 | { 203 | MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes("test")); 204 | 205 | var response = Test(r => r.Respond(HttpStatusCode.PartialContent, "text/plain", ms)); 206 | 207 | Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode); 208 | Assert.Equal("text/plain", response.Content?.Headers.ContentType?.MediaType); 209 | Assert.Equal("test", response.Content?.ReadAsStringAsync().Result); 210 | } 211 | 212 | 213 | [Fact] 214 | public void Respond_HttpStatusCode_headers_mediaTypeString_contentStream() 215 | { 216 | MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes("test")); 217 | 218 | var response = Test(r => r.Respond(HttpStatusCode.PartialContent, new Dictionary { 219 | { "connection", "keep-alive" }, 220 | { "x-hello", "mockhttp" } 221 | }, "text/plain", ms)); 222 | 223 | Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode); 224 | Assert.Equal("text/plain", response.Content?.Headers.ContentType?.MediaType); 225 | Assert.Equal("test", response.Content?.ReadAsStringAsync().Result); 226 | Assert.Equal("keep-alive", response.Headers.Connection.First()); 227 | Assert.Equal("mockhttp", response.Headers.GetValues("x-hello").First()); 228 | } 229 | 230 | [Fact] 231 | public void Respond_HttpStatusCode_mediaTypeString_contentString() 232 | { 233 | var response = Test(r => r.Respond(HttpStatusCode.PartialContent, "text/plain", "test")); 234 | 235 | Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode); 236 | Assert.Equal("text/plain", response.Content?.Headers.ContentType?.MediaType); 237 | Assert.Equal("test", response.Content?.ReadAsStringAsync().Result); 238 | } 239 | 240 | [Fact] 241 | public void Respond_HttpStatusCode_headers_mediaTypeString_contentString() 242 | { 243 | var response = Test(r => r.Respond(HttpStatusCode.PartialContent, new Dictionary { 244 | { "connection", "keep-alive" }, 245 | { "x-hello", "mockhttp" } 246 | }, "text/plain", "test")); 247 | 248 | Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode); 249 | Assert.Equal("text/plain", response.Content?.Headers.ContentType?.MediaType); 250 | Assert.Equal("test", response.Content?.ReadAsStringAsync().Result); 251 | Assert.Equal("keep-alive", response.Headers.Connection.First()); 252 | Assert.Equal("mockhttp", response.Headers.GetValues("x-hello").First()); 253 | } 254 | 255 | [Fact] 256 | public void Respond_HttpMessageHandler() 257 | { 258 | var passthroughHandler = new MockHttpMessageHandler(); 259 | passthroughHandler.Fallback.Respond(new StringContent("test", Encoding.UTF8, "text/plain")); 260 | 261 | var response = Test(r => r.Respond(passthroughHandler)); 262 | 263 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 264 | Assert.Equal("text/plain", response.Content?.Headers.ContentType?.MediaType); 265 | Assert.Equal("test", response.Content?.ReadAsStringAsync().Result); 266 | } 267 | 268 | [Fact] 269 | public void Respond_HttpMessageHandler_headers() 270 | { 271 | var passthroughHandler = new MockHttpMessageHandler(); 272 | passthroughHandler.Fallback.Respond(new Dictionary { 273 | { "connection", "keep-alive" }, 274 | { "x-hello", "mockhttp" } 275 | }, new StringContent("test", Encoding.UTF8, "text/plain")); 276 | 277 | var response = Test(r => r.Respond(passthroughHandler)); 278 | 279 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 280 | Assert.Equal("text/plain", response.Content?.Headers.ContentType?.MediaType); 281 | Assert.Equal("test", response.Content?.ReadAsStringAsync().Result); 282 | Assert.Equal("keep-alive", response.Headers.Connection.First()); 283 | Assert.Equal("mockhttp", response.Headers.GetValues("x-hello").First()); 284 | } 285 | 286 | [Fact] 287 | public void Respond_HttpClient() 288 | { 289 | var passthroughHandler = new MockHttpMessageHandler(); 290 | passthroughHandler.Fallback.Respond(new StringContent("test", Encoding.UTF8, "text/plain")); 291 | 292 | var passthroughClient = new HttpClient(passthroughHandler); 293 | 294 | var response = Test(r => r.Respond(passthroughClient)); 295 | 296 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 297 | Assert.Equal("text/plain", response.Content?.Headers.ContentType?.MediaType); 298 | Assert.Equal("test", response.Content?.ReadAsStringAsync().Result); 299 | } 300 | 301 | [Fact] 302 | public void Respond_Throwing_Exception() 303 | { 304 | var exceptionToThrow = new HttpRequestException("Mocking an HTTP Request Exception."); 305 | 306 | Assert.Throws(() => Test(r => r.Throw(exceptionToThrow))); 307 | } 308 | 309 | private HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "http://www.tempuri.org/path?apple=red&pear=green") 310 | { 311 | Content = new FormUrlEncodedContent(new Dictionary 312 | { 313 | { "data1", "value1" }, 314 | { "data2", "value2" } 315 | }) 316 | }; 317 | 318 | private HttpResponseMessage Test(Action respond) 319 | { 320 | var mockHttp = new MockHttpMessageHandler(); 321 | 322 | var mockedRequest = mockHttp.When("/path"); 323 | respond(mockedRequest); 324 | 325 | return mockedRequest.SendAsync(request, CancellationToken.None).Result; 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/MockedRequestExtentionsWithTests.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Matchers; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Xml; 7 | using Xunit; 8 | 9 | namespace RichardSzalay.MockHttp.Tests; 10 | 11 | /// 12 | /// Sanity check tests for MockedRequest extension methods. One pass/fail per overload 13 | /// 14 | public class MockedRequestExtentionsWithTests 15 | { 16 | [Fact] 17 | public void WithQueryString_name_value() 18 | { 19 | TestPass(r => r.WithQueryString("apple", "red")); 20 | TestFail(r => r.WithQueryString("apple", "green")); 21 | } 22 | 23 | [Fact] 24 | public void WithQueryString_valuesString() 25 | { 26 | TestPass(r => r.WithQueryString("apple=red&pear=green")); 27 | TestFail(r => r.WithQueryString("apple=green&pear=red")); 28 | } 29 | 30 | [Fact] 31 | public void WithQueryString_valuesPairs() 32 | { 33 | TestPass(r => r.WithQueryString(new Dictionary 34 | { 35 | { "apple", "red" } 36 | })); 37 | 38 | TestFail(r => r.WithQueryString(new Dictionary 39 | { 40 | { "apple", "green" } 41 | })); 42 | } 43 | 44 | [Fact] 45 | public void WithFormData_name_value() 46 | { 47 | TestPass(r => r.WithFormData("data1", "value1")); 48 | TestFail(r => r.WithFormData("data1", "value2")); 49 | } 50 | 51 | [Fact] 52 | public void WithFormData_valuesString() 53 | { 54 | TestPass(r => r.WithFormData("data1=value1&data2=value2")); 55 | TestFail(r => r.WithFormData("data1=value2&data2=value1")); 56 | } 57 | 58 | [Fact] 59 | public void WithFormData_valuesPairs() 60 | { 61 | TestPass(r => r.WithFormData(new Dictionary 62 | { 63 | { "data1", "value1" } 64 | })); 65 | 66 | TestFail(r => r.WithFormData(new Dictionary 67 | { 68 | { "data1", "value2" } 69 | })); 70 | } 71 | 72 | [Fact] 73 | public void WithContent() 74 | { 75 | TestPass(r => r.WithContent("data1=value1&data2=value2")); 76 | 77 | TestFail(r => r.WithContent("data1=value2&data2=value1")); 78 | } 79 | 80 | [Fact] 81 | public void WithPartialContent() 82 | { 83 | TestPass(r => r.WithPartialContent("value2")); 84 | 85 | TestFail(r => r.WithPartialContent("value3")); 86 | } 87 | 88 | [Fact] 89 | public void WithAny_matchersEnumerable() 90 | { 91 | TestPass(r => r.WithAny(new List 92 | { 93 | new QueryStringMatcher("apple=blue"), 94 | new QueryStringMatcher("apple=red") 95 | })); 96 | 97 | TestFail(r => r.WithAny(new List 98 | { 99 | new QueryStringMatcher("apple=blue"), 100 | new QueryStringMatcher("apple=green") 101 | })); 102 | } 103 | 104 | [Fact] 105 | public void WithAny_matchersParams() 106 | { 107 | TestPass(r => r.WithAny( 108 | new QueryStringMatcher("apple=blue"), 109 | new QueryStringMatcher("apple=red") 110 | )); 111 | 112 | TestFail(r => r.WithAny( 113 | new QueryStringMatcher("apple=blue"), 114 | new QueryStringMatcher("apple=green") 115 | )); 116 | } 117 | 118 | [Fact] 119 | public void With() 120 | { 121 | TestPass(r => 122 | r.With(req => req.Content?.Headers.ContentType?.MediaType == "application/x-www-form-urlencoded")); 123 | 124 | TestFail(r => 125 | r.With(req => req.Content?.Headers.ContentType?.MediaType == "text/xml")); 126 | } 127 | 128 | [Fact] 129 | public void WithHeaders_name_value() 130 | { 131 | TestPass(r => r.WithHeaders("Accept", "text/plain")); 132 | TestFail(r => r.WithHeaders("Accept", "text/xml")); 133 | } 134 | 135 | [Fact] 136 | public void WithHeaders_valuesString() 137 | { 138 | TestPass(r => r.WithHeaders(@"Accept: text/plain 139 | Accept-Language: en")); 140 | TestFail(r => r.WithHeaders(@"Accept: text/plain 141 | Accept-Language: fr")); 142 | } 143 | 144 | [Fact] 145 | public void WithHeaders_valuesPairs() 146 | { 147 | TestPass(r => r.WithHeaders(new Dictionary 148 | { 149 | { "Accept", "text/plain" } 150 | })); 151 | 152 | TestFail(r => r.WithHeaders(new Dictionary 153 | { 154 | { "Accept", "text/xml" } 155 | })); 156 | } 157 | 158 | [Fact] 159 | public void WithJsonContent_value() 160 | { 161 | TestPass((request, mockRequest) => 162 | { 163 | request.Content = new StringContent(@"{""Value"":true}"); 164 | 165 | return mockRequest.WithJsonContent(new JsonContent(true)); 166 | }); 167 | 168 | TestFail((request, mockRequest) => 169 | { 170 | request.Content = new StringContent(@"{""Value"":false}"); 171 | 172 | return mockRequest.WithJsonContent(new JsonContent(true)); 173 | }); 174 | } 175 | 176 | [Fact] 177 | public void WithJsonContent_predicate() 178 | { 179 | TestPass((request, mockRequest) => 180 | { 181 | request.Content = new StringContent(@"{""Value"":true}"); 182 | 183 | return mockRequest.WithJsonContent(c => c.Value == true); 184 | }); 185 | 186 | TestFail((request, mockRequest) => 187 | { 188 | request.Content = new StringContent(@"{""Value"":false}"); 189 | 190 | return mockRequest.WithJsonContent(c => c.Value == true); 191 | }); 192 | } 193 | 194 | [Fact] 195 | public void WithXmlContent_value() 196 | { 197 | var settings = new XmlWriterSettings() 198 | { 199 | OmitXmlDeclaration = true, 200 | Indent = false 201 | }; 202 | 203 | TestPass((request, mockRequest) => 204 | { 205 | request.Content = new StringContent( 206 | @"true"); 207 | 208 | return mockRequest.WithXmlContent(new XmlContent() { Value = true }, settings: settings); 209 | }); 210 | 211 | TestFail((request, mockRequest) => 212 | { 213 | request.Content = new StringContent( 214 | @"false"); 215 | 216 | return mockRequest.WithXmlContent(new XmlContent() { Value = true }, settings: settings); 217 | }); 218 | } 219 | 220 | [Fact] 221 | public void WithXmlContent_predicate() 222 | { 223 | TestPass((request, mockRequest) => 224 | { 225 | request.Content = new StringContent(@"true"); 226 | 227 | return mockRequest.WithXmlContent(c => c.Value == true); 228 | }); 229 | 230 | TestFail((request, mockRequest) => 231 | { 232 | request.Content = new StringContent(@"false"); 233 | 234 | return mockRequest.WithXmlContent(c => c.Value == true); 235 | }); 236 | } 237 | 238 | private HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "http://www.tempuri.org/path?apple=red&pear=green") 239 | { 240 | Content = new FormUrlEncodedContent(new Dictionary 241 | { 242 | { "data1", "value1" }, 243 | { "data2", "value2" } 244 | }) 245 | }; 246 | 247 | private void TestPass(Func pass) 248 | { 249 | TestPass((_, mock) => pass(mock)); 250 | } 251 | 252 | private void TestPass(Func setup) 253 | { 254 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain")); 255 | request.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue("en")); 256 | 257 | var mockHttp = new MockHttpMessageHandler(); 258 | 259 | var mockRequest = mockHttp.Expect("/path"); 260 | 261 | mockRequest = setup(request, mockRequest); 262 | 263 | var result = mockRequest.Matches(request); 264 | 265 | Assert.True(result); 266 | } 267 | 268 | private void TestFail(Func setup) 269 | { 270 | TestFail((_, mock) => setup(mock)); 271 | } 272 | 273 | private void TestFail(Func setup) 274 | { 275 | var mockHttp = new MockHttpMessageHandler(); 276 | 277 | var mockRequest = mockHttp.Expect("/path"); 278 | 279 | mockRequest = setup(request, mockRequest); 280 | 281 | var result = mockRequest.Matches(request); 282 | 283 | Assert.False(result); 284 | } 285 | 286 | record JsonContent(bool Value); 287 | 288 | public class XmlContent 289 | { 290 | public bool Value { get; set; } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.Tests/RichardSzalay.MockHttp.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | false 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RichardSzalay.MockHttp 5 | 7.0.0 6 | MockHttp for HttpClient 7 | Richard Szalay 8 | Richard Szalay 9 | https://github.com/richardszalay/mockhttp/blob/master/LICENSE 10 | https://github.com/richardszalay/mockhttp 11 | false 12 | Testing layer for Microsoft's HttpClient library 13 | 7.0.0 - New build system and modernise target frameworks (BREAKING) 14 | 6.0.0 - Assemblies are now strong named (binary BREAKING) #1 15 | 5.0.0 - Align with official recommendations on multi-targetting HttpClient: 16 | - Add netstandard2.0 target #61 17 | - Change .NET 4.5 target to use in-band System.Net.Http reference (BREAKING) #61 18 | - Remove PCL profile 111 (BREAKING) #18 19 | 4.0.0 - Default Fallback message now includes request method and URL (BREAKING) 20 | - Deprecated FallbackMessage property removed (BREAKING) 21 | 3.3.0 - Added overloads for including custom headers in the response (thanks Sascha Kiefer!) 22 | 3.2.1 - XML documentation is now included in the NuGet package. Fixes #52 23 | 3.2.0 - MockHttpMessageHandler now tracks successful matches. Fixes #35 24 | - Added WithExactQueryString / WithExactFormData overloads. Fixes #37 25 | - Added BackendDefinitionBehavior to allow matching Backend Definitions when Request Expectations exist, but don't match. Fixes #45 26 | - Fixed typo in Response(HttpResponseMessage) obsolete message. Fixes #44 27 | 3.1.0 - Bump major version. Fixes #50 28 | 1.5.1 - Respond(HttpClient) now works as expected. Fixes #39 29 | - HttpResponseMessage can be disposed without breaking future requests. Fixes #33 30 | 1.5.0 - WithHeaders now also matches against Content-* headers (thanks Cory Lucas!) 31 | 1.4.0 - Cancellations and HttpClient timeouts are now supported. Fixes #29 32 | - Added a .ToHttpClient() convenience method to HttpClientHandler 33 | 1.3.1 - Multiple requests to the same mocked handler now return unique response streams. Fixes #21 34 | 1.3.0 - Added support for .NET Core via the .NET Standard Library (1.1) 35 | - Relative URLs now match correctly on Xamarin Android 36 | 1.2.2 - Root absolute URLs defined with no trailing flash now match those with a slash (and vice versa) 37 | 1.2.1 - HttpResponseMessage.RequestMessage is now assigned correctly 38 | - Form/Query data matching now works with both + and %20 space encodings (thanks Jozef Izso!) 39 | 1.2.0 - Changed PCL profile to support WP8.1 40 | 1.1.0 - Added MockHttpMessageHandler.Fallback and HttpClient passthrough support 41 | Copyright 2022 Richard Szalay 42 | httpclient test mock fake stub 43 | 66 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32210.238 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7DE23763-EAF5-4CDA-B937-1B8C6159104D}" 7 | ProjectSection(SolutionItems) = preProject 8 | CHANGELOG = CHANGELOG 9 | .github\workflows\ci.yml = .github\workflows\ci.yml 10 | LICENSE = LICENSE 11 | Package.props = Package.props 12 | README.md = README.md 13 | EndProjectSection 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RichardSzalay.MockHttp.Tests", "RichardSzalay.MockHttp.Tests\RichardSzalay.MockHttp.Tests.csproj", "{1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}" 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RichardSzalay.MockHttp", "RichardSzalay.MockHttp\RichardSzalay.MockHttp.csproj", "{7E7DA73E-52C4-429A-931D-71D414DA8C76}" 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Debug|ARM = Debug|ARM 23 | Debug|x64 = Debug|x64 24 | Debug|x86 = Debug|x86 25 | Release|Any CPU = Release|Any CPU 26 | Release|ARM = Release|ARM 27 | Release|x64 = Release|x64 28 | Release|x86 = Release|x86 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}.Debug|ARM.ActiveCfg = Debug|Any CPU 34 | {1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}.Debug|ARM.Build.0 = Debug|Any CPU 35 | {1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}.Debug|x64.ActiveCfg = Debug|Any CPU 36 | {1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}.Debug|x64.Build.0 = Debug|Any CPU 37 | {1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}.Debug|x86.ActiveCfg = Debug|Any CPU 38 | {1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}.Debug|x86.Build.0 = Debug|Any CPU 39 | {1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}.Release|ARM.ActiveCfg = Release|Any CPU 42 | {1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}.Release|ARM.Build.0 = Release|Any CPU 43 | {1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}.Release|x64.ActiveCfg = Release|Any CPU 44 | {1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}.Release|x64.Build.0 = Release|Any CPU 45 | {1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}.Release|x86.ActiveCfg = Release|Any CPU 46 | {1E613FE0-AA8E-40D5-B310-BCD2A798CB1D}.Release|x86.Build.0 = Release|Any CPU 47 | {7E7DA73E-52C4-429A-931D-71D414DA8C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {7E7DA73E-52C4-429A-931D-71D414DA8C76}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {7E7DA73E-52C4-429A-931D-71D414DA8C76}.Debug|ARM.ActiveCfg = Debug|Any CPU 50 | {7E7DA73E-52C4-429A-931D-71D414DA8C76}.Debug|ARM.Build.0 = Debug|Any CPU 51 | {7E7DA73E-52C4-429A-931D-71D414DA8C76}.Debug|x64.ActiveCfg = Debug|Any CPU 52 | {7E7DA73E-52C4-429A-931D-71D414DA8C76}.Debug|x64.Build.0 = Debug|Any CPU 53 | {7E7DA73E-52C4-429A-931D-71D414DA8C76}.Debug|x86.ActiveCfg = Debug|Any CPU 54 | {7E7DA73E-52C4-429A-931D-71D414DA8C76}.Debug|x86.Build.0 = Debug|Any CPU 55 | {7E7DA73E-52C4-429A-931D-71D414DA8C76}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {7E7DA73E-52C4-429A-931D-71D414DA8C76}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {7E7DA73E-52C4-429A-931D-71D414DA8C76}.Release|ARM.ActiveCfg = Release|Any CPU 58 | {7E7DA73E-52C4-429A-931D-71D414DA8C76}.Release|ARM.Build.0 = Release|Any CPU 59 | {7E7DA73E-52C4-429A-931D-71D414DA8C76}.Release|x64.ActiveCfg = Release|Any CPU 60 | {7E7DA73E-52C4-429A-931D-71D414DA8C76}.Release|x64.Build.0 = Release|Any CPU 61 | {7E7DA73E-52C4-429A-931D-71D414DA8C76}.Release|x86.ActiveCfg = Release|Any CPU 62 | {7E7DA73E-52C4-429A-931D-71D414DA8C76}.Release|x86.Build.0 = Release|Any CPU 63 | EndGlobalSection 64 | GlobalSection(SolutionProperties) = preSolution 65 | HideSolutionNode = FALSE 66 | EndGlobalSection 67 | GlobalSection(ExtensibilityGlobals) = postSolution 68 | SolutionGuid = {D349BD47-EE2A-4FDF-954F-C2F511313D79} 69 | EndGlobalSection 70 | EndGlobal 71 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/BackendDefinitionBehavior.cs: -------------------------------------------------------------------------------- 1 | namespace RichardSzalay.MockHttp; 2 | 3 | /// 4 | /// Defines the behavior for processing BackendDefinitions when Request Expectations exist 5 | /// 6 | public enum BackendDefinitionBehavior 7 | { 8 | /// 9 | /// Will not match Backend Definitions if Request Expectations exist 10 | /// 11 | NoExpectations = 0, 12 | /// 13 | /// Will match Backend Definitions if the next Request Expectation did not match 14 | /// 15 | Always = 1 16 | } 17 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/CodeAnalysis/Attributes.cs: -------------------------------------------------------------------------------- 1 | #if !NET5_0_OR_GREATER 2 | 3 | namespace System.Diagnostics.CodeAnalysis; 4 | 5 | [DebuggerNonUserCode] 6 | [AttributeUsage(AttributeTargets.Parameter)] 7 | sealed class NotNullWhenAttribute : Attribute 8 | { 9 | public bool ReturnValue { get; } 10 | 11 | public NotNullWhenAttribute(bool returnValue) => 12 | ReturnValue = returnValue; 13 | } 14 | 15 | #endif -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/Formatters/RequestHandlerResultFormatter.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Matchers; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Text; 6 | 7 | namespace RichardSzalay.MockHttp.Formatters 8 | { 9 | internal class RequestHandlerResultFormatter 10 | { 11 | public static string FormatRequestMessage(HttpRequestMessage request) => 12 | $"{request.Method} {request.RequestUri?.AbsoluteUri}"; 13 | 14 | public static string Format(RequestHandlerResult result) 15 | { 16 | var sb = new StringBuilder(); 17 | 18 | if (result.Handler != null) 19 | { 20 | sb.AppendLine(string.Format(Resources.MatchSuccessHeader, FormatRequestMessage(result.Request))); 21 | } 22 | else 23 | { 24 | sb.AppendLine(string.Format(Resources.MatchFailureHeader, FormatRequestMessage(result.Request))); 25 | } 26 | 27 | if (result.RequestExpectationResult != null) 28 | { 29 | if (result.RequestExpectationResult.Success) 30 | { 31 | sb.AppendLine(Resources.RequestExpectationMatchSuccessHeader); 32 | } 33 | else 34 | { 35 | sb.AppendLine(Resources.RequestExpectationMatchFailureHeader); 36 | } 37 | 38 | sb.AppendLine(); 39 | 40 | MockedRequestFormatter.FormatWithResult(sb, result.RequestExpectationResult); 41 | 42 | if (result.UnevaluatedRequestExpectations.Count > 0) 43 | { 44 | sb.AppendLine(string.Format(Resources.SkippedRequestExpectationsHeader, result.UnevaluatedRequestExpectations.Count)); 45 | sb.AppendLine(); 46 | } 47 | 48 | if (result.BackendDefinitionResults.Count > 0) 49 | { 50 | sb.AppendLine(Resources.BackendDefinitionFallbackHeader); 51 | } 52 | else if (result.UnevaluatedBackendDefinitions.Count > 0) 53 | { 54 | sb.AppendLine(string.Format(Resources.NoBackendDefinitionFallbackHeader, result.UnevaluatedBackendDefinitions.Count)); 55 | } 56 | } 57 | 58 | var failedBackendDefinitionsResults = result.BackendDefinitionResults.TakeWhile(r => !r.Success).ToList(); 59 | 60 | if (failedBackendDefinitionsResults.Count > 0) 61 | { 62 | sb.AppendLine(string.Format(Resources.BackendDefinitionsMatchFailedHeader, failedBackendDefinitionsResults.Count)); 63 | sb.AppendLine(); 64 | 65 | foreach (var failedBackendDefinitionResult in failedBackendDefinitionsResults) 66 | { 67 | MockedRequestFormatter.FormatWithResult(sb, failedBackendDefinitionResult); 68 | sb.AppendLine(); 69 | } 70 | } 71 | 72 | var matchedBackendDefinitionResult = result.BackendDefinitionResults 73 | .FirstOrDefault(r => object.ReferenceEquals(r.Handler, result.Handler)); 74 | 75 | if (matchedBackendDefinitionResult != null) 76 | { 77 | sb.AppendLine(Resources.BackendDefinitionMatchSuccessHeader); 78 | sb.AppendLine(); 79 | MockedRequestFormatter.FormatWithResult(sb, matchedBackendDefinitionResult); 80 | } 81 | 82 | return sb.ToString(); 83 | } 84 | } 85 | 86 | internal class MockedRequestFormatter 87 | { 88 | public static void FormatWithResult(StringBuilder sb, MockedRequestResult result) 89 | { 90 | string GetMatcherStatus(IMockedRequestMatcher matcher) 91 | { 92 | if (result.MatcherResults.TryGetValue(matcher, out var matcherResult)) 93 | { 94 | return matcherResult ? Resources.MatcherStatusSuccessLabel : Resources.MatcherStatusFailedLabel; 95 | } 96 | 97 | return Resources.MatcherStatusSkippedLabel; 98 | } 99 | 100 | if (result.Handler is not IEnumerable matchers) 101 | { 102 | if (result.Handler is not null) 103 | { 104 | sb.AppendLine(result.Handler.ToString()); 105 | } 106 | return; 107 | } 108 | 109 | void FormatAllMatchers(IEnumerable matchers, string joiner, int indent) 110 | { 111 | bool first = true; 112 | foreach (var matcher in matchers) 113 | { 114 | if (first) 115 | { 116 | first = false; 117 | } 118 | else 119 | { 120 | sb.Append(' ', indent); 121 | sb.Append(joiner); 122 | } 123 | 124 | if (matcher is AnyMatcher anyMatcher) 125 | { 126 | sb.Append($"[{GetMatcherStatus(matcher)}] {matcher}"); 127 | FormatAllMatchers(anyMatcher, "OR ", indent + 4); 128 | } 129 | else 130 | { 131 | sb.AppendLine($"[{GetMatcherStatus(matcher)}] {matcher}"); 132 | } 133 | } 134 | } 135 | 136 | FormatAllMatchers(matchers, "AND ", 4); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/Formatters/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace RichardSzalay.MockHttp.Formatters { 12 | using System; 13 | using System.Reflection; 14 | 15 | 16 | /// 17 | /// A strongly-typed resource class, for looking up localized strings, etc. 18 | /// 19 | // This class was auto-generated by the StronglyTypedResourceBuilder 20 | // class via a tool like ResGen or Visual Studio. 21 | // To add or remove a member, edit your .ResX file then rerun ResGen 22 | // with the /str option, or rebuild your VS project. 23 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 24 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 25 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 26 | internal class Resources { 27 | 28 | private static global::System.Resources.ResourceManager resourceMan; 29 | 30 | private static global::System.Globalization.CultureInfo resourceCulture; 31 | 32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 33 | internal Resources() { 34 | } 35 | 36 | /// 37 | /// Returns the cached ResourceManager instance used by this class. 38 | /// 39 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 40 | internal static global::System.Resources.ResourceManager ResourceManager { 41 | get { 42 | if (object.ReferenceEquals(resourceMan, null)) { 43 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("RichardSzalay.MockHttp.Formatters.Resources", typeof(Resources).GetTypeInfo().Assembly); 44 | resourceMan = temp; 45 | } 46 | return resourceMan; 47 | } 48 | } 49 | 50 | /// 51 | /// Overrides the current thread's CurrentUICulture property for all 52 | /// resource lookups using this strongly typed resource class. 53 | /// 54 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 55 | internal static global::System.Globalization.CultureInfo Culture { 56 | get { 57 | return resourceCulture; 58 | } 59 | set { 60 | resourceCulture = value; 61 | } 62 | } 63 | 64 | /// 65 | /// Looks up a localized string similar to matches any one of {0}. 66 | /// 67 | internal static string AnyMatcherDescriptor { 68 | get { 69 | return ResourceManager.GetString("AnyMatcherDescriptor", resourceCulture); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized string similar to Backend definitions were still evaluated because the BackendDefinitionBehavior is set to Always. This can be changed by using BackendDefinitionBehavior.NoExpectations. 75 | /// 76 | internal static string BackendDefinitionFallbackHeader { 77 | get { 78 | return ResourceManager.GetString("BackendDefinitionFallbackHeader", resourceCulture); 79 | } 80 | } 81 | 82 | /// 83 | /// Looks up a localized string similar to The following backend definition matched and handled the result:. 84 | /// 85 | internal static string BackendDefinitionMatchSuccessHeader { 86 | get { 87 | return ResourceManager.GetString("BackendDefinitionMatchSuccessHeader", resourceCulture); 88 | } 89 | } 90 | 91 | /// 92 | /// Looks up a localized string similar to {0} backend definitions were evaluated but did not match:. 93 | /// 94 | internal static string BackendDefinitionsMatchFailedHeader { 95 | get { 96 | return ResourceManager.GetString("BackendDefinitionsMatchFailedHeader", resourceCulture); 97 | } 98 | } 99 | 100 | /// 101 | /// Looks up a localized string similar to request body matches {0}. 102 | /// 103 | internal static string ContentMatcherDescriptor { 104 | get { 105 | return ResourceManager.GetString("ContentMatcherDescriptor", resourceCulture); 106 | } 107 | } 108 | 109 | /// 110 | /// Looks up a localized string similar to request matches a custom predicate. 111 | /// 112 | internal static string CustomMatcherDescriptor { 113 | get { 114 | return ResourceManager.GetString("CustomMatcherDescriptor", resourceCulture); 115 | } 116 | } 117 | 118 | /// 119 | /// Looks up a localized string similar to form data exacly matches (no additional keys allowed) {0}. 120 | /// 121 | internal static string FormDataMatcherDescriptor { 122 | get { 123 | return ResourceManager.GetString("FormDataMatcherDescriptor", resourceCulture); 124 | } 125 | } 126 | 127 | /// 128 | /// Looks up a localized string similar to headers match {0}. 129 | /// 130 | internal static string HeadersMatcherDescriptor { 131 | get { 132 | return ResourceManager.GetString("HeadersMatcherDescriptor", resourceCulture); 133 | } 134 | } 135 | 136 | /// 137 | /// Looks up a localized string similar to JSON request body matches custom {0} predicate. 138 | /// 139 | internal static string JsonContentMatcherDescriptor { 140 | get { 141 | return ResourceManager.GetString("JsonContentMatcherDescriptor", resourceCulture); 142 | } 143 | } 144 | 145 | /// 146 | /// Looks up a localized string similar to FAILED. 147 | /// 148 | internal static string MatcherStatusFailedLabel { 149 | get { 150 | return ResourceManager.GetString("MatcherStatusFailedLabel", resourceCulture); 151 | } 152 | } 153 | 154 | /// 155 | /// Looks up a localized string similar to SKIPPED. 156 | /// 157 | internal static string MatcherStatusSkippedLabel { 158 | get { 159 | return ResourceManager.GetString("MatcherStatusSkippedLabel", resourceCulture); 160 | } 161 | } 162 | 163 | /// 164 | /// Looks up a localized string similar to MATCHED. 165 | /// 166 | internal static string MatcherStatusSuccessLabel { 167 | get { 168 | return ResourceManager.GetString("MatcherStatusSuccessLabel", resourceCulture); 169 | } 170 | } 171 | 172 | /// 173 | /// Looks up a localized string similar to Failed to match a mocked request for {0}. 174 | /// 175 | internal static string MatchFailureHeader { 176 | get { 177 | return ResourceManager.GetString("MatchFailureHeader", resourceCulture); 178 | } 179 | } 180 | 181 | /// 182 | /// Looks up a localized string similar to Matched a mocked request for {0}. 183 | /// 184 | internal static string MatchSuccessHeader { 185 | get { 186 | return ResourceManager.GetString("MatchSuccessHeader", resourceCulture); 187 | } 188 | } 189 | 190 | /// 191 | /// Looks up a localized string similar to method matches {0}. 192 | /// 193 | internal static string MethodMatcherDescriptor { 194 | get { 195 | return ResourceManager.GetString("MethodMatcherDescriptor", resourceCulture); 196 | } 197 | } 198 | 199 | /// 200 | /// Looks up a localized string similar to {0} backend definitions were not evaluated because the BackendDefinitionBehavior is set to NoExpectations. This can be changed by using BackendDefinitionBehavior.Always. 201 | /// 202 | internal static string NoBackendDefinitionFallbackHeader { 203 | get { 204 | return ResourceManager.GetString("NoBackendDefinitionFallbackHeader", resourceCulture); 205 | } 206 | } 207 | 208 | /// 209 | /// Looks up a localized string similar to request body partially matches {0}. 210 | /// 211 | internal static string PartialContentMatcherDescriptor { 212 | get { 213 | return ResourceManager.GetString("PartialContentMatcherDescriptor", resourceCulture); 214 | } 215 | } 216 | 217 | /// 218 | /// Looks up a localized string similar to form data matches {0}. 219 | /// 220 | internal static string PartialFormDataMatcherDescriptor { 221 | get { 222 | return ResourceManager.GetString("PartialFormDataMatcherDescriptor", resourceCulture); 223 | } 224 | } 225 | 226 | /// 227 | /// Looks up a localized string similar to query string matches {0}. 228 | /// 229 | internal static string PartialQueryStringMatcherDescriptor { 230 | get { 231 | return ResourceManager.GetString("PartialQueryStringMatcherDescriptor", resourceCulture); 232 | } 233 | } 234 | 235 | /// 236 | /// Looks up a localized string similar to query string exacly matches (no additional keys allowed) {0}. 237 | /// 238 | internal static string QueryStringMatcherDescriptor { 239 | get { 240 | return ResourceManager.GetString("QueryStringMatcherDescriptor", resourceCulture); 241 | } 242 | } 243 | 244 | /// 245 | /// Looks up a localized string similar to The next request expectation failed to match:. 246 | /// 247 | internal static string RequestExpectationMatchFailureHeader { 248 | get { 249 | return ResourceManager.GetString("RequestExpectationMatchFailureHeader", resourceCulture); 250 | } 251 | } 252 | 253 | /// 254 | /// Looks up a localized string similar to The next request expectation matched and handled the result:. 255 | /// 256 | internal static string RequestExpectationMatchSuccessHeader { 257 | get { 258 | return ResourceManager.GetString("RequestExpectationMatchSuccessHeader", resourceCulture); 259 | } 260 | } 261 | 262 | /// 263 | /// Looks up a localized string similar to {0} additional request expectations were not evaluated because request expectations (Expect) must be matched in order. For unordered matches, use backend definitions (When).. 264 | /// 265 | internal static string SkippedRequestExpectationsHeader { 266 | get { 267 | return ResourceManager.GetString("SkippedRequestExpectationsHeader", resourceCulture); 268 | } 269 | } 270 | 271 | /// 272 | /// Looks up a localized string similar to URL matches {0}. 273 | /// 274 | internal static string UrlMatcherDescriptor { 275 | get { 276 | return ResourceManager.GetString("UrlMatcherDescriptor", resourceCulture); 277 | } 278 | } 279 | 280 | /// 281 | /// Looks up a localized string similar to XML request body matches custom {0} predicate. 282 | /// 283 | internal static string XmlContentMatcherDescriptor { 284 | get { 285 | return ResourceManager.GetString("XmlContentMatcherDescriptor", resourceCulture); 286 | } 287 | } 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/Formatters/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | matches any one of {0} 122 | 123 | 124 | Backend definitions were still evaluated because the BackendDefinitionBehavior is set to Always. This can be changed by using BackendDefinitionBehavior.NoExpectations 125 | 126 | 127 | The following backend definition matched and handled the result: 128 | 129 | 130 | {0} backend definitions were evaluated but did not match: 131 | 132 | 133 | request body matches {0} 134 | 135 | 136 | request matches a custom predicate 137 | 138 | 139 | form data exacly matches (no additional keys allowed) {0} 140 | 141 | 142 | headers match {0} 143 | 144 | 145 | JSON request body matches custom {0} predicate 146 | 147 | 148 | FAILED 149 | 150 | 151 | SKIPPED 152 | 153 | 154 | MATCHED 155 | 156 | 157 | Failed to match a mocked request for {0} 158 | 159 | 160 | Matched a mocked request for {0} 161 | 162 | 163 | method matches {0} 164 | 165 | 166 | {0} backend definitions were not evaluated because the BackendDefinitionBehavior is set to NoExpectations. This can be changed by using BackendDefinitionBehavior.Always 167 | 168 | 169 | request body partially matches {0} 170 | 171 | 172 | form data matches {0} 173 | 174 | 175 | query string matches {0} 176 | 177 | 178 | query string exacly matches (no additional keys allowed) {0} 179 | 180 | 181 | The next request expectation failed to match: 182 | 183 | 184 | The next request expectation matched and handled the result: 185 | 186 | 187 | {0} additional request expectations were not evaluated because request expectations (Expect) must be matched in order. For unordered matches, use backend definitions (When). 188 | 189 | 190 | URL matches {0} 191 | 192 | 193 | XML request body matches custom {0} predicate 194 | 195 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/IMockedRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace RichardSzalay.MockHttp; 6 | 7 | /// 8 | /// A preconfigured response to a HTTP request 9 | /// 10 | public interface IMockedRequest 11 | { 12 | /// 13 | /// Determines if a request can be handled by this instance 14 | /// 15 | /// The being sent 16 | /// true if this instance can handle the request; false otherwise 17 | bool Matches(HttpRequestMessage request); 18 | 19 | /// 20 | /// Submits the request to be handled by this instance 21 | /// 22 | /// The request message being sent 23 | /// A for long running requests 24 | /// The to the request 25 | Task SendAsync(HttpRequestMessage message, CancellationToken cancellationToken); 26 | } 27 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/IMockedRequestMatcher.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | 3 | namespace RichardSzalay.MockHttp; 4 | 5 | /// 6 | /// Represents a constraint on a mocked request 7 | /// 8 | public interface IMockedRequestMatcher 9 | { 10 | /// 11 | /// Determines whether the implementation matches a given request 12 | /// 13 | /// The request message being evaluated 14 | /// true if the request was matched; false otherwise 15 | bool Matches(HttpRequestMessage message); 16 | } 17 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/Matchers/AnyMatcher.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Formatters; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace RichardSzalay.MockHttp.Matchers; 8 | 9 | /// 10 | /// A composite matcher that suceeds if any of it's composed matchers succeed 11 | /// 12 | public class AnyMatcher : IMockedRequestMatcher, IEnumerable 13 | { 14 | readonly IEnumerable _matchers; 15 | 16 | /// 17 | /// Construcuts a new instnace of AnyMatcher 18 | /// 19 | /// The list of matchers to evaluate 20 | public AnyMatcher(IEnumerable matchers) 21 | => _matchers = matchers; 22 | 23 | /// 24 | /// Determines whether the implementation matches a given request 25 | /// 26 | /// The request message being evaluated 27 | /// true if any of the supplied matchers succeed; false otherwise 28 | public bool Matches(System.Net.Http.HttpRequestMessage message) 29 | => _matchers.Any(m => m.Matches(message)); 30 | 31 | IEnumerator IEnumerable.GetEnumerator() 32 | => _matchers.GetEnumerator(); 33 | 34 | IEnumerator IEnumerable.GetEnumerator() 35 | => _matchers.GetEnumerator(); 36 | 37 | /// 38 | public override string ToString() 39 | { 40 | var sb = new StringBuilder(); 41 | 42 | var first = true; 43 | 44 | foreach (var matcher in _matchers) 45 | { 46 | if (first) 47 | { 48 | first = false; 49 | sb.AppendFormat(Resources.AnyMatcherDescriptor, matcher.ToString()); 50 | sb.AppendLine(); 51 | } 52 | else 53 | { 54 | sb.AppendLine($" OR {matcher.ToString()}"); 55 | } 56 | } 57 | 58 | return sb.ToString(); 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/Matchers/ContentMatcher.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Formatters; 2 | 3 | namespace RichardSzalay.MockHttp.Matchers; 4 | 5 | /// 6 | /// Matches requests on request content 7 | /// 8 | public class ContentMatcher : IMockedRequestMatcher 9 | { 10 | private string content; 11 | 12 | /// 13 | /// Constructs a new instance of ContentMatcher 14 | /// 15 | /// The content to match 16 | public ContentMatcher(string content) 17 | { 18 | this.content = content; 19 | } 20 | 21 | /// 22 | /// Determines whether the implementation matches a given request 23 | /// 24 | /// The request message being evaluated 25 | /// true if the request was matched; false otherwise 26 | public bool Matches(System.Net.Http.HttpRequestMessage message) 27 | { 28 | if (message.Content == null) 29 | return false; 30 | 31 | string actualContent = message.Content.ReadAsStringAsync().Result; 32 | 33 | return actualContent == content; 34 | } 35 | 36 | /// 37 | public override string ToString() 38 | => string.Format(Resources.ContentMatcherDescriptor, content); 39 | } 40 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/Matchers/CustomMatcher.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Formatters; 2 | using System; 3 | using System.Net.Http; 4 | 5 | namespace RichardSzalay.MockHttp.Matchers; 6 | 7 | /// 8 | /// Matches requests using a custom delegate 9 | /// 10 | public class CustomMatcher : IMockedRequestMatcher 11 | { 12 | readonly Func matcher; 13 | 14 | /// 15 | /// Constructs a new instance of CustomMatcher 16 | /// 17 | /// The matcher delegate 18 | public CustomMatcher(Func matcher) 19 | { 20 | if (matcher == null) 21 | throw new ArgumentNullException("matcher"); 22 | 23 | this.matcher = matcher; 24 | } 25 | 26 | /// 27 | /// Determines whether the implementation matches a given request 28 | /// 29 | /// The request message being evaluated 30 | /// true if the request was matched; false otherwise 31 | public bool Matches(HttpRequestMessage message) 32 | { 33 | return matcher(message); 34 | } 35 | 36 | /// 37 | public override string ToString() 38 | => Resources.CustomMatcherDescriptor; 39 | } 40 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/Matchers/FormDataMatcher.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Formatters; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Linq; 6 | using System.Net.Http; 7 | using System.Text; 8 | 9 | namespace RichardSzalay.MockHttp.Matchers; 10 | 11 | /// 12 | /// Matches requests on form data values 13 | /// 14 | public class FormDataMatcher : IMockedRequestMatcher 15 | { 16 | readonly IEnumerable> values; 17 | readonly bool exact; 18 | 19 | /// 20 | /// Constructs a new instance of FormDataMatcher using a formatted query string 21 | /// 22 | /// Formatted form data (key=value&key2=value2) 23 | /// When true, requests with form data values not included in will not match. Defaults to false 24 | public FormDataMatcher(string formData, bool exact = false) 25 | : this(QueryStringMatcher.ParseQueryString(formData), exact) 26 | { 27 | } 28 | 29 | /// 30 | /// Constructs a new instance of FormDataMatcher using a list of key value pairs to match 31 | /// 32 | /// A list of key value pairs to match 33 | /// When true, requests with form data values not included in will not match. Defaults to false 34 | public FormDataMatcher(IEnumerable> values, bool exact = false) 35 | { 36 | this.values = values; 37 | this.exact = exact; 38 | } 39 | 40 | /// 41 | /// Determines whether the implementation matches a given request 42 | /// 43 | /// The request message being evaluated 44 | /// true if the request was matched; false otherwise 45 | public bool Matches(HttpRequestMessage message) 46 | { 47 | if (!CanProcessContent(message.Content)) 48 | return false; 49 | 50 | var formData = GetFormData(message.Content); 51 | 52 | var containsAllValues = values.All(matchPair => 53 | formData.Any(p => p.Key == matchPair.Key && p.Value == matchPair.Value)); 54 | 55 | if (!containsAllValues) 56 | { 57 | return false; 58 | } 59 | 60 | if (!exact) 61 | { 62 | return true; 63 | } 64 | 65 | return formData.All(matchPair => 66 | values.Any(p => p.Key == matchPair.Key && p.Value == matchPair.Value)); 67 | } 68 | 69 | private IEnumerable> GetFormData(HttpContent content) 70 | { 71 | if (content is MultipartFormDataContent) 72 | { 73 | return ((MultipartFormDataContent)content) 74 | .Where(CanProcessContent) 75 | .SelectMany(GetFormData); 76 | } 77 | 78 | string rawFormData = content.ReadAsStringAsync().Result; 79 | 80 | return QueryStringMatcher.ParseQueryString(rawFormData); 81 | } 82 | 83 | private bool CanProcessContent([NotNullWhen(true)] HttpContent? httpContent) 84 | { 85 | return httpContent != null && 86 | httpContent.Headers.ContentType != null && 87 | (IsFormData(httpContent.Headers.ContentType.MediaType) || 88 | httpContent is MultipartFormDataContent); 89 | } 90 | 91 | private bool IsFormData(string? mediaType) 92 | { 93 | return mediaType == "application/x-www-form-urlencoded"; 94 | } 95 | 96 | /// 97 | public override string ToString() 98 | { 99 | var sb = new StringBuilder(); 100 | 101 | var first = true; 102 | 103 | foreach (var kvp in this.values) 104 | { 105 | if (first) 106 | { 107 | first = false; 108 | } 109 | else 110 | { 111 | sb.Append('&'); 112 | } 113 | 114 | sb.Append(Uri.EscapeDataString(kvp.Key)); 115 | sb.Append('='); 116 | sb.Append(Uri.EscapeDataString(kvp.Value)); 117 | } 118 | 119 | var resource = exact ? Resources.FormDataMatcherDescriptor : Resources.PartialFormDataMatcherDescriptor; 120 | return string.Format(resource, sb.ToString()); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/Matchers/HeadersMatcher.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Formatters; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net.Http; 7 | using System.Net.Http.Headers; 8 | using System.Text; 9 | 10 | namespace RichardSzalay.MockHttp.Matchers; 11 | 12 | /// 13 | /// Matches a request based on its request headers 14 | /// 15 | public class HeadersMatcher : IMockedRequestMatcher 16 | { 17 | readonly IEnumerable> headers; 18 | 19 | /// 20 | /// Constructs a new instance of HeadersMatcher using a list of key value pairs to match 21 | /// 22 | /// A list of key value pairs to match 23 | public HeadersMatcher(IEnumerable> headers) 24 | { 25 | this.headers = headers; 26 | } 27 | 28 | /// 29 | /// Constructs a new instance of HeadersMatcher using a formatted list of headers (Header: Value) 30 | /// 31 | /// A formatted list of headers, separated by Environment.NewLine 32 | public HeadersMatcher(string headers) 33 | : this(ParseHeaders(headers)) 34 | { 35 | 36 | } 37 | 38 | /// 39 | /// Determines whether the implementation matches a given request 40 | /// 41 | /// The request message being evaluated 42 | /// true if the request was matched; false otherwise 43 | public bool Matches(HttpRequestMessage message) 44 | { 45 | return headers.All(h => MatchesHeader(h, message.Headers) || MatchesHeader(h, message.Content?.Headers)); 46 | } 47 | 48 | private bool MatchesHeader(KeyValuePair matchHeader, HttpHeaders? messageHeader) 49 | { 50 | if (messageHeader == null) 51 | return false; 52 | 53 | if (!messageHeader.TryGetValues(matchHeader.Key, out var values) || values == null) 54 | return false; 55 | 56 | return values.Any(v => v == matchHeader.Value); 57 | } 58 | 59 | internal static IEnumerable> ParseHeaders(string headers) 60 | { 61 | List> headerPairs = new List>(); 62 | 63 | using (StringReader reader = new StringReader(headers)) 64 | { 65 | var line = reader.ReadLine(); 66 | 67 | while (line != null) 68 | { 69 | if (line.Trim().Length == 0) 70 | break; 71 | 72 | string[] parts = StringUtil.Split(line, ':', 2); 73 | 74 | if (parts.Length != 2) 75 | throw new ArgumentException("Invalid header: " + line); 76 | 77 | headerPairs.Add(new KeyValuePair(parts[0], parts[1].TrimStart(' '))); 78 | 79 | line = reader.ReadLine(); 80 | } 81 | } 82 | 83 | return headerPairs; 84 | } 85 | 86 | /// 87 | public override string ToString() 88 | { 89 | var sb = new StringBuilder(); 90 | 91 | sb.AppendLine(string.Format(Resources.HeadersMatcherDescriptor, string.Empty)); 92 | 93 | foreach (var kvp in this.headers) 94 | { 95 | sb.Append(" "); 96 | sb.Append(kvp.Key); 97 | sb.Append(": "); 98 | sb.AppendLine(kvp.Value); 99 | } 100 | 101 | return sb.ToString(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/Matchers/JsonContentMatcher.cs: -------------------------------------------------------------------------------- 1 | #if NET5_0_OR_GREATER 2 | 3 | using RichardSzalay.MockHttp.Formatters; 4 | using System; 5 | using System.IO; 6 | using System.Net.Http; 7 | using System.Text; 8 | using System.Text.Json; 9 | 10 | namespace RichardSzalay.MockHttp.Matchers; 11 | 12 | /// 13 | /// Matches requests on a predicate-based match of its JSON content 14 | /// 15 | /// The deserialized type that will be used for comparison 16 | public class JsonContentMatcher : IMockedRequestMatcher 17 | { 18 | private readonly JsonSerializerOptions? serializerOptions; 19 | private readonly Func predicate; 20 | 21 | /// 22 | /// Constructs a new instance of JsonContentMatcher using a predicate to be used for comparison 23 | /// 24 | /// The predicate that will be used to match the deserialized request content 25 | /// Optional. Provide the that will be used to deserialize the request content for comparison. 26 | public JsonContentMatcher(Func predicate, JsonSerializerOptions? serializerOptions = null) 27 | { 28 | this.serializerOptions = serializerOptions; 29 | this.predicate = predicate; 30 | } 31 | 32 | /// 33 | /// Determines whether the implementation matches a given request 34 | /// 35 | /// The request message being evaluated 36 | /// true if the request was matched; false otherwise 37 | public bool Matches(HttpRequestMessage message) 38 | { 39 | if (message.Content == null) 40 | { 41 | return false; 42 | } 43 | 44 | var deserializedContent = Deserialize(message.Content.ReadAsStream()); 45 | 46 | if (deserializedContent == null) 47 | { 48 | return false; 49 | } 50 | 51 | return predicate(deserializedContent); 52 | } 53 | 54 | private T? Deserialize(Stream stream) 55 | { 56 | if (stream.CanSeek) 57 | { 58 | stream.Seek(0, SeekOrigin.Begin); 59 | } 60 | #if NET5_0 61 | return JsonSerializer.DeserializeAsync(stream, serializerOptions) 62 | .GetAwaiter().GetResult(); 63 | #else 64 | return JsonSerializer.Deserialize(stream, serializerOptions); 65 | #endif 66 | } 67 | 68 | /// 69 | public override string ToString() 70 | => string.Format(Resources.JsonContentMatcherDescriptor, typeof(T).Name); 71 | } 72 | 73 | #endif -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/Matchers/MethodMatcher.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Formatters; 2 | using System.Net.Http; 3 | 4 | namespace RichardSzalay.MockHttp.Matchers; 5 | 6 | /// 7 | /// Matches requests based on their HTTP method 8 | /// 9 | public class MethodMatcher : IMockedRequestMatcher 10 | { 11 | readonly HttpMethod method; 12 | 13 | /// 14 | /// Constructs a new instance of MethodMatcher 15 | /// 16 | /// The method to match against 17 | public MethodMatcher(HttpMethod method) 18 | { 19 | this.method = method; 20 | } 21 | 22 | /// 23 | /// Determines whether the implementation matches a given request 24 | /// 25 | /// The request message being evaluated 26 | /// true if the request was matched; false otherwise 27 | public bool Matches(HttpRequestMessage message) 28 | { 29 | return message.Method == method; 30 | } 31 | 32 | /// 33 | public override string ToString() 34 | => string.Format(Resources.MethodMatcherDescriptor, method.ToString()); 35 | } 36 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/Matchers/PartialContentMatcher.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Formatters; 2 | 3 | namespace RichardSzalay.MockHttp.Matchers; 4 | 5 | /// 6 | /// Matches requests on partial request content 7 | /// 8 | public class PartialContentMatcher : IMockedRequestMatcher 9 | { 10 | private string content; 11 | 12 | /// 13 | /// Constructs a new instance of PartialContentMatcher 14 | /// 15 | /// The partial content to match 16 | public PartialContentMatcher(string content) 17 | { 18 | this.content = content; 19 | } 20 | 21 | /// 22 | /// Determines whether the implementation matches a given request 23 | /// 24 | /// The request message being evaluated 25 | /// true if the request was matched; false otherwise 26 | public bool Matches(System.Net.Http.HttpRequestMessage message) 27 | { 28 | if (message.Content == null) 29 | return false; 30 | 31 | string actualContent = message.Content.ReadAsStringAsync().Result; 32 | 33 | return actualContent.IndexOf(content) != -1; 34 | } 35 | 36 | /// 37 | public override string ToString() 38 | => string.Format(Resources.PartialContentMatcherDescriptor, content); 39 | } 40 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/Matchers/QueryStringMatcher.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Formatters; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace RichardSzalay.MockHttp.Matchers; 8 | 9 | /// 10 | /// Matches requests on querystring values 11 | /// 12 | public class QueryStringMatcher : IMockedRequestMatcher 13 | { 14 | readonly IEnumerable> values; 15 | readonly bool exact; 16 | 17 | /// 18 | /// Constructs a new instance of QueryStringMatcher using a formatted query string 19 | /// 20 | /// A formatted query string (key=value&key2=value2) 21 | /// When true, requests with querystring values not included in will not match. Defaults to false 22 | public QueryStringMatcher(string queryString, bool exact = false) 23 | : this(ParseQueryString(queryString), exact) 24 | { 25 | 26 | } 27 | 28 | /// 29 | /// Constructs a new instance of QueryStringMatcher using a list of key value pairs to match 30 | /// 31 | /// A list of key value pairs to match 32 | /// When true, requests with querystring values not included in will not match. Defaults to false 33 | public QueryStringMatcher(IEnumerable> values, bool exact = false) 34 | { 35 | this.values = values; 36 | this.exact = exact; 37 | } 38 | 39 | /// 40 | /// Determines whether the implementation matches a given request 41 | /// 42 | /// The request message being evaluated 43 | /// true if the request was matched; false otherwise 44 | public bool Matches(System.Net.Http.HttpRequestMessage message) 45 | { 46 | if (message.RequestUri == null) 47 | { 48 | return false; 49 | } 50 | 51 | var queryString = ParseQueryString(message.RequestUri.Query.TrimStart('?')); 52 | 53 | var containsAllValues = values.All(matchPair => 54 | queryString.Any(p => p.Key == matchPair.Key && p.Value == matchPair.Value)); 55 | 56 | if (!containsAllValues) 57 | { 58 | return false; 59 | } 60 | 61 | if (!exact) 62 | { 63 | return true; 64 | } 65 | 66 | return queryString.All(matchPair => 67 | values.Any(p => p.Key == matchPair.Key && p.Value == matchPair.Value)); 68 | } 69 | 70 | internal static IEnumerable> ParseQueryString(string input) 71 | { 72 | return input.TrimStart('?').Split('&') 73 | .Select(pair => StringUtil.Split(pair, '=', 2)) 74 | .Select(pair => new KeyValuePair( 75 | UrlDecode(pair[0]), 76 | pair.Length == 2 ? UrlDecode(pair[1]) : "" 77 | )) 78 | .ToList(); 79 | } 80 | 81 | internal static string UrlDecode(string urlEncodedValue) 82 | { 83 | string tmp = urlEncodedValue.Replace("+", "%20"); 84 | return Uri.UnescapeDataString(tmp); 85 | } 86 | 87 | /// 88 | public override string ToString() 89 | { 90 | var sb = new StringBuilder(); 91 | 92 | var first = true; 93 | 94 | foreach (var kvp in this.values) 95 | { 96 | if (first) 97 | { 98 | first = false; 99 | } 100 | else 101 | { 102 | sb.Append('&'); 103 | } 104 | 105 | sb.Append(Uri.EscapeDataString(kvp.Key)); 106 | sb.Append('='); 107 | sb.Append(Uri.EscapeDataString(kvp.Value)); 108 | } 109 | 110 | var resource = exact ? Resources.QueryStringMatcherDescriptor : Resources.PartialQueryStringMatcherDescriptor; 111 | return string.Format(resource, sb.ToString()); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/Matchers/UrlMatcher.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Formatters; 2 | using System; 3 | using System.Net.Http; 4 | 5 | namespace RichardSzalay.MockHttp.Matchers; 6 | 7 | /// 8 | /// Matches requests on their URL 9 | /// 10 | public class UrlMatcher : IMockedRequestMatcher 11 | { 12 | readonly string url; 13 | 14 | /// 15 | /// Constructs a new instance of UrlMatcher 16 | /// 17 | /// The url (relative or absolute) to match 18 | public UrlMatcher(string url) 19 | { 20 | if (UriUtil.TryParse(url, UriKind.Absolute, out var uri)) 21 | url = uri.AbsoluteUri; 22 | 23 | this.url = url; 24 | } 25 | 26 | /// 27 | /// Determines whether the implementation matches a given request 28 | /// 29 | /// The request message being evaluated 30 | /// true if the request was matched; false otherwise 31 | public bool Matches(HttpRequestMessage message) 32 | { 33 | if (String.IsNullOrEmpty(url) || url == "*") 34 | return true; 35 | 36 | if (message.RequestUri == null) 37 | { 38 | return false; 39 | } 40 | 41 | string matchUrl = GetUrlToMatch(message.RequestUri); 42 | 43 | bool startsWithWildcard = url.StartsWith("*", StringComparison.Ordinal); 44 | bool endsWithWildcard = url.EndsWith("*", StringComparison.Ordinal); 45 | 46 | string[] matchParts = url.Split(new[] { '*' }, StringSplitOptions.RemoveEmptyEntries); 47 | 48 | if (matchParts.Length == 0) 49 | return true; 50 | 51 | if (!startsWithWildcard) 52 | { 53 | if (!matchUrl.StartsWith(matchParts[0], StringComparison.Ordinal)) 54 | return false; 55 | } 56 | 57 | int position = 0; 58 | 59 | foreach (var matchPart in matchParts) 60 | { 61 | position = matchUrl.IndexOf(matchPart, position, StringComparison.Ordinal); 62 | 63 | if (position == -1) 64 | return false; 65 | 66 | position += matchPart.Length; 67 | } 68 | 69 | if (!endsWithWildcard && position != matchUrl.Length) 70 | { 71 | return false; 72 | } 73 | 74 | return true; 75 | } 76 | 77 | private string GetUrlToMatch(Uri input) 78 | { 79 | bool matchingFullUrl = UriUtil.IsWellFormedUriString(url.Replace('*', '-'), UriKind.Absolute); 80 | 81 | string source = matchingFullUrl 82 | ? new UriBuilder(input) { Query = "" }.Uri.AbsoluteUri 83 | : input.AbsolutePath; 84 | 85 | return source; 86 | } 87 | 88 | /// 89 | public override string ToString() 90 | => string.Format(Resources.UrlMatcherDescriptor, url); 91 | } -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/Matchers/XmlContentMatcher.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD2_0_OR_GREATER || NET5_0_OR_GREATER 2 | using RichardSzalay.MockHttp.Formatters; 3 | using System; 4 | using System.IO; 5 | using System.Net.Http; 6 | using System.Xml.Serialization; 7 | 8 | namespace RichardSzalay.MockHttp.Matchers; 9 | 10 | /// 11 | /// Matches requests on a predicate-based match of its Xml content 12 | /// 13 | /// The deserialized type that will be used for comparison 14 | public class XmlContentMatcher : IMockedRequestMatcher 15 | { 16 | private readonly XmlSerializer serializer; 17 | private readonly Func predicate; 18 | 19 | /// 20 | /// Constructs a new instance of XmlContentMatcher using a predicate to be used for comparison 21 | /// 22 | /// The predicate that will be used to match the deserialized request content 23 | /// Optional. Provide the that will be used to deserialize the request content for comparison. 24 | public XmlContentMatcher(Func predicate, XmlSerializer? serializer = null) 25 | { 26 | this.serializer = serializer ?? XmlContentMatcher.CreateSerializer(typeof(T)); 27 | this.predicate = predicate; 28 | } 29 | 30 | /// 31 | /// Determines whether the implementation matches a given request 32 | /// 33 | /// The request message being evaluated 34 | /// true if the request was matched; false otherwise 35 | public bool Matches(HttpRequestMessage message) 36 | { 37 | if (message.Content == null) 38 | { 39 | return false; 40 | } 41 | 42 | var stream = message.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); 43 | var deserializedContent = Deserialize(stream); 44 | 45 | if (deserializedContent == null) 46 | { 47 | return false; 48 | } 49 | 50 | return predicate(deserializedContent); 51 | } 52 | 53 | private T? Deserialize(Stream stream) 54 | { 55 | return (T?)serializer.Deserialize(stream); 56 | } 57 | 58 | /// 59 | public override string ToString() 60 | => string.Format(Resources.XmlContentMatcherDescriptor, typeof(T).Name); 61 | } 62 | 63 | /// 64 | /// Static class to house an XmlSerializerFactory instance 65 | /// 66 | public static class XmlContentMatcher 67 | { 68 | private static XmlSerializerFactory SerializerFactory { get; } = new XmlSerializerFactory(); 69 | 70 | /// 71 | /// Create a default instance of XmlSerializer for the given type 72 | /// 73 | /// The type to be serialized 74 | public static XmlSerializer CreateSerializer(Type type) 75 | { 76 | return SerializerFactory.CreateSerializer(type); 77 | } 78 | } 79 | #endif -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/MockHttpMatchException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RichardSzalay.MockHttp 4 | { 5 | /// 6 | /// Exception thrown by the ThrowMatchSummary extension method, indicating that the request could 7 | /// not be matched against any of the mocked requests 8 | /// 9 | public class MockHttpMatchException : Exception 10 | { 11 | /// 12 | /// Creates a new instance of MockHttpMatchException 13 | /// 14 | public MockHttpMatchException(string message) 15 | : base(message) 16 | { 17 | 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/MockHttpMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Matchers; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace RichardSzalay.MockHttp; 10 | 11 | /// 12 | /// Responds to requests using pre-configured responses 13 | /// 14 | public class MockHttpMessageHandler : HttpMessageHandler 15 | { 16 | private Queue requestExpectations = new(); 17 | private List backendDefinitions = new(); 18 | private Dictionary matchCounts = new(); 19 | private object lockObject = new(); 20 | 21 | private int outstandingRequests = 0; 22 | 23 | /// 24 | /// Creates a new instance of MockHttpMessageHandler 25 | /// 26 | public MockHttpMessageHandler(BackendDefinitionBehavior backendDefinitionBehavior = BackendDefinitionBehavior.NoExpectations) 27 | { 28 | this.backendDefinitionBehavior = backendDefinitionBehavior; 29 | 30 | AutoFlush = true; 31 | fallback = new MockedRequest(); 32 | fallback.ThrowMatchSummary(); 33 | } 34 | 35 | private bool autoFlush; 36 | readonly BackendDefinitionBehavior backendDefinitionBehavior; 37 | 38 | /// 39 | /// Requests received while AutoFlush is true will complete instantly. 40 | /// Requests received while AutoFlush is false will not complete until is called 41 | /// 42 | public bool AutoFlush 43 | { 44 | get => autoFlush; 45 | set 46 | { 47 | autoFlush = value; 48 | 49 | if (autoFlush) 50 | { 51 | flusher = new TaskCompletionSource(); 52 | flusher.SetResult(null); 53 | } 54 | else 55 | { 56 | flusher = new TaskCompletionSource(); 57 | pendingFlushers.Enqueue(flusher); 58 | } 59 | } 60 | } 61 | 62 | private Queue> pendingFlushers = new(); 63 | private TaskCompletionSource flusher = default!; // Assigned in ctor via AutoFlush setter 64 | 65 | /// 66 | /// Completes all pendings requests that were received while was false 67 | /// 68 | public void Flush() 69 | { 70 | while (pendingFlushers.Count > 0) 71 | pendingFlushers.Dequeue().SetResult(null); 72 | } 73 | 74 | /// 75 | /// Completes pendings requests that were received while was false 76 | /// 77 | public void Flush(int count) 78 | { 79 | while (pendingFlushers.Count > 0 && count-- > 0) 80 | pendingFlushers.Dequeue().SetResult(null); 81 | } 82 | 83 | /// 84 | /// Creates an HttpClient instance using this MockHttpMessageHandler 85 | /// 86 | /// An instance of HttpClient that can be used to send HTTP request against the configuration of this mock handler 87 | public HttpClient ToHttpClient() 88 | { 89 | return new HttpClient(this); 90 | } 91 | 92 | /// 93 | /// Maps the request to the most appropriate configured response 94 | /// 95 | /// The request being sent 96 | /// The token used to cancel the request 97 | /// A Task containing the future response message 98 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 99 | { 100 | var handler = FindHandler(request); 101 | 102 | return SendAsync(handler, request, cancellationToken); 103 | } 104 | 105 | private IMockedRequest FindHandler(HttpRequestMessage request) 106 | { 107 | var results = new RequestHandlerResult(request); 108 | 109 | if (requestExpectations.Count > 0) 110 | { 111 | var handler = requestExpectations.Peek(); 112 | var handlerResult = EvaluateMockedRequest(handler, request); 113 | results.RequestExpectationResult = handlerResult; 114 | 115 | results.UnevaluatedRequestExpectations.AddRange(requestExpectations.Skip(1)); 116 | 117 | if (handlerResult.Success) 118 | { 119 | requestExpectations.Dequeue(); 120 | 121 | results.Handler = handler; 122 | } 123 | } 124 | 125 | var evaluateBackendDefinitions = backendDefinitionBehavior == BackendDefinitionBehavior.Always 126 | || requestExpectations.Count == 0; 127 | 128 | foreach (var handler in backendDefinitions) 129 | { 130 | var evaludateHandler = results.Handler == null && evaluateBackendDefinitions; 131 | 132 | if (!evaludateHandler) 133 | { 134 | results.UnevaluatedBackendDefinitions.Add(handler); 135 | continue; 136 | } 137 | 138 | var handlerResult = EvaluateMockedRequest(handler, request); 139 | results.BackendDefinitionResults.Add(handlerResult); 140 | 141 | if (handlerResult.Success) 142 | { 143 | results.Handler = handler; 144 | } 145 | } 146 | 147 | SetHandlerResult(request, results); 148 | 149 | return results.Handler ?? Fallback; 150 | } 151 | 152 | private MockedRequestResult EvaluateMockedRequest(IMockedRequest mockedRequest, HttpRequestMessage request) 153 | { 154 | Dictionary matcherResults = new(); 155 | 156 | // Once a decision around public API changes is made, the new API can have matchers 157 | // return a richer object, removing the need for explicit knowledge of 'AnyMatcher' here 158 | bool IsAnyMatch(AnyMatcher matcher) 159 | { 160 | foreach (var childMatcher in matcher) 161 | { 162 | var childResult = childMatcher.Matches(request); 163 | 164 | matcherResults[childMatcher] = childResult; 165 | 166 | if (childResult) 167 | { 168 | return true; 169 | } 170 | } 171 | 172 | return false; 173 | } 174 | 175 | // This is an odd way of achieving this but allows the model to developed/iterated without changing 176 | // the public API (for now) 177 | if (mockedRequest is not IEnumerable matchers) 178 | { 179 | return MockedRequestResult.FromResult(mockedRequest, 180 | mockedRequest.Matches(request)); 181 | } 182 | 183 | var success = true; 184 | 185 | foreach (var matcher in matchers) 186 | { 187 | var matcherResult = matcher switch 188 | { 189 | AnyMatcher anyMatcher => IsAnyMatch(anyMatcher), 190 | _ => matcher.Matches(request) 191 | }; 192 | matcherResults[matcher] = matcherResult; 193 | 194 | if (!matcherResult) 195 | { 196 | success = false; 197 | break; 198 | } 199 | } 200 | 201 | return MockedRequestResult.FromMatcherResults(mockedRequest, matcherResults, success); 202 | } 203 | 204 | #if NET5_0_OR_GREATER 205 | /// 206 | /// Maps the request to the most appropriate configured response 207 | /// 208 | /// The request being sent 209 | /// The token used to cancel the request 210 | /// A Task containing the future response message 211 | protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) 212 | { 213 | // TODO: Throw is AutoFlush is disabled 214 | 215 | return SendAsync(request, cancellationToken) 216 | .GetAwaiter().GetResult(); 217 | } 218 | #endif 219 | 220 | private Task SendAsync(IMockedRequest handler, HttpRequestMessage request, CancellationToken cancellationToken) 221 | { 222 | Interlocked.Increment(ref outstandingRequests); 223 | 224 | IncrementMatchCount(handler); 225 | 226 | if (!AutoFlush) 227 | { 228 | flusher = new TaskCompletionSource(); 229 | pendingFlushers.Enqueue(flusher); 230 | } 231 | 232 | return flusher.Task.ContinueWith(_ => 233 | { 234 | Interlocked.Decrement(ref outstandingRequests); 235 | 236 | cancellationToken.ThrowIfCancellationRequested(); 237 | 238 | var completionSource = new TaskCompletionSource(); 239 | 240 | cancellationToken.Register(() => completionSource.TrySetCanceled()); 241 | 242 | handler.SendAsync(request, cancellationToken) 243 | .ContinueWith(resp => 244 | { 245 | resp.Result.RequestMessage = request; 246 | 247 | if (resp.IsFaulted) 248 | { 249 | completionSource.TrySetException(resp.Exception!); 250 | } 251 | else if (resp.IsCanceled) 252 | { 253 | completionSource.TrySetCanceled(); 254 | } 255 | else 256 | { 257 | completionSource.TrySetResult(resp.Result); 258 | } 259 | }); 260 | 261 | return completionSource.Task; 262 | }).Unwrap(); 263 | } 264 | 265 | private void IncrementMatchCount(IMockedRequest handler) 266 | { 267 | lock (lockObject) 268 | { 269 | matchCounts.TryGetValue(handler, out int count); 270 | matchCounts[handler] = count + 1; 271 | } 272 | } 273 | 274 | private MockedRequest fallback; 275 | 276 | /// 277 | /// Gets the that will handle requests that were otherwise unmatched 278 | /// 279 | public MockedRequest Fallback 280 | { 281 | get => fallback; 282 | } 283 | 284 | /// 285 | /// Adds a request expectation 286 | /// 287 | /// 288 | /// Request expectations: 289 | /// 290 | /// 291 | /// Match once 292 | /// Match in order 293 | /// Match before any backend definitions 294 | /// 295 | /// 296 | /// The that will handle the request 297 | public void AddRequestExpectation(IMockedRequest handler) 298 | { 299 | requestExpectations.Enqueue(handler); 300 | } 301 | 302 | /// 303 | /// Adds a backend definition 304 | /// 305 | /// 306 | /// Backend definitions: 307 | /// 308 | /// 309 | /// Match multiple times 310 | /// Match in any order 311 | /// Match after all request expectations have been met 312 | /// 313 | /// 314 | /// The that will handle the request 315 | public void AddBackendDefinition(IMockedRequest handler) 316 | { 317 | backendDefinitions.Add(handler); 318 | } 319 | 320 | /// 321 | /// Returns the number of times the specified request specification has been met 322 | /// 323 | /// The mocked request 324 | /// The number of times the request has matched 325 | public int GetMatchCount(IMockedRequest request) 326 | { 327 | lock (lockObject) 328 | { 329 | matchCounts.TryGetValue(request, out int count); 330 | return count; 331 | } 332 | } 333 | 334 | /// 335 | /// Disposes the current instance 336 | /// 337 | /// true if called from Dispose(); false if called from dtor() 338 | protected override void Dispose(bool disposing) 339 | { 340 | base.Dispose(disposing); 341 | } 342 | 343 | /// 344 | /// Throws an if there are requests that were received 345 | /// while was true, but have not been completed using 346 | /// 347 | public void VerifyNoOutstandingRequest() 348 | { 349 | var requests = Interlocked.CompareExchange(ref outstandingRequests, 0, 0); 350 | if (requests > 0) 351 | throw new InvalidOperationException("There are " + requests + " outstanding requests. Call Flush() to complete them"); 352 | } 353 | 354 | /// 355 | /// Throws an if there are any requests configured with Expects 356 | /// that have yet to be received 357 | /// 358 | public void VerifyNoOutstandingExpectation() 359 | { 360 | if (requestExpectations.Count > 0) 361 | throw new InvalidOperationException("There are " + requestExpectations.Count + " unfulfilled expectations"); 362 | } 363 | 364 | /// 365 | /// Clears any pending requests configured with Expect 366 | /// 367 | public void ResetExpectations() 368 | { 369 | requestExpectations.Clear(); 370 | } 371 | 372 | /// 373 | /// Clears any mocked requests configured with When 374 | /// 375 | public void ResetBackendDefinitions() 376 | { 377 | backendDefinitions.Clear(); 378 | } 379 | 380 | /// 381 | /// Clears all mocked requests configured with either Expect or When 382 | /// 383 | public void Clear() 384 | { 385 | ResetExpectations(); 386 | ResetBackendDefinitions(); 387 | } 388 | 389 | #pragma warning disable CS0618 // Type or member is obsolete. We need Properties as Options isn't supported < .NET 5 390 | private const string HandlerResultMessageKey = "_MockHttpResult"; 391 | 392 | internal static RequestHandlerResult? GetHandlerResult(HttpRequestMessage request) 393 | { 394 | 395 | if (request.Properties.TryGetValue(HandlerResultMessageKey, out var result) == true && 396 | result is RequestHandlerResult handlerResult) 397 | { 398 | return handlerResult; 399 | } 400 | 401 | return null; 402 | } 403 | 404 | internal static void SetHandlerResult(HttpRequestMessage request, RequestHandlerResult handlerResult) 405 | { 406 | 407 | request.Properties[HandlerResultMessageKey] = handlerResult; 408 | } 409 | 410 | #pragma warning restore CS0618 // Type or member is obsolete 411 | } 412 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/MockHttpMessageHandlerExtensions.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Matchers; 2 | using System.Net.Http; 3 | 4 | namespace RichardSzalay.MockHttp; 5 | 6 | /// 7 | /// Provides extension methods for 8 | /// 9 | public static class MockHttpMessageHandlerExtensions 10 | { 11 | /// 12 | /// Adds a backend definition 13 | /// 14 | /// The source handler 15 | /// The HTTP method to match 16 | /// The URL (absolute or relative, may contain * wildcards) to match 17 | /// The instance 18 | public static MockedRequest When(this MockHttpMessageHandler handler, HttpMethod method, string url) 19 | { 20 | var message = new MockedRequest(url); 21 | message.With(new MethodMatcher(method)); 22 | 23 | handler.AddBackendDefinition(message); 24 | 25 | return message; 26 | } 27 | 28 | /// 29 | /// Adds a backend definition 30 | /// 31 | /// The source handler 32 | /// The URL (absolute or relative, may contain * wildcards) to match 33 | /// The instance 34 | public static MockedRequest When(this MockHttpMessageHandler handler, string url) 35 | { 36 | var message = new MockedRequest(url); 37 | 38 | handler.AddBackendDefinition(message); 39 | 40 | return message; 41 | } 42 | 43 | /// 44 | /// Adds a request expectation 45 | /// 46 | /// The source handler 47 | /// The HTTP method to match 48 | /// The URL (absolute or relative, may contain * wildcards) to match 49 | /// The instance 50 | public static MockedRequest Expect(this MockHttpMessageHandler handler, HttpMethod method, string url) 51 | { 52 | var message = new MockedRequest(url); 53 | message.With(new MethodMatcher(method)); 54 | 55 | handler.AddRequestExpectation(message); 56 | 57 | return message; 58 | } 59 | 60 | /// 61 | /// Adds a request expectation 62 | /// 63 | /// The source handler 64 | /// The URL (absolute or relative, may contain * wildcards) to match 65 | /// The instance 66 | public static MockedRequest Expect(this MockHttpMessageHandler handler, string url) 67 | { 68 | var message = new MockedRequest(url); 69 | 70 | handler.AddRequestExpectation(message); 71 | 72 | return message; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/MockedRequest.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp.Matchers; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Net.Http; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace RichardSzalay.MockHttp; 11 | 12 | /// 13 | /// A preconfigured response to a HTTP request 14 | /// 15 | public class MockedRequest : IMockedRequest, IEnumerable 16 | { 17 | private List matchers = new List(); 18 | private Func>? response; 19 | 20 | /// 21 | /// Creates a new MockedRequest with no initial matchers 22 | /// 23 | public MockedRequest() 24 | { 25 | } 26 | 27 | /// 28 | /// Creates a new MockedRequest with an initial URL (and optionally query string) matcher 29 | /// 30 | /// An absolute or relative URL that may contain a query string 31 | public MockedRequest(string url) 32 | { 33 | string[] urlParts = StringUtil.Split(url, '?', 2); 34 | 35 | if (urlParts.Length == 2) 36 | url = urlParts[0]; 37 | 38 | if (urlParts.Length == 2) 39 | With(new QueryStringMatcher(urlParts[1])); 40 | 41 | With(new UrlMatcher(url)); 42 | } 43 | 44 | /// 45 | /// Determines if a request can be handled by this instance 46 | /// 47 | /// The being sent 48 | /// true if this instance can handle the request; false otherwise 49 | public bool Matches(HttpRequestMessage message) 50 | { 51 | return matchers.Count == 0 || matchers.All(m => m.Matches(message)); 52 | } 53 | 54 | /// 55 | /// Constraints the request using custom logic 56 | /// 57 | /// The instance 58 | public MockedRequest With(IMockedRequestMatcher matcher) 59 | { 60 | matchers.Add(matcher); 61 | return this; 62 | } 63 | 64 | /// 65 | /// Sets the response of ther 66 | /// 67 | /// 68 | public void Respond(Func> handler) 69 | { 70 | Respond(_ => handler()); 71 | } 72 | 73 | /// 74 | /// Supplies a response to the submitted request 75 | /// 76 | /// The callback that will be used to supply the response 77 | public MockedRequest Respond(Func> handler) 78 | { 79 | response = handler; 80 | return this; 81 | } 82 | 83 | /// 84 | /// Provides the configured response in relation to the request supplied 85 | /// 86 | /// The request being sent 87 | /// The token used to cancel the request 88 | /// A response was not configured for this request 89 | /// A Task containing the future response message 90 | public Task SendAsync(HttpRequestMessage message, CancellationToken cancellationToken) 91 | { 92 | if (response is null) 93 | { 94 | throw new InvalidOperationException("A response was not configured for this request"); 95 | } 96 | 97 | return response(message); 98 | } 99 | 100 | IEnumerator IEnumerable.GetEnumerator() 101 | => this.matchers.GetEnumerator(); 102 | 103 | IEnumerator IEnumerable.GetEnumerator() 104 | => this.matchers.GetEnumerator(); 105 | } 106 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/MockedRequestJsonExtensions.cs: -------------------------------------------------------------------------------- 1 | #if NET5_0_OR_GREATER 2 | using RichardSzalay.MockHttp.Matchers; 3 | using System; 4 | using System.Text.Json; 5 | 6 | namespace RichardSzalay.MockHttp; 7 | 8 | /// 9 | /// Provides JSON-related extension methods for 10 | /// 11 | public static class MockedRequestJsonExtensions 12 | { 13 | /// 14 | /// Requires that the request content contains JSON that matches 15 | /// 16 | /// 17 | /// The request content must exactly match the serialized JSON of 18 | /// 19 | /// The type that represents the JSON request 20 | /// The source mocked request 21 | /// The value that, when serialized to JSON, must match the request content 22 | /// Optional. Provide the that will be used to serialize for comparison. 23 | /// 24 | public static MockedRequest WithJsonContent(this MockedRequest source, T content, JsonSerializerOptions? serializerOptions = null) 25 | { 26 | var serializedJson = JsonSerializer.Serialize(content, serializerOptions); 27 | 28 | return source.WithContent(serializedJson); 29 | } 30 | 31 | /// 32 | /// Requires that the request content contains JSON that, when deserialized, matches 33 | /// 34 | /// 35 | /// The source mocked request 36 | /// The predicate that will be used to match the deserialized request content 37 | /// Optional. Provide the that will be used to deserialize the request content for comparison. 38 | /// The instance 39 | public static MockedRequest WithJsonContent(this MockedRequest source, Func predicate, JsonSerializerOptions? serializerOptions = null) 40 | { 41 | return source.With(new JsonContentMatcher(predicate, serializerOptions)); 42 | } 43 | } 44 | 45 | #endif -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/MockedRequestXmlExtensions.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD2_0_OR_GREATER || NET5_0_OR_GREATER 2 | using RichardSzalay.MockHttp.Matchers; 3 | using System; 4 | using System.IO; 5 | using System.Text; 6 | using System.Xml; 7 | using System.Xml.Serialization; 8 | 9 | namespace RichardSzalay.MockHttp; 10 | 11 | /// 12 | /// Provides XML-related extension methods for 13 | /// 14 | public static class MockedRequestXmlExtensions 15 | { 16 | /// 17 | /// Requires that the request content contains Xml that matches 18 | /// 19 | /// 20 | /// The request content must exactly match the serialized Xml of 21 | /// 22 | /// The type that represents the Xml request 23 | /// The source mocked request 24 | /// The value that, when serialized to Xml, must match the request content 25 | /// Optional. Provide the that will be used to serialize for comparison. 26 | /// Optional. Provide the that will be used to serialize for comparison. 27 | /// 28 | public static MockedRequest WithXmlContent(this MockedRequest source, T content, XmlSerializer? serializer = null, XmlWriterSettings? settings = null) 29 | { 30 | serializer = serializer ?? XmlContentMatcher.CreateSerializer(typeof(T)); 31 | 32 | var ms = new MemoryStream(); 33 | var writer = new StreamWriter(ms); 34 | var xmlWriter = XmlWriter.Create(writer, settings); 35 | serializer.Serialize(xmlWriter, content); 36 | 37 | return source.WithContent(Encoding.UTF8.GetString(ms.ToArray())); 38 | } 39 | 40 | /// 41 | /// Requires that the request content contains Xml that, when deserialized, matches 42 | /// 43 | /// 44 | /// The source mocked request 45 | /// The predicate that will be used to match the deserialized request content 46 | /// Optional. Provide the that will be used to deserialize the request content for comparison. 47 | /// The instance 48 | public static MockedRequest WithXmlContent(this MockedRequest source, Func predicate, XmlSerializer? serializer = null) 49 | { 50 | return source.With(new XmlContentMatcher(predicate, serializer)); 51 | } 52 | } 53 | 54 | #endif -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/RequestHandlerResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Http; 3 | 4 | namespace RichardSzalay.MockHttp 5 | { 6 | internal class RequestHandlerResult 7 | { 8 | public RequestHandlerResult(HttpRequestMessage request) 9 | { 10 | this.Request = request; 11 | } 12 | 13 | public HttpRequestMessage Request { get; } 14 | public IMockedRequest? Handler { get; set; } 15 | 16 | public MockedRequestResult? RequestExpectationResult { get; set; } 17 | public List UnevaluatedRequestExpectations { get; } = new(); 18 | 19 | public List BackendDefinitionResults { get; } = new(); 20 | public List UnevaluatedBackendDefinitions { get; } = new(); 21 | } 22 | 23 | internal class MockedRequestResult 24 | { 25 | private MockedRequestResult() 26 | { 27 | } 28 | 29 | public IMockedRequest? Handler { get; set; } = default!; 30 | 31 | public Dictionary MatcherResults { get; private set; } = new(); 32 | public bool Success { get; internal set; } 33 | 34 | internal static MockedRequestResult FromMatcherResults(IMockedRequest handler, Dictionary matcherResults, bool success) 35 | { 36 | return new() 37 | { 38 | Handler = handler, 39 | MatcherResults = matcherResults, 40 | Success = success 41 | }; 42 | } 43 | 44 | internal static MockedRequestResult FromResult(IMockedRequest handler, bool success) 45 | { 46 | return new() 47 | { 48 | Handler = handler, 49 | Success = success 50 | }; 51 | } 52 | } 53 | 54 | internal class MockedRequestMatcherResult 55 | { 56 | } 57 | } -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/RichardSzalay.MockHttp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | latest 5 | enable 6 | netstandard1.1;netstandard2.0;net5.0;net6.0 7 | RichardSzalay.MockHttp 8 | true 9 | ..\mockhttp.snk 10 | 11 | 12 | 13 | 14 | 15 | bin\$(Configuration)\$(TargetFramework)\RichardSzalay.MockHttp.xml 16 | 17 | 18 | 19 | bin\$(Configuration)\$(TargetFramework)\RichardSzalay.MockHttp.xml 20 | 21 | 22 | 23 | README.md 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | True 37 | True 38 | Resources.resx 39 | 40 | 41 | 42 | 43 | 44 | ResXFileCodeGenerator 45 | Resources.Designer.cs 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/StringUtil.cs: -------------------------------------------------------------------------------- 1 | namespace RichardSzalay.MockHttp; 2 | 3 | internal static class StringUtil 4 | { 5 | public static string[] Split(string input, char c, int count) 6 | { 7 | int index = input.IndexOf(c); 8 | 9 | return index == -1 10 | ? new[] { input } 11 | : new[] { input.Substring(0, index), input.Substring(index + 1) }; 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/TaskEx.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace RichardSzalay.MockHttp; 4 | 5 | class TaskEx 6 | { 7 | public static Task FromResult(T result) 8 | { 9 | var tcs = new TaskCompletionSource(); 10 | tcs.SetResult(result); 11 | return tcs.Task; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /RichardSzalay.MockHttp/UriUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace RichardSzalay.MockHttp; 5 | 6 | internal static class UriUtil 7 | { 8 | public static bool IsWellFormedUriString(string url, UriKind kind) 9 | { 10 | return TryParse(url, kind, out _); 11 | 12 | } 13 | 14 | // MonoAndroid parses relative URI's and adds a "file://" protocol, which causes the matcher to fail 15 | public static bool TryParse(string url, UriKind kind, [NotNullWhen(true)] out Uri? output) 16 | { 17 | if (!Uri.TryCreate(url, kind, out output)) 18 | { 19 | return false; 20 | } 21 | 22 | bool isAndroidFalsePositive = output.Scheme == "file" && !url.StartsWith("file://", StringComparison.Ordinal); 23 | 24 | return !isAndroidFalsePositive; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /mockhttp.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardszalay/mockhttp/7b0d72f5a5d243193d8b9f1142ee59e78de98b3a/mockhttp.snk --------------------------------------------------------------------------------