Compare commits

...

40 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
Stef Heyenrath
070e4b6ab9 2.1.0 2026-03-29 13:16:23 +02:00
m4tchl0ck
f919929cb7 Add injectable IScenarioStateStore for distributed scenario state (#1430)
* Move ScenarioState to Abstractions and add IScenarioStateStore interface

ScenarioState is moved to the Abstractions project so it can be referenced
by the new IScenarioStateStore interface. The interface defines the contract
for storing and retrieving scenario states, enabling distributed implementations.

* Add InMemoryScenarioStateStore default implementation

Wraps ConcurrentDictionary with OrdinalIgnoreCase comparer, preserving
exact current behavior. The Update method encapsulates read-modify-write
so distributed implementations can make it atomic.

* Wire IScenarioStateStore into middleware options, settings, and consumers

Replace direct ConcurrentDictionary<string, ScenarioState> usage with
IScenarioStateStore across all consumer files. The store is injectable
via WireMockServerSettings.ScenarioStateStore, defaulting to the
InMemoryScenarioStateStore for backward compatibility.

* Add FileBasedScenarioStateStore for persistent scenario state

In-memory ConcurrentDictionary backed by JSON file persistence in
__admin/scenarios/. Reads from cache, mutations write through to disk.
Constructor loads existing state from disk on startup.

* Make ScenarioStateStore non-nullable with default InMemoryScenarioStateStore

Move InMemoryScenarioStateStore from WireMock.Net.Minimal to
WireMock.Net.Shared so it lives alongside WireMockServerSettings.
This allows WireMockServerSettings.ScenarioStateStore to be
non-nullable with a default value, following the same pattern as
DefaultJsonSerializer. The null-coalescing fallback in
WireMockMiddlewareOptionsHelper is no longer needed.
2026-03-25 13:04:44 +01:00
Logan Dam
cdd33695e5 Add helpers for query params fluent MappingModelBuilder (#1425)
* Add helpers for query params

* add example with query params

* add fluent helpers for WithHeaders and WithCookies

---------

Co-authored-by: Logan Dam <Logan.Dam@rabobank.com>
Co-authored-by: Stef Heyenrath <Stef.Heyenrath@gmail.com>
2026-03-14 10:39:30 +01:00
Stef Heyenrath
c4caa25eb6 Fix WithWebSocketProxy_Should_Proxy_Binary_Messages 2026-03-14 10:39:04 +01:00
Stef Heyenrath
ca788cb9b0 Add WireMockAspNetCoreLogger to log Kestrel warnings/errors (#1432)
* Add WireMockAspNetCoreLogger

* fix tests

* x

* .
2026-03-14 10:30:56 +01:00
Stef Heyenrath
d08ce944b6 Fix WireMockLogger implementation in dotnet-WireMock (#1431) 2026-03-13 18:34:47 +01:00
Stef Heyenrath
0a9f37e857 readme 2026-03-11 18:14:09 +01:00
122 changed files with 6659 additions and 1982 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,29 @@
# 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)
- [#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)
- [#1431](https://github.com/wiremock/WireMock.Net/pull/1431) - Fix WireMockLogger implementation in dotnet-WireMock [bug] contributed by [StefH](https://github.com/StefH)
- [#1432](https://github.com/wiremock/WireMock.Net/pull/1432) - Add WireMockAspNetCoreLogger to log Kestrel warnings/errors [feature] contributed by [StefH](https://github.com/StefH)
# 2.0.0 (11 March 2026) # 2.0.0 (11 March 2026)
- [#1359](https://github.com/wiremock/WireMock.Net/pull/1359) - Version 2.x contributed by [StefH](https://github.com/StefH) - [#1359](https://github.com/wiremock/WireMock.Net/pull/1359) - Version 2.x contributed by [StefH](https://github.com/StefH)
- [#1394](https://github.com/wiremock/WireMock.Net/pull/1394) - MappingSerializer (Newtonsoft or SystemText)-Json [feature] contributed by [StefH](https://github.com/StefH) - [#1394](https://github.com/wiremock/WireMock.Net/pull/1394) - MappingSerializer (Newtonsoft or SystemText)-Json [feature] contributed by [StefH](https://github.com/StefH)
@@ -1356,11 +1382,11 @@
- [#145](https://github.com/wiremock/WireMock.Net/pull/145) - Cancellation token not passed to server instance in .NET Core 2 [bug] contributed by [Bob11327](https://github.com/Bob11327) - [#145](https://github.com/wiremock/WireMock.Net/pull/145) - Cancellation token not passed to server instance in .NET Core 2 [bug] contributed by [Bob11327](https://github.com/Bob11327)
# 1.0.3.18 (25 May 2018) # 1.0.3.18 (25 May 2018)
- [#142](https://github.com/wiremock/WireMock.Net/pull/142) - Allow all headers to be set as Response headers contributed by [StefH](https://github.com/StefH) - [#142](https://github.com/wiremock/WireMock.Net/pull/142) - Allow all headers to be set as Response headers [bug] contributed by [StefH](https://github.com/StefH)
- [#122](https://github.com/wiremock/WireMock.Net/issues/122) - WireMock.Net not responding in unit tests - same works in console application - [#122](https://github.com/wiremock/WireMock.Net/issues/122) - WireMock.Net not responding in unit tests - same works in console application
- [#126](https://github.com/wiremock/WireMock.Net/issues/126) - Question: UsingHead always returns 0 for Content-Length header even when explicitly specified - [#126](https://github.com/wiremock/WireMock.Net/issues/126) - Question: UsingHead always returns 0 for Content-Length header even when explicitly specified
- [#132](https://github.com/wiremock/WireMock.Net/issues/132) - LogEntries not being recorded on subsequent tests - [#132](https://github.com/wiremock/WireMock.Net/issues/132) - LogEntries not being recorded on subsequent tests
- [#136](https://github.com/wiremock/WireMock.Net/issues/136) - Question: Does the WireMock send Content-Length response header - [#136](https://github.com/wiremock/WireMock.Net/issues/136) - Question: Does the WireMock send Content-Length response header [bug]
- [#137](https://github.com/wiremock/WireMock.Net/issues/137) - Question: How to specify Transfer-Encoding response header? - [#137](https://github.com/wiremock/WireMock.Net/issues/137) - Question: How to specify Transfer-Encoding response header?
- [#139](https://github.com/wiremock/WireMock.Net/issues/139) - Wiki link https://github.com/StefH/WireMock.Net/wiki/Record-(via-proxy)-and-Save is dead - [#139](https://github.com/wiremock/WireMock.Net/issues/139) - Wiki link https://github.com/StefH/WireMock.Net/wiki/Record-(via-proxy)-and-Save is dead

View File

@@ -4,7 +4,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<VersionPrefix>2.0.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.0.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,8 +1,6 @@
# 2.0.0 (11 March 2026) # 2.4.0 (24 April 2026)
- #1359 Version 2.x - #1437 Added feature to enable and disable mappings [feature]
- #1394 MappingSerializer (Newtonsoft or SystemText)-Json [feature] - #1450 Bump OpenTelemetry.Exporter.OpenTelemetryProtocol from 1.14.0 to 1.15.x [dependencies, .NET]
- #1341 Configurable JSON serialization support (Newtonsoft.Json vs System.Text.Json) [feature] - #1421 Deactivate mapping without deleting it [feature]
- #1422 WireMock.Net seems to be incompatible with Microsoft.Owin.Security.Interop [bug]
- #1424 WireMock.Net.FluentAssertions is incompatible with WireMock.Net.Aspire [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)
@@ -71,11 +72,12 @@ A C# .NET version based on [mock4net](https://github.com/alexvictoor/mock4net) w
| &nbsp;&nbsp;**WireMock.Net.OpenTelemetry** | [![NuGet Badge WireMock.Net.OpenTelemetry](https://img.shields.io/nuget/v/WireMock.Net.OpenTelemetry)](https://www.nuget.org/packages/WireMock.Net.ProtoBuf) | [![MyGet Badge WireMock.Net.OpenTelemetry](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.OpenTelemetry?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.OpenTelemetry) | &nbsp;&nbsp;**WireMock.Net.OpenTelemetry** | [![NuGet Badge WireMock.Net.OpenTelemetry](https://img.shields.io/nuget/v/WireMock.Net.OpenTelemetry)](https://www.nuget.org/packages/WireMock.Net.ProtoBuf) | [![MyGet Badge WireMock.Net.OpenTelemetry](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.OpenTelemetry?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.OpenTelemetry)
| | | | | | | |
| &nbsp;&nbsp;**WireMock.Net.RestClient** | [![NuGet Badge WireMock.Net.RestClient](https://img.shields.io/nuget/v/WireMock.Net.RestClient)](https://www.nuget.org/packages/WireMock.Net.RestClient) | [![MyGet Badge WireMock.Net.RestClient](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.RestClient?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.RestClient) | &nbsp;&nbsp;**WireMock.Net.RestClient** | [![NuGet Badge WireMock.Net.RestClient](https://img.shields.io/nuget/v/WireMock.Net.RestClient)](https://www.nuget.org/packages/WireMock.Net.RestClient) | [![MyGet Badge WireMock.Net.RestClient](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.RestClient?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.RestClient)
| &nbsp;&nbsp;**WireMock.Net.RestClient.AwesomeAssertions** | [![NuGet Badge WireMock.Net.RestClient.AwesomeAssertions](https://img.shields.io/nuget/v/WireMock.Net.RestClient.AwesomeAssertions)](https://www.nuget.org/packages/WireMock.Net.RestClient.AwesomeAssertions) | [![MyGet Badge WireMock.Net.RestClient.AwesomeAssertions](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.RestClient.AwesomeAssertions?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.RestClient.AwesomeAssertions)
| &nbsp;&nbsp;**WireMock.Org.RestClient** | [![NuGet Badge WireMock.Org.RestClient](https://img.shields.io/nuget/v/WireMock.Org.RestClient)](https://www.nuget.org/packages/WireMock.Org.RestClient) | [![MyGet Badge WireMock.Org.RestClient](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Org.RestClient?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Org.RestClient) | &nbsp;&nbsp;**WireMock.Org.RestClient** | [![NuGet Badge WireMock.Org.RestClient](https://img.shields.io/nuget/v/WireMock.Org.RestClient)](https://www.nuget.org/packages/WireMock.Org.RestClient) | [![MyGet Badge WireMock.Org.RestClient](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Org.RestClient?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Org.RestClient)
<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

@@ -82,6 +82,51 @@ class Program
) )
); );
mappingBuilder.Given(m => m
.WithRequest(req => req
.WithPath("/testRequestWithQueryParams")
.UsingGet()
.WithParams(p => p.WithParam("param1", pb => pb.WithExactMatcher("value1")))
).WithResponse(rsp => rsp
.WithHeaders(h => h.Add("Content-Type", "application/json"))
.WithStatusCode(200)
.WithBodyAsJson(new
{
status = "ok"
}, true)
)
);
mappingBuilder.Given(m => m
.WithRequest(req => req
.WithPath("/testRequestWithHeaders")
.UsingGet()
.WithHeaders(h => h.WithHeader("Accept", hb => hb.WithExactMatcher("application/json")))
).WithResponse(rsp => rsp
.WithHeaders(h => h.Add("Content-Type", "application/json"))
.WithStatusCode(200)
.WithBodyAsJson(new
{
status = "ok"
}, true)
)
);
mappingBuilder.Given(m => m
.WithRequest(req => req
.WithPath("/testRequestWithCookie")
.UsingGet()
.WithCookies(c => c.WithCookie("cookie1", cb => cb.WithExactMatcher("cookievalue1")))
).WithResponse(rsp => rsp
.WithHeaders(h => h.Add("Content-Type", "application/json"))
.WithStatusCode(200)
.WithBodyAsJson(new
{
status = "ok"
}, true)
)
);
var result = await mappingBuilder.BuildAndPostAsync().ConfigureAwait(false); var result = await mappingBuilder.BuildAndPostAsync().ConfigureAwait(false);
Console.WriteLine($"result = {JsonConvert.SerializeObject(result)}"); Console.WriteLine($"result = {JsonConvert.SerializeObject(result)}");

View File

@@ -8,6 +8,7 @@ using System.Linq;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using SharpYaml.Model;
using WireMock.Logging; using WireMock.Logging;
using WireMock.Matchers; using WireMock.Matchers;
using WireMock.Models; using WireMock.Models;
@@ -288,7 +289,24 @@ namespace WireMock.Net.ConsoleApplication
var todos = new Dictionary<int, Todo>(); var todos = new Dictionary<int, Todo>();
var server = WireMockServer.Start(); var server = WireMockServer.Start(new WireMockServerSettings
{
Logger = new WireMockConsoleLogger(),
Port = 9091
});
server
.WhenRequest(r => r
.WithPath("/Content-Length")
.UsingAnyMethod()
)
.ThenRespondWith(r => r
.WithStatusCode(HttpStatusCode.OK)
.WithHeader("Content-Length", "42")
);
System.Console.ReadLine();
//server //server
// .Given(Request.Create() // .Given(Request.Create()

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,4 +1,4 @@
<?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
@@ -7,6 +7,11 @@
<handlers> <handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" /> <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers> </handlers>
<httpProtocol>
<customHeaders>
<add name="X-Frame-Options" value="SAMEORIGIN" />
</customHeaders>
</httpProtocol>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" /> <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" />
</system.webServer> </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

@@ -0,0 +1,39 @@
// Copyright © WireMock.Net
namespace WireMock.Admin.Mappings;
public partial class ArrayMatcherModelBuilder
{
public ArrayMatcherModelBuilder WithExactMatcher(object pattern, bool rejectOnMatch = false, bool ignoreCase = false)
{
return WithMatcher("ExactMatcher", pattern, rejectOnMatch, ignoreCase);
}
public ArrayMatcherModelBuilder WithWildcardMatcher(object pattern, bool rejectOnMatch = false, bool ignoreCase = false)
{
return WithMatcher("WildcardMatcher", pattern, rejectOnMatch, ignoreCase);
}
public ArrayMatcherModelBuilder WithRegexMatcher(object pattern, bool rejectOnMatch = false, bool ignoreCase = false)
{
return WithMatcher("RegexMatcher", pattern, rejectOnMatch, ignoreCase);
}
public ArrayMatcherModelBuilder WithNotNullOrEmptyMatcher(bool rejectOnMatch = false)
{
return Add(mb => mb
.WithName("NotNullOrEmptyMatcher")
.WithRejectOnMatch(rejectOnMatch)
);
}
private ArrayMatcherModelBuilder WithMatcher(string name, object pattern, bool rejectOnMatch, bool ignoreCase = false)
{
return Add(mb => mb
.WithName(name)
.WithPattern(pattern)
.WithRejectOnMatch(rejectOnMatch)
.WithIgnoreCase(ignoreCase)
);
}
}

View File

@@ -0,0 +1,19 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.Text;
namespace WireMock.Admin.Mappings;
public partial class IListCookieModelBuilder
{
public IListCookieModelBuilder WithCookie(string name, Action<IListMatcherModelBuilder> action, bool rejectOnMatch = false)
{
return Add(cookieBuilder => cookieBuilder
.WithName(name)
.WithRejectOnMatch(rejectOnMatch)
.WithMatchers(matchersBuilder => action(matchersBuilder))
);
}
}

View File

@@ -0,0 +1,19 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.Text;
namespace WireMock.Admin.Mappings;
public partial class IListHeaderModelBuilder
{
public IListHeaderModelBuilder WithHeader(string name, Action<IListMatcherModelBuilder> action, bool rejectOnMatch = false)
{
return Add(headerBuilder => headerBuilder
.WithName(name)
.WithRejectOnMatch(rejectOnMatch)
.WithMatchers(matchersBuilder => action(matchersBuilder))
);
}
}

View File

@@ -0,0 +1,39 @@
// Copyright © WireMock.Net
namespace WireMock.Admin.Mappings;
public partial class IListMatcherModelBuilder
{
public IListMatcherModelBuilder WithExactMatcher(object pattern, bool rejectOnMatch = false, bool ignoreCase = false)
{
return WithMatcher("ExactMatcher", pattern, rejectOnMatch, ignoreCase);
}
public IListMatcherModelBuilder WithWildcardMatcher(object pattern, bool rejectOnMatch = false, bool ignoreCase = false)
{
return WithMatcher("WildcardMatcher", pattern, rejectOnMatch, ignoreCase);
}
public IListMatcherModelBuilder WithRegexMatcher(object pattern, bool rejectOnMatch = false, bool ignoreCase = false)
{
return WithMatcher("RegexMatcher", pattern, rejectOnMatch, ignoreCase);
}
public IListMatcherModelBuilder WithNotNullOrEmptyMatcher(bool rejectOnMatch = false)
{
return Add(mb => mb
.WithName("NotNullOrEmptyMatcher")
.WithRejectOnMatch(rejectOnMatch)
);
}
private IListMatcherModelBuilder WithMatcher(string name, object pattern, bool rejectOnMatch, bool ignoreCase = false)
{
return Add(mb => mb
.WithName(name)
.WithPattern(pattern)
.WithRejectOnMatch(rejectOnMatch)
.WithIgnoreCase(ignoreCase)
);
}
}

View File

@@ -0,0 +1,17 @@
// Copyright © WireMock.Net
using System;
namespace WireMock.Admin.Mappings;
public partial class IListParamModelBuilder
{
public IListParamModelBuilder WithParam(string name, Action<ArrayMatcherModelBuilder> action, bool rejectOnMatch = false)
{
return Add(paramBuilder => paramBuilder
.WithName(name)
.WithRejectOnMatch(rejectOnMatch)
.WithMatchers(matchersBuilder => action(matchersBuilder))
);
}
}

View File

@@ -0,0 +1,24 @@
// Copyright © WireMock.Net
using System.Diagnostics.CodeAnalysis;
namespace WireMock.Handlers;
public interface IScenarioStateStore
{
bool TryGet(string name, [NotNullWhen(true)] out ScenarioState? state);
IReadOnlyList<ScenarioState> GetAll();
bool ContainsKey(string name);
bool TryAdd(string name, ScenarioState scenarioState);
ScenarioState AddOrUpdate(string name, Func<string, ScenarioState> addFactory, Func<string, ScenarioState, ScenarioState> updateFactory);
ScenarioState? Update(string name, Action<ScenarioState> updateAction);
bool TryRemove(string name);
void Clear();
}

View File

@@ -10,7 +10,7 @@ public class MatchDetail
/// <summary> /// <summary>
/// Gets or sets the type of the matcher. /// Gets or sets the type of the matcher.
/// </summary> /// </summary>
public required Type MatcherType { get; set; } public required string MatcherType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type of the matcher. /// Gets or sets the type of the matcher.

View File

@@ -33,12 +33,6 @@ public interface IWireMockServer : IDisposable
/// </summary> /// </summary>
IReadOnlyList<MappingModel> MappingModels { get; } IReadOnlyList<MappingModel> MappingModels { get; }
// <summary>
// Gets the mappings.
// </summary>
//[PublicAPI]
//IEnumerable<IMapping> Mappings { get; }
/// <summary> /// <summary>
/// Gets the ports. /// Gets the ports.
/// </summary> /// </summary>
@@ -69,8 +63,6 @@ public interface IWireMockServer : IDisposable
/// </summary> /// </summary>
string? Provider { get; } string? Provider { get; }
//ConcurrentDictionary<string, ScenarioState> Scenarios { get; }
/// <summary> /// <summary>
/// Occurs when [log entries changed]. /// Occurs when [log entries changed].
/// </summary> /// </summary>
@@ -115,8 +107,6 @@ public interface IWireMockServer : IDisposable
/// <returns>The <see cref="IReadOnlyList{ILogEntry}"/>.</returns> /// <returns>The <see cref="IReadOnlyList{ILogEntry}"/>.</returns>
IReadOnlyList<ILogEntry> FindLogEntries(params IRequestMatcher[] matchers); IReadOnlyList<ILogEntry> FindLogEntries(params IRequestMatcher[] matchers);
// IRespondWithAProvider Given(IRequestMatcher requestMatcher, bool saveToFile = false);
/// <summary> /// <summary>
/// Reads a static mapping file and adds or updates a single mapping. /// Reads a static mapping file and adds or updates a single mapping.
/// ///

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

@@ -0,0 +1,145 @@
// Copyright © WireMock.Net
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
using Stef.Validation;
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
{
private readonly ConcurrentDictionary<string, ScenarioState> _scenarios = new(StringComparer.OrdinalIgnoreCase);
private readonly string _scenariosFolder;
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)
{
Guard.NotNullOrEmpty(rootFolder);
_scenariosFolder = Path.Combine(rootFolder, "__admin", "scenarios");
Directory.CreateDirectory(_scenariosFolder);
LoadScenariosFromDisk();
}
/// <inheritdoc />
public bool TryGet(string name, [NotNullWhen(true)] out ScenarioState? state)
{
return _scenarios.TryGetValue(name, out state);
}
/// <inheritdoc />
public IReadOnlyList<ScenarioState> GetAll()
{
return _scenarios.Values.ToArray();
}
/// <inheritdoc />
public bool ContainsKey(string name)
{
return _scenarios.ContainsKey(name);
}
/// <inheritdoc />
public bool TryAdd(string name, ScenarioState scenarioState)
{
if (_scenarios.TryAdd(name, scenarioState))
{
WriteScenarioToFile(name, scenarioState);
return true;
}
return false;
}
/// <inheritdoc />
public ScenarioState AddOrUpdate(string name, Func<string, ScenarioState> addFactory, Func<string, ScenarioState, ScenarioState> updateFactory)
{
lock (_lock)
{
var result = _scenarios.AddOrUpdate(name, addFactory, updateFactory);
WriteScenarioToFile(name, result);
return result;
}
}
/// <inheritdoc />
public ScenarioState? Update(string name, Action<ScenarioState> updateAction)
{
lock (_lock)
{
if (_scenarios.TryGetValue(name, out var state))
{
updateAction(state);
WriteScenarioToFile(name, state);
return state;
}
return null;
}
}
/// <inheritdoc />
public bool TryRemove(string name)
{
if (_scenarios.TryRemove(name, out _))
{
DeleteScenarioFile(name);
return true;
}
return false;
}
/// <inheritdoc />
public void Clear()
{
_scenarios.Clear();
foreach (var file in Directory.GetFiles(_scenariosFolder, "*.json"))
{
File.Delete(file);
}
}
private string GetScenarioFilePath(string name)
{
var sanitized = string.Concat(name.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
return Path.Combine(_scenariosFolder, sanitized + ".json");
}
private void WriteScenarioToFile(string name, ScenarioState state)
{
var json = JsonConvert.SerializeObject(state, Formatting.Indented);
File.WriteAllText(GetScenarioFilePath(name), json);
}
private void DeleteScenarioFile(string name)
{
var path = GetScenarioFilePath(name);
if (File.Exists(path))
{
File.Delete(path);
}
}
private void LoadScenariosFromDisk()
{
foreach (var file in Directory.GetFiles(_scenariosFolder, "*.json"))
{
var json = File.ReadAllText(file);
var state = JsonConvert.DeserializeObject<ScenarioState>(json);
if (state != null)
{
_scenarios.TryAdd(state.Name, state);
}
}
}
}

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

@@ -0,0 +1,38 @@
// Copyright © WireMock.Net
using Microsoft.Extensions.Logging;
namespace WireMock.Logging;
internal sealed class WireMockAspNetCoreLogger(IWireMockLogger logger, string categoryName) : ILogger
{
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Warning;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
var message = formatter(state, exception);
if (exception != null)
{
message = $"{message} | Exception: {exception}";
}
switch (logLevel)
{
case LogLevel.Warning:
logger.Warn("[{0}] {1}", categoryName, message);
break;
default:
logger.Error("[{0}] {1}", categoryName, message);
break;
}
}
}

View File

@@ -0,0 +1,19 @@
// Copyright © WireMock.Net
using Microsoft.Extensions.Logging;
namespace WireMock.Logging;
internal sealed class WireMockAspNetCoreLoggerProvider : ILoggerProvider
{
private readonly IWireMockLogger _logger;
public WireMockAspNetCoreLoggerProvider(IWireMockLogger logger)
{
_logger = logger;
}
public ILogger CreateLogger(string categoryName) => new WireMockAspNetCoreLogger(_logger, categoryName);
public void Dispose() { }
}

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,7 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Linq;
namespace WireMock.Matchers.Request; namespace WireMock.Matchers.Request;
/// <summary> /// <summary>
@@ -30,7 +28,7 @@ public class RequestMatchResult : IRequestMatchResult
return AddMatchDetail(new MatchDetail return AddMatchDetail(new MatchDetail
{ {
Name = matcherType.Name.Replace("RequestMessage", string.Empty), Name = matcherType.Name.Replace("RequestMessage", string.Empty),
MatcherType = matcherType, MatcherType = matcherType.Name,
Score = score, Score = score,
Exception = exception Exception = exception
}); });

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

@@ -1,9 +1,9 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Linq;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Stef.Validation; using Stef.Validation;
using WireMock.Logging; using WireMock.Logging;
using WireMock.Owin.Mappers; using WireMock.Owin.Mappers;
@@ -57,6 +57,12 @@ internal partial class AspNetCoreSelfHost
_host = builder _host = builder
.UseSetting("suppressStatusMessages", "True") // https://andrewlock.net/suppressing-the-startup-and-shutdown-messages-in-asp-net-core/ .UseSetting("suppressStatusMessages", "True") // https://andrewlock.net/suppressing-the-startup-and-shutdown-messages-in-asp-net-core/
.ConfigureAppConfigurationUsingEnvironmentVariables() .ConfigureAppConfigurationUsingEnvironmentVariables()
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddProvider(new WireMockAspNetCoreLoggerProvider(_logger));
logging.SetMinimumLevel(LogLevel.Warning);
})
.ConfigureServices(services => .ConfigureServices(services =>
{ {
services.AddSingleton(_wireMockMiddlewareOptions); services.AddSingleton(_wireMockMiddlewareOptions);
@@ -169,10 +175,10 @@ internal partial class AspNetCoreSelfHost
return _host.RunAsync(token); return _host.RunAsync(token);
} }
catch (Exception e) catch (Exception ex)
{ {
RunningException = e; RunningException = ex;
_logger.Error(e.ToString()); _logger.Error("Error while RunAsync", ex);
IsStarted = false; IsStarted = false;

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;
@@ -27,7 +28,7 @@ internal interface IWireMockMiddlewareOptions
ConcurrentDictionary<Guid, IMapping> Mappings { get; } ConcurrentDictionary<Guid, IMapping> Mappings { get; }
ConcurrentDictionary<string, ScenarioState> Scenarios { get; } IScenarioStateStore ScenarioStateStore { get; set; }
ConcurrentObservableCollection<LogEntry> LogEntries { get; } ConcurrentObservableCollection<LogEntry> LogEntries { get; }
@@ -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

@@ -1,31 +1,27 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Net; using System.Net;
using System.Reflection;
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> /// <summary>
/// OwinResponseMapper /// OwinResponseMapper
/// </summary> /// </summary>
internal class OwinResponseMapper : IOwinResponseMapper internal class OwinResponseMapper(IWireMockMiddlewareOptions options) : IOwinResponseMapper
{ {
private readonly IRandomizerNumber<double> _randomizerDouble = RandomizerFactory.GetRandomizer(new FieldOptionsDouble { Min = 0, Max = 1 }); private readonly IRandomizerNumber<double> _randomizerDouble = RandomizerFactory.GetRandomizer(new FieldOptionsDouble { Min = 0, Max = 1 });
private readonly IRandomizerBytes _randomizerBytes = RandomizerFactory.GetRandomizer(new FieldOptionsBytes { Min = 100, Max = 200 }); private readonly IRandomizerBytes _randomizerBytes = RandomizerFactory.GetRandomizer(new FieldOptionsBytes { Min = 100, Max = 200 });
private readonly IWireMockMiddlewareOptions _options;
private readonly Encoding _utf8NoBom = new UTF8Encoding(false); private readonly Encoding _utf8NoBom = new UTF8Encoding(false);
// https://msdn.microsoft.com/en-us/library/78h415ay(v=vs.110).aspx // https://msdn.microsoft.com/en-us/library/78h415ay(v=vs.110).aspx
@@ -44,15 +40,6 @@ namespace WireMock.Owin.Mappers
} }
}; };
/// <summary>
/// Constructor
/// </summary>
/// <param name="options">The IWireMockMiddlewareOptions.</param>
public OwinResponseMapper(IWireMockMiddlewareOptions options)
{
_options = Guard.NotNull(options);
}
/// <inheritdoc /> /// <inheritdoc />
public async Task MapAsync(IResponseMessage? responseMessage, HttpResponse response) public async Task MapAsync(IResponseMessage? responseMessage, HttpResponse response)
{ {
@@ -88,20 +75,16 @@ namespace WireMock.Owin.Mappers
break; break;
} }
var statusCodeType = responseMessage.StatusCode?.GetType(); if (responseMessage.StatusCode is HttpStatusCode or int)
if (statusCodeType != null)
{ {
if (statusCodeType == typeof(int) || statusCodeType == typeof(int?) || statusCodeType.GetTypeInfo().IsEnum) response.StatusCode = MapStatusCode((int)responseMessage.StatusCode);
{
response.StatusCode = MapStatusCode((int)responseMessage.StatusCode!);
} }
else if (statusCodeType == typeof(string)) else if (responseMessage.StatusCode is string statusCodeAsString)
{ {
// Note: this case will also match on null // Note: this case will also match on null
int.TryParse(responseMessage.StatusCode as string, out var statusCodeTypeAsInt); _ = int.TryParse(statusCodeAsString, out var statusCodeTypeAsInt);
response.StatusCode = MapStatusCode(statusCodeTypeAsInt); response.StatusCode = MapStatusCode(statusCodeTypeAsInt);
} }
}
SetResponseHeaders(responseMessage, bytes != null, response); SetResponseHeaders(responseMessage, bytes != null, response);
@@ -113,7 +96,7 @@ namespace WireMock.Owin.Mappers
} }
catch (Exception ex) catch (Exception ex)
{ {
_options.Logger.Warn("Error writing response body. Exception : {0}", ex); options.Logger.Warn("Error writing response body. Exception : {0}", ex);
} }
} }
@@ -142,7 +125,7 @@ namespace WireMock.Owin.Mappers
private int MapStatusCode(int code) private int MapStatusCode(int code)
{ {
if (_options.AllowOnlyDefinedHttpStatusCodeInResponse == true && !Enum.IsDefined(typeof(HttpStatusCode), code)) if (options.AllowOnlyDefinedHttpStatusCodeInResponse == true && !Enum.IsDefined(typeof(HttpStatusCode), code))
{ {
return (int)HttpStatusCode.OK; return (int)HttpStatusCode.OK;
} }
@@ -165,8 +148,13 @@ namespace WireMock.Owin.Mappers
return (bodyData.Encoding ?? _utf8NoBom).GetBytes(bodyData.BodyAsString!); return (bodyData.Encoding ?? _utf8NoBom).GetBytes(bodyData.BodyAsString!);
case BodyType.Json: case BodyType.Json:
var formatting = bodyData.BodyAsJsonIndented == true ? Formatting.Indented : Formatting.None; var jsonConverterOptions = new JsonConverterOptions
var jsonBody = JsonConvert.SerializeObject(bodyData.BodyAsJson, new JsonSerializerSettings { Formatting = formatting, NullValueHandling = NullValueHandling.Ignore }); {
WriteIndented = bodyData.BodyAsJsonIndented == true,
IgnoreNullValues = true
};
var jsonBody = options.DefaultJsonSerializer.Serialize(bodyData.BodyAsJson!, jsonConverterOptions);
return (bodyData.Encoding ?? _utf8NoBom).GetBytes(jsonBody); return (bodyData.Encoding ?? _utf8NoBom).GetBytes(jsonBody);
case BodyType.ProtoBuf: case BodyType.ProtoBuf:
@@ -181,10 +169,10 @@ namespace WireMock.Owin.Mappers
return bodyData.BodyAsBytes; return bodyData.BodyAsBytes;
case BodyType.File: case BodyType.File:
return _options.FileSystemHandler?.ReadResponseBodyAsFile(bodyData.BodyAsFile!); return options.FileSystemHandler?.ReadResponseBodyAsFile(bodyData.BodyAsFile!);
case BodyType.MultiPart: case BodyType.MultiPart:
_options.Logger.Warn("MultiPart body type is not handled!"); options.Logger.Warn("MultiPart body type is not handled!");
break; break;
case BodyType.None: case BodyType.None:
@@ -254,4 +242,3 @@ namespace WireMock.Owin.Mappers
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();
@@ -89,14 +90,13 @@ internal class MappingMatcher(IWireMockMiddlewareOptions options, IRandomizerDou
private string? GetNextState(IMapping mapping) private string? GetNextState(IMapping mapping)
{ {
// If the mapping does not have a scenario or _options.Scenarios does not contain this scenario from the mapping, // If the mapping does not have a scenario or the store does not contain this scenario,
// just return null to indicate that there is no next state. // just return null to indicate that there is no next state.
if (mapping.Scenario == null || !_options.Scenarios.ContainsKey(mapping.Scenario)) if (mapping.Scenario == null)
{ {
return null; return null;
} }
// Else just return the next state return _options.ScenarioStateStore.TryGet(mapping.Scenario, out var state) ? state.NextState : null;
return _options.Scenarios[mapping.Scenario].NextState;
} }
} }

View File

@@ -81,9 +81,9 @@ internal class WireMockMiddleware(
} }
// Set scenario start // Set scenario start
if (!options.Scenarios.ContainsKey(mapping.Scenario) && mapping.IsStartState) if (!options.ScenarioStateStore.ContainsKey(mapping.Scenario) && mapping.IsStartState)
{ {
options.Scenarios.TryAdd(mapping.Scenario, new ScenarioState options.ScenarioStateStore.TryAdd(mapping.Scenario, new ScenarioState
{ {
Name = mapping.Scenario Name = mapping.Scenario
}); });
@@ -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);
@@ -233,8 +236,8 @@ internal class WireMockMiddleware(
private void UpdateScenarioState(IMapping mapping) private void UpdateScenarioState(IMapping mapping)
{ {
var scenario = options.Scenarios[mapping.Scenario!]; options.ScenarioStateStore.Update(mapping.Scenario!, scenario =>
{
// Increase the number of times this state has been executed // Increase the number of times this state has been executed
scenario.Counter++; scenario.Counter++;
@@ -248,5 +251,6 @@ internal class WireMockMiddleware(
// Else just update Started and Finished // Else just update Started and Finished
scenario.Started = true; scenario.Started = true;
scenario.Finished = mapping.NextState == null; scenario.Finished = mapping.NextState == null;
});
} }
} }

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;
@@ -27,7 +29,7 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions
public ConcurrentDictionary<Guid, IMapping> Mappings { get; } = new ConcurrentDictionary<Guid, IMapping>(); public ConcurrentDictionary<Guid, IMapping> Mappings { get; } = new ConcurrentDictionary<Guid, IMapping>();
public ConcurrentDictionary<string, ScenarioState> Scenarios { get; } = new(StringComparer.OrdinalIgnoreCase); public IScenarioStateStore ScenarioStateStore { get; set; } = new InMemoryScenarioStateStore();
public ConcurrentObservableCollection<LogEntry> LogEntries { get; } = new(); public ConcurrentObservableCollection<LogEntry> LogEntries { get; } = new();
@@ -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

@@ -27,6 +27,7 @@ internal static class WireMockMiddlewareOptionsHelper
options.FileSystemHandler = settings.FileSystemHandler; options.FileSystemHandler = settings.FileSystemHandler;
options.HandleRequestsSynchronously = settings.HandleRequestsSynchronously; options.HandleRequestsSynchronously = settings.HandleRequestsSynchronously;
options.Logger = settings.Logger; options.Logger = settings.Logger;
options.ScenarioStateStore = settings.ScenarioStateStore;
options.MaxRequestLogCount = settings.MaxRequestLogCount; options.MaxRequestLogCount = settings.MaxRequestLogCount;
options.PostWireMockMiddlewareInit = settings.PostWireMockMiddlewareInit; options.PostWireMockMiddlewareInit = settings.PostWireMockMiddlewareInit;
options.PreWireMockMiddlewareInit = settings.PreWireMockMiddlewareInit; options.PreWireMockMiddlewareInit = settings.PreWireMockMiddlewareInit;

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)!;
internal static T[] DeserializeObjectToArray<T>(object value) case JsonType.Object:
{ var singleResult = jsonConverter.Deserialize<T>(value, JsonSerializationConstants.JsonConverterOptionsWithDateParsingNone);
if (value is JArray jArray)
{
return jArray.ToObject<T[]>()!;
}
if (value is JObject jObject)
{
var singleResult = jObject.ToObject<T>();
return [singleResult!]; return [singleResult!];
}
#if NETSTANDARD2_0_OR_GREATER || NETCOREAPP3_1_OR_GREATER || NET6_0_OR_GREATER || NET461 default:
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."); throw new InvalidOperationException("Cannot deserialize the provided value to an array or object.");
} }
} }
internal T[] DeserializeObjectToArray<T>(object value)
{
var json = jsonConverter.Serialize(value, JsonSerializationConstants.JsonConverterOptionsWithDateParsingNone);
return DeserializeJsonToArray<T>(json);
}
}

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));
@@ -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
@@ -672,7 +717,7 @@ public partial class WireMockServer
#region Scenarios #region Scenarios
private IResponseMessage ScenariosGet(HttpContext _, IRequestMessage requestMessage) private IResponseMessage ScenariosGet(HttpContext _, IRequestMessage requestMessage)
{ {
var scenariosStates = Scenarios.Values.Select(s => new ScenarioStateModel var scenariosStates = Scenarios.Select(s => new ScenarioStateModel
{ {
Name = s.Name, Name = s.Name,
NextState = s.NextState, NextState = s.NextState,
@@ -705,7 +750,7 @@ public partial class WireMockServer
private IResponseMessage ScenariosSetState(HttpContext _, IRequestMessage requestMessage) private IResponseMessage ScenariosSetState(HttpContext _, IRequestMessage requestMessage)
{ {
var name = Enumerable.Reverse(requestMessage.Path.Split('/')).Skip(1).First(); var name = Enumerable.Reverse(requestMessage.Path.Split('/')).Skip(1).First();
if (!_options.Scenarios.ContainsKey(name)) if (!_options.ScenarioStateStore.ContainsKey(name))
{ {
ResponseMessageBuilder.Create(HttpStatusCode.NotFound, $"No scenario found by name '{name}'."); ResponseMessageBuilder.Create(HttpStatusCode.NotFound, $"No scenario found by name '{name}'.");
} }
@@ -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

@@ -2,10 +2,7 @@
// This source file is based on mock4net by Alexandre Victoor which is licensed under the Apache 2.0 License. // This source file is based on mock4net by Alexandre Victoor which is licensed under the Apache 2.0 License.
// For more details see 'mock4net/LICENSE.txt' and 'mock4net/readme.md' in this project root. // For more details see 'mock4net/LICENSE.txt' and 'mock4net/readme.md' in this project root.
using System.Collections.Concurrent;
using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using AnyOfTypes; using AnyOfTypes;
using JetBrains.Annotations; using JetBrains.Annotations;
using JsonConverter.Newtonsoft.Json; using JsonConverter.Newtonsoft.Json;
@@ -93,7 +90,8 @@ public partial class WireMockServer : IWireMockServer
/// Gets the scenarios. /// Gets the scenarios.
/// </summary> /// </summary>
[PublicAPI] [PublicAPI]
public ConcurrentDictionary<string, ScenarioState> Scenarios => new(_options.Scenarios); public IReadOnlyList<ScenarioState> Scenarios =>
_options.ScenarioStateStore.GetAll();
#region IDisposable Members #region IDisposable Members
/// <summary> /// <summary>
@@ -416,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();
@@ -580,14 +579,14 @@ public partial class WireMockServer : IWireMockServer
[PublicAPI] [PublicAPI]
public void ResetScenarios() public void ResetScenarios()
{ {
_options.Scenarios.Clear(); _options.ScenarioStateStore.Clear();
} }
/// <inheritdoc /> /// <inheritdoc />
[PublicAPI] [PublicAPI]
public bool ResetScenario(string name) public bool ResetScenario(string name)
{ {
return _options.Scenarios.ContainsKey(name) && _options.Scenarios.TryRemove(name, out _); return _options.ScenarioStateStore.TryRemove(name);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -599,7 +598,7 @@ public partial class WireMockServer : IWireMockServer
return ResetScenario(name); return ResetScenario(name);
} }
_options.Scenarios.AddOrUpdate( _options.ScenarioStateStore.AddOrUpdate(
name, name,
_ => new ScenarioState _ => new ScenarioState
{ {

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

@@ -0,0 +1,68 @@
// Copyright © WireMock.Net
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
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
{
private readonly ConcurrentDictionary<string, ScenarioState> _scenarios = new(StringComparer.OrdinalIgnoreCase);
/// <inheritdoc />
public bool TryGet(string name, [NotNullWhen(true)] out ScenarioState? state)
{
return _scenarios.TryGetValue(name, out state);
}
/// <inheritdoc />
public IReadOnlyList<ScenarioState> GetAll()
{
return _scenarios.Values.ToArray();
}
/// <inheritdoc />
public bool ContainsKey(string name)
{
return _scenarios.ContainsKey(name);
}
/// <inheritdoc />
public bool TryAdd(string name, ScenarioState scenarioState)
{
return _scenarios.TryAdd(name, scenarioState);
}
/// <inheritdoc />
public ScenarioState AddOrUpdate(string name, Func<string, ScenarioState> addFactory, Func<string, ScenarioState, ScenarioState> updateFactory)
{
return _scenarios.AddOrUpdate(name, addFactory, updateFactory);
}
/// <inheritdoc />
public ScenarioState? Update(string name, Action<ScenarioState> updateAction)
{
if (_scenarios.TryGetValue(name, out var state))
{
updateAction(state);
return state;
}
return null;
}
/// <inheritdoc />
public bool TryRemove(string name)
{
return _scenarios.TryRemove(name, out _);
}
/// <inheritdoc />
public void 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

@@ -1,6 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Linq;
using Stef.Validation; using Stef.Validation;
using WireMock.Extensions; using WireMock.Extensions;
using WireMock.Matchers.Request; using WireMock.Matchers.Request;
@@ -119,7 +118,7 @@ public class MatchResult
return new MatchDetail return new MatchDetail
{ {
Name = Name, Name = Name,
MatcherType = typeof(MatchResult), MatcherType = typeof(MatchResult).Name,
Score = Score, Score = Score,
Exception = Exception, Exception = Exception,
MatchDetails = MatchResults?.Select(mr => mr.ToMatchDetail()).ToArray() MatchDetails = MatchResults?.Select(mr => mr.ToMatchDetail()).ToArray()

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,17 @@ 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]
[JsonIgnore]
public IScenarioStateStore ScenarioStateStore { get; set; } = new InMemoryScenarioStateStore();
/// <summary> /// <summary>
/// Action which can be used to add additional Handlebars registrations. [Optional] /// Action which can be used to add additional Handlebars registrations. [Optional]
/// </summary> /// </summary>
@@ -351,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

@@ -1,6 +1,7 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Newtonsoft.Json.Linq;
using WireMock.Admin.Mappings; using WireMock.Admin.Mappings;
using WireMock.Admin.Requests; using WireMock.Admin.Requests;
using WireMock.Types; using WireMock.Types;
@@ -9,10 +10,13 @@ namespace WireMock.Net.Json;
[JsonSourceGenerationOptions(WriteIndented = true)] [JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(EncodingModel))] [JsonSerializable(typeof(EncodingModel))]
[JsonSerializable(typeof(JArray))]
[JsonSerializable(typeof(JObject))]
[JsonSerializable(typeof(LogEntryModel))] [JsonSerializable(typeof(LogEntryModel))]
[JsonSerializable(typeof(LogRequestModel))] [JsonSerializable(typeof(LogRequestModel))]
[JsonSerializable(typeof(LogResponseModel))] [JsonSerializable(typeof(LogResponseModel))]
[JsonSerializable(typeof(LogRequestMatchModel))] [JsonSerializable(typeof(LogRequestMatchModel))]
[JsonSerializable(typeof(StatusModel))]
[JsonSerializable(typeof(WireMockList<string>))] [JsonSerializable(typeof(WireMockList<string>))]
internal partial class SourceGenerationContext : JsonSerializerContext internal partial class SourceGenerationContext : JsonSerializerContext
{ {

View File

@@ -1,6 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
using System.Text.Json; using System.Text.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using WireMock.Admin.Requests; using WireMock.Admin.Requests;
@@ -11,11 +10,6 @@ namespace WireMock.Net;
public class WireMockLogger : IWireMockLogger public class WireMockLogger : IWireMockLogger
{ {
private readonly JsonSerializerOptions _options = new()
{
WriteIndented = true
};
private readonly ILogger _logger; private readonly ILogger _logger;
public WireMockLogger(ILogger logger) public WireMockLogger(ILogger logger)

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();
}
}

Some files were not shown because too many files have changed in this diff Show More