Compare commits

...

33 Commits

Author SHA1 Message Date
Stef Heyenrath
1e07f3f7f3 0.11.0 2026-04-26 18:59:33 +02:00
Stef Heyenrath
64d3e4cbf3 fix tests 2026-04-26 09:09:40 +02:00
Stef Heyenrath
47b0bf594f . 2026-04-26 08:52:56 +02:00
Stef Heyenrath
31636e5e40 ... 2026-04-25 09:39:44 +02:00
Stef Heyenrath
32f42105b1 Merge branch 'master' into SystemTextJsonMatcher 2026-04-24 16:49:02 +02:00
Stef Heyenrath
8bf42904ab 2.4.0 2026-04-24 16:48:28 +02:00
dependabot[bot]
0a48b40021 Bump OpenTelemetry.Exporter.OpenTelemetryProtocol from 1.14.0 to 1.15.x (#1450)
* Bump OpenTelemetry.Exporter.OpenTelemetryProtocol from 1.14.0 to 1.15.3

---
updated-dependencies:
- dependency-name: OpenTelemetry.Exporter.OpenTelemetryProtocol
  dependency-version: 1.15.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Upgrade all OpenTelemetry related NuGet packages to latest versions

Agent-Logs-Url: https://github.com/wiremock/WireMock.Net/sessions/5b2f4449-ec3f-49f1-afbe-654e19a97a33

Co-authored-by: StefH <249938+StefH@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: StefH <249938+StefH@users.noreply.github.com>
2026-04-24 08:34:00 +02:00
Jayaraman Venkatesan
1962437dcd Added feature to enable and disable mappings (#1437)
* feat/1421 added feature to enable and disable mappings

* feat/1421 updated test constants to reflect 2 new admin endpoints /enable and /disable

* feat/1421 updated tests to fix flakyness - removed delay before assertion that is causing upstream connection from proxy to teardown prematurely before test ends

* feat/1421 addressing PR comments - Updated logic to represent IsDisable insted of IsEnabled
2026-04-24 08:07:37 +02:00
Stef Heyenrath
7d1dcc6fb1 x 2026-04-20 19:37:09 +02:00
Stef Heyenrath
82277c7804 Merge branch 'master' into SystemTextJsonMatcher 2026-04-20 18:55:02 +02:00
Stef Heyenrath
85d61a1877 2.3.0 2026-04-20 18:53:59 +02:00
Stef Heyenrath
b105fd3706 . 2026-04-19 19:20:00 +02:00
Stef Heyenrath
21cc70e77b . 2026-04-19 11:05:55 +02:00
Stef Heyenrath
65bf469906 more tests 2026-04-18 15:41:35 +02:00
Stef Heyenrath
3fcc530d30 Merge branch 'master' into SystemTextJsonMatcher 2026-04-18 09:43:59 +02:00
Stef Heyenrath
885911203b Use DefaultJsonSerializer for BodyAsJson-Response (#1448) 2026-04-18 09:43:24 +02:00
Stef Heyenrath
ebbedda098 . 2026-04-18 08:31:29 +02:00
Stef Heyenrath
b55842afae Update src/WireMock.Net.Minimal/Properties/AssemblyInfo.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-18 08:21:37 +02:00
Stef Heyenrath
8d5f98dc0e Update src/WireMock.Net/WireMock.Net.csproj
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-18 08:21:12 +02:00
Stef Heyenrath
18d6b4958c Update test/WireMock.Net.Tests/WebSockets/WebSocketIntegrationTests.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-18 08:20:58 +02:00
Stef Heyenrath
fece70a9d2 Update test/WireMock.Net.Tests/Pact/PactTests.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-18 08:19:57 +02:00
Stef Heyenrath
ce35fe14af new projectx 2026-04-17 18:07:46 +02:00
Stef Heyenrath
6cb41494c8 . 2026-04-17 15:27:43 +02:00
Stef Heyenrath
6e402acd80 , 2026-04-17 14:42:43 +02:00
Stef Heyenrath
9853f055f2 SystemTextJsonMatcher 2026-04-17 14:32:55 +02:00
Stef Heyenrath
1e591d5f8a Fix ExactMatcher and JsonMatcher not working for ISO dates as string (#1443) 2026-04-17 13:32:26 +02:00
Stef Heyenrath
02b7e3744e Update instructions.md (#1444) 2026-04-17 13:23:29 +02:00
dependabot[bot]
6e2a4d7e04 Bump log4net from 2.0.15 to 3.3.0 (#1440)
---
updated-dependencies:
- dependency-name: log4net
  dependency-version: 3.3.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-16 07:49:17 +02:00
Jayaraman Venkatesan
479bb0b8ec bug/wiremock-1268 moving Scenario state change before global response delay (#1436)
Co-authored-by: Stef Heyenrath <Stef.Heyenrath@gmail.com>
2026-04-03 10:51:24 +02:00
Stef Heyenrath
a453e00fdb Fix WireMock.Net.WebApplication.IIS example (#1435) 2026-03-31 22:52:27 +02:00
Stef Heyenrath
3214c2ebc7 2.2.0 2026-03-30 19:51:09 +02:00
Stef Heyenrath
6c6a42979e Add comments for ScenarioStateStore related code (#1433) 2026-03-30 19:49:28 +02:00
Stef Heyenrath
b4f5b9256c Upgrade Scriban.Signed (#1434) 2026-03-30 19:49:16 +02:00
99 changed files with 4863 additions and 1138 deletions

View File

@@ -1,4 +1,5 @@
# Copilot Instructions # Copilot Instructions
## Project Guidelines ## Project Guidelines
- All new byte[xx] calls should use using var data = ArrayPool<byte>.Shared.Lease(xx); instead of directly allocating byte arrays - When running tests in this workspace, do not run tests for the net48 target framework.
- When changing System.Text.Json code in this workspace, verify API availability for netstandard2.0 and netstandard2.1 instead of assuming newer APIs exist.

15
.github/instructions/instructions.md vendored Normal file
View File

@@ -0,0 +1,15 @@
---
description: 'Guidelines for this solution'
applyTo: '**/*.csproj, **/*.cs'
---
# Multi .NET Framework Targeting
## Instructions
- The main project "WireMock.Net.Minimal" targets `netstandard2.0` and `net8.0`. Ensure that any new code or dependencies are compatible with these frameworks.
# C# Guidelines
## Instructions
- When a new ByteArray is needed, do not use `var data = new byte[bufferSize];`. Always use `var data = ArrayPool<byte>.Shared.Lease(bufferSize);`.

View File

@@ -1,3 +1,23 @@
# 2.4.0 (24 April 2026)
- [#1437](https://github.com/wiremock/WireMock.Net/pull/1437) - Added feature to enable and disable mappings [feature] contributed by [jayaraman-venkatesan](https://github.com/jayaraman-venkatesan)
- [#1450](https://github.com/wiremock/WireMock.Net/pull/1450) - Bump OpenTelemetry.Exporter.OpenTelemetryProtocol from 1.14.0 to 1.15.x [dependencies, .NET] contributed by [dependabot[bot]](https://github.com/apps/dependabot)
- [#1421](https://github.com/wiremock/WireMock.Net/issues/1421) - Deactivate mapping without deleting it [feature]
# 2.3.0 (20 April 2026)
- [#1436](https://github.com/wiremock/WireMock.Net/pull/1436) - Moving Scenario state change before global response delay is set [feature] contributed by [jayaraman-venkatesan](https://github.com/jayaraman-venkatesan)
- [#1440](https://github.com/wiremock/WireMock.Net/pull/1440) - Bump log4net from 2.0.15 to 3.3.0 [dependencies] contributed by [dependabot[bot]](https://github.com/apps/dependabot)
- [#1443](https://github.com/wiremock/WireMock.Net/pull/1443) - Fix ExactMatcher and JsonMatcher not working for ISO dates as string [bug] contributed by [StefH](https://github.com/StefH)
- [#1444](https://github.com/wiremock/WireMock.Net/pull/1444) - Update instructions.md [refactor] contributed by [StefH](https://github.com/StefH)
- [#1448](https://github.com/wiremock/WireMock.Net/pull/1448) - Use DefaultJsonSerializer for BodyAsJson-Response [bug] contributed by [StefH](https://github.com/StefH)
- [#1205](https://github.com/wiremock/WireMock.Net/issues/1205) - System.Private.Uri 4.3.0 Blackduck security High vulnerability [bug]
- [#1260](https://github.com/wiremock/WireMock.Net/issues/1260) - Scenario state change before a response delay timeout ends [bug]
- [#1441](https://github.com/wiremock/WireMock.Net/issues/1441) - ExactMatcher and JsonMatcher not working for ISO dates in v2.2.0 [bug]
- [#1446](https://github.com/wiremock/WireMock.Net/issues/1446) - WithBodyAsJson does not return correctly formatted json when using SystemTextJsonConverter [bug]
# 2.2.0 (30 March 2026)
- [#1433](https://github.com/wiremock/WireMock.Net/pull/1433) - Add comments for ScenarioStateStore related code [refactor] contributed by [StefH](https://github.com/StefH)
- [#1434](https://github.com/wiremock/WireMock.Net/pull/1434) - Upgrade Scriban.Signed [security] contributed by [StefH](https://github.com/StefH)
# 2.1.0 (29 March 2026) # 2.1.0 (29 March 2026)
- [#1425](https://github.com/wiremock/WireMock.Net/pull/1425) - Add helpers for query params fluent MappingModelBuilder [feature] contributed by [biltongza](https://github.com/biltongza) - [#1425](https://github.com/wiremock/WireMock.Net/pull/1425) - Add helpers for query params fluent MappingModelBuilder [feature] contributed by [biltongza](https://github.com/biltongza)
- [#1430](https://github.com/wiremock/WireMock.Net/pull/1430) - Add injectable IScenarioStateStore for distributed scenario state [feature] contributed by [m4tchl0ck](https://github.com/m4tchl0ck) - [#1430](https://github.com/wiremock/WireMock.Net/pull/1430) - Add injectable IScenarioStateStore for distributed scenario state [feature] contributed by [m4tchl0ck](https://github.com/m4tchl0ck)

View File

@@ -4,7 +4,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<VersionPrefix>2.1.0</VersionPrefix> <VersionPrefix>2.4.0</VersionPrefix>
<PackageIcon>WireMock.Net-Logo.png</PackageIcon> <PackageIcon>WireMock.Net-Logo.png</PackageIcon>
<PackageProjectUrl>https://github.com/wiremock/WireMock.Net</PackageProjectUrl> <PackageProjectUrl>https://github.com/wiremock/WireMock.Net</PackageProjectUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression> <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>

View File

@@ -1,7 +1,7 @@
rem https://github.com/StefH/GitHubReleaseNotes rem https://github.com/StefH/GitHubReleaseNotes
SET version=2.1.0 SET version=2.4.0
GitHubReleaseNotes --output CHANGELOG.md --skip-empty-releases --exclude-labels wontfix test question invalid doc duplicate example environment --version %version% --token %GH_TOKEN% GitHubReleaseNotes --output CHANGELOG.md --skip-empty-releases --exclude-labels wontfix test question invalid doc duplicate example environment --version %version% --token %GH_TOKEN%
GitHubReleaseNotes --output PackageReleaseNotes.txt --skip-empty-releases --exclude-labels test question invalid doc duplicate example environment --template PackageReleaseNotes.template --version %version% --token %GH_TOKEN% GitHubReleaseNotes --output PackageReleaseNotes.txt --skip-empty-releases --exclude-labels wontfix test question invalid doc duplicate example environment --template PackageReleaseNotes.template --version %version% --token %GH_TOKEN%

View File

@@ -1,7 +1,6 @@
# 2.1.0 (29 March 2026) # 2.4.0 (24 April 2026)
- #1425 Add helpers for query params fluent MappingModelBuilder [feature] - #1437 Added feature to enable and disable mappings [feature]
- #1430 Add injectable IScenarioStateStore for distributed scenario state [feature] - #1450 Bump OpenTelemetry.Exporter.OpenTelemetryProtocol from 1.14.0 to 1.15.x [dependencies, .NET]
- #1431 Fix WireMockLogger implementation in dotnet-WireMock [bug] - #1421 Deactivate mapping without deleting it [feature]
- #1432 Add WireMockAspNetCoreLogger to log Kestrel warnings/errors [feature]
The full release notes can be found here: https://github.com/wiremock/WireMock.Net/blob/master/CHANGELOG.md The full release notes can be found here: https://github.com/wiremock/WireMock.Net/blob/master/CHANGELOG.md

View File

@@ -64,6 +64,7 @@ A C# .NET version based on [mock4net](https://github.com/alexvictoor/mock4net) w
| | | | | | | |
| &nbsp;&nbsp;**WireMock.Net.Extensions.Routing** | [![NuGet Badge WireMock.Net.Extensions.Routing](https://img.shields.io/nuget/v/WireMock.Net.Extensions.Routing)](https://www.nuget.org/packages/WireMock.Net.Extensions.Routing) | [![MyGet Badge WireMock.Net.Extensions.Routing](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.Extensions.Routing?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.Extensions.Routing) | &nbsp;&nbsp;**WireMock.Net.Extensions.Routing** | [![NuGet Badge WireMock.Net.Extensions.Routing](https://img.shields.io/nuget/v/WireMock.Net.Extensions.Routing)](https://www.nuget.org/packages/WireMock.Net.Extensions.Routing) | [![MyGet Badge WireMock.Net.Extensions.Routing](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.Extensions.Routing?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.Extensions.Routing)
| &nbsp;&nbsp;**WireMock.Net.Matchers.CSharpCode** | [![NuGet Badge WireMock.Net.Matchers.CSharpCode](https://img.shields.io/nuget/v/WireMock.Net.Matchers.CSharpCode)](https://www.nuget.org/packages/WireMock.Net.Matchers.CSharpCode) | [![MyGet Badge WireMock.Net.Matchers.CSharpCode](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.Matchers.CSharpCode?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.Matchers.CSharpCode) | &nbsp;&nbsp;**WireMock.Net.Matchers.CSharpCode** | [![NuGet Badge WireMock.Net.Matchers.CSharpCode](https://img.shields.io/nuget/v/WireMock.Net.Matchers.CSharpCode)](https://www.nuget.org/packages/WireMock.Net.Matchers.CSharpCode) | [![MyGet Badge WireMock.Net.Matchers.CSharpCode](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.Matchers.CSharpCode?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.Matchers.CSharpCode)
| &nbsp;&nbsp;**WireMock.Net.Matchers.SystemTextJsonPath** | [![NuGet Badge WireMock.Net.Matchers.SystemTextJsonPath](https://img.shields.io/nuget/v/WireMock.Net.Matchers.SystemTextJsonPath)](https://www.nuget.org/packages/WireMock.Net.Matchers.SystemTextJsonPath) | [![MyGet Badge WireMock.Net.Matchers.SystemTextJsonPath](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.Matchers.SystemTextJsonPath?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.Matchers.SystemTextJsonPath)
| &nbsp;&nbsp;**WireMock.Net.OpenApiParser** | [![NuGet Badge WireMock.Net.OpenApiParser](https://img.shields.io/nuget/v/WireMock.Net.OpenApiParser)](https://www.nuget.org/packages/WireMock.Net.OpenApiParser) | [![MyGet Badge WireMock.Net.OpenApiParser](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.OpenApiParser?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.OpenApiParser) | &nbsp;&nbsp;**WireMock.Net.OpenApiParser** | [![NuGet Badge WireMock.Net.OpenApiParser](https://img.shields.io/nuget/v/WireMock.Net.OpenApiParser)](https://www.nuget.org/packages/WireMock.Net.OpenApiParser) | [![MyGet Badge WireMock.Net.OpenApiParser](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.OpenApiParser?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.OpenApiParser)
| &nbsp;&nbsp;**WireMock.Net.MimePart** | [![NuGet Badge WireMock.Net.MimePart](https://img.shields.io/nuget/v/WireMock.Net.MimePart)](https://www.nuget.org/packages/WireMock.Net.MimePart) | [![MyGet Badge WireMock.Net.MimePart](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.MimePart?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.MimePart) | &nbsp;&nbsp;**WireMock.Net.MimePart** | [![NuGet Badge WireMock.Net.MimePart](https://img.shields.io/nuget/v/WireMock.Net.MimePart)](https://www.nuget.org/packages/WireMock.Net.MimePart) | [![MyGet Badge WireMock.Net.MimePart](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.MimePart?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.MimePart)
| &nbsp;&nbsp;**WireMock.Net.GraphQL** | [![NuGet Badge WireMock.Net.GraphQL](https://img.shields.io/nuget/v/WireMock.Net.GraphQL)](https://www.nuget.org/packages/WireMock.Net.GraphQL) | [![MyGet Badge WireMock.Net.GraphQL](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.GraphQL?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.GraphQL) | &nbsp;&nbsp;**WireMock.Net.GraphQL** | [![NuGet Badge WireMock.Net.GraphQL](https://img.shields.io/nuget/v/WireMock.Net.GraphQL)](https://www.nuget.org/packages/WireMock.Net.GraphQL) | [![MyGet Badge WireMock.Net.GraphQL](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.GraphQL?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.GraphQL)
@@ -76,7 +77,7 @@ A C# .NET version based on [mock4net](https://github.com/alexvictoor/mock4net) w
<br /> <br />
🔺 **WireMock.Net.Minimal** does not include *WireMock.Net.MimePart*, *WireMock.Net.GraphQL*, *WireMock.Net.ProtoBuf* and *WireMock.Net.OpenTelemetry*. 🔺 **WireMock.Net.Minimal** does not include *WireMock.Net.MimePart*, *WireMock.Net.GraphQL*, *WireMock.Net.ProtoBuf*, *WireMock.Net.OpenTelemetry* and *WireMock.Net.Matchers.SystemTextJsonPath*.
--- ---

View File

@@ -1,4 +1,3 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18 # Visual Studio Version 18
VisualStudioVersion = 18.0.11205.157 VisualStudioVersion = 18.0.11205.157
@@ -156,6 +155,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.TestWebApplica
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.RestClient.AwesomeAssertions", "src\WireMock.Net.RestClient.AwesomeAssertions\WireMock.Net.RestClient.AwesomeAssertions.csproj", "{F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.RestClient.AwesomeAssertions", "src\WireMock.Net.RestClient.AwesomeAssertions\WireMock.Net.RestClient.AwesomeAssertions.csproj", "{F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.Matchers.SystemTextJsonPath", "src\WireMock.Net.Matchers.SystemTextJsonPath\WireMock.Net.Matchers.SystemTextJsonPath.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -850,6 +851,18 @@ Global
{F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Release|x64.Build.0 = Release|Any CPU {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Release|x64.Build.0 = Release|Any CPU
{F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Release|x86.ActiveCfg = Release|Any CPU {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Release|x86.ActiveCfg = Release|Any CPU
{F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Release|x86.Build.0 = Release|Any CPU {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Release|x86.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -914,6 +927,7 @@ Global
{2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B} = {985E0ADB-D4B4-473A-AA40-567E279B7946} {2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
{3B05CC76-C3CB-8667-6B65-3129DFB25681} = {0BB8B634-407A-4610-A91F-11586990767A} {3B05CC76-C3CB-8667-6B65-3129DFB25681} = {0BB8B634-407A-4610-A91F-11586990767A}
{F4B2B967-98D7-4D93-9A5C-5EF7B84B941A} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458} SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458}

View File

@@ -12,11 +12,11 @@
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.4.0" /> <PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.4.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="8.0.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.1" /> <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" /> <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" /> <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" /> <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.8.0" /> <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -18,7 +18,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\WireMock.Net\WireMock.Net.csproj" /> <ProjectReference Include="..\..\src\WireMock.Net\WireMock.Net.csproj" />
<PackageReference Include="log4net" Version="2.0.15" /> <PackageReference Include="log4net" Version="3.3.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
</ItemGroup> </ItemGroup>

View File

@@ -8,9 +8,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.14.0" /> <PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.14.0" /> <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.14.0" /> <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,29 +1,18 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;
namespace WireMock.Net.WebApplication; namespace WireMock.Net.WebApplication;
public class App : IHostedService public class App(IWireMockService service) : IHostedService
{ {
private readonly IWireMockService _service;
public App(IWireMockService service)
{
_service = service;
}
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
_service.Start(); service.Start();
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
{ {
_service.Stop(); service.Stop();
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View File

@@ -1,9 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using WireMock.Settings; using WireMock.Settings;
namespace WireMock.Net.WebApplication; namespace WireMock.Net.WebApplication;

View File

@@ -15,7 +15,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\WireMock.Net.Abstractions\WireMock.Net.Abstractions.csproj" />
<ProjectReference Include="..\..\src\WireMock.Net\WireMock.Net.csproj" /> <ProjectReference Include="..\..\src\WireMock.Net\WireMock.Net.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -1,7 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using WireMock.Admin.Requests; using WireMock.Admin.Requests;

View File

@@ -15,7 +15,7 @@
"WireMockServerSettings": { "WireMockServerSettings": {
"StartAdminInterface": true, "StartAdminInterface": true,
"Urls": [ "Urls": [
"http://localhost" "http://localhost:0"
] ]
} }
} }

View File

@@ -1,12 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<!-- <!--
Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380 Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380
--> -->
<system.webServer> <system.webServer>
<handlers> <handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" /> <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers> </handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" /> <httpProtocol>
</system.webServer> <customHeaders>
<add name="X-Frame-Options" value="SAMEORIGIN" />
</customHeaders>
</httpProtocol>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" />
</system.webServer>
</configuration> </configuration>

View File

@@ -61,6 +61,11 @@ public class MappingModel
/// </summary> /// </summary>
public int? TimesInSameState { get; set; } public int? TimesInSameState { get; set; }
/// <summary>
/// Value to determine if the mapping is disabled. Defaults to <c>null</c> (not disabled).
/// </summary>
public bool? IsDisabled { get; set; }
/// <summary> /// <summary>
/// The request model. /// The request model.
/// </summary> /// </summary>

View File

@@ -1,7 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
namespace WireMock.Handlers; namespace WireMock.Handlers;

View File

@@ -25,7 +25,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="JsonConverter.Abstractions" Version="0.8.0" /> <PackageReference Include="JsonConverter.Abstractions" Version="0.11.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,184 @@
// Copyright © WireMock.Net
using System.Text.Json;
using System.Text.Json.Nodes;
using AnyOfTypes;
using Json.Path;
using Stef.Validation;
using WireMock.Extensions;
using WireMock.Models;
using WireMock.Util;
namespace WireMock.Matchers;
/// <summary>
/// SystemTextJsonPathMatcher - behaves the same as JsonPathMatcher but uses System.Text.Json and Json.Path instead of Newtonsoft.Json.
/// </summary>
/// <seealso cref="ISystemTextJsonPathMatcher" />
public class SystemTextJsonPathMatcher : ISystemTextJsonPathMatcher
{
private readonly AnyOf<string, StringPattern>[] _patterns;
/// <inheritdoc />
public MatchBehaviour MatchBehaviour { get; }
/// <inheritdoc />
public object Value { get; }
/// <summary>
/// Initializes a new instance of the <see cref="SystemTextJsonPathMatcher"/> class.
/// </summary>
/// <param name="patterns">The patterns.</param>
public SystemTextJsonPathMatcher(params string[] patterns)
: this(MatchBehaviour.AcceptOnMatch, MatchOperator.Or, patterns.ToAnyOfPatterns())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SystemTextJsonPathMatcher"/> class.
/// </summary>
/// <param name="patterns">The patterns.</param>
public SystemTextJsonPathMatcher(params AnyOf<string, StringPattern>[] patterns)
: this(MatchBehaviour.AcceptOnMatch, MatchOperator.Or, patterns)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SystemTextJsonPathMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="matchOperator">The <see cref="Matchers.MatchOperator"/> to use. (default = "Or")</param>
/// <param name="patterns">The patterns.</param>
public SystemTextJsonPathMatcher(
MatchBehaviour matchBehaviour,
MatchOperator matchOperator = MatchOperator.Or,
params AnyOf<string, StringPattern>[] patterns)
{
_patterns = Guard.NotNull(patterns);
MatchBehaviour = matchBehaviour;
MatchOperator = matchOperator;
Value = patterns;
}
/// <inheritdoc />
public MatchResult IsMatch(string? input)
{
var score = MatchScores.Mismatch;
Exception? exception = null;
if (!string.IsNullOrWhiteSpace(input))
{
try
{
var node = JsonNode.Parse(input!);
score = IsMatchInternal(node);
}
catch (Exception ex)
{
exception = ex;
}
}
return MatchResult.From(Name, MatchBehaviourHelper.Convert(MatchBehaviour, score), exception);
}
/// <inheritdoc />
public MatchResult IsMatch(object? input)
{
var score = MatchScores.Mismatch;
Exception? exception = null;
// When input is null or byte[], return Mismatch.
if (input != null && input is not byte[])
{
try
{
JsonNode? node = input switch
{
JsonNode jsonNode => jsonNode,
string str => JsonNode.Parse(str),
_ => JsonNode.Parse(JsonSerializer.Serialize(input))
};
score = IsMatchInternal(node);
}
catch (Exception ex)
{
exception = ex;
}
}
return MatchResult.From(Name, MatchBehaviourHelper.Convert(MatchBehaviour, score), exception);
}
/// <inheritdoc />
public AnyOf<string, StringPattern>[] GetPatterns()
{
return _patterns;
}
/// <inheritdoc />
public MatchOperator MatchOperator { get; }
/// <inheritdoc />
public string Name => nameof(SystemTextJsonPathMatcher);
/// <inheritdoc />
public string GetCSharpCodeArguments()
{
return $"new {Name}" +
$"(" +
$"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " +
$"{MatchOperator.GetFullyQualifiedEnumValue()}, " +
$"{MappingConverterUtils.ToCSharpCodeArguments(_patterns)}" +
$")";
}
private double IsMatchInternal(JsonNode? node)
{
// JsonPath.Net requires the node to be inside an object or array for filter expressions.
// Similar to JsonPathMatcher's ConvertJTokenToJArrayIfNeeded, wrap a plain object in an array
// when it's an object with a single non-array child property.
var evaluationNode = WrapIfNeeded(node);
var values = _patterns
.Select(pattern =>
{
var path = JsonPath.Parse(pattern.GetPattern());
var result = path.Evaluate(evaluationNode);
return result.Matches is { Count: > 0 };
})
.ToArray();
return MatchScores.ToScore(values, MatchOperator);
}
// Mirrors JsonPathMatcher.ConvertJTokenToJArrayIfNeeded:
// If the node is an object with exactly one property whose value is not already an array,
// wrap that value in an array so that filter expressions (e.g. [?(@.x == y)]) can match.
private static JsonNode? WrapIfNeeded(JsonNode? node)
{
if (node is not JsonObject obj)
{
return node;
}
var properties = obj.ToList();
if (properties.Count != 1)
{
return node;
}
var single = properties[0];
if (single.Value is JsonArray)
{
return node;
}
var clonedValue = JsonNode.Parse(single.Value?.ToJsonString() ?? "null");
return new JsonObject
{
[single.Key] = new JsonArray(clonedValue)
};
}
}

View File

@@ -0,0 +1,5 @@
// Copyright © WireMock.Net
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("WireMock.Net.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]

View File

@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>A SystemTextJsonPathMatcher which can be used to match WireMock.Net Requests using JsonPath.Net.</Description>
<AssemblyTitle>WireMock.Net.Matchers.SystemTextJsonPath</AssemblyTitle>
<Authors>Stef Heyenrath</Authors>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>wiremock;matchers;matcher;jsonpath;systemtextjson</PackageTags>
<RootNamespace>WireMock</RootNamespace>
<PackageId>WireMock.Net.Matchers.SystemTextJsonPath</PackageId>
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<!--<DelaySign>true</DelaySign>-->
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug - Sonar'">
<CodeAnalysisRuleSet>../WireMock.Net/WireMock.Net.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WireMock.Net.Shared\WireMock.Net.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="JsonPath.Net" Version="3.0.2" />
</ItemGroup>
</Project>

View File

@@ -1,7 +1,8 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System; using JsonConverter.Abstractions;
using System.Collections.Generic; using JsonConverter.Newtonsoft.Json;
using Newtonsoft.Json;
using WireMock.Matchers.Helpers; using WireMock.Matchers.Helpers;
using WireMock.Models.Mime; using WireMock.Models.Mime;
using WireMock.Util; using WireMock.Util;
@@ -15,6 +16,8 @@ public class MimePartMatcher : IMimePartMatcher
{ {
private readonly IList<(string Name, Func<IMimePartData, MatchResult> func)> _matcherFunctions; private readonly IList<(string Name, Func<IMimePartData, MatchResult> func)> _matcherFunctions;
private readonly IJsonConverter _jsonConverter;
/// <inheritdoc /> /// <inheritdoc />
public string Name => nameof(MimePartMatcher); public string Name => nameof(MimePartMatcher);
@@ -41,7 +44,8 @@ public class MimePartMatcher : IMimePartMatcher
IStringMatcher? contentTypeMatcher, IStringMatcher? contentTypeMatcher,
IStringMatcher? contentDispositionMatcher, IStringMatcher? contentDispositionMatcher,
IStringMatcher? contentTransferEncodingMatcher, IStringMatcher? contentTransferEncodingMatcher,
IMatcher? contentMatcher IMatcher? contentMatcher,
IJsonConverter? jsonConverter = null
) )
{ {
MatchBehaviour = matchBehaviour; MatchBehaviour = matchBehaviour;
@@ -49,6 +53,7 @@ public class MimePartMatcher : IMimePartMatcher
ContentDispositionMatcher = contentDispositionMatcher; ContentDispositionMatcher = contentDispositionMatcher;
ContentTransferEncodingMatcher = contentTransferEncodingMatcher; ContentTransferEncodingMatcher = contentTransferEncodingMatcher;
ContentMatcher = contentMatcher; ContentMatcher = contentMatcher;
_jsonConverter = jsonConverter ?? new NewtonsoftJsonConverter();
_matcherFunctions = []; _matcherFunctions = [];
if (ContentTypeMatcher != null) if (ContentTypeMatcher != null)
@@ -107,7 +112,8 @@ public class MimePartMatcher : IMimePartMatcher
ContentType = GetContentTypeAsString(mimePart.ContentType), ContentType = GetContentTypeAsString(mimePart.ContentType),
DeserializeJson = true, DeserializeJson = true,
ContentEncoding = null, // mimePart.ContentType?.CharsetEncoding.ToString(), ContentEncoding = null, // mimePart.ContentType?.CharsetEncoding.ToString(),
DecompressGZipAndDeflate = true DecompressGZipAndDeflate = true,
DefaultJsonConverter = _jsonConverter
}; };
var bodyData = BodyParser.ParseAsync(bodyParserSettings).ConfigureAwait(false).GetAwaiter().GetResult(); var bodyData = BodyParser.ParseAsync(bodyParserSettings).ConfigureAwait(false).GetAwaiter().GetResult();

View File

@@ -1,43 +1,53 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Stef.Validation;
namespace WireMock.Handlers; namespace WireMock.Handlers;
/// <summary>
/// Provides a file-based implementation of <see cref="IScenarioStateStore" /> that persists scenario states to disk and allows concurrent access.
/// </summary>
public class FileBasedScenarioStateStore : IScenarioStateStore public class FileBasedScenarioStateStore : IScenarioStateStore
{ {
private readonly ConcurrentDictionary<string, ScenarioState> _scenarios = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, ScenarioState> _scenarios = new(StringComparer.OrdinalIgnoreCase);
private readonly string _scenariosFolder; private readonly string _scenariosFolder;
private readonly object _lock = new(); private readonly object _lock = new();
/// <summary>
/// Initializes a new instance of the FileBasedScenarioStateStore class using the specified root folder as the base directory for scenario state storage.
/// </summary>
/// <param name="rootFolder">The root directory under which scenario state data will be stored. Must be a valid file system path.</param>
public FileBasedScenarioStateStore(string rootFolder) public FileBasedScenarioStateStore(string rootFolder)
{ {
Guard.NotNullOrEmpty(rootFolder);
_scenariosFolder = Path.Combine(rootFolder, "__admin", "scenarios"); _scenariosFolder = Path.Combine(rootFolder, "__admin", "scenarios");
Directory.CreateDirectory(_scenariosFolder); Directory.CreateDirectory(_scenariosFolder);
LoadScenariosFromDisk(); LoadScenariosFromDisk();
} }
/// <inheritdoc />
public bool TryGet(string name, [NotNullWhen(true)] out ScenarioState? state) public bool TryGet(string name, [NotNullWhen(true)] out ScenarioState? state)
{ {
return _scenarios.TryGetValue(name, out state); return _scenarios.TryGetValue(name, out state);
} }
/// <inheritdoc />
public IReadOnlyList<ScenarioState> GetAll() public IReadOnlyList<ScenarioState> GetAll()
{ {
return _scenarios.Values.ToArray(); return _scenarios.Values.ToArray();
} }
/// <inheritdoc />
public bool ContainsKey(string name) public bool ContainsKey(string name)
{ {
return _scenarios.ContainsKey(name); return _scenarios.ContainsKey(name);
} }
/// <inheritdoc />
public bool TryAdd(string name, ScenarioState scenarioState) public bool TryAdd(string name, ScenarioState scenarioState)
{ {
if (_scenarios.TryAdd(name, scenarioState)) if (_scenarios.TryAdd(name, scenarioState))
@@ -49,6 +59,7 @@ public class FileBasedScenarioStateStore : IScenarioStateStore
return false; return false;
} }
/// <inheritdoc />
public ScenarioState AddOrUpdate(string name, Func<string, ScenarioState> addFactory, Func<string, ScenarioState, ScenarioState> updateFactory) public ScenarioState AddOrUpdate(string name, Func<string, ScenarioState> addFactory, Func<string, ScenarioState, ScenarioState> updateFactory)
{ {
lock (_lock) lock (_lock)
@@ -59,6 +70,7 @@ public class FileBasedScenarioStateStore : IScenarioStateStore
} }
} }
/// <inheritdoc />
public ScenarioState? Update(string name, Action<ScenarioState> updateAction) public ScenarioState? Update(string name, Action<ScenarioState> updateAction)
{ {
lock (_lock) lock (_lock)
@@ -74,6 +86,7 @@ public class FileBasedScenarioStateStore : IScenarioStateStore
} }
} }
/// <inheritdoc />
public bool TryRemove(string name) public bool TryRemove(string name)
{ {
if (_scenarios.TryRemove(name, out _)) if (_scenarios.TryRemove(name, out _))
@@ -85,6 +98,7 @@ public class FileBasedScenarioStateStore : IScenarioStateStore
return false; return false;
} }
/// <inheritdoc />
public void Clear() public void Clear()
{ {
_scenarios.Clear(); _scenarios.Clear();

View File

@@ -1,8 +1,7 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using JsonConverter.Abstractions;
using WireMock.Util; using WireMock.Util;
namespace WireMock.Http; namespace WireMock.Http;
@@ -15,7 +14,8 @@ internal static class HttpResponseMessageHelper
Uri originalUri, Uri originalUri,
bool deserializeJson, bool deserializeJson,
bool decompressGzipAndDeflate, bool decompressGzipAndDeflate,
bool deserializeFormUrlEncoded) bool deserializeFormUrlEncoded,
IJsonConverter jsonConverter)
{ {
var responseMessage = new ResponseMessage { StatusCode = (int)httpResponseMessage.StatusCode }; var responseMessage = new ResponseMessage { StatusCode = (int)httpResponseMessage.StatusCode };
@@ -45,7 +45,8 @@ internal static class HttpResponseMessageHelper
DeserializeJson = deserializeJson, DeserializeJson = deserializeJson,
ContentEncoding = contentEncodingHeader?.FirstOrDefault(), ContentEncoding = contentEncodingHeader?.FirstOrDefault(),
DecompressGZipAndDeflate = decompressGzipAndDeflate, DecompressGZipAndDeflate = decompressGzipAndDeflate,
DeserializeFormUrlEncoded = deserializeFormUrlEncoded DeserializeFormUrlEncoded = deserializeFormUrlEncoded,
DefaultJsonConverter = jsonConverter
}; };
responseMessage.BodyData = await BodyParser.ParseAsync(bodyParserSettings).ConfigureAwait(false); responseMessage.BodyData = await BodyParser.ParseAsync(bodyParserSettings).ConfigureAwait(false);
} }

View File

@@ -62,6 +62,9 @@ public class Mapping : IMapping
/// <inheritdoc /> /// <inheritdoc />
public bool IsProxy => Provider is ProxyAsyncResponseProvider; public bool IsProxy => Provider is ProxyAsyncResponseProvider;
/// <inheritdoc />
public bool IsDisabled { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public bool LogMapping => Provider is not (DynamicResponseProvider or DynamicAsyncResponseProvider); public bool LogMapping => Provider is not (DynamicResponseProvider or DynamicAsyncResponseProvider);

View File

@@ -1,6 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Linq;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using WireMock.Util; using WireMock.Util;

View File

@@ -0,0 +1,173 @@
// Copyright © WireMock.Net
using System.Text.Json;
using WireMock.Util;
namespace WireMock.Matchers;
/// <summary>
/// Generic AbstractSystemTextJsonPartialMatcher - uses System.Text.Json instead of Newtonsoft.Json.
/// </summary>
public abstract class AbstractSystemTextJsonPartialMatcher : SystemTextJsonMatcher
{
/// <summary>
/// Initializes a new instance of the <see cref="AbstractSystemTextJsonPartialMatcher"/> class.
/// </summary>
protected AbstractSystemTextJsonPartialMatcher(string value, bool ignoreCase = false, bool regex = false)
: base(value, ignoreCase, regex)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AbstractSystemTextJsonPartialMatcher"/> class.
/// </summary>
protected AbstractSystemTextJsonPartialMatcher(object value, bool ignoreCase = false, bool regex = false)
: base(value, ignoreCase, regex)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AbstractSystemTextJsonPartialMatcher"/> class.
/// </summary>
protected AbstractSystemTextJsonPartialMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false)
: base(matchBehaviour, value, ignoreCase, regex)
{
}
/// <inheritdoc />
protected override bool IsMatch(JsonElement value, JsonElement? input)
{
if (input == null)
{
return false;
}
var inputElement = input.Value;
// Regex on a string value
if (Regex && value.ValueKind == JsonValueKind.String)
{
var valueAsString = value.GetString()!;
var inputAsString = GetStringValue(inputElement);
var (valid, result) = RegexUtils.MatchRegex(valueAsString, inputAsString);
if (valid)
{
return result;
}
}
// Guid comparison: both strings, both parseable as Guid
if (value.ValueKind == JsonValueKind.String && inputElement.ValueKind == JsonValueKind.String)
{
var valueStr = value.GetString()!;
var inputStr = inputElement.GetString()!;
if (Guid.TryParse(valueStr, out var vg) && Guid.TryParse(inputStr, out var ig))
{
return IsMatch(vg.ToString(), ig.ToString());
}
}
// Type mismatch (after regex/guid checks)
if (value.ValueKind != inputElement.ValueKind)
{
return false;
}
switch (value.ValueKind)
{
case JsonValueKind.Object:
{
var nestedValues = value.EnumerateObject().ToArray();
if (nestedValues.Length == 0)
{
return true;
}
return nestedValues.All(pair =>
{
var selected = SelectElement(inputElement, pair.Name);
return selected != null && IsMatch(pair.Value, selected.Value);
});
}
case JsonValueKind.Array:
{
var valuesArray = value.EnumerateArray().ToArray();
if (valuesArray.Length == 0)
{
return true;
}
var tokenArray = inputElement.EnumerateArray().ToArray();
if (tokenArray.Length == 0)
{
return false;
}
return valuesArray.All(subFilter => tokenArray.Any(subToken => IsMatch(subFilter, subToken)));
}
default:
return IsMatch(GetStringValue(value), GetStringValue(inputElement));
}
}
/// <summary>
/// Check if two strings are a match (matching can be done exact or wildcard).
/// </summary>
protected abstract bool IsMatch(string value, string input);
/// <summary>
/// Selects a <see cref="JsonElement"/> from an object using a key that may be a plain property name,
/// a dotted path (e.g. "a.b.c") or bracket notation (e.g. "['name.with.dot']"),
/// mirroring Newtonsoft's <c>SelectToken</c> + direct indexer fallback.
/// </summary>
private static JsonElement? SelectElement(JsonElement input, string key)
{
if (input.ValueKind != JsonValueKind.Object)
{
return null;
}
// Direct property access (also handles keys containing colons or dots that are literal property names)
if (input.TryGetProperty(key, out var direct))
{
return direct;
}
// Bracket notation: ['property.name.with.dots']
if (key.StartsWith("['") && key.EndsWith("']"))
{
var propertyName = key.Substring(2, key.Length - 4);
return input.TryGetProperty(propertyName, out var bracketValue) ? bracketValue : null;
}
// Dotted path: a.b.c
if (key.Contains('.'))
{
var parts = key.Split('.');
var current = input;
foreach (var part in parts)
{
if (current.ValueKind != JsonValueKind.Object || !current.TryGetProperty(part, out var next))
{
return null;
}
current = next;
}
return current;
}
return null;
}
private static string GetStringValue(JsonElement element)
{
return element.ValueKind == JsonValueKind.String
? element.GetString()!
: element.GetRawText();
}
}

View File

@@ -1,6 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Linq;
using AnyOfTypes; using AnyOfTypes;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Stef.Validation; using Stef.Validation;
@@ -13,9 +12,8 @@ namespace WireMock.Matchers;
/// <summary> /// <summary>
/// JsonPathMatcher /// JsonPathMatcher
/// </summary> /// </summary>
/// <seealso cref="IStringMatcher" /> /// <seealso cref="IJsonPathMatcher" />
/// <seealso cref="IObjectMatcher" /> public class JsonPathMatcher : IJsonPathMatcher
public class JsonPathMatcher : IStringMatcher, IObjectMatcher
{ {
private readonly AnyOf<string, StringPattern>[] _patterns; private readonly AnyOf<string, StringPattern>[] _patterns;

View File

@@ -1,11 +1,12 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Linq; using System.Collections;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Stef.Validation; using Stef.Validation;
using WireMock.Extensions; using WireMock.Extensions;
using WireMock.Serialization;
using WireMock.Util; using WireMock.Util;
using JsonUtils = WireMock.Util.JsonUtils;
namespace WireMock.Matchers; namespace WireMock.Matchers;
@@ -69,7 +70,7 @@ public class JsonMatcher : IJsonMatcher
Regex = regex; Regex = regex;
Value = value; Value = value;
_valueAsJToken = JsonUtils.ConvertValueToJToken(value); _valueAsJToken = ConvertValueToJToken(value);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -83,7 +84,7 @@ public class JsonMatcher : IJsonMatcher
{ {
try try
{ {
var inputAsJToken = JsonUtils.ConvertValueToJToken(input); var inputAsJToken = ConvertValueToJToken(input);
var match = IsMatch(RenameJToken(_valueAsJToken), RenameJToken(inputAsJToken)); var match = IsMatch(RenameJToken(_valueAsJToken), RenameJToken(inputAsJToken));
score = MatchScores.ToScore(match); score = MatchScores.ToScore(match);
@@ -241,6 +242,18 @@ public class JsonMatcher : IJsonMatcher
return new JObject(renamedProperties); return new JObject(renamedProperties);
} }
private static JToken ConvertValueToJToken(object value)
{
// Check if JToken, string, IEnumerable or object
return value switch
{
JToken tokenValue => tokenValue,
string stringValue => JsonConvert.DeserializeObject<JToken>(stringValue, JsonSerializationConstants.JsonDeserializerSettingsWithDateParsingNone)!,
IEnumerable enumerableValue => JArray.FromObject(enumerableValue),
_ => JObject.FromObject(value),
};
}
private static string? ToUpper(string? input) private static string? ToUpper(string? input)
{ {
return input?.ToUpperInvariant(); return input?.ToUpperInvariant();

View File

@@ -1,6 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Linq;
using Stef.Validation; using Stef.Validation;
using WireMock.Matchers.Helpers; using WireMock.Matchers.Helpers;
using WireMock.Util; using WireMock.Util;

View File

@@ -0,0 +1,282 @@
// Copyright © WireMock.Net
using System.Collections;
using System.Text.Json;
using Stef.Validation;
using WireMock.Extensions;
using WireMock.Util;
namespace WireMock.Matchers;
/// <summary>
/// SystemTextJsonMatcher - behaves the same as <see cref="JsonMatcher"/> but uses System.Text.Json instead of Newtonsoft.Json.
/// </summary>
public class SystemTextJsonMatcher : IJsonMatcher
{
private static readonly JsonSerializerOptions DefaultSerializerOptions = new()
{
PropertyNameCaseInsensitive = false
};
/// <inheritdoc />
public virtual string Name => nameof(SystemTextJsonMatcher);
/// <inheritdoc />
public object Value { get; }
/// <inheritdoc />
public MatchBehaviour MatchBehaviour { get; }
/// <inheritdoc cref="IIgnoreCaseMatcher.IgnoreCase"/>
public bool IgnoreCase { get; }
/// <summary>
/// Support Regex
/// </summary>
public bool Regex { get; }
private readonly JsonElement _valueAsJsonElement;
/// <summary>
/// Initializes a new instance of the <see cref="SystemTextJsonMatcher"/> class.
/// </summary>
/// <param name="value">The string value to check for equality.</param>
/// <param name="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
/// <param name="regex">Support Regex.</param>
public SystemTextJsonMatcher(string value, bool ignoreCase = false, bool regex = false)
: this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SystemTextJsonMatcher"/> class.
/// </summary>
/// <param name="value">The object value to check for equality.</param>
/// <param name="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
/// <param name="regex">Support Regex.</param>
public SystemTextJsonMatcher(object value, bool ignoreCase = false, bool regex = false)
: this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SystemTextJsonMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="value">The value to check for equality.</param>
/// <param name="ignoreCase">Ignore the case from the PropertyName and PropertyValue (string only).</param>
/// <param name="regex">Support Regex.</param>
public SystemTextJsonMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false)
{
Guard.NotNull(value);
MatchBehaviour = matchBehaviour;
IgnoreCase = ignoreCase;
Regex = regex;
Value = value;
_valueAsJsonElement = ConvertToJsonElement(value);
}
/// <inheritdoc />
public MatchResult IsMatch(object? input)
{
var score = MatchScores.Mismatch;
Exception? error = null;
// When input is null or byte[], return Mismatch.
if (input != null && input is not byte[])
{
try
{
var inputAsJsonElement = ConvertToJsonElement(input);
var match = IsMatch(NormalizeElement(_valueAsJsonElement), NormalizeElement(inputAsJsonElement));
score = MatchScores.ToScore(match);
}
catch (Exception ex)
{
error = ex;
}
}
return MatchResult.From(Name, MatchBehaviourHelper.Convert(MatchBehaviour, score), error);
}
/// <inheritdoc />
public virtual string GetCSharpCodeArguments()
{
return $"new {Name}" +
$"(" +
$"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " +
$"{CSharpFormatter.ConvertToAnonymousObjectDefinition(Value, 3)}, " +
$"{CSharpFormatter.ToCSharpBooleanLiteral(IgnoreCase)}, " +
$"{CSharpFormatter.ToCSharpBooleanLiteral(Regex)}" +
$")";
}
/// <summary>
/// Compares the input against the matcher value
/// </summary>
protected virtual bool IsMatch(JsonElement value, JsonElement? input)
{
if (input == null)
{
return false;
}
var inputElement = input.Value;
// If using Regex and the value is a string, use the MatchRegex method.
if (Regex && value.ValueKind == JsonValueKind.String)
{
var valueAsString = value.GetString()!;
var inputAsString = inputElement.ValueKind == JsonValueKind.String
? inputElement.GetString()!
: inputElement.GetRawText();
var (valid, result) = RegexUtils.MatchRegex(valueAsString, inputAsString);
if (valid)
{
return result;
}
}
// If the value is a Guid (string) and input is a string, or vice versa, compare as strings.
if (value.ValueKind == JsonValueKind.String && inputElement.ValueKind == JsonValueKind.String)
{
var valueStr = value.GetString()!;
var inputStr = inputElement.GetString()!;
if (Guid.TryParse(valueStr, out var valueGuid) && Guid.TryParse(inputStr, out var inputGuid))
{
return valueGuid == inputGuid;
}
}
switch (value.ValueKind)
{
case JsonValueKind.Object:
{
if (inputElement.ValueKind != JsonValueKind.Object)
{
return false;
}
var valueProperties = value.EnumerateObject().ToDictionary(p => p.Name, p => p.Value);
var inputProperties = inputElement.EnumerateObject().ToDictionary(p => p.Name, p => p.Value);
if (valueProperties.Count != inputProperties.Count)
{
return false;
}
foreach (var pair in valueProperties)
{
if (!inputProperties.TryGetValue(pair.Key, out var inputPropValue))
{
return false;
}
if (!IsMatch(pair.Value, inputPropValue))
{
return false;
}
}
return true;
}
case JsonValueKind.Array:
{
if (inputElement.ValueKind != JsonValueKind.Array)
{
return false;
}
var valueArray = value.EnumerateArray().ToArray();
var inputArray = inputElement.EnumerateArray().ToArray();
if (valueArray.Length != inputArray.Length)
{
return false;
}
return !valueArray.Where((valueToken, index) => !IsMatch(valueToken, inputArray[index])).Any();
}
default:
return value.GetRawText() == inputElement.GetRawText();
}
}
private JsonElement NormalizeElement(JsonElement element)
{
if (!IgnoreCase)
{
return element;
}
var normalized = NormalizeValue(element);
return ConvertToJsonElement(normalized);
}
private object NormalizeValue(JsonElement element)
{
switch (element.ValueKind)
{
case JsonValueKind.Object:
{
var dict = new Dictionary<string, object?>();
foreach (var prop in element.EnumerateObject())
{
var normalizedKey = prop.Name.ToUpperInvariant();
dict[normalizedKey] = NormalizeValue(prop.Value);
}
return dict;
}
case JsonValueKind.Array:
{
if (Regex)
{
return element.EnumerateArray().Select(e => (object)e.GetRawText()).ToArray();
}
return element.EnumerateArray().Select(NormalizeValue).ToArray();
}
case JsonValueKind.String:
{
var str = element.GetString()!;
return Regex ? str : str.ToUpperInvariant();
}
default:
return element.GetRawText();
}
}
private static JsonElement ConvertToJsonElement(object value)
{
switch (value)
{
case JsonElement jsonElement:
return jsonElement;
case JsonDocument jsonDocument:
return jsonDocument.RootElement;
case string stringValue:
return JsonDocument.Parse(stringValue).RootElement;
case IEnumerable enumerableValue when value is not string:
return JsonSerializer.SerializeToElement(enumerableValue, DefaultSerializerOptions);
default:
var json = JsonSerializer.Serialize(value, DefaultSerializerOptions);
return JsonDocument.Parse(json).RootElement;
}
}
}

View File

@@ -0,0 +1,52 @@
// Copyright © WireMock.Net
using WireMock.Extensions;
using WireMock.Util;
namespace WireMock.Matchers;
/// <summary>
/// SystemTextJsonPartialMatcher - uses System.Text.Json instead of Newtonsoft.Json.
/// </summary>
public class SystemTextJsonPartialMatcher : AbstractSystemTextJsonPartialMatcher
{
/// <inheritdoc />
public override string Name => nameof(SystemTextJsonPartialMatcher);
/// <inheritdoc />
public SystemTextJsonPartialMatcher(string value, bool ignoreCase = false, bool regex = false)
: base(value, ignoreCase, regex)
{
}
/// <inheritdoc />
public SystemTextJsonPartialMatcher(object value, bool ignoreCase = false, bool regex = false)
: base(value, ignoreCase, regex)
{
}
/// <inheritdoc />
public SystemTextJsonPartialMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false)
: base(matchBehaviour, value, ignoreCase, regex)
{
}
/// <inheritdoc />
protected override bool IsMatch(string value, string input)
{
var exactStringMatcher = new ExactMatcher(MatchBehaviour.AcceptOnMatch, IgnoreCase, MatchOperator.Or, value);
return exactStringMatcher.IsMatch(input).IsPerfect();
}
/// <inheritdoc />
public override string GetCSharpCodeArguments()
{
return $"new {Name}" +
$"(" +
$"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " +
$"{CSharpFormatter.ConvertToAnonymousObjectDefinition(Value, 3)}, " +
$"{CSharpFormatter.ToCSharpBooleanLiteral(IgnoreCase)}, " +
$"{CSharpFormatter.ToCSharpBooleanLiteral(Regex)}" +
$")";
}
}

View File

@@ -0,0 +1,52 @@
// Copyright © WireMock.Net
using WireMock.Extensions;
using WireMock.Util;
namespace WireMock.Matchers;
/// <summary>
/// SystemTextJsonPartialWildcardMatcher - uses System.Text.Json instead of Newtonsoft.Json.
/// </summary>
public class SystemTextJsonPartialWildcardMatcher : AbstractSystemTextJsonPartialMatcher
{
/// <inheritdoc />
public override string Name => nameof(SystemTextJsonPartialWildcardMatcher);
/// <inheritdoc />
public SystemTextJsonPartialWildcardMatcher(string value, bool ignoreCase = false, bool regex = false)
: base(value, ignoreCase, regex)
{
}
/// <inheritdoc />
public SystemTextJsonPartialWildcardMatcher(object value, bool ignoreCase = false, bool regex = false)
: base(value, ignoreCase, regex)
{
}
/// <inheritdoc />
public SystemTextJsonPartialWildcardMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false)
: base(matchBehaviour, value, ignoreCase, regex)
{
}
/// <inheritdoc />
protected override bool IsMatch(string value, string input)
{
var wildcardStringMatcher = new WildcardMatcher(MatchBehaviour.AcceptOnMatch, value, IgnoreCase);
return wildcardStringMatcher.IsMatch(input).IsPerfect();
}
/// <inheritdoc />
public override string GetCSharpCodeArguments()
{
return $"new {Name}" +
$"(" +
$"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " +
$"{CSharpFormatter.ConvertToAnonymousObjectDefinition(Value, 3)}, " +
$"{CSharpFormatter.ToCSharpBooleanLiteral(IgnoreCase)}, " +
$"{CSharpFormatter.ToCSharpBooleanLiteral(Regex)}" +
$")";
}
}

View File

@@ -2,6 +2,7 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using JsonConverter.Abstractions;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using WireMock.Handlers; using WireMock.Handlers;
@@ -99,4 +100,12 @@ internal interface IWireMockMiddlewareOptions
/// WebSocket settings. /// WebSocket settings.
/// </summary> /// </summary>
WebSocketSettings? WebSocketSettings { get; set; } WebSocketSettings? WebSocketSettings { get; set; }
/// <summary>
/// Gets or sets the default JSON converter used for serialization.
/// </summary>
/// <remarks>
/// Set this property to customize how objects are serialized to and deserialized from JSON during mapping.
/// </remarks>
IJsonConverter DefaultJsonSerializer { get; set; }
} }

View File

@@ -53,7 +53,8 @@ internal class OwinRequestMapper : IOwinRequestMapper
ContentType = request.ContentType, ContentType = request.ContentType,
DeserializeJson = !options.DisableJsonBodyParsing.GetValueOrDefault(false), DeserializeJson = !options.DisableJsonBodyParsing.GetValueOrDefault(false),
ContentEncoding = contentEncodingHeader?.FirstOrDefault(), ContentEncoding = contentEncodingHeader?.FirstOrDefault(),
DecompressGZipAndDeflate = !options.DisableRequestBodyDecompressing.GetValueOrDefault(false) DecompressGZipAndDeflate = !options.DisableRequestBodyDecompressing.GetValueOrDefault(false),
DefaultJsonConverter = options.DefaultJsonSerializer
}; };
body = await BodyParser.ParseAsync(bodyParserSettings).ConfigureAwait(false); body = await BodyParser.ParseAsync(bodyParserSettings).ConfigureAwait(false);

View File

@@ -3,249 +3,242 @@
using System.Globalization; using System.Globalization;
using System.Net; using System.Net;
using System.Text; using System.Text;
using JsonConverter.Abstractions;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using RandomDataGenerator.FieldOptions; using RandomDataGenerator.FieldOptions;
using RandomDataGenerator.Randomizers; using RandomDataGenerator.Randomizers;
using Stef.Validation;
using WireMock.Http; using WireMock.Http;
using WireMock.ResponseBuilders; using WireMock.ResponseBuilders;
using WireMock.ResponseProviders; using WireMock.ResponseProviders;
using WireMock.Types; using WireMock.Types;
using WireMock.Util; using WireMock.Util;
namespace WireMock.Owin.Mappers namespace WireMock.Owin.Mappers;
/// <summary>
/// OwinResponseMapper
/// </summary>
internal class OwinResponseMapper(IWireMockMiddlewareOptions options) : IOwinResponseMapper
{ {
/// <summary> private readonly IRandomizerNumber<double> _randomizerDouble = RandomizerFactory.GetRandomizer(new FieldOptionsDouble { Min = 0, Max = 1 });
/// OwinResponseMapper private readonly IRandomizerBytes _randomizerBytes = RandomizerFactory.GetRandomizer(new FieldOptionsBytes { Min = 100, Max = 200 });
/// </summary> private readonly Encoding _utf8NoBom = new UTF8Encoding(false);
internal class OwinResponseMapper : IOwinResponseMapper
// https://msdn.microsoft.com/en-us/library/78h415ay(v=vs.110).aspx
private static readonly IDictionary<string, Action<HttpResponse, bool, WireMockList<string>>> ResponseHeadersToFix =
new Dictionary<string, Action<HttpResponse, bool, WireMockList<string>>>(StringComparer.OrdinalIgnoreCase)
{
{ HttpKnownHeaderNames.ContentType, (r, _, v) => r.ContentType = v.FirstOrDefault() },
{ HttpKnownHeaderNames.ContentLength, (r, hasBody, v) =>
{
// Only set the Content-Length header if the response does not have a body
if (!hasBody && long.TryParse(v.FirstOrDefault(), out var contentLength))
{
r.ContentLength = contentLength;
}
}
}
};
/// <inheritdoc />
public async Task MapAsync(IResponseMessage? responseMessage, HttpResponse response)
{ {
private readonly IRandomizerNumber<double> _randomizerDouble = RandomizerFactory.GetRandomizer(new FieldOptionsDouble { Min = 0, Max = 1 }); if (responseMessage == null || responseMessage is WebSocketHandledResponse)
private readonly IRandomizerBytes _randomizerBytes = RandomizerFactory.GetRandomizer(new FieldOptionsBytes { Min = 100, Max = 200 });
private readonly IWireMockMiddlewareOptions _options;
private readonly Encoding _utf8NoBom = new UTF8Encoding(false);
// https://msdn.microsoft.com/en-us/library/78h415ay(v=vs.110).aspx
private static readonly IDictionary<string, Action<HttpResponse, bool, WireMockList<string>>> ResponseHeadersToFix =
new Dictionary<string, Action<HttpResponse, bool, WireMockList<string>>>(StringComparer.OrdinalIgnoreCase)
{
{ HttpKnownHeaderNames.ContentType, (r, _, v) => r.ContentType = v.FirstOrDefault() },
{ HttpKnownHeaderNames.ContentLength, (r, hasBody, v) =>
{
// Only set the Content-Length header if the response does not have a body
if (!hasBody && long.TryParse(v.FirstOrDefault(), out var contentLength))
{
r.ContentLength = contentLength;
}
}
}
};
/// <summary>
/// Constructor
/// </summary>
/// <param name="options">The IWireMockMiddlewareOptions.</param>
public OwinResponseMapper(IWireMockMiddlewareOptions options)
{ {
_options = Guard.NotNull(options); return;
} }
/// <inheritdoc /> var bodyData = responseMessage.BodyData;
public async Task MapAsync(IResponseMessage? responseMessage, HttpResponse response) if (bodyData?.GetDetectedBodyType() == BodyType.SseString)
{ {
if (responseMessage == null || responseMessage is WebSocketHandledResponse) await HandleSseStringAsync(responseMessage, response, bodyData);
{ return;
return; }
}
var bodyData = responseMessage.BodyData; byte[]? bytes;
if (bodyData?.GetDetectedBodyType() == BodyType.SseString) switch (responseMessage.FaultType)
{ {
await HandleSseStringAsync(responseMessage, response, bodyData); case FaultType.EMPTY_RESPONSE:
return; bytes = IsFault(responseMessage) ? [] : await GetNormalBodyAsync(responseMessage).ConfigureAwait(false);
} break;
byte[]? bytes; case FaultType.MALFORMED_RESPONSE_CHUNK:
switch (responseMessage.FaultType) bytes = await GetNormalBodyAsync(responseMessage).ConfigureAwait(false) ?? [];
{ if (IsFault(responseMessage))
case FaultType.EMPTY_RESPONSE:
bytes = IsFault(responseMessage) ? [] : await GetNormalBodyAsync(responseMessage).ConfigureAwait(false);
break;
case FaultType.MALFORMED_RESPONSE_CHUNK:
bytes = await GetNormalBodyAsync(responseMessage).ConfigureAwait(false) ?? [];
if (IsFault(responseMessage))
{
bytes = bytes.Take(bytes.Length / 2).Union(_randomizerBytes.Generate()).ToArray();
}
break;
default:
bytes = await GetNormalBodyAsync(responseMessage).ConfigureAwait(false);
break;
}
if (responseMessage.StatusCode is HttpStatusCode or int)
{
response.StatusCode = MapStatusCode((int) responseMessage.StatusCode);
}
else if (responseMessage.StatusCode is string statusCodeAsString)
{
// Note: this case will also match on null
_ = int.TryParse(statusCodeAsString, out var statusCodeTypeAsInt);
response.StatusCode = MapStatusCode(statusCodeTypeAsInt);
}
SetResponseHeaders(responseMessage, bytes != null, response);
if (bytes != null)
{
try
{ {
await response.Body.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); bytes = bytes.Take(bytes.Length / 2).Union(_randomizerBytes.Generate()).ToArray();
} }
catch (Exception ex) break;
default:
bytes = await GetNormalBodyAsync(responseMessage).ConfigureAwait(false);
break;
}
if (responseMessage.StatusCode is HttpStatusCode or int)
{
response.StatusCode = MapStatusCode((int)responseMessage.StatusCode);
}
else if (responseMessage.StatusCode is string statusCodeAsString)
{
// Note: this case will also match on null
_ = int.TryParse(statusCodeAsString, out var statusCodeTypeAsInt);
response.StatusCode = MapStatusCode(statusCodeTypeAsInt);
}
SetResponseHeaders(responseMessage, bytes != null, response);
if (bytes != null)
{
try
{
await response.Body.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
}
catch (Exception ex)
{
options.Logger.Warn("Error writing response body. Exception : {0}", ex);
}
}
SetResponseTrailingHeaders(responseMessage, response);
}
private static async Task HandleSseStringAsync(IResponseMessage responseMessage, HttpResponse response, IBodyData bodyData)
{
if (bodyData.SseStringQueue == null)
{
return;
}
SetResponseHeaders(responseMessage, true, response);
string? text;
do
{
if (bodyData.SseStringQueue.TryRead(out text))
{
await response.WriteAsync(text);
await response.Body.FlushAsync();
}
} while (text != null);
}
private int MapStatusCode(int code)
{
if (options.AllowOnlyDefinedHttpStatusCodeInResponse == true && !Enum.IsDefined(typeof(HttpStatusCode), code))
{
return (int)HttpStatusCode.OK;
}
return code;
}
private bool IsFault(IResponseMessage responseMessage)
{
return responseMessage.FaultPercentage == null || _randomizerDouble.Generate() <= responseMessage.FaultPercentage;
}
private async Task<byte[]?> GetNormalBodyAsync(IResponseMessage responseMessage)
{
var bodyData = responseMessage.BodyData;
switch (bodyData?.GetDetectedBodyType())
{
case BodyType.String:
case BodyType.FormUrlEncoded:
return (bodyData.Encoding ?? _utf8NoBom).GetBytes(bodyData.BodyAsString!);
case BodyType.Json:
var jsonConverterOptions = new JsonConverterOptions
{ {
_options.Logger.Warn("Error writing response body. Exception : {0}", ex); WriteIndented = bodyData.BodyAsJsonIndented == true,
} IgnoreNullValues = true
} };
SetResponseTrailingHeaders(responseMessage, response); var jsonBody = options.DefaultJsonSerializer.Serialize(bodyData.BodyAsJson!, jsonConverterOptions);
} return (bodyData.Encoding ?? _utf8NoBom).GetBytes(jsonBody);
private static async Task HandleSseStringAsync(IResponseMessage responseMessage, HttpResponse response, IBodyData bodyData) case BodyType.ProtoBuf:
{ if (TypeLoader.TryLoadStaticInstance<IProtoBufUtils>(out var protoBufUtils))
if (bodyData.SseStringQueue == null)
{
return;
}
SetResponseHeaders(responseMessage, true, response);
string? text;
do
{
if (bodyData.SseStringQueue.TryRead(out text))
{ {
await response.WriteAsync(text); var protoDefinitions = bodyData.ProtoDefinition?.Invoke().Texts;
await response.Body.FlushAsync(); return await protoBufUtils.GetProtoBufMessageWithHeaderAsync(protoDefinitions, bodyData.ProtoBufMessageType, bodyData.BodyAsJson).ConfigureAwait(false);
} }
} while (text != null); break;
case BodyType.Bytes:
return bodyData.BodyAsBytes;
case BodyType.File:
return options.FileSystemHandler?.ReadResponseBodyAsFile(bodyData.BodyAsFile!);
case BodyType.MultiPart:
options.Logger.Warn("MultiPart body type is not handled!");
break;
case BodyType.None:
break;
} }
private int MapStatusCode(int code) return null;
}
private static void SetResponseHeaders(IResponseMessage responseMessage, bool hasBody, HttpResponse response)
{
// Force setting the Date header (#577)
AppendResponseHeader(
response,
HttpKnownHeaderNames.Date,
[DateTime.UtcNow.ToString(CultureInfo.InvariantCulture.DateTimeFormat.RFC1123Pattern, CultureInfo.InvariantCulture)]
);
// Set other headers
foreach (var item in responseMessage.Headers!)
{ {
if (_options.AllowOnlyDefinedHttpStatusCodeInResponse == true && !Enum.IsDefined(typeof(HttpStatusCode), code)) var headerName = item.Key;
var value = item.Value;
if (ResponseHeadersToFix.TryGetValue(headerName, out var action))
{ {
return (int)HttpStatusCode.OK; action.Invoke(response, hasBody, value);
} }
else
return code;
}
private bool IsFault(IResponseMessage responseMessage)
{
return responseMessage.FaultPercentage == null || _randomizerDouble.Generate() <= responseMessage.FaultPercentage;
}
private async Task<byte[]?> GetNormalBodyAsync(IResponseMessage responseMessage)
{
var bodyData = responseMessage.BodyData;
switch (bodyData?.GetDetectedBodyType())
{ {
case BodyType.String: // Check if this response header can be added (#148, #227 and #720)
case BodyType.FormUrlEncoded: if (!HttpKnownHeaderNames.IsRestrictedResponseHeader(headerName))
return (bodyData.Encoding ?? _utf8NoBom).GetBytes(bodyData.BodyAsString!);
case BodyType.Json:
var formatting = bodyData.BodyAsJsonIndented == true ? Formatting.Indented : Formatting.None;
var jsonBody = JsonConvert.SerializeObject(bodyData.BodyAsJson, new JsonSerializerSettings { Formatting = formatting, NullValueHandling = NullValueHandling.Ignore });
return (bodyData.Encoding ?? _utf8NoBom).GetBytes(jsonBody);
case BodyType.ProtoBuf:
if (TypeLoader.TryLoadStaticInstance<IProtoBufUtils>(out var protoBufUtils))
{
var protoDefinitions = bodyData.ProtoDefinition?.Invoke().Texts;
return await protoBufUtils.GetProtoBufMessageWithHeaderAsync(protoDefinitions, bodyData.ProtoBufMessageType, bodyData.BodyAsJson).ConfigureAwait(false);
}
break;
case BodyType.Bytes:
return bodyData.BodyAsBytes;
case BodyType.File:
return _options.FileSystemHandler?.ReadResponseBodyAsFile(bodyData.BodyAsFile!);
case BodyType.MultiPart:
_options.Logger.Warn("MultiPart body type is not handled!");
break;
case BodyType.None:
break;
}
return null;
}
private static void SetResponseHeaders(IResponseMessage responseMessage, bool hasBody, HttpResponse response)
{
// Force setting the Date header (#577)
AppendResponseHeader(
response,
HttpKnownHeaderNames.Date,
[DateTime.UtcNow.ToString(CultureInfo.InvariantCulture.DateTimeFormat.RFC1123Pattern, CultureInfo.InvariantCulture)]
);
// Set other headers
foreach (var item in responseMessage.Headers!)
{
var headerName = item.Key;
var value = item.Value;
if (ResponseHeadersToFix.TryGetValue(headerName, out var action))
{ {
action.Invoke(response, hasBody, value); AppendResponseHeader(response, headerName, value.ToArray());
}
else
{
// Check if this response header can be added (#148, #227 and #720)
if (!HttpKnownHeaderNames.IsRestrictedResponseHeader(headerName))
{
AppendResponseHeader(response, headerName, value.ToArray());
}
} }
} }
} }
}
private static void SetResponseTrailingHeaders(IResponseMessage responseMessage, HttpResponse response)
{ private static void SetResponseTrailingHeaders(IResponseMessage responseMessage, HttpResponse response)
if (responseMessage.TrailingHeaders == null) {
{ if (responseMessage.TrailingHeaders == null)
return; {
} return;
}
#if TRAILINGHEADERS
foreach (var (headerName, value) in responseMessage.TrailingHeaders) #if TRAILINGHEADERS
{ foreach (var (headerName, value) in responseMessage.TrailingHeaders)
if (ResponseHeadersToFix.TryGetValue(headerName, out var action)) {
{ if (ResponseHeadersToFix.TryGetValue(headerName, out var action))
action.Invoke(response, false, value); {
} action.Invoke(response, false, value);
else }
{ else
// Check if this trailing header can be added to the response {
if (response.SupportsTrailers() && !HttpKnownHeaderNames.IsRestrictedResponseHeader(headerName)) // Check if this trailing header can be added to the response
{ if (response.SupportsTrailers() && !HttpKnownHeaderNames.IsRestrictedResponseHeader(headerName))
response.AppendTrailer(headerName, new Microsoft.Extensions.Primitives.StringValues(value.ToArray())); {
} response.AppendTrailer(headerName, new Microsoft.Extensions.Primitives.StringValues(value.ToArray()));
} }
} }
#endif }
} #endif
}
private static void AppendResponseHeader(HttpResponse response, string headerName, string[] values)
{ private static void AppendResponseHeader(HttpResponse response, string headerName, string[] values)
response.Headers.Append(headerName, values); {
} response.Headers.Append(headerName, values);
} }
} }

View File

@@ -19,6 +19,7 @@ internal class MappingMatcher(IWireMockMiddlewareOptions options, IRandomizerDou
var possibleMappings = new List<MappingMatcherResult>(); var possibleMappings = new List<MappingMatcherResult>();
var mappings = _options.Mappings.Values var mappings = _options.Mappings.Values
.Where(m => !m.IsDisabled)
.Where(m => m.TimeSettings.IsValid()) .Where(m => m.TimeSettings.IsValid())
.Where(m => m.Probability is null || _randomizerDoubleBetween0And1.Generate() <= m.Probability) .Where(m => m.Probability is null || _randomizerDoubleBetween0And1.Generate() <= m.Probability)
.ToArray(); .ToArray();

View File

@@ -122,6 +122,14 @@ internal class WireMockMiddleware(
} }
} }
// Transition scenario state immediately after matching, before any delay (global or
// per-mapping) so that concurrent retries arriving during a delay period see the
// updated state and match the correct next mapping instead of re-matching this one.
if (targetMapping.Scenario != null)
{
UpdateScenarioState(targetMapping);
}
if (!targetMapping.IsAdminInterface && options.RequestProcessingDelay > TimeSpan.Zero) if (!targetMapping.IsAdminInterface && options.RequestProcessingDelay > TimeSpan.Zero)
{ {
await Task.Delay(options.RequestProcessingDelay.Value).ConfigureAwait(false); await Task.Delay(options.RequestProcessingDelay.Value).ConfigureAwait(false);
@@ -147,11 +155,6 @@ internal class WireMockMiddleware(
} }
} }
if (targetMapping.Scenario != null)
{
UpdateScenarioState(targetMapping);
}
if (!targetMapping.IsAdminInterface && targetMapping.Webhooks?.Length > 0) if (!targetMapping.IsAdminInterface && targetMapping.Webhooks?.Length > 0)
{ {
await SendToWebhooksAsync(targetMapping, request, response).ConfigureAwait(false); await SendToWebhooksAsync(targetMapping, request, response).ConfigureAwait(false);

View File

@@ -1,7 +1,6 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using WireMock.Logging; using WireMock.Logging;
using WireMock.Owin.ActivityTracing; using WireMock.Owin.ActivityTracing;
using WireMock.Serialization; using WireMock.Serialization;
@@ -42,7 +41,7 @@ internal class WireMockMiddlewareLogger(
if (_options.SaveUnmatchedRequests == true && match?.RequestMatchResult is not { IsPerfectMatch: true }) if (_options.SaveUnmatchedRequests == true && match?.RequestMatchResult is not { IsPerfectMatch: true })
{ {
var filename = $"{logEntry.Guid}.LogEntry.json"; var filename = $"{logEntry.Guid}.LogEntry.json";
_options.FileSystemHandler?.WriteUnmatchedRequest(filename, JsonUtils.Serialize(logEntry)); _options.FileSystemHandler?.WriteUnmatchedRequest(filename, _options.DefaultJsonSerializer.Serialize(logEntry));
} }
} }
catch catch

View File

@@ -2,6 +2,8 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using JsonConverter.Abstractions;
using JsonConverter.Newtonsoft.Json;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using WireMock.Handlers; using WireMock.Handlers;
@@ -108,5 +110,9 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions
/// <inheritdoc /> /// <inheritdoc />
public ConcurrentDictionary<Guid, WebSocketConnectionRegistry> WebSocketRegistries { get; } = new(); public ConcurrentDictionary<Guid, WebSocketConnectionRegistry> WebSocketRegistries { get; } = new();
/// <inheritdoc />
public WebSocketSettings? WebSocketSettings { get; set; } public WebSocketSettings? WebSocketSettings { get; set; }
/// <inheritdoc />
public IJsonConverter DefaultJsonSerializer { get; set; } = new NewtonsoftJsonConverter();
} }

View File

@@ -48,7 +48,8 @@ internal class ProxyHelper(WireMockServerSettings settings)
originalUri, originalUri,
deserializeJson, deserializeJson,
decompressGzipAndDeflate, decompressGzipAndDeflate,
deserializeFormUrlEncoded deserializeFormUrlEncoded,
_settings.DefaultJsonSerializer
).ConfigureAwait(false); ).ConfigureAwait(false);
IMapping? newMapping = null; IMapping? newMapping = null;

View File

@@ -197,7 +197,7 @@ public partial class Response : IResponseBuilder
if (ProxyAndRecordSettings != null && _httpClientForProxy != null) if (ProxyAndRecordSettings != null && _httpClientForProxy != null)
{ {
string RemoveFirstOccurrence(string source, string find) static string RemoveFirstOccurrence(string source, string find)
{ {
int place = source.IndexOf(find, StringComparison.OrdinalIgnoreCase); int place = source.IndexOf(find, StringComparison.OrdinalIgnoreCase);
return place >= 0 ? source.Remove(place, find.Length) : source; return place >= 0 ? source.Remove(place, find.Length) : source;
@@ -265,7 +265,7 @@ public partial class Response : IResponseBuilder
var decoded = await protoBufMatcher.DecodeAsync(requestMessage.BodyData?.BodyAsBytes).ConfigureAwait(false); var decoded = await protoBufMatcher.DecodeAsync(requestMessage.BodyData?.BodyAsBytes).ConfigureAwait(false);
if (decoded != null) if (decoded != null)
{ {
requestMessageImplementation.BodyAsJson = JsonUtils.ConvertValueToJToken(decoded); requestMessageImplementation.BodyAsJson = settings.DefaultJsonSerializer.ToJsonToken(decoded);
} }
} }
} }

View File

@@ -275,6 +275,7 @@ internal class MappingConverter(MatcherMapper mapper)
TimesInSameState = !string.IsNullOrWhiteSpace(mapping.NextState) ? mapping.TimesInSameState : null, TimesInSameState = !string.IsNullOrWhiteSpace(mapping.NextState) ? mapping.TimesInSameState : null,
Data = mapping.Data, Data = mapping.Data,
Probability = mapping.Probability, Probability = mapping.Probability,
IsDisabled = mapping.IsDisabled ? true : null,
Request = new RequestModel Request = new RequestModel
{ {
Headers = headerMatchers.Any() ? headerMatchers.Select(hm => new HeaderModel Headers = headerMatchers.Any() ? headerMatchers.Select(hm => new HeaderModel

View File

@@ -1,10 +1,7 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using JsonConverter.Abstractions; using JsonConverter.Abstractions;
using Newtonsoft.Json.Linq; using JsonConverter.Abstractions.Models;
#if NETSTANDARD2_0_OR_GREATER || NETCOREAPP3_1_OR_GREATER || NET6_0_OR_GREATER || NET461
using System.Text.Json;
#endif
namespace WireMock.Serialization; namespace WireMock.Serialization;
@@ -12,37 +9,23 @@ internal class MappingSerializer(IJsonConverter jsonConverter)
{ {
internal T[] DeserializeJsonToArray<T>(string value) internal T[] DeserializeJsonToArray<T>(string value)
{ {
return DeserializeObjectToArray<T>(jsonConverter.Deserialize<object>(value)!); switch (JsonTypeHelper.GetJsonType(value))
{
case JsonType.Array:
return jsonConverter.Deserialize<T[]>(value, JsonSerializationConstants.JsonConverterOptionsWithDateParsingNone)!;
case JsonType.Object:
var singleResult = jsonConverter.Deserialize<T>(value, JsonSerializationConstants.JsonConverterOptionsWithDateParsingNone);
return [singleResult!];
default:
throw new InvalidOperationException("Cannot deserialize the provided value to an array or object.");
}
} }
internal static T[] DeserializeObjectToArray<T>(object value) internal T[] DeserializeObjectToArray<T>(object value)
{ {
if (value is JArray jArray) var json = jsonConverter.Serialize(value, JsonSerializationConstants.JsonConverterOptionsWithDateParsingNone);
{ return DeserializeJsonToArray<T>(json);
return jArray.ToObject<T[]>()!;
}
if (value is JObject jObject)
{
var singleResult = jObject.ToObject<T>();
return [singleResult!];
}
#if NETSTANDARD2_0_OR_GREATER || NETCOREAPP3_1_OR_GREATER || NET6_0_OR_GREATER || NET461
if (value is JsonElement jElement)
{
if (jElement.ValueKind == JsonValueKind.Array)
{
return jElement.Deserialize<T[]>()!;
}
if (jElement.ValueKind == JsonValueKind.Object)
{
var singleResult = jElement.Deserialize<T>();
return [singleResult!];
}
}
#endif
throw new InvalidOperationException("Cannot deserialize the provided value to an array or object.");
} }
} }

View File

@@ -106,9 +106,29 @@ internal class MatcherMapper
var valueForJsonPartialWildcardMatcher = matcherModel.Pattern ?? matcherModel.Patterns; var valueForJsonPartialWildcardMatcher = matcherModel.Pattern ?? matcherModel.Patterns;
return new JsonPartialWildcardMatcher(matchBehaviour, valueForJsonPartialWildcardMatcher!, ignoreCase, useRegex); return new JsonPartialWildcardMatcher(matchBehaviour, valueForJsonPartialWildcardMatcher!, ignoreCase, useRegex);
case nameof(SystemTextJsonMatcher):
var valueForSystemTextJsonMatcher = matcherModel.Pattern ?? matcherModel.Patterns;
return new SystemTextJsonMatcher(matchBehaviour, valueForSystemTextJsonMatcher!, ignoreCase, useRegex);
case nameof(SystemTextJsonPartialMatcher):
var valueForSystemTextJsonPartialMatcher = matcherModel.Pattern ?? matcherModel.Patterns;
return new SystemTextJsonPartialMatcher(matchBehaviour, valueForSystemTextJsonPartialMatcher!, ignoreCase, useRegex);
case nameof(SystemTextJsonPartialWildcardMatcher):
var valueForSystemTextJsonPartialWildcardMatcher = matcherModel.Pattern ?? matcherModel.Patterns;
return new SystemTextJsonPartialWildcardMatcher(matchBehaviour, valueForSystemTextJsonPartialWildcardMatcher!, ignoreCase, useRegex);
case nameof(JsonPathMatcher): case nameof(JsonPathMatcher):
return new JsonPathMatcher(matchBehaviour, matchOperator, stringPatterns); return new JsonPathMatcher(matchBehaviour, matchOperator, stringPatterns);
case "SystemTextJsonPathMatcher":
if (TypeLoader.TryLoadNewInstance<ISystemTextJsonPathMatcher>(out var systemTextJsonPathMatcher, matchBehaviour, matchOperator, stringPatterns))
{
return systemTextJsonPathMatcher;
}
throw new InvalidOperationException("The 'SystemTextJsonPathMatcher' cannot be loaded. Please install the WireMock.Net.Matchers.SystemTextJsonPath package.");
case nameof(JmesPathMatcher): case nameof(JmesPathMatcher):
return new JmesPathMatcher(matchBehaviour, matchOperator, stringPatterns); return new JmesPathMatcher(matchBehaviour, matchOperator, stringPatterns);
@@ -171,6 +191,10 @@ internal class MatcherMapper
model.Regex = jsonMatcher.Regex; model.Regex = jsonMatcher.Regex;
break; break;
case SystemTextJsonMatcher stjMatcher:
model.Regex = stjMatcher.Regex;
break;
case XPathMatcher xpathMatcher: case XPathMatcher xpathMatcher:
model.XmlNamespaceMap = xpathMatcher.XmlNamespaceMap; model.XmlNamespaceMap = xpathMatcher.XmlNamespaceMap;
break; break;

View File

@@ -234,6 +234,13 @@ public interface IRespondWithAProvider
/// <returns>The <see cref="IRespondWithAProvider"/>.</returns> /// <returns>The <see cref="IRespondWithAProvider"/>.</returns>
IRespondWithAProvider WithProbability(double probability); IRespondWithAProvider WithProbability(double probability);
/// <summary>
/// Define whether this mapping is disabled. Defaults to <c>false</c>.
/// </summary>
/// <param name="isDisabled">Whether this mapping is disabled.</param>
/// <returns>The <see cref="IRespondWithAProvider"/>.</returns>
IRespondWithAProvider WithIsDisabled(bool isDisabled);
/// <summary> /// <summary>
/// Define a Grpc ProtoDefinition which is used for the request and the response. /// Define a Grpc ProtoDefinition which is used for the request and the response.
/// This can be a ProtoDefinition as a string, or an id when the ProtoDefinitions are defined at the WireMockServer. /// This can be a ProtoDefinition as a string, or an id when the ProtoDefinitions are defined at the WireMockServer.

View File

@@ -37,6 +37,7 @@ internal class RespondWithAProvider : IRespondWithAProvider
private int _timesInSameState = 1; private int _timesInSameState = 1;
private bool? _useWebhookFireAndForget; private bool? _useWebhookFireAndForget;
private double? _probability; private double? _probability;
private bool _isDisabled = false;
private GraphQLSchemaDetails? _graphQLSchemaDetails; // Future Use. private GraphQLSchemaDetails? _graphQLSchemaDetails; // Future Use.
public Guid Guid { get; private set; } public Guid Guid { get; private set; }
@@ -108,6 +109,11 @@ internal class RespondWithAProvider : IRespondWithAProvider
mapping.WithProbability(_probability.Value); mapping.WithProbability(_probability.Value);
} }
if (_isDisabled)
{
mapping.IsDisabled = true;
}
if (ProtoDefinition != null) if (ProtoDefinition != null)
{ {
mapping.WithProtoDefinition(ProtoDefinition.Value); mapping.WithProtoDefinition(ProtoDefinition.Value);
@@ -354,6 +360,13 @@ internal class RespondWithAProvider : IRespondWithAProvider
return this; return this;
} }
/// <inheritdoc />
public IRespondWithAProvider WithIsDisabled(bool isDisabled)
{
_isDisabled = isDisabled;
return this;
}
/// <inheritdoc /> /// <inheritdoc />
public IRespondWithAProvider WithProtoDefinition(params string[] protoDefinitionOrId) public IRespondWithAProvider WithProtoDefinition(params string[] protoDefinitionOrId)
{ {

View File

@@ -1,6 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Linq;
using System.Net; using System.Net;
using System.Text; using System.Text;
using JetBrains.Annotations; using JetBrains.Annotations;
@@ -26,9 +25,6 @@ using WireMock.Util;
namespace WireMock.Server; namespace WireMock.Server;
/// <summary>
/// The fluent mock server.
/// </summary>
public partial class WireMockServer public partial class WireMockServer
{ {
private const int EnhancedFileSystemWatcherTimeoutMs = 1000; private const int EnhancedFileSystemWatcherTimeoutMs = 1000;
@@ -61,6 +57,8 @@ public partial class WireMockServer
public string OpenApi => $"{_prefix}/openapi"; public string OpenApi => $"{_prefix}/openapi";
public RegexMatcher MappingsGuidPathMatcher => new($"^{_prefixEscaped}\\/mappings\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$"); public RegexMatcher MappingsGuidPathMatcher => new($"^{_prefixEscaped}\\/mappings\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
public RegexMatcher MappingsGuidEnablePathMatcher => new($"^{_prefixEscaped}\\/mappings\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})\\/enable$");
public RegexMatcher MappingsGuidDisablePathMatcher => new($"^{_prefixEscaped}\\/mappings\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})\\/disable$");
public RegexMatcher MappingsCodeGuidPathMatcher => new($"^{_prefixEscaped}\\/mappings\\/code\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$"); public RegexMatcher MappingsCodeGuidPathMatcher => new($"^{_prefixEscaped}\\/mappings\\/code\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
public RegexMatcher RequestsGuidPathMatcher => new($"^{_prefixEscaped}\\/requests\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$"); public RegexMatcher RequestsGuidPathMatcher => new($"^{_prefixEscaped}\\/requests\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
public RegexMatcher ScenariosNameMatcher => new($"^{_prefixEscaped}\\/scenarios\\/.+$"); public RegexMatcher ScenariosNameMatcher => new($"^{_prefixEscaped}\\/scenarios\\/.+$");
@@ -104,6 +102,12 @@ public partial class WireMockServer
Given(Request.Create().WithPath(_adminPaths.MappingsGuidPathMatcher).UsingPut().WithHeader(HttpKnownHeaderNames.ContentType, AdminRequestContentTypeJson)).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingPut)); Given(Request.Create().WithPath(_adminPaths.MappingsGuidPathMatcher).UsingPut().WithHeader(HttpKnownHeaderNames.ContentType, AdminRequestContentTypeJson)).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingPut));
Given(Request.Create().WithPath(_adminPaths.MappingsGuidPathMatcher).UsingDelete()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingDelete)); Given(Request.Create().WithPath(_adminPaths.MappingsGuidPathMatcher).UsingDelete()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingDelete));
// __admin/mappings/{guid}/enable
Given(Request.Create().WithPath(_adminPaths.MappingsGuidEnablePathMatcher).UsingPut()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingEnable));
// __admin/mappings/{guid}/disable
Given(Request.Create().WithPath(_adminPaths.MappingsGuidDisablePathMatcher).UsingPut()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingDisable));
// __admin/mappings/code/{guid} // __admin/mappings/code/{guid}
Given(Request.Create().WithPath(_adminPaths.MappingsCodeGuidPathMatcher).UsingGet()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingCodeGet)); Given(Request.Create().WithPath(_adminPaths.MappingsCodeGuidPathMatcher).UsingGet()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingCodeGet));
@@ -342,7 +346,7 @@ public partial class WireMockServer
} }
o.CorsPolicyOptions = corsPolicyOptions; o.CorsPolicyOptions = corsPolicyOptions;
o.ClientCertificateMode = (Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode) _settings.ClientCertificateMode; o.ClientCertificateMode = (Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode)_settings.ClientCertificateMode;
o.AcceptAnyClientCertificate = _settings.AcceptAnyClientCertificate; o.AcceptAnyClientCertificate = _settings.AcceptAnyClientCertificate;
}); });
@@ -430,6 +434,47 @@ public partial class WireMockServer
var lastPart = requestMessage.Path.Split('/').LastOrDefault(); var lastPart = requestMessage.Path.Split('/').LastOrDefault();
return Guid.TryParse(lastPart, out guid); return Guid.TryParse(lastPart, out guid);
} }
private static bool TryParseGuidFromSecondToLastSegment(IRequestMessage requestMessage, out Guid guid)
{
var parts = requestMessage.Path.Split('/');
if (parts.Length >= 2 && Guid.TryParse(parts[parts.Length - 2], out guid))
return true;
guid = Guid.Empty;
return false;
}
private IResponseMessage MappingEnable(HttpContext _, IRequestMessage requestMessage)
{
if (TryParseGuidFromSecondToLastSegment(requestMessage, out var guid))
{
var mapping = Mappings.FirstOrDefault(m => !m.IsAdminInterface && m.Guid == guid);
if (mapping != null)
{
mapping.IsDisabled = false;
return ResponseMessageBuilder.Create(HttpStatusCode.OK, "Mapping enabled", guid);
}
}
_settings.Logger.Warn("HttpStatusCode set to 404 : Mapping not found");
return ResponseMessageBuilder.Create(HttpStatusCode.NotFound, "Mapping not found");
}
private IResponseMessage MappingDisable(HttpContext _, IRequestMessage requestMessage)
{
if (TryParseGuidFromSecondToLastSegment(requestMessage, out var guid))
{
var mapping = Mappings.FirstOrDefault(m => !m.IsAdminInterface && m.Guid == guid);
if (mapping != null)
{
mapping.IsDisabled = true;
return ResponseMessageBuilder.Create(HttpStatusCode.OK, "Mapping disabled", guid);
}
}
_settings.Logger.Warn("HttpStatusCode set to 404 : Mapping not found");
return ResponseMessageBuilder.Create(HttpStatusCode.NotFound, "Mapping not found");
}
#endregion Mapping/{guid} #endregion Mapping/{guid}
#region Mappings #region Mappings
@@ -838,6 +883,18 @@ public partial class WireMockServer
}; };
} }
private T[] DeserializeRequestMessageToArray<T>(IRequestMessage requestMessage)
{
if (requestMessage.BodyData?.DetectedBodyType == BodyType.Json && requestMessage.BodyData.BodyAsJson != null)
{
var bodyAsJson = requestMessage.BodyData.BodyAsJson!;
return _mappingSerializer.DeserializeObjectToArray<T>(bodyAsJson);
}
throw new NotSupportedException();
}
private static Encoding? ToEncoding(EncodingModel? encodingModel) private static Encoding? ToEncoding(EncodingModel? encodingModel)
{ {
return encodingModel != null ? Encoding.GetEncoding(encodingModel.CodePage) : null; return encodingModel != null ? Encoding.GetEncoding(encodingModel.CodePage) : null;
@@ -857,28 +914,16 @@ public partial class WireMockServer
}; };
} }
private static T[] DeserializeRequestMessageToArray<T>(IRequestMessage requestMessage) private T DeserializeObject<T>(IRequestMessage requestMessage)
{
if (requestMessage.BodyData?.DetectedBodyType == BodyType.Json && requestMessage.BodyData.BodyAsJson != null)
{
var bodyAsJson = requestMessage.BodyData.BodyAsJson!;
return MappingSerializer.DeserializeObjectToArray<T>(bodyAsJson);
}
throw new NotSupportedException();
}
private static T DeserializeObject<T>(IRequestMessage requestMessage)
{ {
switch (requestMessage.BodyData?.DetectedBodyType) switch (requestMessage.BodyData?.DetectedBodyType)
{ {
case BodyType.String: case BodyType.String when requestMessage.BodyData?.BodyAsString != null:
case BodyType.FormUrlEncoded: case BodyType.FormUrlEncoded when requestMessage.BodyData?.BodyAsString != null:
return JsonUtils.DeserializeObject<T>(requestMessage.BodyData.BodyAsString!); return _settings.DefaultJsonSerializer.Deserialize<T>(requestMessage.BodyData.BodyAsString)!;
case BodyType.Json when requestMessage.BodyData?.BodyAsJson != null: case BodyType.Json when requestMessage.BodyData?.BodyAsJson != null:
return ((JObject)requestMessage.BodyData.BodyAsJson).ToObject<T>()!; return _settings.DefaultJsonSerializer.ParseJsonToken<T>(requestMessage.BodyData.BodyAsJson)!;
default: default:
throw new NotSupportedException(); throw new NotSupportedException();

View File

@@ -1,6 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Linq;
using Stef.Validation; using Stef.Validation;
using WireMock.Admin.Mappings; using WireMock.Admin.Mappings;
using WireMock.Matchers; using WireMock.Matchers;
@@ -120,6 +119,11 @@ public partial class WireMockServer
respondProvider.WithProbability(mappingModel.Probability.Value); respondProvider.WithProbability(mappingModel.Probability.Value);
} }
if (mappingModel.IsDisabled == true)
{
respondProvider.WithIsDisabled(true);
}
// ProtoDefinition is defined at Mapping level // ProtoDefinition is defined at Mapping level
if (mappingModel.ProtoDefinition != null) if (mappingModel.ProtoDefinition != null)
{ {
@@ -148,7 +152,7 @@ public partial class WireMockServer
} }
else else
{ {
var clientIPModel = JsonUtils.ParseJTokenToObject<ClientIPModel>(requestModel.ClientIP); var clientIPModel = _settings.DefaultJsonSerializer.ParseJsonToken<ClientIPModel>(requestModel.ClientIP);
if (clientIPModel.Matchers != null) if (clientIPModel.Matchers != null)
{ {
requestBuilder = requestBuilder.WithPath(clientIPModel.Matchers.Select(_matcherMapper.Map).OfType<IStringMatcher>().ToArray()); requestBuilder = requestBuilder.WithPath(clientIPModel.Matchers.Select(_matcherMapper.Map).OfType<IStringMatcher>().ToArray());
@@ -164,7 +168,7 @@ public partial class WireMockServer
} }
else else
{ {
var pathModel = JsonUtils.ParseJTokenToObject<PathModel>(requestModel.Path); var pathModel = _settings.DefaultJsonSerializer.ParseJsonToken<PathModel>(requestModel.Path);
if (pathModel.Matchers != null) if (pathModel.Matchers != null)
{ {
var matchOperator = StringUtils.ParseMatchOperator(pathModel.MatchOperator); var matchOperator = StringUtils.ParseMatchOperator(pathModel.MatchOperator);
@@ -180,7 +184,7 @@ public partial class WireMockServer
} }
else else
{ {
var urlModel = JsonUtils.ParseJTokenToObject<UrlModel>(requestModel.Url); var urlModel = _settings.DefaultJsonSerializer.ParseJsonToken<UrlModel>(requestModel.Url);
if (urlModel.Matchers != null) if (urlModel.Matchers != null)
{ {
var matchOperator = StringUtils.ParseMatchOperator(urlModel.MatchOperator); var matchOperator = StringUtils.ParseMatchOperator(urlModel.MatchOperator);
@@ -266,7 +270,7 @@ public partial class WireMockServer
return requestBuilder; return requestBuilder;
} }
private static IResponseBuilder InitResponseBuilder(ResponseModel responseModel) private IResponseBuilder InitResponseBuilder(ResponseModel responseModel)
{ {
var responseBuilder = Response.Create(); var responseBuilder = Response.Create();
@@ -331,7 +335,7 @@ public partial class WireMockServer
} }
else else
{ {
var headers = JsonUtils.ParseJTokenToObject<string[]>(entry.Value); var headers = _settings.DefaultJsonSerializer.ParseJsonToken<string[]>(entry.Value);
responseBuilder.WithHeader(entry.Key, headers); responseBuilder.WithHeader(entry.Key, headers);
} }
} }
@@ -357,7 +361,7 @@ public partial class WireMockServer
} }
else else
{ {
var headers = JsonUtils.ParseJTokenToObject<string[]>(entry.Value); var headers = _settings.DefaultJsonSerializer.ParseJsonToken<string[]>(entry.Value);
responseBuilder.WithTrailingHeader(entry.Key, headers); responseBuilder.WithTrailingHeader(entry.Key, headers);
} }
} }

View File

@@ -414,6 +414,7 @@ public partial class WireMockServer : IWireMockServer
_options.CorsPolicyOptions = _settings.CorsPolicyOptions; _options.CorsPolicyOptions = _settings.CorsPolicyOptions;
_options.ClientCertificateMode = (Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode)_settings.ClientCertificateMode; _options.ClientCertificateMode = (Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode)_settings.ClientCertificateMode;
_options.AcceptAnyClientCertificate = _settings.AcceptAnyClientCertificate; _options.AcceptAnyClientCertificate = _settings.AcceptAnyClientCertificate;
_options.DefaultJsonSerializer = _settings.DefaultJsonSerializer;
_httpServer = new AspNetCoreSelfHost(_options, urlOptions); _httpServer = new AspNetCoreSelfHost(_options, urlOptions);
var startTask = _httpServer.StartAsync(); var startTask = _httpServer.StartAsync();

View File

@@ -10,6 +10,7 @@ using WireMock.Constants;
using WireMock.Logging; using WireMock.Logging;
using WireMock.Models; using WireMock.Models;
using WireMock.Types; using WireMock.Types;
using WireMock.Transformers;
using WireMock.Util; using WireMock.Util;
namespace WireMock.Settings; namespace WireMock.Settings;
@@ -74,6 +75,8 @@ public static class WireMockServerSettingsParser
WatchStaticMappingsInSubdirectories = parser.GetBoolValue(nameof(WireMockServerSettings.WatchStaticMappingsInSubdirectories)), WatchStaticMappingsInSubdirectories = parser.GetBoolValue(nameof(WireMockServerSettings.WatchStaticMappingsInSubdirectories)),
}; };
settings.DefaultJsonBodyTransformer = new NewtonsoftJsonBodyTransformer(settings);
#if USE_ASPNETCORE #if USE_ASPNETCORE
settings.CorsPolicyOptions = parser.GetEnumValue(nameof(WireMockServerSettings.CorsPolicyOptions), CorsPolicyOptions.None); settings.CorsPolicyOptions = parser.GetEnumValue(nameof(WireMockServerSettings.CorsPolicyOptions), CorsPolicyOptions.None);
settings.ClientCertificateMode = parser.GetEnumValue(nameof(WireMockServerSettings.ClientCertificateMode), ClientCertificateMode.NoCertificate); settings.ClientCertificateMode = parser.GetEnumValue(nameof(WireMockServerSettings.ClientCertificateMode), ClientCertificateMode.NoCertificate);

View File

@@ -1,6 +1,7 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using HandlebarsDotNet; using HandlebarsDotNet;
using WireMock.Transformers;
namespace WireMock.Transformers.Handlebars; namespace WireMock.Transformers.Handlebars;

View File

@@ -1,14 +0,0 @@
// Copyright © WireMock.Net
using WireMock.Handlers;
namespace WireMock.Transformers;
internal interface ITransformerContext
{
IFileSystemHandler FileSystemHandler { get; }
string ParseAndRender(string text, object model);
object? ParseAndEvaluate(string text, object model);
}

View File

@@ -19,7 +19,7 @@ internal class WireMockListAccessor : IListAccessor, IObjectAccessor
return target?.ToString() ?? string.Empty; return target?.ToString() ?? string.Empty;
} }
public void SetValue(TemplateContext context, SourceSpan span, object target, int index, object value) public void SetValue(TemplateContext context, SourceSpan span, object target, int index, object? value)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@@ -46,7 +46,7 @@ internal class WireMockListAccessor : IListAccessor, IObjectAccessor
throw new NotImplementedException(); throw new NotImplementedException();
} }
public bool TrySetValue(TemplateContext context, SourceSpan span, object target, string member, object value) public bool TrySetValue(TemplateContext context, SourceSpan span, object target, string member, object? value)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@@ -56,7 +56,7 @@ internal class WireMockListAccessor : IListAccessor, IObjectAccessor
throw new NotImplementedException(); throw new NotImplementedException();
} }
public bool TrySetItem(TemplateContext context, SourceSpan span, object target, object index, object value) public bool TrySetItem(TemplateContext context, SourceSpan span, object target, object index, object? value)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@@ -8,9 +8,9 @@ namespace WireMock.Transformers.Scriban;
internal class WireMockTemplateContext : TemplateContext internal class WireMockTemplateContext : TemplateContext
{ {
protected override IObjectAccessor GetMemberAccessorImpl(object target) protected override IObjectAccessor? GetMemberAccessorImpl(object target)
{ {
return target?.GetType().GetGenericTypeDefinition() == typeof(WireMockList<>) ? return target.GetType().GetGenericTypeDefinition() == typeof(WireMockList<>) ?
new WireMockListAccessor() : new WireMockListAccessor() :
base.GetMemberAccessorImpl(target); base.GetMemberAccessorImpl(target);
} }

View File

@@ -1,10 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Collections;
using System.Linq;
using HandlebarsDotNet.Helpers.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Stef.Validation; using Stef.Validation;
using WireMock.Settings; using WireMock.Settings;
using WireMock.Types; using WireMock.Types;
@@ -14,17 +9,13 @@ namespace WireMock.Transformers;
internal class Transformer : ITransformer internal class Transformer : ITransformer
{ {
private readonly JsonSerializer _jsonSerializer; private readonly IJsonBodyTransformer _jsonBodyTransformer;
private readonly ITransformerContextFactory _factory; private readonly ITransformerContextFactory _factory;
public Transformer(WireMockServerSettings settings, ITransformerContextFactory factory) public Transformer(WireMockServerSettings settings, ITransformerContextFactory factory)
{ {
_factory = Guard.NotNull(factory); _factory = Guard.NotNull(factory);
_jsonBodyTransformer = Guard.NotNull(settings).DefaultJsonBodyTransformer;
_jsonSerializer = new JsonSerializer
{
Culture = Guard.NotNull(settings).Culture
};
} }
public IBodyData? TransformBody( public IBodyData? TransformBody(
@@ -121,13 +112,17 @@ internal class Transformer : ITransformer
}); });
} }
private IBodyData? TransformBodyData(ITransformerContext transformerContext, ReplaceNodeOptions options, TransformModel model, IBodyData original, bool useTransformerForBodyAsFile) private BodyData? TransformBodyData(ITransformerContext transformerContext, ReplaceNodeOptions options, TransformModel model, IBodyData original, bool useTransformerForBodyAsFile)
{ {
switch (original.DetectedBodyType) switch (original.DetectedBodyType)
{ {
case BodyType.Json: case BodyType.Json:
case BodyType.ProtoBuf: case BodyType.ProtoBuf:
return TransformBodyAsJson(transformerContext, options, model, original); return _jsonBodyTransformer.TransformBodyAsJson(
transformerContext,
options,
model,
original);
case BodyType.File: case BodyType.File:
return TransformBodyAsFile(transformerContext, model, original, useTransformerForBodyAsFile); return TransformBodyAsFile(transformerContext, model, original, useTransformerForBodyAsFile);
@@ -159,185 +154,7 @@ internal class Transformer : ITransformer
return newHeaders; return newHeaders;
} }
private IBodyData TransformBodyAsJson(ITransformerContext transformerContext, ReplaceNodeOptions options, object model, IBodyData original) private static BodyData TransformBodyAsString(ITransformerContext transformerContext, object model, IBodyData original)
{
JToken? jToken = null;
switch (original.BodyAsJson)
{
case JObject bodyAsJObject:
jToken = bodyAsJObject.DeepClone();
WalkNode(transformerContext, options, jToken, model);
break;
case JArray bodyAsJArray:
jToken = bodyAsJArray.DeepClone();
WalkNode(transformerContext, options, jToken, model);
break;
case var bodyAsEnumerable when bodyAsEnumerable is IEnumerable and not string:
jToken = JArray.FromObject(bodyAsEnumerable, _jsonSerializer);
WalkNode(transformerContext, options, jToken, model);
break;
case string bodyAsString:
jToken = ReplaceSingleNode(transformerContext, options, bodyAsString, model);
break;
case not null:
jToken = JObject.FromObject(original.BodyAsJson, _jsonSerializer);
WalkNode(transformerContext, options, jToken, model);
break;
}
return new BodyData
{
Encoding = original.Encoding,
DetectedBodyType = original.DetectedBodyType,
DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType,
ProtoDefinition = original.ProtoDefinition,
ProtoBufMessageType = original.ProtoBufMessageType,
BodyAsJson = jToken
};
}
private JToken ReplaceSingleNode(ITransformerContext transformerContext, ReplaceNodeOptions options, string stringValue, object model)
{
var transformedString = transformerContext.ParseAndRender(stringValue, model);
if (!string.Equals(stringValue, transformedString))
{
const string property = "_";
JObject dummy = JObject.Parse($"{{ \"{property}\": null }}");
if (dummy[property] == null)
{
// TODO: check if just returning null is fine
return string.Empty;
}
JToken node = dummy[property]!;
ReplaceNodeValue(options, node, transformedString);
return dummy[property]!;
}
return stringValue;
}
private void WalkNode(ITransformerContext transformerContext, ReplaceNodeOptions options, JToken node, object model)
{
switch (node.Type)
{
case JTokenType.Object:
// In case of Object, loop all children. Do a ToArray() to avoid `Collection was modified` exceptions.
foreach (var child in node.Children<JProperty>().ToArray())
{
WalkNode(transformerContext, options, child.Value, model);
}
break;
case JTokenType.Array:
// In case of Array, loop all items. Do a ToArray() to avoid `Collection was modified` exceptions.
foreach (var child in node.Children().ToArray())
{
WalkNode(transformerContext, options, child, model);
}
break;
case JTokenType.String:
// In case of string, try to transform the value.
var stringValue = node.Value<string>();
if (string.IsNullOrEmpty(stringValue))
{
return;
}
var transformed = transformerContext.ParseAndEvaluate(stringValue!, model);
if (!Equals(stringValue, transformed))
{
ReplaceNodeValue(options, node, transformed);
}
break;
}
}
// ReSharper disable once UnusedParameter.Local
private void ReplaceNodeValue(ReplaceNodeOptions options, JToken node, object? transformedValue)
{
switch (transformedValue)
{
case JValue jValue:
node.Replace(jValue);
return;
case string transformedString:
var (isConvertedFromString, convertedValueFromString) = TryConvert(options, transformedString);
if (isConvertedFromString)
{
node.Replace(JToken.FromObject(convertedValueFromString, _jsonSerializer));
}
else
{
node.Replace(ParseAsJObject(transformedString));
}
break;
case WireMockList<string> strings:
switch (strings.Count)
{
case 1:
node.Replace(ParseAsJObject(strings[0]));
return;
case > 1:
node.Replace(JToken.FromObject(strings.ToArray(), _jsonSerializer));
return;
}
break;
case { }:
var (isConverted, convertedValue) = TryConvert(options, transformedValue);
if (isConverted)
{
node.Replace(JToken.FromObject(convertedValue, _jsonSerializer));
}
return;
default: // It's null, skip it. Maybe remove it ?
return;
}
}
private static JToken ParseAsJObject(string stringValue)
{
return JsonUtils.TryParseAsJObject(stringValue, out var parsedAsjObject) ? parsedAsjObject : stringValue;
}
private static (bool IsConverted, object ConvertedValue) TryConvert(ReplaceNodeOptions options, object value)
{
var valueAsString = value as string;
if (options == ReplaceNodeOptions.Evaluate)
{
if (valueAsString != null && WrappedString.TryDecode(valueAsString, out var decoded))
{
return (true, decoded);
}
return (false, value);
}
if (valueAsString != null)
{
return WrappedString.TryDecode(valueAsString, out var decoded) ?
(true, decoded) :
StringUtils.TryConvertToKnownType(valueAsString);
}
return (false, value);
}
private static IBodyData TransformBodyAsString(ITransformerContext transformerContext, object model, IBodyData original)
{ {
return new BodyData return new BodyData
{ {
@@ -348,7 +165,7 @@ internal class Transformer : ITransformer
}; };
} }
private static IBodyData TransformBodyAsFile(ITransformerContext transformerContext, object model, IBodyData original, bool useTransformerForBodyAsFile) private static BodyData TransformBodyAsFile(ITransformerContext transformerContext, object model, IBodyData original, bool useTransformerForBodyAsFile)
{ {
var transformedBodyAsFilename = transformerContext.ParseAndRender(original.BodyAsFile!, model); var transformedBodyAsFilename = transformerContext.ParseAndRender(original.BodyAsFile!, model);

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<Description>Minimal version from the lightweight Http Mocking Server for .NET</Description> <Description>Minimal version from the lightweight Http Mocking Server for .NET</Description>
<AssemblyTitle>WireMock.Net.Minimal</AssemblyTitle> <AssemblyTitle>WireMock.Net.Minimal</AssemblyTitle>
@@ -43,7 +43,7 @@
<PackageReference Include="SimMetrics.Net" Version="1.0.5" /> <PackageReference Include="SimMetrics.Net" Version="1.0.5" />
<PackageReference Include="TinyMapper.Signed" Version="4.0.0" /> <PackageReference Include="TinyMapper.Signed" Version="4.0.0" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="6.34.0" /> <PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="6.34.0" />
<PackageReference Include="Scriban.Signed" Version="5.5.0" /> <PackageReference Include="Scriban.Signed" Version="7.0.6" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -25,7 +25,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.8.0" /> <PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.11.0" />
<PackageReference Include="NUnit" Version="4.4.0" /> <PackageReference Include="NUnit" Version="4.4.0" />
<PackageReference Include="Stef.Validation" Version="0.2.0" /> <PackageReference Include="Stef.Validation" Version="0.2.0" />
</ItemGroup> </ItemGroup>

View File

@@ -25,9 +25,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.14.0" /> <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.14.0" /> <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.14.0" /> <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -163,6 +163,22 @@ public interface IWireMockAdminApi
[Header("Content-Type", "application/json")] [Header("Content-Type", "application/json")]
Task<StatusModel> PutMappingAsync([Path] Guid guid, [Body] MappingModel mapping, CancellationToken cancellationToken = default); Task<StatusModel> PutMappingAsync([Path] Guid guid, [Body] MappingModel mapping, CancellationToken cancellationToken = default);
/// <summary>
/// Enable a mapping based on the guid.
/// </summary>
/// <param name="guid">The Guid.</param>
/// <param name="cancellationToken">The optional cancellationToken.</param>
[Put("mappings/{guid}/enable")]
Task<StatusModel> EnableMappingAsync([Path] Guid guid, CancellationToken cancellationToken = default);
/// <summary>
/// Disable a mapping based on the guid.
/// </summary>
/// <param name="guid">The Guid.</param>
/// <param name="cancellationToken">The optional cancellationToken.</param>
[Put("mappings/{guid}/disable")]
Task<StatusModel> DisableMappingAsync([Path] Guid guid, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Delete a mapping based on the guid /// Delete a mapping based on the guid
/// </summary> /// </summary>

View File

@@ -33,7 +33,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.8.0" /> <PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.11.0" />
<PackageReference Include="RestEase" Version="1.6.4" /> <PackageReference Include="RestEase" Version="1.6.4" />
<PackageReference Include="Stef.Validation" Version="0.2.0" /> <PackageReference Include="Stef.Validation" Version="0.2.0" />
</ItemGroup> </ItemGroup>

View File

@@ -1,42 +1,48 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace WireMock.Handlers; namespace WireMock.Handlers;
/// <summary>
/// Provides an in-memory implementation of the <see cref="IScenarioStateStore" /> interface for managing scenario state objects by name.
/// </summary>
public class InMemoryScenarioStateStore : IScenarioStateStore public class InMemoryScenarioStateStore : IScenarioStateStore
{ {
private readonly ConcurrentDictionary<string, ScenarioState> _scenarios = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, ScenarioState> _scenarios = new(StringComparer.OrdinalIgnoreCase);
/// <inheritdoc />
public bool TryGet(string name, [NotNullWhen(true)] out ScenarioState? state) public bool TryGet(string name, [NotNullWhen(true)] out ScenarioState? state)
{ {
return _scenarios.TryGetValue(name, out state); return _scenarios.TryGetValue(name, out state);
} }
/// <inheritdoc />
public IReadOnlyList<ScenarioState> GetAll() public IReadOnlyList<ScenarioState> GetAll()
{ {
return _scenarios.Values.ToArray(); return _scenarios.Values.ToArray();
} }
/// <inheritdoc />
public bool ContainsKey(string name) public bool ContainsKey(string name)
{ {
return _scenarios.ContainsKey(name); return _scenarios.ContainsKey(name);
} }
/// <inheritdoc />
public bool TryAdd(string name, ScenarioState scenarioState) public bool TryAdd(string name, ScenarioState scenarioState)
{ {
return _scenarios.TryAdd(name, scenarioState); return _scenarios.TryAdd(name, scenarioState);
} }
/// <inheritdoc />
public ScenarioState AddOrUpdate(string name, Func<string, ScenarioState> addFactory, Func<string, ScenarioState, ScenarioState> updateFactory) public ScenarioState AddOrUpdate(string name, Func<string, ScenarioState> addFactory, Func<string, ScenarioState, ScenarioState> updateFactory)
{ {
return _scenarios.AddOrUpdate(name, addFactory, updateFactory); return _scenarios.AddOrUpdate(name, addFactory, updateFactory);
} }
/// <inheritdoc />
public ScenarioState? Update(string name, Action<ScenarioState> updateAction) public ScenarioState? Update(string name, Action<ScenarioState> updateAction)
{ {
if (_scenarios.TryGetValue(name, out var state)) if (_scenarios.TryGetValue(name, out var state))
@@ -48,11 +54,13 @@ public class InMemoryScenarioStateStore : IScenarioStateStore
return null; return null;
} }
/// <inheritdoc />
public bool TryRemove(string name) public bool TryRemove(string name)
{ {
return _scenarios.TryRemove(name, out _); return _scenarios.TryRemove(name, out _);
} }
/// <inheritdoc />
public void Clear() public void Clear()
{ {
_scenarios.Clear(); _scenarios.Clear();

View File

@@ -108,6 +108,14 @@ public interface IMapping
/// </value> /// </value>
bool IsProxy { get; } bool IsProxy { get; }
/// <summary>
/// Gets a value indicating whether this mapping is disabled.
/// </summary>
/// <value>
/// <c>true</c> if this mapping is disabled; otherwise, <c>false</c>.
/// </value>
bool IsDisabled { get; set; }
/// <summary> /// <summary>
/// Gets a value indicating whether this mapping to be logged. /// Gets a value indicating whether this mapping to be logged.
/// </summary> /// </summary>

View File

@@ -0,0 +1,11 @@
// Copyright © WireMock.Net
namespace WireMock.Matchers;
/// <summary>
/// IJsonPathMatcher
/// <seealso cref="IStringMatcher"/> and <seealso cref="IObjectMatcher"/>.
/// </summary>
public interface IJsonPathMatcher : IStringMatcher, IObjectMatcher
{
}

View File

@@ -0,0 +1,11 @@
// Copyright © WireMock.Net
namespace WireMock.Matchers;
/// <summary>
/// ISystemTextJsonPathMatcher
/// <seealso cref="IJsonPathMatcher"/>.
/// </summary>
public interface ISystemTextJsonPathMatcher : IJsonPathMatcher
{
}

View File

@@ -7,6 +7,7 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("WireMock.Net.GraphQL, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")] [assembly: InternalsVisibleTo("WireMock.Net.GraphQL, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]
[assembly: InternalsVisibleTo("WireMock.Net.ProtoBuf, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")] [assembly: InternalsVisibleTo("WireMock.Net.ProtoBuf, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]
[assembly: InternalsVisibleTo("WireMock.Net.Matchers.CSharpCode, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")] [assembly: InternalsVisibleTo("WireMock.Net.Matchers.CSharpCode, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]
[assembly: InternalsVisibleTo("WireMock.Net.Matchers.SystemTextJsonPath, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]
[assembly: InternalsVisibleTo("WireMock.Net.OpenTelemetry, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")] [assembly: InternalsVisibleTo("WireMock.Net.OpenTelemetry, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]
// [assembly: InternalsVisibleTo("WireMock.Net.StandAlone, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")] // [assembly: InternalsVisibleTo("WireMock.Net.StandAlone, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]
[assembly: InternalsVisibleTo("WireMock.Net.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")] [assembly: InternalsVisibleTo("WireMock.Net.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")]

View File

@@ -14,11 +14,17 @@ internal static class JsonSerializationConstants
IgnoreNullValues = true IgnoreNullValues = true
}; };
//internal static readonly JsonSerializerSettings JsonSerializerSettingsDefault = new() internal static readonly JsonConverterOptions JsonConverterOptionsIncludeNullValues = new()
//{ {
// Formatting = Formatting.Indented, WriteIndented = true,
// NullValueHandling = NullValueHandling.Ignore IgnoreNullValues = false
//}; };
internal static readonly JsonConverterOptions JsonConverterOptionsWithDateParsingNone = new()
{
WriteIndented = true,
DateParseHandling = 0
};
internal static readonly JsonSerializerSettings JsonSerializerSettingsIncludeNullValues = new() internal static readonly JsonSerializerSettings JsonSerializerSettingsIncludeNullValues = new()
{ {

View File

@@ -14,6 +14,7 @@ using WireMock.Logging;
using WireMock.Matchers; using WireMock.Matchers;
using WireMock.Models; using WireMock.Models;
using WireMock.RegularExpressions; using WireMock.RegularExpressions;
using WireMock.Transformers;
using WireMock.Types; using WireMock.Types;
namespace WireMock.Settings; namespace WireMock.Settings;
@@ -175,6 +176,13 @@ public class WireMockServerSettings
[JsonIgnore] [JsonIgnore]
public IFileSystemHandler FileSystemHandler { get; set; } = null!; public IFileSystemHandler FileSystemHandler { get; set; } = null!;
/// <summary>
/// Gets or sets the store used to persist scenario state information.
/// </summary>
/// <remarks>
/// The scenario state store manages the storage and retrieval of state data associated with scenarios.
/// By default, an in-memory implementation is used, but this property can be set to a custom implementation to support alternative storage mechanisms such as databases or distributed caches.
/// </remarks>
[PublicAPI] [PublicAPI]
[JsonIgnore] [JsonIgnore]
public IScenarioStateStore ScenarioStateStore { get; set; } = new InMemoryScenarioStateStore(); public IScenarioStateStore ScenarioStateStore { get; set; } = new InMemoryScenarioStateStore();
@@ -355,11 +363,31 @@ public class WireMockServerSettings
/// Default is <see cref="NewtonsoftJsonConverter"/>. /// Default is <see cref="NewtonsoftJsonConverter"/>.
/// </remarks> /// </remarks>
[PublicAPI] [PublicAPI]
public IJsonConverter DefaultJsonSerializer { get; set; } = new NewtonsoftJsonConverter(); public IJsonConverter DefaultJsonSerializer { get; set; }
/// <summary>
/// Gets or sets the default JSON body transformer used for template-based JSON body transformations.
/// </summary>
/// <remarks>
/// Set this property to provide a custom implementation for transforming JSON and ProtoBuf body content.
/// Default is <see cref="NewtonsoftJsonBodyTransformer"/>.
/// </remarks>
[PublicAPI]
[JsonIgnore]
public IJsonBodyTransformer DefaultJsonBodyTransformer { get; set; }
/// <summary> /// <summary>
/// WebSocket settings. /// WebSocket settings.
/// </summary> /// </summary>
[PublicAPI] [PublicAPI]
public WebSocketSettings? WebSocketSettings { get; set; } public WebSocketSettings? WebSocketSettings { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="WireMockServerSettings"/> class.
/// </summary>
public WireMockServerSettings()
{
DefaultJsonSerializer = new NewtonsoftJsonConverter();
DefaultJsonBodyTransformer = new NewtonsoftJsonBodyTransformer(this);
}
} }

View File

@@ -0,0 +1,28 @@
// Copyright © WireMock.Net
using JetBrains.Annotations;
using WireMock.Types;
using WireMock.Util;
namespace WireMock.Transformers;
/// <summary>
/// Defines the contract for transforming JSON-like body data using a transformer context.
/// </summary>
[PublicAPI]
public interface IJsonBodyTransformer
{
/// <summary>
/// Transforms the JSON body using the provided transformer context and model.
/// </summary>
/// <param name="transformerContext">The transformer context used to render and evaluate template values.</param>
/// <param name="options">The JSON node replacement behavior to apply during transformation.</param>
/// <param name="model">The model used when rendering or evaluating template values.</param>
/// <param name="original">The original body data to transform.</param>
/// <returns>The transformed JSON body data.</returns>
BodyData TransformBodyAsJson(
ITransformerContext transformerContext,
ReplaceNodeOptions options,
object model,
IBodyData original);
}

View File

@@ -0,0 +1,34 @@
// Copyright © WireMock.Net
using JetBrains.Annotations;
using WireMock.Handlers;
namespace WireMock.Transformers;
/// <summary>
/// Defines the transformer context used to render and evaluate templates during response transformation.
/// </summary>
[PublicAPI]
public interface ITransformerContext
{
/// <summary>
/// Gets the file system handler used by the current transformer context.
/// </summary>
IFileSystemHandler FileSystemHandler { get; }
/// <summary>
/// Renders the specified template text using the supplied model.
/// </summary>
/// <param name="text">The template text to render.</param>
/// <param name="model">The model used during rendering.</param>
/// <returns>The rendered text.</returns>
string ParseAndRender(string text, object model);
/// <summary>
/// Evaluates the specified template text using the supplied model.
/// </summary>
/// <param name="text">The template text to evaluate.</param>
/// <param name="model">The model used during evaluation.</param>
/// <returns>The evaluated value.</returns>
object? ParseAndEvaluate(string text, object model);
}

View File

@@ -0,0 +1,270 @@
// Copyright © WireMock.Net
using System.Collections;
using HandlebarsDotNet.Helpers.Models;
using JetBrains.Annotations;
using JsonConverter.Newtonsoft.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using WireMock.Settings;
using WireMock.Types;
using WireMock.Util;
namespace WireMock.Transformers;
/// <summary>
/// Default JSON body transformer implementation based on Newtonsoft.Json.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="NewtonsoftJsonBodyTransformer"/> class.
/// </remarks>
/// <param name="settings">The server settings used to configure JSON transformation behavior.</param>
[PublicAPI]
public class NewtonsoftJsonBodyTransformer(WireMockServerSettings settings) : IJsonBodyTransformer
{
private readonly NewtonsoftJsonConverter _jsonConverter = new();
/// <inheritdoc />
public BodyData TransformBodyAsJson(
ITransformerContext transformerContext,
ReplaceNodeOptions options,
object model,
IBodyData original)
{
var jsonSerializer = new JsonSerializer
{
Culture = settings.Culture
};
JToken? jToken = null;
switch (original.BodyAsJson)
{
case JObject bodyAsJObject:
jToken = bodyAsJObject.DeepClone();
WalkNode(transformerContext, jsonSerializer, options, jToken, model);
break;
case JArray bodyAsJArray:
jToken = bodyAsJArray.DeepClone();
WalkNode(transformerContext, jsonSerializer, options, jToken, model);
break;
case var bodyAsEnumerable when bodyAsEnumerable is IEnumerable and not string:
jToken = JArray.FromObject(bodyAsEnumerable, jsonSerializer);
WalkNode(transformerContext, jsonSerializer, options, jToken, model);
break;
case string bodyAsString:
jToken = ReplaceSingleNode(transformerContext, jsonSerializer, options, bodyAsString, model);
break;
case not null:
jToken = JObject.FromObject(original.BodyAsJson, jsonSerializer);
WalkNode(transformerContext, jsonSerializer, options, jToken, model);
break;
}
return new BodyData
{
Encoding = original.Encoding,
DetectedBodyType = original.DetectedBodyType,
DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType,
ProtoDefinition = original.ProtoDefinition,
ProtoBufMessageType = original.ProtoBufMessageType,
BodyAsJson = jToken
};
}
private JToken ParseAsJObject(string stringValue)
{
if (_jsonConverter.IsValidJson(stringValue))
{
try
{
// Try to convert this string into a JObject
return JObject.Parse(stringValue!);
}
catch
{
}
}
return stringValue;
}
private JToken ReplaceSingleNode(ITransformerContext transformerContext, JsonSerializer jsonSerializer, ReplaceNodeOptions options, string stringValue, object model)
{
var transformedString = transformerContext.ParseAndRender(stringValue, model);
if (!string.Equals(stringValue, transformedString))
{
const string property = "_";
JObject dummy = JObject.Parse($"{{ \"{property}\": null }}");
if (dummy[property] == null)
{
return string.Empty;
}
JToken node = dummy[property]!;
ReplaceNodeValue(jsonSerializer, options, node, transformedString);
return dummy[property]!;
}
return stringValue;
}
private void WalkNode(ITransformerContext transformerContext, JsonSerializer jsonSerializer, ReplaceNodeOptions options, JToken node, object model)
{
switch (node.Type)
{
case JTokenType.Object:
foreach (var child in node.Children<JProperty>().ToArray())
{
WalkNode(transformerContext, jsonSerializer, options, child.Value, model);
}
break;
case JTokenType.Array:
foreach (var child in node.Children().ToArray())
{
WalkNode(transformerContext, jsonSerializer, options, child, model);
}
break;
case JTokenType.String:
var stringValue = node.Value<string>();
if (string.IsNullOrEmpty(stringValue))
{
return;
}
var transformed = transformerContext.ParseAndEvaluate(stringValue!, model);
if (!Equals(stringValue, transformed))
{
ReplaceNodeValue(jsonSerializer, options, node, transformed);
}
break;
}
}
private void ReplaceNodeValue(JsonSerializer jsonSerializer, ReplaceNodeOptions options, JToken node, object? transformedValue)
{
switch (transformedValue)
{
case JValue jValue:
node.Replace(jValue);
return;
case string transformedString:
var (isConvertedFromString, convertedValueFromString) = TryConvert(options, transformedString);
if (isConvertedFromString)
{
node.Replace(JToken.FromObject(convertedValueFromString, jsonSerializer));
}
else
{
node.Replace(ParseAsJObject(transformedString));
}
break;
case WireMockList<string> strings:
switch (strings.Count)
{
case 1:
node.Replace(ParseAsJObject(strings[0]));
return;
case > 1:
node.Replace(JToken.FromObject(strings.ToArray(), jsonSerializer));
return;
}
break;
case { }:
var (isConverted, convertedValue) = TryConvert(options, transformedValue);
if (isConverted)
{
node.Replace(JToken.FromObject(convertedValue, jsonSerializer));
}
return;
default:
return;
}
}
private static (bool IsConverted, object ConvertedValue) TryConvert(ReplaceNodeOptions options, object value)
{
var valueAsString = value as string;
if (options == ReplaceNodeOptions.Evaluate)
{
if (valueAsString != null && WrappedString.TryDecode(valueAsString, out var decoded))
{
return (true, decoded);
}
return (false, value);
}
if (valueAsString != null)
{
return WrappedString.TryDecode(valueAsString, out var decoded)
? (true, decoded)
: TryConvertToKnownType(valueAsString);
}
return (false, value);
}
internal static (bool IsConverted, object ConvertedValue) TryConvertToKnownType(string value)
{
if (bool.TryParse(value, out var boolResult))
{
return (true, boolResult);
}
if (int.TryParse(value, out var intResult))
{
return (true, intResult);
}
if (long.TryParse(value, out var longResult))
{
return (true, longResult);
}
if (double.TryParse(value, out var doubleResult))
{
return (true, doubleResult);
}
if (Guid.TryParseExact(value, "D", out var guidResult))
{
return (true, guidResult);
}
if (TimeSpan.TryParse(value, out var timeSpanResult))
{
return (true, timeSpanResult);
}
if (DateTime.TryParse(value, out var dateTimeResult))
{
return (true, dateTimeResult);
}
if ((value.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase) ||
value.StartsWith("ftps://", StringComparison.OrdinalIgnoreCase) ||
value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) &&
Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var uriResult))
{
return (true, uriResult);
}
return (false, value);
}
}

View File

@@ -0,0 +1,207 @@
// Copyright © WireMock.Net
using System.Collections;
using System.Text.Json;
using System.Text.Json.Nodes;
using HandlebarsDotNet.Helpers.Models;
using JetBrains.Annotations;
using JsonConverter.System.Text.Json;
using WireMock.Types;
using WireMock.Util;
namespace WireMock.Transformers;
/// <summary>
/// JSON body transformer implementation based on System.Text.Json.
/// </summary>
[PublicAPI]
public class SystemTextJsonBodyTransformer() : IJsonBodyTransformer
{
private readonly SystemTextJsonConverter _jsonConverter = new();
/// <inheritdoc />
public BodyData TransformBodyAsJson(
ITransformerContext transformerContext,
ReplaceNodeOptions options,
object model,
IBodyData original)
{
JsonNode? jsonNode = null;
switch (original.BodyAsJson)
{
case JsonObject bodyAsJsonObject:
jsonNode = CloneNode(bodyAsJsonObject);
jsonNode = WalkNode(transformerContext, options, jsonNode, model);
break;
case JsonArray bodyAsJsonArray:
jsonNode = CloneNode(bodyAsJsonArray);
jsonNode = WalkNode(transformerContext, options, jsonNode, model);
break;
case var bodyAsEnumerable when bodyAsEnumerable is IEnumerable and not string:
jsonNode = JsonSerializer.SerializeToNode(bodyAsEnumerable);
if (jsonNode != null)
{
jsonNode = WalkNode(transformerContext, options, jsonNode, model);
}
break;
case string bodyAsString:
jsonNode = ReplaceSingleNode(transformerContext, options, bodyAsString, model);
break;
case not null:
jsonNode = JsonSerializer.SerializeToNode(original.BodyAsJson);
if (jsonNode != null)
{
jsonNode = WalkNode(transformerContext, options, jsonNode, model);
}
break;
}
return new BodyData
{
Encoding = original.Encoding,
DetectedBodyType = original.DetectedBodyType,
DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType,
ProtoDefinition = original.ProtoDefinition,
ProtoBufMessageType = original.ProtoBufMessageType,
BodyAsJson = jsonNode
};
}
private JsonNode ParseAsJsonObject(string stringValue)
{
if (_jsonConverter.IsValidJson(stringValue))
{
try
{
var parsed = JsonNode.Parse(stringValue);
if (parsed is JsonObject)
{
return parsed;
}
}
catch
{
// Ignore and return as string.
}
}
return JsonValue.Create(stringValue)!;
}
private JsonNode? ReplaceSingleNode(ITransformerContext transformerContext, ReplaceNodeOptions options, string stringValue, object model)
{
var transformedString = transformerContext.ParseAndRender(stringValue, model);
if (!string.Equals(stringValue, transformedString))
{
return ReplaceNodeValue(options, transformedString);
}
return JsonValue.Create(stringValue);
}
private JsonNode? WalkNode(ITransformerContext transformerContext, ReplaceNodeOptions options, JsonNode? node, object model)
{
switch (node)
{
case JsonObject jsonObject:
foreach (var property in jsonObject.ToArray())
{
jsonObject[property.Key] = WalkNode(transformerContext, options, property.Value, model);
}
return jsonObject;
case JsonArray jsonArray:
for (var i = 0; i < jsonArray.Count; i++)
{
jsonArray[i] = WalkNode(transformerContext, options, jsonArray[i], model);
}
return jsonArray;
case JsonValue jsonValue when jsonValue.TryGetValue<string>(out var stringValue):
if (string.IsNullOrEmpty(stringValue))
{
return jsonValue;
}
var transformed = transformerContext.ParseAndEvaluate(stringValue!, model);
return !Equals(stringValue, transformed) ? ReplaceNodeValue(options, transformed) ?? jsonValue : jsonValue;
default:
return node;
}
}
private JsonNode? ReplaceNodeValue(ReplaceNodeOptions options, object? transformedValue)
{
switch (transformedValue)
{
case JsonNode jsonNode:
return CloneNode(jsonNode);
case string transformedString:
var (isConvertedFromString, convertedValueFromString) = TryConvert(options, transformedString);
return isConvertedFromString
? JsonSerializer.SerializeToNode(convertedValueFromString)
: ParseAsJsonObject(transformedString);
case WireMockList<string> strings:
switch (strings.Count)
{
case 1:
return ParseAsJsonObject(strings[0]);
case > 1:
return JsonSerializer.SerializeToNode(strings.ToArray());
}
break;
case { }:
var (isConverted, convertedValue) = TryConvert(options, transformedValue);
if (isConverted)
{
return JsonSerializer.SerializeToNode(convertedValue);
}
break;
}
return null;
}
private static JsonNode? CloneNode(JsonNode? node)
{
#if NET8_0_OR_GREATER
return node?.DeepClone();
#else
return node == null ? null : JsonNode.Parse(node.ToJsonString());
#endif
}
private static (bool IsConverted, object ConvertedValue) TryConvert(ReplaceNodeOptions options, object value)
{
var valueAsString = value as string;
if (options == ReplaceNodeOptions.Evaluate)
{
if (valueAsString != null && WrappedString.TryDecode(valueAsString, out var decoded))
{
return (true, decoded);
}
return (false, value);
}
if (valueAsString != null)
{
return WrappedString.TryDecode(valueAsString, out var decoded)
? (true, decoded)
: NewtonsoftJsonBodyTransformer.TryConvertToKnownType(valueAsString);
}
return (false, value);
}
}

View File

@@ -1,6 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Linq;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
using Stef.Validation; using Stef.Validation;
@@ -129,11 +128,11 @@ internal static class BodyParser
{ {
Guard.NotNull(settings); Guard.NotNull(settings);
var bodyWithContentEncoding = await ReadBytesAsync(settings.Stream, settings.ContentEncoding, settings.DecompressGZipAndDeflate).ConfigureAwait(false); var (ContentType, Bytes) = await ReadBytesAsync(settings.Stream, settings.ContentEncoding, settings.DecompressGZipAndDeflate).ConfigureAwait(false);
var data = new BodyData var data = new BodyData
{ {
BodyAsBytes = bodyWithContentEncoding.Bytes, BodyAsBytes = Bytes,
DetectedCompression = bodyWithContentEncoding.ContentType, DetectedCompression = ContentType,
DetectedBodyType = BodyType.Bytes, DetectedBodyType = BodyType.Bytes,
DetectedBodyTypeFromContentType = DetectBodyTypeFromContentType(settings.ContentType) DetectedBodyTypeFromContentType = DetectBodyTypeFromContentType(settings.ContentType)
}; };
@@ -169,17 +168,17 @@ internal static class BodyParser
data.DetectedBodyType = BodyType.FormUrlEncoded; data.DetectedBodyType = BodyType.FormUrlEncoded;
} }
// If string is not null or empty, try to deserialize the string to a JObject // If string is not null or empty, try to deserialize the string
if (settings.DeserializeJson && JsonUtils.IsJson(data.BodyAsString)) if (settings.DeserializeJson && settings.DefaultJsonConverter.IsValidJson(data.BodyAsString))
{ {
try try
{ {
data.BodyAsJson = JsonUtils.DeserializeObject(data.BodyAsString); data.BodyAsJson = settings.DefaultJsonConverter.Deserialize<object>(data.BodyAsString);
data.DetectedBodyType = BodyType.Json; data.DetectedBodyType = BodyType.Json;
} }
catch catch
{ {
// JsonConvert failed, just ignore. // JsonConverter failed, just ignore.
} }
} }
} }
@@ -202,7 +201,7 @@ internal static class BodyParser
return (null, data); return (null, data);
} }
public static bool IsProbablyText(byte[] data) private static bool IsProbablyText(byte[] data)
{ {
if (data.Length == 0) if (data.Length == 0)
{ {

View File

@@ -1,6 +1,7 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.IO; using JsonConverter.Abstractions;
using JsonConverter.Newtonsoft.Json;
namespace WireMock.Util; namespace WireMock.Util;
@@ -35,4 +36,13 @@ internal class BodyParserSettings
/// Try to deserialize the body as FormUrlEncoded. /// Try to deserialize the body as FormUrlEncoded.
/// </summary> /// </summary>
public bool DeserializeFormUrlEncoded { get; set; } = true; public bool DeserializeFormUrlEncoded { get; set; } = true;
/// <summary>
/// Gets or sets the default JSON converter used for deserialization.
/// </summary>
/// <remarks>
/// Set this property to customize how objects are serialized to and deserialized from JSON during mapping.
/// Default is <see cref="NewtonsoftJsonConverter"/>.
/// </remarks>
public IJsonConverter DefaultJsonConverter { get; set; } = new NewtonsoftJsonConverter();
} }

View File

@@ -1,7 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
using System.Collections;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text; using System.Text;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -35,7 +33,7 @@ internal static class JsonUtils
try try
{ {
// Try to convert this string into a JToken // Try to convert this string into a JObject
value = JObject.Parse(strInput!); value = JObject.Parse(strInput!);
return true; return true;
} }
@@ -45,28 +43,12 @@ internal static class JsonUtils
} }
} }
public static string Serialize(object value)
{
return JsonConvert.SerializeObject(value, JsonSerializationConstants.JsonSerializerSettingsIncludeNullValues);
}
public static byte[] SerializeAsPactFile(object value) public static byte[] SerializeAsPactFile(object value)
{ {
var json = JsonConvert.SerializeObject(value, JsonSerializationConstants.JsonSerializerSettingsPact); var json = JsonConvert.SerializeObject(value, JsonSerializationConstants.JsonSerializerSettingsPact);
return Encoding.UTF8.GetBytes(json); return Encoding.UTF8.GetBytes(json);
} }
/// <summary>
/// Load a Newtonsoft.Json.Linq.JObject from a string that contains JSON.
/// Using : DateParseHandling = DateParseHandling.None
/// </summary>
/// <param name="json">A System.String that contains JSON.</param>
/// <returns>A Newtonsoft.Json.Linq.JToken populated from the string that contains JSON.</returns>
public static JToken Parse(string json)
{
return JsonConvert.DeserializeObject<JToken>(json, JsonSerializationConstants.JsonDeserializerSettingsWithDateParsingNone)!;
}
/// <summary> /// <summary>
/// Deserializes the JSON to a .NET object. /// Deserializes the JSON to a .NET object.
/// Using : DateParseHandling = DateParseHandling.None /// Using : DateParseHandling = DateParseHandling.None
@@ -100,38 +82,4 @@ internal static class JsonUtils
return default; return default;
} }
} }
public static T ParseJTokenToObject<T>(object? value)
{
if (value != null && value.GetType() == typeof(T))
{
return (T)value;
}
return value switch
{
JToken tokenValue => tokenValue.ToObject<T>()!,
_ => throw new NotSupportedException($"Unable to convert value to {typeof(T)}.")
};
}
public static JToken ConvertValueToJToken(object value)
{
// Check if JToken, string, IEnumerable or object
switch (value)
{
case JToken tokenValue:
return tokenValue;
case string stringValue:
return Parse(stringValue);
case IEnumerable enumerableValue:
return JArray.FromObject(enumerableValue);
default:
return JObject.FromObject(value);
}
}
} }

View File

@@ -3,7 +3,6 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection; using System.Reflection;
using Stef.Validation; using Stef.Validation;

View File

@@ -30,7 +30,8 @@
<PackageReference Include="Stef.Validation" Version="0.2.0" /> <PackageReference Include="Stef.Validation" Version="0.2.0" />
<PackageReference Include="AnyOf" Version="0.5.0.1" /> <PackageReference Include="AnyOf" Version="0.5.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.8.0" /> <PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.11.0" />
<PackageReference Include="JsonConverter.System.Text.Json" Version="0.11.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -34,5 +34,6 @@
<ProjectReference Include="../WireMock.Net.GraphQL/WireMock.Net.GraphQL.csproj" /> <ProjectReference Include="../WireMock.Net.GraphQL/WireMock.Net.GraphQL.csproj" />
<ProjectReference Include="../WireMock.Net.ProtoBuf/WireMock.Net.ProtoBuf.csproj" /> <ProjectReference Include="../WireMock.Net.ProtoBuf/WireMock.Net.ProtoBuf.csproj" />
<ProjectReference Include="../WireMock.Net.OpenTelemetry/WireMock.Net.OpenTelemetry.csproj" /> <ProjectReference Include="../WireMock.Net.OpenTelemetry/WireMock.Net.OpenTelemetry.csproj" />
<ProjectReference Include="../WireMock.Net.Matchers.SystemTextJsonPath/WireMock.Net.Matchers.SystemTextJsonPath.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -11,12 +11,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AwesomeAssertions" Version="9.4.0" /> <PackageReference Include="AwesomeAssertions" Version="9.4.0" />
<PackageReference Include="coverlet.collector" Version="6.0.4"> <PackageReference Include="coverlet.collector" Version="8.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="WireMock.Net" Version="1.25.0" /> <PackageReference Include="WireMock.Net" Version="2.1.0" />
<PackageReference Include="xunit.v3" Version="3.2.2" /> <PackageReference Include="xunit.v3" Version="3.2.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5"> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

View File

@@ -0,0 +1,134 @@
// Copyright © WireMock.Net
using RestEase;
using WireMock.Admin.Mappings;
using WireMock.Client;
using WireMock.Server;
namespace WireMock.Net.Tests.AdminApi;
public partial class WireMockAdminApiTests
{
[Fact]
public async Task IWireMockAdminApi_PostMappingAsync_WithIsDisabledTrue_DoesNotMatchRequests()
{
// Arrange
var ct = TestContext.Current.CancellationToken;
using var server = WireMockServer.StartWithAdminInterface();
var api = RestClient.For<IWireMockAdminApi>(server.Urls[0]);
var httpClient = server.CreateClient();
var model = new MappingModel
{
Request = new RequestModel { Path = "/foo", Methods = ["GET"] },
Response = new ResponseModel { Body = "hello", StatusCode = 200 },
IsDisabled = true
};
// Act — POST the disabled mapping
var postResult = await api.PostMappingAsync(model, ct);
postResult.Should().NotBeNull();
// Assert — request should not be matched (404)
var response = await httpClient.GetAsync("/foo", ct);
((int)response.StatusCode).Should().Be(404);
// Assert — mapping exists but IsDisabled is true
server.Mappings.Where(m => !m.IsAdminInterface).Should().ContainSingle(m => m.IsDisabled == true);
}
[Fact]
public async Task IWireMockAdminApi_DisableMappingAsync_PreventsMatching()
{
// Arrange
var ct = TestContext.Current.CancellationToken;
using var server = WireMockServer.StartWithAdminInterface();
var api = RestClient.For<IWireMockAdminApi>(server.Urls[0]);
var httpClient = server.CreateClient();
var model = new MappingModel
{
Request = new RequestModel { Path = "/bar", Methods = ["GET"] },
Response = new ResponseModel { Body = "world", StatusCode = 200 }
};
var postResult = await api.PostMappingAsync(model, ct);
var guid = postResult.Guid!.Value;
// Assert — mapping matches before disable
var before = await httpClient.GetAsync("/bar", ct);
((int)before.StatusCode).Should().Be(200);
// Act — disable
var disableResult = await api.DisableMappingAsync(guid, ct);
disableResult.Status.Should().Be("Mapping disabled");
// Assert — no match after disable
var after = await httpClient.GetAsync("/bar", ct);
((int)after.StatusCode).Should().Be(404);
}
[Fact]
public async Task IWireMockAdminApi_EnableMappingAsync_ResumesMatching()
{
// Arrange
var ct = TestContext.Current.CancellationToken;
using var server = WireMockServer.StartWithAdminInterface();
var api = RestClient.For<IWireMockAdminApi>(server.Urls[0]);
var httpClient = server.CreateClient();
var model = new MappingModel
{
Request = new RequestModel { Path = "/baz", Methods = ["GET"] },
Response = new ResponseModel { Body = "re-enabled", StatusCode = 200 },
IsDisabled = true
};
var postResult = await api.PostMappingAsync(model, ct);
var guid = postResult.Guid!.Value;
// Assert — no match while disabled
var before = await httpClient.GetAsync("/baz", ct);
((int)before.StatusCode).Should().Be(404);
// Act — enable
var enableResult = await api.EnableMappingAsync(guid, ct);
enableResult.Status.Should().Be("Mapping enabled");
// Assert — mapping matches after enable
var after = await httpClient.GetAsync("/baz", ct);
((int)after.StatusCode).Should().Be(200);
}
[Fact]
public async Task IWireMockAdminApi_GetMappingAsync_ReturnsIsDisabledTrue_WhenDisabled()
{
// Arrange
var ct = TestContext.Current.CancellationToken;
using var server = WireMockServer.StartWithAdminInterface();
var api = RestClient.For<IWireMockAdminApi>(server.Urls[0]);
var disabledModel = new MappingModel
{
Request = new RequestModel { Path = "/check-disabled" },
Response = new ResponseModel { Body = "x", StatusCode = 200 },
IsDisabled = true
};
var enabledModel = new MappingModel
{
Request = new RequestModel { Path = "/check-enabled" },
Response = new ResponseModel { Body = "y", StatusCode = 200 }
};
var disabledPost = await api.PostMappingAsync(disabledModel, ct);
var enabledPost = await api.PostMappingAsync(enabledModel, ct);
// Act
var disabledGot = await api.GetMappingAsync(disabledPost.Guid!.Value, ct);
var enabledGot = await api.GetMappingAsync(enabledPost.Guid!.Value, ct);
// Assert — disabled mapping serializes IsDisabled = true
disabledGot.IsDisabled.Should().BeTrue();
// Assert — enabled mapping omits IsDisabled (null = default not disabled)
enabledGot.IsDisabled.Should().BeNull();
}
}

View File

@@ -6,5 +6,5 @@ internal static class Constants
{ {
internal const int NumStaticMappings = 10; internal const int NumStaticMappings = 10;
internal const int NumAdminMappings = 37; internal const int NumAdminMappings = 39;
} }

View File

@@ -0,0 +1,369 @@
// Copyright © WireMock.Net
using System.Text.Json;
using WireMock.Matchers;
namespace WireMock.Net.Tests.Matchers;
public class SystemTextJsonMatcherTests
{
public enum NormalEnumStj
{
Abc
}
public class Test1Stj
{
public NormalEnumStj NormalEnum { get; set; }
}
[Fact]
public void SystemTextJsonMatcher_GetName()
{
// Assign
var matcher = new SystemTextJsonMatcher("{}");
// Act
var name = matcher.Name;
// Assert
name.Should().Be("SystemTextJsonMatcher");
}
[Fact]
public void SystemTextJsonMatcher_GetValue()
{
// Assign
var matcher = new SystemTextJsonMatcher("{}");
// Act
var value = matcher.Value;
// Assert
value.Should().Be("{}");
}
[Fact]
public void SystemTextJsonMatcher_WithInvalidStringValue_Should_ThrowException()
{
// Act
Action action = () => new SystemTextJsonMatcher(MatchBehaviour.AcceptOnMatch, "{ \"Id\"");
// Assert
action.Should().Throw<JsonException>();
}
[Fact]
public void SystemTextJsonMatcher_WithInvalidObjectValue_Should_ThrowException()
{
// Act
Action action = () => new SystemTextJsonMatcher(MatchBehaviour.AcceptOnMatch, new MemoryStream());
// Assert
action.Should().Throw<Exception>();
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_WithInvalidValue_Should_ReturnMismatch_And_Exception_ShouldBeSet()
{
// Assign
var matcher = new SystemTextJsonMatcher("{}");
// Act
var result = matcher.IsMatch(new MemoryStream());
// Assert
result.Score.Should().Be(MatchScores.Mismatch);
result.Exception.Should().NotBeNull();
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_ByteArray()
{
// Assign
var bytes = new byte[0];
var matcher = new SystemTextJsonMatcher("{}");
// Act
var match = matcher.IsMatch(bytes).Score;
// Assert
match.Should().Be(0);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_NullString()
{
// Assign
string? s = null;
var matcher = new SystemTextJsonMatcher("{}");
// Act
var match = matcher.IsMatch(s).Score;
// Assert
match.Should().Be(0);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_NullObject()
{
// Assign
object? o = null;
var matcher = new SystemTextJsonMatcher("{}");
// Act
var match = matcher.IsMatch(o).Score;
// Assert
match.Should().Be(0);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_JsonArrayAsString()
{
// Assign
var matcher = new SystemTextJsonMatcher("[ \"x\", \"y\" ]");
// Act
var jsonElement = JsonDocument.Parse("[ \"x\", \"y\" ]").RootElement;
var match = matcher.IsMatch(jsonElement).Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_JsonObjectAsString_ShouldMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher("{ \"Id\" : 1, \"Name\" : \"Test\" }");
// Act
var jsonElement = JsonDocument.Parse("{ \"Id\" : 1, \"Name\" : \"Test\" }").RootElement;
var match = matcher.IsMatch(jsonElement).Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_AnonymousObject_ShouldMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher(new { Id = 1, Name = "Test" });
// Act
var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_AnonymousObject_ShouldNotMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher(new { Id = 1, Name = "Test" });
// Act
var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\", \"Other\" : \"abc\" }").Score;
// Assert
Assert.Equal(MatchScores.Mismatch, match);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_WithIgnoreCaseTrue_JsonObject()
{
// Assign
var matcher = new SystemTextJsonMatcher(new { id = 1, Name = "test" }, true);
// Act
var match = matcher.IsMatch("{ \"Id\" : 1, \"NaMe\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_WithIgnoreCaseTrue_JsonObjectParsed()
{
// Assign
var matcher = new SystemTextJsonMatcher(new { Id = 1, Name = "TESt" }, true);
// Act
var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_JsonObjectAsString_RejectOnMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher(MatchBehaviour.RejectOnMatch, "{ \"Id\" : 1, \"Name\" : \"Test\" }");
// Act
var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(0.0, match);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_JsonObjectWithDateTimeOffsetAsString()
{
// Assign
var matcher = new SystemTextJsonMatcher("{ \"preferredAt\" : \"2019-11-21T10:32:53.2210009+00:00\" }");
// Act
var match = matcher.IsMatch("{ \"preferredAt\" : \"2019-11-21T10:32:53.2210009+00:00\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_NormalEnum()
{
// Assign
var matcher = new SystemTextJsonMatcher(new Test1Stj { NormalEnum = NormalEnumStj.Abc });
// Act
var match = matcher.IsMatch("{ \"NormalEnum\" : 0 }").Score;
// Assert
match.Should().Be(1.0);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_WithRegexTrue_ShouldMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher(new { Id = "^\\d+$", Name = "Test" }, regex: true);
// Act
var match = matcher.IsMatch("{ \"Id\" : \"42\", \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_WithRegexTrue_Complex_ShouldMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher(new
{
Complex = new
{
Id = "^\\d+$",
Name = ".*"
}
}, regex: true);
// Act
var match = matcher.IsMatch("{ \"Complex\" : { \"Id\" : \"42\", \"Name\" : \"Test\" } }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_WithRegexTrue_Complex_ShouldNotMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher(new
{
Complex = new
{
Id = "^\\d+$",
Name = ".*"
}
}, regex: true);
// Act
var match = matcher.IsMatch("{ \"Complex\" : { \"Id\" : \"42\", \"Name\" : \"Test\", \"Other\" : \"Other\" } }").Score;
// Assert
Assert.Equal(MatchScores.Mismatch, match);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_WithRegexTrue_Array_ShouldMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher(new
{
Array = new[] { "^\\d+$", ".*" }
}, regex: true);
// Act
var match = matcher.IsMatch("{ \"Array\" : [ \"42\", \"test\" ] }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_WithRegexTrue_Array_ShouldNotMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher(new
{
Array = new[] { "^\\d+$", ".*" }
}, regex: true);
// Act
var match = matcher.IsMatch("{ \"Array\" : [ \"42\", \"test\", \"other\" ] }").Score;
// Assert
Assert.Equal(MatchScores.Mismatch, match);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_GuidAndString()
{
// Assign
var id = Guid.NewGuid();
var idAsString = id.ToString();
var matcher = new SystemTextJsonMatcher(new { Id = id });
// Act
var match = matcher.IsMatch($"{{ \"Id\" : \"{idAsString}\" }}").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_StringAndGuid()
{
// Assign
var id = Guid.NewGuid();
var idAsString = id.ToString();
var matcher = new SystemTextJsonMatcher(new { Id = idAsString });
// Act
var match = matcher.IsMatch($"{{ \"Id\" : \"{id}\" }}").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonMatcher_IsMatch_JsonElement_ShouldMatch()
{
// Assign
var matcher = new SystemTextJsonMatcher(new { Id = 1, Name = "Test" });
// Act
var jsonElement = JsonDocument.Parse("{ \"Id\" : 1, \"Name\" : \"Test\" }").RootElement;
var match = matcher.IsMatch(jsonElement).Score;
// Assert
Assert.Equal(1.0, match);
}
}

View File

@@ -0,0 +1,410 @@
// Copyright © WireMock.Net
using System.Text.Json;
using WireMock.Matchers;
namespace WireMock.Net.Tests.Matchers;
public class SystemTextJsonPartialMatcherTests
{
[Fact]
public void SystemTextJsonPartialMatcher_GetName()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher("{}");
// Act
string name = matcher.Name;
// Assert
name.Should().Be("SystemTextJsonPartialMatcher");
}
[Fact]
public void SystemTextJsonPartialMatcher_GetValue()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher("{}");
// Act
object value = matcher.Value;
// Assert
value.Should().Be("{}");
}
[Fact]
public void SystemTextJsonPartialMatcher_WithInvalidStringValue_Should_ThrowException()
{
// Act
Action action = () => new SystemTextJsonPartialMatcher(MatchBehaviour.AcceptOnMatch, "{ \"Id\"");
// Assert
action.Should().Throw<JsonException>();
}
[Fact]
public void SystemTextJsonPartialMatcher_WithInvalidObjectValue_Should_ThrowException()
{
// Act
Action action = () => new SystemTextJsonPartialMatcher(MatchBehaviour.AcceptOnMatch, new MemoryStream());
// Assert
action.Should().Throw<Exception>();
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_WithInvalidValue_Should_ReturnMismatch_And_Exception_ShouldBeSet()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher("{}");
// Act
var result = matcher.IsMatch(new MemoryStream());
// Assert
result.Score.Should().Be(MatchScores.Mismatch);
result.Exception.Should().NotBeNull();
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_ByteArray()
{
// Assign
var bytes = new byte[0];
var matcher = new SystemTextJsonPartialMatcher("{}");
// Act
double match = matcher.IsMatch(bytes).Score;
// Assert
match.Should().Be(0);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_NullString()
{
// Assign
string? s = null;
var matcher = new SystemTextJsonPartialMatcher("{}");
// Act
double match = matcher.IsMatch(s).Score;
// Assert
match.Should().Be(0);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_NullObject()
{
// Assign
object? o = null;
var matcher = new SystemTextJsonPartialMatcher("{}");
// Act
double match = matcher.IsMatch(o).Score;
// Assert
match.Should().Be(0);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_JsonArray()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher(new[] { "x", "y" });
// Act
double match = matcher.IsMatch("[ \"x\", \"y\" ]").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_JsonObject()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher(new { Id = 1, Name = "Test" });
// Act
double match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_WithRegexTrue()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher(new { Id = "^\\d+$", Name = "Test" }, false, true);
// Act
double match = matcher.IsMatch("{ \"Id\" : \"1\", \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_WithRegexFalse()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher(new { Id = "^\\d+$", Name = "Test" });
// Act
double match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(0.0, match);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_GuidAsString_UsingRegex()
{
var guid = new Guid("1111238e-b775-44a9-a263-95e570135c94");
var matcher = new SystemTextJsonPartialMatcher(new
{
Id = 1,
Name = "^1111[a-fA-F0-9]{4}(-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$"
}, false, true);
// Act
double match = matcher.IsMatch($"{{ \"Id\" : 1, \"Name\" : \"{guid}\" }}").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_WithIgnoreCaseTrue_JsonObject()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher(new { id = 1, Name = "test" }, true);
// Act
double match = matcher.IsMatch("{ \"Id\" : 1, \"NaMe\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_JsonObjectParsed()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher(new { Id = 1, Name = "Test" });
// Act
double match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_WithIgnoreCaseTrue_JsonObjectParsed()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher(new { Id = 1, Name = "TESt" }, true);
// Act
double match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_JsonArrayAsString()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher("[ \"x\", \"y\" ]");
// Act
double match = matcher.IsMatch("[ \"x\", \"y\" ]").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_JsonObjectAsString()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher("{ \"Id\" : 1, \"Name\" : \"Test\" }");
// Act
double match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_JsonObjectAsStringWithDottedPropertyName()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher("{ \"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\" : \"Test\" }");
// Act
double match = matcher.IsMatch("{ \"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_GuidAsString()
{
// Assign
var guid = Guid.NewGuid();
var matcher = new SystemTextJsonPartialMatcher(new { Id = 1, Name = guid });
// Act
double match = matcher.IsMatch($"{{ \"Id\" : 1, \"Name\" : \"{guid}\" }}").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_WithIgnoreCaseTrue_JsonObjectAsString()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher("{ \"Id\" : 1, \"Name\" : \"test\" }", true);
// Act
double match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_JsonObjectAsString_RejectOnMatch()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher(MatchBehaviour.RejectOnMatch, "{ \"Id\" : 1, \"Name\" : \"Test\" }");
// Act
double match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(0.0, match);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_JsonObjectWithDateTimeOffsetAsString()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher("{ \"preferredAt\" : \"2019-11-21T10:32:53.2210009+00:00\" }");
// Act
double match = matcher.IsMatch("{ \"preferredAt\" : \"2019-11-21T10:32:53.2210009+00:00\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Theory]
[InlineData("{\"test\":\"abc\"}", "{\"test\":\"abc\",\"other\":\"xyz\"}")]
[InlineData("\"test\"", "\"test\"")]
[InlineData("123", "123")]
[InlineData("[\"test\"]", "[\"test\"]")]
[InlineData("[\"test\"]", "[\"test\", \"other\"]")]
[InlineData("[123]", "[123]")]
[InlineData("[123]", "[123, 456]")]
[InlineData("{ \"test\":\"value\" }", "{\"test\":\"value\",\"other\":123}")]
[InlineData("{ \"test\":\"value\" }", "{\"test\":\"value\"}")]
[InlineData("{\"test\":{\"nested\":\"value\"}}", "{\"test\":{\"nested\":\"value\"}}")]
public void SystemTextJsonPartialMatcher_IsMatch_StringInputValidMatch(string value, string input)
{
// Assign
var matcher = new SystemTextJsonPartialMatcher(value);
// Act
double match = matcher.IsMatch(input).Score;
// Assert
Assert.Equal(1.0, match);
}
[Theory]
[InlineData("\"test\"", null)]
[InlineData("\"test1\"", "\"test2\"")]
[InlineData("123", "1234")]
[InlineData("[\"test\"]", "[\"test1\"]")]
[InlineData("[\"test\"]", "[\"test1\", \"test2\"]")]
[InlineData("[123]", "[1234]")]
[InlineData("{}", "\"test\"")]
[InlineData("{ \"test\":\"value\" }", "{\"test\":\"value2\"}")]
[InlineData("{ \"test.nested\":\"value\" }", "{\"test\":{\"nested\":\"value1\"}}")]
[InlineData("{\"test\":{\"test1\":\"value\"}}", "{\"test\":{\"test1\":\"value1\"}}")]
[InlineData("[{ \"test.nested\":\"value\" }]", "[{\"test\":{\"nested\":\"value1\"}}]")]
public void SystemTextJsonPartialMatcher_IsMatch_StringInputWithInvalidMatch(string value, string? input)
{
// Assign
var matcher = new SystemTextJsonPartialMatcher(value);
// Act
double match = matcher.IsMatch(input).Score;
// Assert
Assert.Equal(0.0, match);
}
[Theory]
[InlineData("{ \"test.nested\":123 }", "{\"test\":{\"nested\":123}}")]
[InlineData("{ \"test.nested\":[123, 456] }", "{\"test\":{\"nested\":[123, 456]}}")]
[InlineData("{ \"test.nested\":\"value\" }", "{\"test\":{\"nested\":\"value\"}}")]
[InlineData("{ \"['name.with.dot']\":\"value\" }", "{\"name.with.dot\":\"value\"}")]
[InlineData("[{ \"test.nested\":\"value\" }]", "[{\"test\":{\"nested\":\"value\"}}]")]
[InlineData("[{ \"['name.with.dot']\":\"value\" }]", "[{\"name.with.dot\":\"value\"}]")]
public void SystemTextJsonPartialMatcher_IsMatch_ValueAsPathValidMatch(string value, string input)
{
// Assign
var matcher = new SystemTextJsonPartialMatcher(value);
// Act
double match = matcher.IsMatch(input).Score;
// Assert
Assert.Equal(1.0, match);
}
[Theory]
[InlineData("{ \"test.nested\":123 }", "{\"test\":{\"nested\":456}}")]
[InlineData("{ \"test.nested\":[123, 456] }", "{\"test\":{\"nested\":[1, 2]}}")]
[InlineData("{ \"test.nested\":\"value\" }", "{\"test\":{\"nested\":\"value1\"}}")]
[InlineData("{ \"['name.with.dot']\":\"value\" }", "{\"name.with.dot\":\"value1\"}")]
[InlineData("[{ \"test.nested\":\"value\" }]", "[{\"test\":{\"nested\":\"value1\"}}]")]
[InlineData("[{ \"['name.with.dot']\":\"value\" }]", "[{\"name.with.dot\":\"value1\"}]")]
public void SystemTextJsonPartialMatcher_IsMatch_ValueAsPathInvalidMatch(string value, string input)
{
// Assign
var matcher = new SystemTextJsonPartialMatcher(value);
// Act
double match = matcher.IsMatch(input).Score;
// Assert
Assert.Equal(0.0, match);
}
[Fact]
public void SystemTextJsonPartialMatcher_IsMatch_JsonElement_ShouldMatch()
{
// Assign
var matcher = new SystemTextJsonPartialMatcher(new { Id = 1, Name = "Test" });
// Act
var jsonElement = JsonDocument.Parse("{ \"Id\" : 1, \"Name\" : \"Test\", \"Extra\" : \"value\" }").RootElement;
double match = matcher.IsMatch(jsonElement).Score;
// Assert
Assert.Equal(1.0, match);
}
}

View File

@@ -0,0 +1,381 @@
// Copyright © WireMock.Net
using System.Text.Json;
using WireMock.Matchers;
namespace WireMock.Net.Tests.Matchers;
public class SystemTextJsonPartialWildcardMatcherTests
{
[Fact]
public void SystemTextJsonPartialWildcardMatcher_GetName()
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher("{}");
// Act
var name = matcher.Name;
// Assert
name.Should().Be("SystemTextJsonPartialWildcardMatcher");
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_GetValue()
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher("{}");
// Act
var value = matcher.Value;
// Assert
value.Should().Be("{}");
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_WithInvalidStringValue_Should_ThrowException()
{
// Act
Action action = () => new SystemTextJsonPartialWildcardMatcher(MatchBehaviour.AcceptOnMatch, "{ \"Id\"");
// Assert
action.Should().Throw<JsonException>();
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_WithInvalidObjectValue_Should_ThrowException()
{
// Act
Action action = () => new SystemTextJsonPartialWildcardMatcher(MatchBehaviour.AcceptOnMatch, new MemoryStream());
// Assert
action.Should().Throw<Exception>();
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_WithInvalidValue_Should_ReturnMismatch_And_Exception_ShouldBeSet()
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher("{}");
// Act
var result = matcher.IsMatch(new MemoryStream());
// Assert
result.Score.Should().Be(MatchScores.Mismatch);
result.Exception.Should().NotBeNull();
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_ByteArray()
{
// Assign
var bytes = new byte[0];
var matcher = new SystemTextJsonPartialWildcardMatcher("{}");
// Act
var match = matcher.IsMatch(bytes).Score;
// Assert
match.Should().Be(0);
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_NullString()
{
// Assign
string? s = null;
var matcher = new SystemTextJsonPartialWildcardMatcher("{}");
// Act
var match = matcher.IsMatch(s).Score;
// Assert
match.Should().Be(0);
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_NullObject()
{
// Assign
object? o = null;
var matcher = new SystemTextJsonPartialWildcardMatcher("{}");
// Act
var match = matcher.IsMatch(o).Score;
// Assert
match.Should().Be(0);
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_JsonArray()
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher(new[] { "x", "y" });
// Act
var match = matcher.IsMatch("[ \"x\", \"y\" ]").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_JsonObject()
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher(new { Id = 1, Name = "Test" });
// Act
var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_WithIgnoreCaseTrue_JsonObject()
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher(new { id = 1, Name = "test" }, true);
// Act
var match = matcher.IsMatch("{ \"Id\" : 1, \"NaMe\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_JsonObjectParsed()
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher(new { Id = 1, Name = "Test" });
// Act
var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_WithIgnoreCaseTrue_JsonObjectParsed()
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher(new { Id = 1, Name = "TESt" }, true);
// Act
var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_JsonArrayAsString()
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher("[ \"x\", \"y\" ]");
// Act
var match = matcher.IsMatch("[ \"x\", \"y\" ]").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_JsonObjectAsString()
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher("{ \"Id\" : 1, \"Name\" : \"Test\" }");
// Act
var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_WithIgnoreCaseTrue_JsonObjectAsString()
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher("{ \"Id\" : 1, \"Name\" : \"test\" }", true);
// Act
var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_JsonObjectAsString_RejectOnMatch()
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher(MatchBehaviour.RejectOnMatch, "{ \"Id\" : 1, \"Name\" : \"Test\" }");
// Act
var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score;
// Assert
Assert.Equal(0.0, match);
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_JsonObjectWithDateTimeOffsetAsString()
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher("{ \"preferredAt\" : \"2019-11-21T10:32:53.2210009+00:00\" }");
// Act
var match = matcher.IsMatch("{ \"preferredAt\" : \"2019-11-21T10:32:53.2210009+00:00\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Theory]
[InlineData("{\"test\":\"abc\"}", "{\"test\":\"abc\",\"other\":\"xyz\"}")]
[InlineData("\"test\"", "\"test\"")]
[InlineData("123", "123")]
[InlineData("[\"test\"]", "[\"test\"]")]
[InlineData("[\"test\"]", "[\"test\", \"other\"]")]
[InlineData("[123]", "[123]")]
[InlineData("[123]", "[123, 456]")]
[InlineData("{ \"test\":\"value\" }", "{\"test\":\"value\",\"other\":123}")]
[InlineData("{ \"test\":\"value\" }", "{\"test\":\"value\"}")]
[InlineData("{\"test\":{\"nested\":\"value\"}}", "{\"test\":{\"nested\":\"value\"}}")]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_StringInput_IsValidMatch(string value, string input)
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher(value);
// Act
var match = matcher.IsMatch(input).Score;
// Assert
Assert.Equal(1.0, match);
}
[Theory]
[InlineData("{\"test\":\"*\"}", "{\"test\":\"xxx\",\"other\":\"xyz\"}")]
[InlineData("\"t*t\"", "\"test\"")]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_StringInputWithWildcard_IsValidMatch(string value, string input)
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher(value);
// Act
var match = matcher.IsMatch(input).Score;
// Assert
match.Should().Be(1.0);
}
[Theory]
[InlineData("\"test\"", null)]
[InlineData("\"test1\"", "\"test2\"")]
[InlineData("123", "1234")]
[InlineData("[\"test\"]", "[\"test1\"]")]
[InlineData("[\"test\"]", "[\"test1\", \"test2\"]")]
[InlineData("[123]", "[1234]")]
[InlineData("{}", "\"test\"")]
[InlineData("{ \"test\":\"value\" }", "{\"test\":\"value2\"}")]
[InlineData("{ \"test.nested\":\"value\" }", "{\"test\":{\"nested\":\"value1\"}}")]
[InlineData("{\"test\":{\"test1\":\"value\"}}", "{\"test\":{\"test1\":\"value1\"}}")]
[InlineData("[{ \"test.nested\":\"value\" }]", "[{\"test\":{\"nested\":\"value1\"}}]")]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_StringInputWithInvalidMatch(string value, string? input)
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher(value);
// Act
var match = matcher.IsMatch(input).Score;
// Assert
Assert.Equal(0.0, match);
}
[Theory]
[InlineData("{ \"test.nested\":123 }", "{\"test\":{\"nested\":123}}")]
[InlineData("{ \"test.nested\":[123, 456] }", "{\"test\":{\"nested\":[123, 456]}}")]
[InlineData("{ \"test.nested\":\"value\" }", "{\"test\":{\"nested\":\"value\"}}")]
[InlineData("{ \"['name.with.dot']\":\"value\" }", "{\"name.with.dot\":\"value\"}")]
[InlineData("[{ \"test.nested\":\"value\" }]", "[{\"test\":{\"nested\":\"value\"}}]")]
[InlineData("[{ \"['name.with.dot']\":\"value\" }]", "[{\"name.with.dot\":\"value\"}]")]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_ValueAsPathValidMatch(string value, string input)
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher(value);
// Act
var match = matcher.IsMatch(input).Score;
// Assert
Assert.Equal(1.0, match);
}
[Theory]
[InlineData("{ \"test.nested\":123 }", "{\"test\":{\"nested\":456}}")]
[InlineData("{ \"test.nested\":[123, 456] }", "{\"test\":{\"nested\":[1, 2]}}")]
[InlineData("{ \"test.nested\":\"value\" }", "{\"test\":{\"nested\":\"value1\"}}")]
[InlineData("{ \"['name.with.dot']\":\"value\" }", "{\"name.with.dot\":\"value1\"}")]
[InlineData("[{ \"test.nested\":\"value\" }]", "[{\"test\":{\"nested\":\"value1\"}}]")]
[InlineData("[{ \"['name.with.dot']\":\"value\" }]", "[{\"name.with.dot\":\"value1\"}]")]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_ValueAsPathInvalidMatch(string value, string input)
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher(value);
// Act
var match = matcher.IsMatch(input).Score;
// Assert
Assert.Equal(0.0, match);
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_WithIgnoreCaseTrueAndRegexTrue_JsonObject1()
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher(new { id = 1, Number = "^\\d+$" }, ignoreCase: true, regex: true);
// Act
var match = matcher.IsMatch("{ \"Id\" : 1, \"Number\" : \"42\" }").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_WithIgnoreCaseTrueAndRegexTrue_JsonObject2()
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher(new { method = "initialize", id = "^[a-f0-9]{32}-[0-9]$" }, ignoreCase: true, regex: true);
// Act
var match = matcher.IsMatch("{\"jsonrpc\":\"2.0\",\"id\":\"ec475f56d4694b48bc737500ba575b35-1\",\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"GitHub Test\",\"version\":\"1.0.0\"}}}").Score;
// Assert
Assert.Equal(1.0, match);
}
[Fact]
public void SystemTextJsonPartialWildcardMatcher_IsMatch_JsonElement_ShouldMatch()
{
// Assign
var matcher = new SystemTextJsonPartialWildcardMatcher(new { Id = 1, Name = "Test" });
// Act
var jsonElement = JsonDocument.Parse("{ \"Id\" : 1, \"Name\" : \"Test\", \"Extra\" : \"value\" }").RootElement;
var match = matcher.IsMatch(jsonElement).Score;
// Assert
Assert.Equal(1.0, match);
}
}

View File

@@ -0,0 +1,400 @@
// Copyright © WireMock.Net
using System.Text.Json.Nodes;
using WireMock.Matchers;
namespace WireMock.Net.Tests.Matchers;
public class SystemTextJsonPathMatcherTests
{
[Fact]
public void SystemTextJsonPathMatcher_GetName()
{
// Arrange
var matcher = new SystemTextJsonPathMatcher("X");
// Act
string name = matcher.Name;
// Assert
name.Should().Be("SystemTextJsonPathMatcher");
}
[Fact]
public void SystemTextJsonPathMatcher_GetPatterns()
{
// Arrange
var matcher = new SystemTextJsonPathMatcher("X");
// Act
var patterns = matcher.GetPatterns();
// Assert
patterns.Should().ContainSingle("X");
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_ByteArray()
{
// Arrange
var bytes = new byte[0];
var matcher = new SystemTextJsonPathMatcher("$.Id");
// Act
double match = matcher.IsMatch(bytes).Score;
// Assert
match.Should().Be(0);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_NullString()
{
// Arrange
var matcher = new SystemTextJsonPathMatcher("$.Id");
// Act
double match = matcher.IsMatch(null).Score;
// Assert
match.Should().Be(0);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_EmptyString()
{
// Arrange
var matcher = new SystemTextJsonPathMatcher("$.Id");
// Act
double match = matcher.IsMatch(string.Empty).Score;
// Assert
match.Should().Be(0);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_NullObject()
{
// Arrange
object? o = null;
var matcher = new SystemTextJsonPathMatcher("$.Id");
// Act
double match = matcher.IsMatch(o).Score;
// Assert
match.Should().Be(0);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_String_Exception_Mismatch()
{
// Arrange
var matcher = new SystemTextJsonPathMatcher("$.Id");
// Act
double match = matcher.IsMatch("not-json").Score;
// Assert
match.Should().Be(0);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_AnonymousObject()
{
// Arrange - RFC 9535: filter expression requires an array context
var matcher = new SystemTextJsonPathMatcher("$[?(@.Id == 1)]");
// Act
double match = matcher.IsMatch(new[] { new { Id = 1, Name = "Test" } }).Score;
// Assert
match.Should().Be(1);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_AnonymousObject_WithNestedObject()
{
// Arrange
var matcher = new SystemTextJsonPathMatcher("$.things[?(@.name == 'x')]");
// Act
double match = matcher.IsMatch(new { things = new { name = "x" } }).Score;
// Assert
match.Should().Be(1);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_String_WithNestedObject()
{
// Arrange
var json = "{ \"things\": { \"name\": \"x\" } }";
var matcher = new SystemTextJsonPathMatcher("$.things[?(@.name == 'x')]");
// Act
double match = matcher.IsMatch(json).Score;
// Assert
match.Should().Be(1);
}
[Fact]
public void SystemTextJsonPathMatcher_IsNoMatch_String_WithNestedObject()
{
// Arrange
var json = "{ \"things\": { \"name\": \"y\" } }";
var matcher = new SystemTextJsonPathMatcher("$.things[?(@.name == 'x')]");
// Act
double match = matcher.IsMatch(json).Score;
// Assert
match.Should().Be(0);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_JsonNode()
{
// Arrange - RFC 9535: filter expression requires an array context
string[] patterns = { "$[?(@.Id == 1)]" };
var matcher = new SystemTextJsonPathMatcher(patterns);
// Act
var node = JsonNode.Parse("[{\"Id\":1,\"Name\":\"Test\"}]");
double match = matcher.IsMatch(node).Score;
// Assert
match.Should().Be(1);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_JsonNode_Parsed()
{
// Arrange - RFC 9535: filter expression requires an array context
var matcher = new SystemTextJsonPathMatcher("$[?(@.Id == 1)]");
// Act
double match = matcher.IsMatch(JsonNode.Parse("[{\"Id\":1,\"Name\":\"Test\"}]")).Score;
// Assert
match.Should().Be(1);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_RejectOnMatch()
{
// Arrange - RFC 9535: filter expression requires an array context
var matcher = new SystemTextJsonPathMatcher(MatchBehaviour.RejectOnMatch, MatchOperator.Or, "$[?(@.Id == 1)]");
// Act
double match = matcher.IsMatch(JsonNode.Parse("[{\"Id\":1,\"Name\":\"Test\"}]")).Score;
// Assert
match.Should().Be(0.0);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_ArrayOneLevel()
{
// Arrange
var matcher = new SystemTextJsonPathMatcher("$.arr[0].line1");
// Act
double match = matcher.IsMatch(JsonNode.Parse(@"{
""name"": ""PathSelectorTest"",
""test"": ""test"",
""test2"": ""test2"",
""arr"": [{
""line1"": ""line1""
}]
}")).Score;
// Assert
match.Should().Be(1.0);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_ObjectMatch()
{
// Arrange
var matcher = new SystemTextJsonPathMatcher("$.test");
// Act
double match = matcher.IsMatch(JsonNode.Parse(@"{
""name"": ""PathSelectorTest"",
""test"": ""test"",
""test2"": ""test2"",
""arr"": [
{
""line1"": ""line1""
}
]
}")).Score;
// Assert
match.Should().Be(1.0);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_DoesntMatch()
{
// Arrange
var matcher = new SystemTextJsonPathMatcher("$.test3");
// Act
double match = matcher.IsMatch(JsonNode.Parse(@"{
""name"": ""PathSelectorTest"",
""test"": ""test"",
""test2"": ""test2"",
""arr"": [
{
""line1"": ""line1""
}
]
}")).Score;
// Assert
match.Should().Be(0.0);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_DoesntMatchInArray()
{
// Arrange
var matcher = new SystemTextJsonPathMatcher("$arr[0].line1");
// Act
double match = matcher.IsMatch(JsonNode.Parse(@"{
""name"": ""PathSelectorTest"",
""test"": ""test"",
""test2"": ""test2"",
""arr"": []
}")).Score;
// Assert
match.Should().Be(0.0);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_DoesntMatchNoObjectsInArray()
{
// Arrange
var matcher = new SystemTextJsonPathMatcher("$arr[2].line1");
// Act
double match = matcher.IsMatch(JsonNode.Parse(@"{
""name"": ""PathSelectorTest"",
""test"": ""test"",
""test2"": ""test2"",
""arr"": []
}")).Score;
// Assert
match.Should().Be(0.0);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_NestedArrays()
{
// Arrange
var matcher = new SystemTextJsonPathMatcher("$.arr[0].sub[0].subline1");
// Act
double match = matcher.IsMatch(JsonNode.Parse(@"{
""name"": ""PathSelectorTest"",
""test"": ""test"",
""test2"": ""test2"",
""arr"": [{
""line1"": ""line1"",
""sub"":[
{
""subline1"":""subline1""
}]
}]
}")).Score;
// Assert
match.Should().Be(1.0);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_MultiplePatternsUsingMatchOperatorAnd()
{
// Arrange
var matcher = new SystemTextJsonPathMatcher(MatchBehaviour.AcceptOnMatch, MatchOperator.And, "$.arr[0].sub[0].subline1", "$.arr[0].line2");
// Act
double match = matcher.IsMatch(JsonNode.Parse(@"{
""name"": ""PathSelectorTest"",
""test"": ""test"",
""test2"": ""test2"",
""arr"": [{
""line1"": ""line1"",
""sub"":[
{
""subline1"":""subline1""
}]
}]
}")).Score;
// Assert
match.Should().Be(0);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_MultiplePatternsUsingMatchOperatorOr()
{
// Arrange
var matcher = new SystemTextJsonPathMatcher(MatchBehaviour.AcceptOnMatch, MatchOperator.Or, "$.arr[0].sub[0].subline2", "$.arr[0].line1");
// Act
double match = matcher.IsMatch(JsonNode.Parse(@"{
""name"": ""PathSelectorTest"",
""test"": ""test"",
""test2"": ""test2"",
""arr"": [{
""line1"": ""line1"",
""sub"":[
{
""subline1"":""subline1""
}]
}]
}")).Score;
// Assert
match.Should().Be(1);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_String_ArrayOneLevel()
{
// Arrange
var matcher = new SystemTextJsonPathMatcher("$.arr[0].line1");
// Act
double match = matcher.IsMatch(@"{
""name"": ""PathSelectorTest"",
""arr"": [{
""line1"": ""line1""
}]
}").Score;
// Assert
match.Should().Be(1.0);
}
[Fact]
public void SystemTextJsonPathMatcher_IsMatch_String_DoesntMatch()
{
// Arrange
var matcher = new SystemTextJsonPathMatcher("$.test3");
// Act
double match = matcher.IsMatch(@"{ ""test"": ""test"" }").Score;
// Assert
match.Should().Be(0.0);
}
}

View File

@@ -9,6 +9,8 @@ using WireMock.Util;
using WireMock.Owin; using WireMock.Owin;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using JsonConverter.Newtonsoft.Json;
using JsonConverter.System.Text.Json;
namespace WireMock.Net.Tests.Owin.Mappers; namespace WireMock.Net.Tests.Owin.Mappers;
@@ -34,6 +36,7 @@ public class OwinResponseMapperTests
_optionsMock = new Mock<IWireMockMiddlewareOptions>(); _optionsMock = new Mock<IWireMockMiddlewareOptions>();
_optionsMock.SetupAllProperties(); _optionsMock.SetupAllProperties();
_optionsMock.SetupGet(o => o.FileSystemHandler).Returns(_fileSystemHandlerMock.Object); _optionsMock.SetupGet(o => o.FileSystemHandler).Returns(_fileSystemHandlerMock.Object);
_optionsMock.SetupGet(o => o.DefaultJsonSerializer).Returns(new NewtonsoftJsonConverter());
_headers = new Mock<IHeaderDictionary>(); _headers = new Mock<IHeaderDictionary>();
_headers.SetupAllProperties(); _headers.SetupAllProperties();
@@ -186,7 +189,7 @@ public class OwinResponseMapperTests
} }
[Fact] [Fact]
public async Task OwinResponseMapper_MapAsync_BodyAsJson() public async Task OwinResponseMapper_MapAsync_BodyAsJson_UsingNewtonsoftJson()
{ {
// Arrange // Arrange
var json = new { t = "x", i = (string?)null }; var json = new { t = "x", i = (string?)null };
@@ -203,6 +206,25 @@ public class OwinResponseMapperTests
_stream.Verify(s => s.WriteAsync(new byte[] { 123, 34, 116, 34, 58, 34, 120, 34, 125 }, 0, 9, It.IsAny<CancellationToken>()), Times.Once); _stream.Verify(s => s.WriteAsync(new byte[] { 123, 34, 116, 34, 58, 34, 120, 34, 125 }, 0, 9, It.IsAny<CancellationToken>()), Times.Once);
} }
[Fact]
public async Task OwinResponseMapper_MapAsync_BodyAsJson_UsingSystemTextJson()
{
// Arrange
var json = new { t = "x", i = (string?)null };
var responseMessage = new ResponseMessage
{
Headers = new Dictionary<string, WireMockList<string>>(),
BodyData = new BodyData { DetectedBodyType = BodyType.Json, BodyAsJson = json, BodyAsJsonIndented = false }
};
_optionsMock.SetupGet(o => o.DefaultJsonSerializer).Returns(new SystemTextJsonConverter());
// Act
await _sut.MapAsync(responseMessage, _responseMock.Object);
// Assert
_stream.Verify(s => s.WriteAsync(new byte[] { 123, 34, 116, 34, 58, 34, 120, 34, 125 }, 0, 9, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact] [Fact]
public async Task OwinResponseMapper_MapAsync_SetResponseHeaders() public async Task OwinResponseMapper_MapAsync_SetResponseHeaders()
{ {

View File

@@ -56,6 +56,7 @@ public class MappingMatcherTests
{ {
// Assign // Assign
var mappingMock = new Mock<IMapping>(); var mappingMock = new Mock<IMapping>();
mappingMock.SetupGet(m => m.IsDisabled).Returns(false);
mappingMock.Setup(m => m.GetRequestMatchResult(It.IsAny<RequestMessage>(), It.IsAny<string>())).Throws<Exception>(); mappingMock.Setup(m => m.GetRequestMatchResult(It.IsAny<RequestMessage>(), It.IsAny<string>())).Throws<Exception>();
var mappings = new ConcurrentDictionary<Guid, IMapping>(); var mappings = new ConcurrentDictionary<Guid, IMapping>();
@@ -229,6 +230,35 @@ public class MappingMatcherTests
result.Match!.Mapping.Guid.Should().Be(withProbability); result.Match!.Mapping.Guid.Should().Be(withProbability);
} }
[Fact]
public void MappingMatcher_FindBestMatch_WhenMappingIsDisabled_ShouldReturnNull()
{
// Assign
var guid = Guid.Parse("00000000-0000-0000-0000-000000000001");
var mappingMock = new Mock<IMapping>();
mappingMock.SetupGet(m => m.Guid).Returns(guid);
mappingMock.SetupGet(m => m.IsDisabled).Returns(true);
mappingMock.SetupGet(m => m.Probability).Returns((double?)null);
var matchResult = new RequestMatchResult();
matchResult.AddScore(typeof(object), 1.0, null);
mappingMock.Setup(m => m.GetRequestMatchResult(It.IsAny<RequestMessage>(), It.IsAny<string>())).Returns(matchResult);
var mappings = new ConcurrentDictionary<Guid, IMapping>();
mappings.TryAdd(guid, mappingMock.Object);
_optionsMock.Setup(o => o.Mappings).Returns(mappings);
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1");
// Act
var result = _sut.FindBestMatch(request);
// Assert
result.Match.Should().BeNull();
result.Partial.Should().BeNull();
mappingMock.Verify(m => m.GetRequestMatchResult(It.IsAny<RequestMessage>(), It.IsAny<string>()), Times.Never);
}
private static ConcurrentDictionary<Guid, IMapping> InitMappings(params (Guid guid, double[] scores, double? probability)[] matches) private static ConcurrentDictionary<Guid, IMapping> InitMappings(params (Guid guid, double[] scores, double? probability)[] matches)
{ {
var mappings = new ConcurrentDictionary<Guid, IMapping>(); var mappings = new ConcurrentDictionary<Guid, IMapping>();
@@ -237,6 +267,7 @@ public class MappingMatcherTests
{ {
var mappingMock = new Mock<IMapping>(); var mappingMock = new Mock<IMapping>();
mappingMock.SetupGet(m => m.Guid).Returns(match.guid); mappingMock.SetupGet(m => m.Guid).Returns(match.guid);
mappingMock.SetupGet(m => m.IsDisabled).Returns(false);
var requestMatchResult = new RequestMatchResult(); var requestMatchResult = new RequestMatchResult();
foreach (var score in match.scores) foreach (var score in match.scores)

View File

@@ -11,6 +11,7 @@ using WireMock.Server;
namespace WireMock.Net.Tests.Pact; namespace WireMock.Net.Tests.Pact;
[Collection(nameof(PactTests))]
public class PactTests public class PactTests
{ {
[Fact] [Fact]

View File

@@ -3,7 +3,6 @@
using System.Text; using System.Text;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using WireMock.Matchers; using WireMock.Matchers;
using WireMock.Matchers.Request; using WireMock.Matchers.Request;
using WireMock.Models; using WireMock.Models;
@@ -323,7 +322,7 @@ public class RequestBuilderWithBodyTests
} }
[Fact] [Fact]
public void Request_WithBodyJson_PathMatcher_false() public void Request_WithBodyJson_JsonPathMatcher_false()
{ {
// Arrange // Arrange
var spec = Request.Create().UsingAnyMethod().WithBody(new JsonPathMatcher("$.things[?(@.name == 'RequiredThing')]")); var spec = Request.Create().UsingAnyMethod().WithBody(new JsonPathMatcher("$.things[?(@.name == 'RequiredThing')]"));
@@ -368,10 +367,10 @@ public class RequestBuilderWithBodyTests
public void Request_WithBody_Array_JsonPathMatcher_1() public void Request_WithBody_Array_JsonPathMatcher_1()
{ {
// Arrange // Arrange
var spec = Request.Create().UsingAnyMethod().WithBody(new JsonPathMatcher("$..books[?(@.price < 10)]")); var spec = Request.Create().UsingAnyMethod().WithBody(new JsonPathMatcher("$[?(@.Id == 1)]"));
// Act // Act
string jsonString = "{ \"books\": [ { \"category\": \"test1\", \"price\": 8.95 }, { \"category\": \"test2\", \"price\": 20 } ] }"; string jsonString = "[{\"Id\": 1, \"Name\": \"Test\"}, {\"Id\": 2, \"Name\": \"Test2\"}]";
var bodyData = new BodyData var bodyData = new BodyData
{ {
BodyAsJson = JsonConvert.DeserializeObject(jsonString), BodyAsJson = JsonConvert.DeserializeObject(jsonString),
@@ -391,10 +390,10 @@ public class RequestBuilderWithBodyTests
public void Request_WithBody_Array_JsonPathMatcher_2() public void Request_WithBody_Array_JsonPathMatcher_2()
{ {
// Arrange // Arrange
var spec = Request.Create().UsingAnyMethod().WithBody(new JsonPathMatcher("$..[?(@.Id == 1)]")); var spec = Request.Create().UsingAnyMethod().WithBody(new JsonPathMatcher("$.test"));
// Act // Act
string jsonString = "{ \"Id\": 1, \"Name\": \"Test\" }"; string jsonString = "{\"name\": \"PathSelectorTest\", \"test\": \"test\", \"test2\": \"test2\", \"arr\": [{\"line1\": \"line1\"}]}";
var bodyData = new BodyData var bodyData = new BodyData
{ {
BodyAsJson = JsonConvert.DeserializeObject(jsonString), BodyAsJson = JsonConvert.DeserializeObject(jsonString),
@@ -479,5 +478,132 @@ public class RequestBuilderWithBodyTests
var requestMatchResult = new RequestMatchResult(); var requestMatchResult = new RequestMatchResult();
requestBuilder.GetMatchingScore(request, requestMatchResult).Should().Be(1.0); requestBuilder.GetMatchingScore(request, requestMatchResult).Should().Be(1.0);
} }
[Fact]
public void Request_WithBody_SystemTextJsonPathMatcher_true()
{
// Arrange
var spec = Request.Create().UsingAnyMethod().WithBody(new SystemTextJsonPathMatcher("$..things[?(@.name == 'RequiredThing')]"));
// Act
var body = new BodyData
{
BodyAsString = "{ \"things\": [ { \"name\": \"RequiredThing\" }, { \"name\": \"Wiremock\" } ] }",
DetectedBodyType = BodyType.String
};
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, body);
// Assert
var requestMatchResult = new RequestMatchResult();
spec.GetMatchingScore(request, requestMatchResult).Should().Be(1.0);
}
[Fact]
public void Request_WithBodyJson_SystemTextJsonPathMatcher_false()
{
// Arrange
var spec = Request.Create().UsingAnyMethod().WithBody(new SystemTextJsonPathMatcher("$.things[?(@.name == 'RequiredThing')]"));
// Act
var body = new BodyData
{
BodyAsString = "{ \"things\": { \"name\": \"Wiremock\" } }",
DetectedBodyType = BodyType.String
};
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, body);
// Assert
var requestMatchResult = new RequestMatchResult();
spec.GetMatchingScore(request, requestMatchResult).Should().NotBe(1.0);
}
[Fact]
public void Request_WithBody_Object_SystemTextJsonPathMatcher_true()
{
// Arrange
var spec = Request.Create().UsingAnyMethod().WithBody(new SystemTextJsonPathMatcher("$.arr[0].line1"));
// Act
string jsonString = "{\"name\": \"PathSelectorTest\", \"test\": \"test\", \"test2\": \"test2\", \"arr\": [{\"line1\": \"line1\"}]}";
var bodyData = new BodyData
{
BodyAsString = jsonString,
Encoding = Encoding.UTF8,
DetectedBodyType = BodyType.String
};
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, bodyData);
// Assert
var requestMatchResult = new RequestMatchResult();
spec.GetMatchingScore(request, requestMatchResult).Should().Be(1.0);
}
[Fact]
public void Request_WithBody_Array_SystemTextJsonPathMatcher_1()
{
// Arrange - RFC 9535: filter expression requires an array context
var spec = Request.Create().UsingAnyMethod().WithBody(new SystemTextJsonPathMatcher("$[?(@.Id == 1)]"));
// Act
string jsonString = "[{\"Id\": 1, \"Name\": \"Test\"}, {\"Id\": 2, \"Name\": \"Test2\"}]";
var bodyData = new BodyData
{
BodyAsString = jsonString,
Encoding = Encoding.UTF8,
DetectedBodyType = BodyType.String
};
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, bodyData);
// Assert
var requestMatchResult = new RequestMatchResult();
spec.GetMatchingScore(request, requestMatchResult).Should().Be(1.0);
}
[Fact]
public void Request_WithBody_Array_SystemTextJsonPathMatcher_2()
{
// Arrange
var spec = Request.Create().UsingAnyMethod().WithBody(new SystemTextJsonPathMatcher("$.test"));
// Act
string jsonString = "{\"name\": \"PathSelectorTest\", \"test\": \"test\", \"test2\": \"test2\", \"arr\": [{\"line1\": \"line1\"}]}";
var bodyData = new BodyData
{
BodyAsString = jsonString,
Encoding = Encoding.UTF8,
DetectedBodyType = BodyType.String
};
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, bodyData);
// Assert
var requestMatchResult = new RequestMatchResult();
double result = spec.GetMatchingScore(request, requestMatchResult);
result.Should().Be(1.0);
}
[Fact]
public void Request_WithBody_SystemTextJsonPathMatcher_false()
{
// Arrange
var spec = Request.Create().UsingAnyMethod().WithBody(new SystemTextJsonPathMatcher("$.nonexistent"));
// Act
string jsonString = "{\"name\": \"Test\", \"arr\": [{\"line1\": \"line1\"}]}";
var bodyData = new BodyData
{
BodyAsString = jsonString,
Encoding = Encoding.UTF8,
DetectedBodyType = BodyType.String
};
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, bodyData);
// Assert
var requestMatchResult = new RequestMatchResult();
spec.GetMatchingScore(request, requestMatchResult).Should().NotBe(1.0);
}
} }

View File

@@ -3,6 +3,8 @@
using JsonConverter.Newtonsoft.Json; using JsonConverter.Newtonsoft.Json;
using WireMock.Admin.Mappings; using WireMock.Admin.Mappings;
using WireMock.Serialization; using WireMock.Serialization;
using Newtonsoft.Json.Linq;
#if NET8_0_OR_GREATER #if NET8_0_OR_GREATER
using JsonConverter.System.Text.Json; using JsonConverter.System.Text.Json;
#endif #endif
@@ -319,5 +321,92 @@ public class MappingSerializerTests
act.Should().Throw<InvalidOperationException>() act.Should().Throw<InvalidOperationException>()
.WithMessage("Cannot deserialize the provided value to an array or object."); .WithMessage("Cannot deserialize the provided value to an array or object.");
} }
[Fact]
public void MappingSerializer_DeserializeJsonToArray_WithNewtonsoftJson_DateTimeStringInQueryParamExactMatcherPattern_ShouldPreservePatternAsString()
{
// Arrange
var jsonConverter = new NewtonsoftJsonConverter();
var serializer = new MappingSerializer(jsonConverter);
var mappingJson =
"""
{
"Guid": "12345678-1234-1234-1234-aaaaaaaaaaaa",
"Request": {
"Path": "/api/report",
"Methods": ["GET"],
"Params": [
{
"Name": "asOfDate",
"Matchers": [
{
"Name": "ExactMatcher",
"Pattern": "2021-11-10T13:39:13.705"
}
]
}
]
},
"Response": {
"StatusCode": 200
}
}
""";
// Act
var result = serializer.DeserializeJsonToArray<MappingModel>(mappingJson);
// Assert
result.Should().HaveCount(1);
var matcher = result[0].Request!.Params![0].Matchers![0];
matcher.Name.Should().Be("ExactMatcher");
matcher.Pattern.Should().BeOfType<string>()
.Which.Should().Be("2021-11-10T13:39:13.705",
"datetime-format strings in ExactMatcher Pattern fields must survive deserialization as strings, " +
"not be auto-converted to DateTime by Newtonsoft.Json's DateParseHandling.DateTime");
}
[Fact]
public void MappingSerializer_DeserializeJsonToArray_WithNewtonsoftJson_DateTimeStringInJsonMatcherBodyPattern_ShouldPreservePatternAsString()
{
// Arrange
var jsonConverter = new NewtonsoftJsonConverter();
var serializer = new MappingSerializer(jsonConverter);
// Pattern is an INLINE JSON object (not a string) - this is how WireMock mapping files store
// JsonMatcher patterns when recorded. Newtonsoft with DateParseHandling.DateTime will convert
// the datetime value inside the JObject to JTokenType.Date during deserialization.
var mappingJson =
"""
{
"Guid": "12345678-1234-1234-1234-bbbbbbbbbbbb",
"Request": {
"Path": "/api/report",
"Methods": ["POST"],
"Body": {
"Matcher": {
"Name": "JsonMatcher",
"Pattern": {"Date": "2021-09-30T00:00:00Z", "Names": ["Cash"]}
}
}
},
"Response": {
"StatusCode": 200
}
}
""";
// Act
var result = serializer.DeserializeJsonToArray<MappingModel>(mappingJson);
// Assert - datetime values inside the JObject pattern must remain JTokenType.String.
result.Should().HaveCount(1);
var matcher = result[0].Request!.Body!.Matcher!;
matcher.Name.Should().Be("JsonMatcher");
var patternJObject = matcher.Pattern.Should().BeOfType<JObject>().Subject;
patternJObject["Date"]!.Type.Should().Be(JTokenType.String,
"datetime-format strings inside an inline JsonMatcher body pattern must retain JTokenType.String " +
"after deserialization; if DateParseHandling.DateTime auto-converts them to JTokenType.Date, " +
"JToken.DeepEquals will fail against incoming request bodies parsed with DateParseHandling.None");
}
#endif #endif
} }

File diff suppressed because it is too large Load Diff

View File

@@ -413,6 +413,120 @@ public class StatefulBehaviorTests
action.Should().ThrowAsync<HttpRequestException>(); action.Should().ThrowAsync<HttpRequestException>();
} }
[Fact]
public async Task Scenarios_FirstRequestWithDelay_StateTransitions_BeforeDelayCompletes()
{
// Arrange
var cancellationToken = TestContext.Current.CancellationToken;
var path = $"/foo_{Guid.NewGuid()}";
using var server = WireMockServer.Start();
// Mapping 1: start state, has a 500 ms delay
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("1260")
.WillSetStateTo("State1")
.RespondWith(Response.Create()
.WithBody("delayed response")
.WithDelay(TimeSpan.FromMilliseconds(500)));
// Mapping 2: only matches after state has transitioned to "State1"
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("1260")
.WhenStateIs("State1")
.RespondWith(Response.Create().WithBody("immediate response"));
var client = new HttpClient();
// Act: fire request 1 but don't await it yet — it will sit in a 500 ms delay
var request1Task = client.GetStringAsync(server.Url + path, cancellationToken);
// Give the server a moment to match & transition state before the delay completes
await Task.Delay(100, cancellationToken);
// Request 2 is sent while request 1 is still being delayed.
// After the fix the state has already transitioned, so request 2 matches Mapping 2.
var response2 = await client.GetStringAsync(server.Url + path, cancellationToken);
var response1 = await request1Task;
// Assert
response1.Should().Be("delayed response");
response2.Should().Be("immediate response");
}
[Fact]
public async Task Scenarios_WithGlobalRequestProcessingDelay_StateTransitions_BeforeDelayCompletes()
{
// Arrange: use the global RequestProcessingDelay instead of a per-mapping delay
var cancellationToken = TestContext.Current.CancellationToken;
var path = $"/foo_{Guid.NewGuid()}";
using var server = WireMockServer.Start();
server.AddGlobalProcessingDelay(TimeSpan.FromMilliseconds(500));
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("s")
.WillSetStateTo("State1")
.RespondWith(Response.Create().WithBody("delayed response"));
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("s")
.WhenStateIs("State1")
.RespondWith(Response.Create().WithBody("immediate response"));
var client = new HttpClient();
// Act
var request1Task = client.GetStringAsync(server.Url + path, cancellationToken);
await Task.Delay(100, cancellationToken);
var response2 = await client.GetStringAsync(server.Url + path, cancellationToken);
var response1 = await request1Task;
// Assert
response1.Should().Be("delayed response");
response2.Should().Be("immediate response");
}
[Fact]
public async Task Scenarios_WithDelay_And_TimesInSameState_Should_Transition_After_Required_Hits()
{
// Arrange
var cancellationToken = TestContext.Current.CancellationToken;
var path = $"/foo_{Guid.NewGuid()}";
using var server = WireMockServer.Start();
// Mapping 1: requires 2 hits before transitioning; has a short delay
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("s")
.WillSetStateTo("State1", 2)
.RespondWith(Response.Create()
.WithBody("first")
.WithDelay(TimeSpan.FromMilliseconds(50)));
// Mapping 2: matches after state is "State1"
server
.Given(Request.Create().WithPath(path).UsingGet())
.InScenario("s")
.WhenStateIs("State1")
.RespondWith(Response.Create().WithBody("second"));
var client = new HttpClient();
// Act
var response1 = await client.GetStringAsync(server.Url + path, cancellationToken);
var response2 = await client.GetStringAsync(server.Url + path, cancellationToken);
var response3 = await client.GetStringAsync(server.Url + path, cancellationToken);
// Assert: state only transitions after 2 hits, so request 3 is the first to match Mapping 2
response1.Should().Be("first");
response2.Should().Be("first");
response3.Should().Be("second");
}
[Fact] [Fact]
public async Task Scenarios_Should_process_request_if_equals_state_and_multiple_state_defined() public async Task Scenarios_Should_process_request_if_equals_state_and_multiple_state_defined()
{ {

View File

@@ -0,0 +1,164 @@
// Copyright © WireMock.Net
using System.Text;
using System.Text.Json.Nodes;
using Moq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using WireMock.Handlers;
using WireMock.Settings;
using WireMock.Transformers;
using WireMock.Types;
using WireMock.Util;
namespace WireMock.Net.Tests.Transformers;
public class JsonBodyTransformerTests
{
public static TheoryData<JsonBodyTransformerTestContext> Transformers
{
get
{
return
[
new JsonBodyTransformerTestContext(
() => new NewtonsoftJsonBodyTransformer(new WireMockServerSettings()),
JObject.Parse,
body => ((JToken)body).ToString(Formatting.None)),
new JsonBodyTransformerTestContext(
() => new SystemTextJsonBodyTransformer(),
json => JsonNode.Parse(json)!,
body => ((JsonNode)body).ToJsonString())
];
}
}
[Theory]
[MemberData(nameof(Transformers))]
public void TransformBodyAsJson_Replaces_String_Value_And_Preserves_Original(JsonBodyTransformerTestContext testContext)
{
// Arrange
var transformer = testContext.CreateTransformer();
var originalJson = testContext.ParseJson("{\"value\":\"{{number}}\"}");
var bodyData = new BodyData
{
Encoding = Encoding.UTF8,
DetectedBodyType = BodyType.Json,
DetectedBodyTypeFromContentType = BodyType.Json,
ProtoBufMessageType = "My.Message",
BodyAsJson = originalJson
};
var transformerContext = new FakeTransformerContext(
text => text,
text => text == "{{number}}" ? "123" : text);
// Act
var result = transformer.TransformBodyAsJson(transformerContext, ReplaceNodeOptions.EvaluateAndTryToConvert, new { }, bodyData);
// Assert
result.Encoding.Should().Be(Encoding.UTF8);
result.DetectedBodyType.Should().Be(BodyType.Json);
result.DetectedBodyTypeFromContentType.Should().Be(BodyType.Json);
result.ProtoBufMessageType.Should().Be("My.Message");
result.BodyAsJson.Should().NotBeNull();
testContext.SerializeJson(result.BodyAsJson).Should().Be("{\"value\":123}");
testContext.SerializeJson(originalJson).Should().Be("{\"value\":\"{{number}}\"}");
}
[Theory]
[MemberData(nameof(Transformers))]
public void TransformBodyAsJson_With_String_Body_Replaces_Single_Node_With_Object(JsonBodyTransformerTestContext testContext)
{
// Arrange
var transformer = testContext.CreateTransformer();
var bodyData = new BodyData
{
DetectedBodyType = BodyType.Json,
BodyAsJson = "{{json}}"
};
var transformerContext = new FakeTransformerContext(
text => text == "{{json}}" ? "{\"name\":\"test\"}" : text,
text => text);
// Act
var result = transformer.TransformBodyAsJson(transformerContext, ReplaceNodeOptions.EvaluateAndTryToConvert, new { }, bodyData);
// Assert
result.BodyAsJson.Should().NotBeNull();
testContext.SerializeJson(result.BodyAsJson).Should().Be("{\"name\":\"test\"}");
}
[Theory]
[MemberData(nameof(Transformers))]
public void TransformBodyAsJson_Replaces_String_Value_With_WireMockList_As_Array(JsonBodyTransformerTestContext testContext)
{
// Arrange
var transformer = testContext.CreateTransformer();
var bodyData = new BodyData
{
DetectedBodyType = BodyType.Json,
BodyAsJson = testContext.ParseJson("{\"values\":\"{{list}}\"}")
};
var transformerContext = new FakeTransformerContext(
text => text,
text => text == "{{list}}" ? new WireMockList<string>(["a", "b"]) : text);
// Act
var result = transformer.TransformBodyAsJson(transformerContext, ReplaceNodeOptions.EvaluateAndTryToConvert, new { }, bodyData);
// Assert
result.BodyAsJson.Should().NotBeNull();
testContext.SerializeJson(result.BodyAsJson).Should().Be("{\"values\":[\"a\",\"b\"]}");
}
public sealed class JsonBodyTransformerTestContext
{
private readonly Func<IJsonBodyTransformer> _createTransformer;
private readonly Func<string, object> _parseJson;
private readonly Func<object, string> _serializeJson;
public JsonBodyTransformerTestContext(
Func<IJsonBodyTransformer> createTransformer,
Func<string, object> parseJson,
Func<object, string> serializeJson)
{
_createTransformer = createTransformer;
_parseJson = parseJson;
_serializeJson = serializeJson;
}
public IJsonBodyTransformer CreateTransformer()
{
return _createTransformer();
}
public object ParseJson(string json)
{
return _parseJson(json);
}
public string SerializeJson(object body)
{
return _serializeJson(body);
}
}
private sealed class FakeTransformerContext(Func<string, string> parseAndRender, Func<string, object> parseAndEvaluate) : ITransformerContext
{
public IFileSystemHandler FileSystemHandler { get; private set; } = Mock.Of<IFileSystemHandler>();
public string ParseAndRender(string text, object model)
{
return parseAndRender(text);
}
public object ParseAndEvaluate(string text, object model)
{
return parseAndEvaluate(text);
}
}
}

View File

@@ -750,6 +750,8 @@ public class WebSocketIntegrationTests(ITestOutputHelper output, ITestContextAcc
{ {
await client.SendAsync(testMessage, cancellationToken: _ct); await client.SendAsync(testMessage, cancellationToken: _ct);
await Task.Delay(250, _ct);
var received = await client.ReceiveAsTextAsync(cancellationToken: _ct); var received = await client.ReceiveAsTextAsync(cancellationToken: _ct);
received.Should().Be(testMessage, $"message '{testMessage}' should be proxied and echoed back"); received.Should().Be(testMessage, $"message '{testMessage}' should be proxied and echoed back");
} }

View File

@@ -50,6 +50,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\WireMock.Net.AwesomeAssertions\WireMock.Net.AwesomeAssertions.csproj" /> <ProjectReference Include="..\..\src\WireMock.Net.AwesomeAssertions\WireMock.Net.AwesomeAssertions.csproj" />
<ProjectReference Include="..\..\src\WireMock.Net.Matchers.CSharpCode\WireMock.Net.Matchers.CSharpCode.csproj" /> <ProjectReference Include="..\..\src\WireMock.Net.Matchers.CSharpCode\WireMock.Net.Matchers.CSharpCode.csproj" />
<ProjectReference Include="..\..\src\WireMock.Net.Matchers.SystemTextJsonPath\WireMock.Net.Matchers.SystemTextJsonPath.csproj" />
<ProjectReference Include="..\..\src\WireMock.Net.RestClient.AwesomeAssertions\WireMock.Net.RestClient.AwesomeAssertions.csproj" /> <ProjectReference Include="..\..\src\WireMock.Net.RestClient.AwesomeAssertions\WireMock.Net.RestClient.AwesomeAssertions.csproj" />
<ProjectReference Include="..\..\src\WireMock.Net.RestClient\WireMock.Net.RestClient.csproj" /> <ProjectReference Include="..\..\src\WireMock.Net.RestClient\WireMock.Net.RestClient.csproj" />
<ProjectReference Include="..\..\src\WireMock.Net.xUnit.v3\WireMock.Net.xUnit.v3.csproj" /> <ProjectReference Include="..\..\src\WireMock.Net.xUnit.v3\WireMock.Net.xUnit.v3.csproj" />
@@ -75,7 +76,6 @@
</PackageReference> </PackageReference>
<PackageReference Include="Moq" Version="4.20.72" /> <PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="SimMetrics.Net" Version="1.0.5" /> <PackageReference Include="SimMetrics.Net" Version="1.0.5" />
<PackageReference Include="JsonConverter.System.Text.Json" Version="0.8.0" />
<PackageReference Include="Google.Protobuf" Version="3.33.5" /> <PackageReference Include="Google.Protobuf" Version="3.33.5" />
<PackageReference Include="Grpc.Net.Client" Version="2.76.0" /> <PackageReference Include="Grpc.Net.Client" Version="2.76.0" />
<PackageReference Include="Grpc.Tools" Version="2.78.0"> <PackageReference Include="Grpc.Tools" Version="2.78.0">

View File

@@ -4,10 +4,14 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text; using System.Text;
using System.Text.Json.Serialization;
using JsonConverter.System.Text.Json;
using WireMock.Matchers; using WireMock.Matchers;
using WireMock.Net.Xunit;
using WireMock.RequestBuilders; using WireMock.RequestBuilders;
using WireMock.ResponseBuilders; using WireMock.ResponseBuilders;
using WireMock.Server; using WireMock.Server;
using WireMock.Settings;
namespace WireMock.Net.Tests; namespace WireMock.Net.Tests;
@@ -18,11 +22,41 @@ public partial class WireMockServerTests
public string? Hi { get; set; } public string? Hi { get; set; }
} }
public class Person
{
[JsonPropertyName("first_name")]
public string FirstName { get; set; } = string.Empty;
[JsonPropertyName("last_name")]
public string LastName { get; set; } = string.Empty;
}
[Fact]
public async Task WireMockServer_WithBodyAsJson_UsingWireMockServerSettings_SystemTextJsonConverter_ShouldReturnCorrectResponse()
{
// Arange
var person = new Person { FirstName = "John", LastName = "Smith" };
using var server = WireMockServer.Start(new WireMockServerSettings
{
DefaultJsonSerializer = new SystemTextJsonConverter()
});
// Act
server
.Given(Request.Create().UsingAnyMethod())
.RespondWith(Response.Create().WithBodyAsJson(person));
var response = await server.CreateClient().GetStringAsync("/", _ct);
// Assert
response.Should().BeEquivalentTo("{\"first_name\":\"John\",\"last_name\":\"Smith\"}");
}
[Fact] [Fact]
public async Task WireMockServer_WithBodyAsJson_Using_PostAsJsonAsync_And_MultipleJmesPathMatchers_ShouldMatch() public async Task WireMockServer_WithBodyAsJson_Using_PostAsJsonAsync_And_MultipleJmesPathMatchers_ShouldMatch()
{ {
// Arrange // Arrange
var cancellationToken = TestContext.Current.CancellationToken;
using var server = WireMockServer.Start(); using var server = WireMockServer.Start();
server.Given( server.Given(
Request.Create() Request.Create()
@@ -56,7 +90,7 @@ public partial class WireMockServerTests
var requestUri = new Uri($"http://localhost:{server.Port}/a"); var requestUri = new Uri($"http://localhost:{server.Port}/a");
var json = new { requestId = "1", value = "A" }; var json = new { requestId = "1", value = "A" };
var response = await server.CreateClient().PostAsJsonAsync(requestUri, json, cancellationToken); var response = await server.CreateClient().PostAsJsonAsync(requestUri, json, _ct);
// Assert // Assert
response.StatusCode.Should().Be(HttpStatusCode.OK); response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -68,7 +102,6 @@ public partial class WireMockServerTests
public async Task WireMockServer_WithBodyAsJson_Using_PostAsJsonAsync_And_MultipleJmesPathMatchers_ShouldMatch_BestMatching() public async Task WireMockServer_WithBodyAsJson_Using_PostAsJsonAsync_And_MultipleJmesPathMatchers_ShouldMatch_BestMatching()
{ {
// Arrange // Arrange
var cancellationToken = TestContext.Current.CancellationToken;
using var server = WireMockServer.Start(); using var server = WireMockServer.Start();
server.Given( server.Given(
Request.Create() Request.Create()
@@ -105,7 +138,7 @@ public partial class WireMockServerTests
var requestUri = new Uri($"http://localhost:{server.Port}/a"); var requestUri = new Uri($"http://localhost:{server.Port}/a");
var json = new { extra = "X", requestId = "1", value = "A" }; var json = new { extra = "X", requestId = "1", value = "A" };
var response = await server.CreateClient().PostAsJsonAsync(requestUri, json, cancellationToken); var response = await server.CreateClient().PostAsJsonAsync(requestUri, json, _ct);
// Assert // Assert
response.StatusCode.Should().Be(HttpStatusCode.OK); response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -117,7 +150,6 @@ public partial class WireMockServerTests
public async Task WireMockServer_WithBodyAsJson_Using_PostAsJsonAsync_And_WildcardMatcher_ShouldMatch() public async Task WireMockServer_WithBodyAsJson_Using_PostAsJsonAsync_And_WildcardMatcher_ShouldMatch()
{ {
// Arrange // Arrange
var cancellationToken = TestContext.Current.CancellationToken;
using var server = WireMockServer.Start(); using var server = WireMockServer.Start();
server.Given( server.Given(
Request.Create().UsingPost().WithPath("/foo").WithBody(new WildcardMatcher("*Hello*")) Request.Create().UsingPost().WithPath("/foo").WithBody(new WildcardMatcher("*Hello*"))
@@ -132,7 +164,7 @@ public partial class WireMockServerTests
}; };
// Act // Act
var response = await new HttpClient().PostAsJsonAsync("http://localhost:" + server.Ports[0] + "/foo", jsonObject, cancellationToken); var response = await new HttpClient().PostAsJsonAsync("http://localhost:" + server.Ports[0] + "/foo", jsonObject, _ct);
// Assert // Assert
response.StatusCode.Should().Be(HttpStatusCode.OK); response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -144,7 +176,6 @@ public partial class WireMockServerTests
public async Task WireMockServer_WithBodyAsJson_Using_PostAsync_And_WildcardMatcher_ShouldMatch() public async Task WireMockServer_WithBodyAsJson_Using_PostAsync_And_WildcardMatcher_ShouldMatch()
{ {
// Arrange // Arrange
var cancellationToken = TestContext.Current.CancellationToken;
using var server = WireMockServer.Start(); using var server = WireMockServer.Start();
server.Given( server.Given(
Request.Create().UsingPost().WithPath("/foo").WithBody(new WildcardMatcher("*Hello*")) Request.Create().UsingPost().WithPath("/foo").WithBody(new WildcardMatcher("*Hello*"))
@@ -154,7 +185,7 @@ public partial class WireMockServerTests
); );
// Act // Act
var response = await new HttpClient().PostAsync("http://localhost:" + server.Ports[0] + "/foo", new StringContent("{ Hi = \"Hello World\" }"), cancellationToken); var response = await new HttpClient().PostAsync("http://localhost:" + server.Ports[0] + "/foo", new StringContent("{ Hi = \"Hello World\" }"), _ct);
// Assert // Assert
response.StatusCode.Should().Be(HttpStatusCode.OK); response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -166,7 +197,6 @@ public partial class WireMockServerTests
public async Task WireMockServer_WithBodyAsJson_Using_PostAsync_And_JsonPartialWildcardMatcher_ShouldMatch() public async Task WireMockServer_WithBodyAsJson_Using_PostAsync_And_JsonPartialWildcardMatcher_ShouldMatch()
{ {
// Arrange // Arrange
var cancellationToken = TestContext.Current.CancellationToken;
using var server = WireMockServer.Start(); using var server = WireMockServer.Start();
var matcher = new JsonPartialWildcardMatcher(new { method = "initialize", id = "^[a-f0-9]{32}-[0-9]$" }, ignoreCase: true, regex: true); var matcher = new JsonPartialWildcardMatcher(new { method = "initialize", id = "^[a-f0-9]{32}-[0-9]$" }, ignoreCase: true, regex: true);
@@ -188,13 +218,13 @@ public partial class WireMockServerTests
// Act // Act
var content = "{\"jsonrpc\":\"2.0\",\"id\":\"ec475f56d4694b48bc737500ba575b35-1\",\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"GitHub Test\",\"version\":\"1.0.0\"}}}"; var content = "{\"jsonrpc\":\"2.0\",\"id\":\"ec475f56d4694b48bc737500ba575b35-1\",\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"GitHub Test\",\"version\":\"1.0.0\"}}}";
var response = await new HttpClient() var response = await new HttpClient()
.PostAsync($"{server.Url}/foo", new StringContent(content, Encoding.UTF8, "application/json"), cancellationToken) .PostAsync($"{server.Url}/foo", new StringContent(content, Encoding.UTF8, "application/json"), _ct)
; ;
// Assert // Assert
response.StatusCode.Should().Be(HttpStatusCode.OK); response.StatusCode.Should().Be(HttpStatusCode.OK);
var responseText = await response.Content.ReadAsStringAsync(cancellationToken); var responseText = await response.Content.ReadAsStringAsync(_ct);
responseText.Should().Contain("ec475f56d4694b48bc737500ba575b35-1"); responseText.Should().Contain("ec475f56d4694b48bc737500ba575b35-1");
} }
@@ -203,10 +233,13 @@ public partial class WireMockServerTests
public async Task WireMockServer_WithBodyAsJson_Using_PostAsync_And_JsonPartialWildcardMatcher_And_SystemTextJson_ShouldMatch() public async Task WireMockServer_WithBodyAsJson_Using_PostAsync_And_JsonPartialWildcardMatcher_And_SystemTextJson_ShouldMatch()
{ {
// Arrange // Arrange
var cancellationToken = TestContext.Current.CancellationToken; using var server = WireMockServer.Start(settings =>
using var server = WireMockServer.Start(x => x.DefaultJsonSerializer = new JsonConverter.System.Text.Json.SystemTextJsonConverter() ); {
settings.Logger = new TestOutputHelperWireMockLogger(testOutputHelper);
settings.DefaultJsonSerializer = new SystemTextJsonConverter();
});
var matcher = new JsonPartialWildcardMatcher(new { id = "^[a-f0-9]{32}-[0-9]$" }, ignoreCase: true, regex: true); var matcher = new SystemTextJsonPartialWildcardMatcher(new { id = "^[a-f0-9]{32}-[0-9]$" }, ignoreCase: true, regex: true);
server.Given(Request.Create() server.Given(Request.Create()
.WithHeader("Content-Type", "application/json*") .WithHeader("Content-Type", "application/json*")
.UsingPost() .UsingPost()
@@ -220,12 +253,12 @@ public partial class WireMockServerTests
// Act // Act
var content = """{"id":"ec475f56d4694b48bc737500ba575b35-1"}"""; var content = """{"id":"ec475f56d4694b48bc737500ba575b35-1"}""";
using var httpClient = new HttpClient(); using var httpClient = new HttpClient();
var response = await httpClient.PostAsync($"{server.Url}/system-text-json", new StringContent(content, Encoding.UTF8, "application/json"), cancellationToken); var response = await httpClient.PostAsync($"{server.Url}/system-text-json", new StringContent(content, Encoding.UTF8, "application/json"), _ct);
// Assert // Assert
response.StatusCode.Should().Be(HttpStatusCode.OK); response.StatusCode.Should().Be(HttpStatusCode.OK);
var responseText = await response.Content.ReadAsStringAsync(cancellationToken); var responseText = await response.Content.ReadAsStringAsync(_ct);
responseText.Should().Contain("OK"); responseText.Should().Contain("OK");
} }
#endif #endif
@@ -234,7 +267,6 @@ public partial class WireMockServerTests
public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithFunc() public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithFunc()
{ {
// Arrange // Arrange
var cancelationToken = TestContext.Current.CancellationToken;
using var server = WireMockServer.Start(); using var server = WireMockServer.Start();
server.Given( server.Given(
Request.Create() Request.Create()
@@ -249,7 +281,7 @@ public partial class WireMockServerTests
// Act // Act
var content = new FormUrlEncodedContent([new KeyValuePair<string, string>("key1", "value1")]); var content = new FormUrlEncodedContent([new KeyValuePair<string, string>("key1", "value1")]);
var response = await new HttpClient() var response = await new HttpClient()
.PostAsync($"{server.Url}/foo", content, cancelationToken); .PostAsync($"{server.Url}/foo", content, _ct);
// Assert // Assert
response.StatusCode.Should().Be(HttpStatusCode.OK); response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -261,7 +293,6 @@ public partial class WireMockServerTests
public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithExactMatcher() public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithExactMatcher()
{ {
// Arrange // Arrange
var cancellationToken = TestContext.Current.CancellationToken;
using var server = WireMockServer.Start(); using var server = WireMockServer.Start();
server.Given( server.Given(
Request.Create() Request.Create()
@@ -282,7 +313,7 @@ public partial class WireMockServerTests
new KeyValuePair<string, string>("email", "johndoe@example.com") new KeyValuePair<string, string>("email", "johndoe@example.com")
]); ]);
using var httpClient = new HttpClient(); using var httpClient = new HttpClient();
var response = await httpClient.PostAsync($"{server.Url}/foo", content, cancellationToken) var response = await httpClient.PostAsync($"{server.Url}/foo", content, _ct)
; ;
// Assert // Assert
response.StatusCode.Should().Be(HttpStatusCode.OK); response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -294,7 +325,6 @@ public partial class WireMockServerTests
public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithFormUrlEncodedMatcher() public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithFormUrlEncodedMatcher()
{ {
// Arrange // Arrange
var cancelationToken = TestContext.Current.CancellationToken;
var matcher = new FormUrlEncodedMatcher(["email=johndoe@example.com", "name=John Doe"]); var matcher = new FormUrlEncodedMatcher(["email=johndoe@example.com", "name=John Doe"]);
using var server = WireMockServer.Start(); using var server = WireMockServer.Start();
server.Given( server.Given(
@@ -326,7 +356,7 @@ public partial class WireMockServerTests
new KeyValuePair<string, string>("email", "johndoe@example.com") new KeyValuePair<string, string>("email", "johndoe@example.com")
]); ]);
var responseOrdered = await new HttpClient() var responseOrdered = await new HttpClient()
.PostAsync($"{server.Url}/foo", contentOrdered, cancelationToken) .PostAsync($"{server.Url}/foo", contentOrdered, _ct)
; ;
// Assert 1 // Assert 1
@@ -340,7 +370,7 @@ public partial class WireMockServerTests
new KeyValuePair<string, string>("name", "John Doe"), new KeyValuePair<string, string>("name", "John Doe"),
]); ]);
var responseUnordered = await new HttpClient() var responseUnordered = await new HttpClient()
.PostAsync($"{server.Url}/bar", contentUnordered, cancelationToken) .PostAsync($"{server.Url}/bar", contentUnordered, _ct)
; ;
// Assert 2 // Assert 2
@@ -353,7 +383,6 @@ public partial class WireMockServerTests
public async Task WireMockServer_WithSseBody() public async Task WireMockServer_WithSseBody()
{ {
// Arrange // Arrange
var cancellationToken = TestContext.Current.CancellationToken;
using var server = WireMockServer.Start(); using var server = WireMockServer.Start();
server server
.WhenRequest(r => r .WhenRequest(r => r
@@ -387,9 +416,8 @@ public partial class WireMockServerTests
using var client = new HttpClient(); using var client = new HttpClient();
// Act 1 // Act 1
var normal = await client.GetAsync(server.Url, cancellationToken) var normal = await client.GetAsync(server.Url, _ct);
; (await normal.Content.ReadAsStringAsync(_ct)).Should().Be("normal");
(await normal.Content.ReadAsStringAsync(cancellationToken)).Should().Be("normal");
// Act 2 // Act 2
using var response = await client.GetStreamAsync($"{server.Url}/sse", _ct); using var response = await client.GetStreamAsync($"{server.Url}/sse", _ct);