├── .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 | [](https://www.nuget.org/packages/RichardSzalay.MockHttp/)[](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