Compare commits

..

48 Commits

Author SHA1 Message Date
Stef Heyenrath
5f6f5663e1 mm 2026-01-02 21:37:07 +01:00
Stef Heyenrath
803e983645 Merge branch 'master' into MappingSerializer 2026-01-02 21:32:14 +01:00
Stef Heyenrath
c88e7378a7 1.22.0 2026-01-02 21:30:59 +01:00
Vadim Hatsura
b090296559 chore(testcontainers): bump up Testcontainers to version 4.10.0 (#1412) 2026-01-02 21:25:28 +01:00
Stef Heyenrath
8f81fb0d96 test 2026-01-02 21:23:12 +01:00
Stef Heyenrath
e5afd69f7c 1.21.0 2025-12-25 15:00:54 +01:00
Stef Heyenrath
b4af082586 Merge branch 'master' into MappingSerializer 2025-12-25 13:57:28 +01:00
Stef Heyenrath
f38133d7a4 Fix readyness-check for Testcontainers (#1408)
* Add XUnit Logging to TestcontainersTests

* .
2025-12-25 13:56:29 +01:00
Stef Heyenrath
3011509c51 Merge branch 'master' into MappingSerializer 2025-12-24 16:59:57 +01:00
Stef Heyenrath
597c95000e vmImage: 'windows-2025' (#1407) 2025-12-24 16:59:02 +01:00
Stef Heyenrath
ad150280fd Merge branch 'master' into MappingSerializer 2025-12-24 15:30:00 +01:00
Stef Heyenrath
4617b99c30 [Collection("Grpc")] 2025-12-24 12:32:56 +01:00
Stef Heyenrath
6bb506e1f8 Merge branch 'master' into MappingSerializer 2025-12-24 12:18:04 +01:00
Stef Heyenrath
ffd4d89946 Re-enable TestcontainersTestsGrpc (#1406)
* Re-enable TestcontainersTestsGrpc

* //[Collection("Grpc")]
2025-12-24 12:16:56 +01:00
Stef Heyenrath
2d46c86f47 1.20.0 2025-12-24 10:11:53 +01:00
Stef Heyenrath
75f4fbe9d0 Fix Testcontainers AddProtoDefinition (#1405)
* Fix Testcontainers AddProtoDefinition

* .

* UntilHttpRequestIsSucceeded

* WireMockContainer.ContainerPort

* System.Net/System.Net.Http

* ...

* WithWaitStrategy

* MaxHealthCheckRetries

* for

* _adminApi

* static

* ...

* testOutputHelper.WriteLine("Dumping WireMock logs:");

* Console.WriteLine(

* testOutputHelper.WriteLine("Dumping WireMock.Net mappings:");

* fix WithWaitStrategy

* [Fact]

* <PackageReference Include="ProtoBufJsonConverter" Version="0.11.0" />

* [Collection("Grpc")] / [Fact(Skip = "TODO")]

* ...
2025-12-24 10:09:30 +01:00
Stef Heyenrath
b40fce694d Merge branch 'master' into MappingSerializer 2025-12-21 09:50:09 +01:00
Stef Heyenrath
16e3872402 Run the Grpc TestcontainersTests sequential (#1402) 2025-12-21 09:40:16 +01:00
Stef Heyenrath
79f758fd4d Merge branch 'master' into MappingSerializer 2025-12-20 13:44:43 +01:00
Stef Heyenrath
4c797c328f Add WireMock.Net.NUnit project (#1400)
* Add WireMock.Net.NUnit project

* <Version>0.0.1-preview-01</Version>

* --v
2025-12-20 13:43:54 +01:00
Stef Heyenrath
aafe2339e4 Merge branch 'master' into MappingSerializer 2025-12-20 12:09:09 +01:00
Stef Heyenrath
a5e75a7278 Fix Grpc tests (#1401)
* Fix some Grpc tests

* await Task.Delay(1000);

* ports
2025-12-20 12:08:41 +01:00
Stef Heyenrath
708af55051 0.8.0 2025-12-20 10:32:26 +01:00
Stef Heyenrath
e6e4138466 Merge branch 'master' into MappingSerializer 2025-12-20 08:16:30 +01:00
Stef Heyenrath
56f65c19e2 Upgrade RamlToOpenApiConverter and YamlDotNet (#1399)
* Upgrade RamlToOpenApiConverter and YamlDotNet

* fix
2025-12-19 18:33:58 +01:00
Stef Heyenrath
6cc8c04486 Merge branch 'master' into MappingSerializer 2025-12-19 17:55:16 +01:00
Stef Heyenrath
6aef4816a5 WireMockServer_WithRequiredClientCertificates_Should_Work_Correct --> IgnoreOnContinuousIntegrationFact 2025-12-19 17:52:20 +01:00
Stef Heyenrath
dd4770a78b . 2025-12-18 18:15:37 +01:00
Stef Heyenrath
de15e0d48e json 2025-12-16 23:36:19 +01:00
Stef Heyenrath
f6452d1dce MappingSerializer 2025-12-16 22:37:26 +01:00
Stef Heyenrath
197a211a52 TestcontainersTests 2025-12-13 11:48:48 +01:00
Stef Heyenrath
3cfeec6035 1.19.0 2025-12-12 11:16:38 +01:00
Stef Heyenrath
b57d5e7548 WireMockContainerBuilder: allow only docker images named wiremock (#1392) 2025-12-11 22:21:39 +01:00
Stef Heyenrath
36b89afce5 fix CI link in Readme 2025-12-11 11:25:28 +01:00
Stef Heyenrath
e2acac55a4 Update WireMockContainerBuilder (WithImage and WithCustomImage) (#1391)
* Update WireMockContainerBuilder (WithImage and WithCustomImage)

* .
2025-12-11 10:55:31 +01:00
Stef Heyenrath
ceabd27ce0 1.18.0 2025-12-09 18:28:28 +01:00
Stef Heyenrath
f8e2c7ee90 Add WithBodyAsType to RequestMatcher (#1388)
* Add WithBody<T>

* .

* t

* t2
2025-12-08 19:15:14 +01:00
Stef Heyenrath
c25d8f33d2 1.17.0 2025-12-07 10:55:07 +01:00
Stef Heyenrath
6da190e596 Aspire: Add WithProtoDefinition to support proto definition at server level (#1383)
* Add property UseHttp2 to WireMockServerArguments

* .

* additionalUrls

* ok?

* WireMockServerArguments

* fx

* AddProtoDefinition

* ...

* FIX

* Always add the lifecycle hook to support dynamic mappings and proto definitions
2025-12-07 10:50:11 +01:00
Stef Heyenrath
44388ce80d Fix random delay in mapping json file (#1386) 2025-11-25 20:54:06 +01:00
Stef Heyenrath
5e25ca767d Fix BuildId (#1384) 2025-11-23 11:19:39 +01:00
Stef Heyenrath
0cc583a4a3 WireMock.Net.xUnit.v3 (netstandard2.0) 2025-11-18 18:52:07 +01:00
Stef Heyenrath
f9633adac1 1.16.0 2025-11-18 18:45:12 +01:00
Stef Heyenrath
37bad618a3 Add WireMock.Net.xUnit.v3 project (#1380)
* Add WireMock.Net.xUnit.v3 project

* .
2025-11-18 18:42:28 +01:00
Johannes Häggqvist
8e69f36f04 Add WireMockHealthCheck in WireMock.Net.Aspire (#1375)
* Add WireMockHealthCheck

For use with Aspire, to make WaitFor(wiremock) more useful.
Calls /__admin/health and checks the result, as well as checks if mappings using AdminApiMappingBuilder has been submitted to the server.

This created a catch-22 problem where the mappings were not submitted until the health check was healthy, but the health check was not healthy until the mappings were submitted.

To avoid this, the WireMockServerLifecycleHook class has been slightly re-arranged, and is now using the AfterEndpointsAllocatedAsync callback rather than the AfterResourcesCreatedAsync callback. Within which a separate Task is created that waits until the server is ready and submits the mappings.

* Move WireMockMappingState to its own file

* Dispose the cancellation tokens in WireMockServerLifecycleHook
2025-11-17 20:14:42 +01:00
Stef Heyenrath
21601889e0 Check if the path is valid when using WithPath(...) (#1377) 2025-11-08 09:02:00 +01:00
Stef Heyenrath
dfeabf228e WireMock.Net.OpenApiParser : support Examples (#1366) 2025-11-08 07:45:38 +01:00
Stef Heyenrath
1feb0ade70 Fix wiki links (#1373)
* Change all links from wiki to documention website

* .

* doc

* ws
2025-10-26 10:13:58 +01:00
97 changed files with 3874 additions and 8182 deletions

View File

@@ -1,3 +1,43 @@
# 1.22.0 (02 January 2026)
- [#1412](https://github.com/wiremock/WireMock.Net/pull/1412) - chore(testcontainers): bump up Testcontainers to version 4.10.0 [feature] contributed by [vhatsura](https://github.com/vhatsura)
- [#1411](https://github.com/wiremock/WireMock.Net/issues/1411) - WireMock.Net.Testcontainers isn't compatible with Testcontainers 4.10.0 [bug]
# 1.21.0 (25 December 2025)
- [#1408](https://github.com/wiremock/WireMock.Net/pull/1408) - Fix readyness-check for Testcontainers [bug] contributed by [StefH](https://github.com/StefH)
# 1.20.0 (24 December 2025)
- [#1399](https://github.com/wiremock/WireMock.Net/pull/1399) - Upgrade RamlToOpenApiConverter and YamlDotNet [feature] contributed by [StefH](https://github.com/StefH)
- [#1400](https://github.com/wiremock/WireMock.Net/pull/1400) - Add WireMock.Net.NUnit project [feature] contributed by [StefH](https://github.com/StefH)
- [#1405](https://github.com/wiremock/WireMock.Net/pull/1405) - Fix Testcontainers AddProtoDefinition [bug] contributed by [StefH](https://github.com/StefH)
- [#1398](https://github.com/wiremock/WireMock.Net/issues/1398) - Upgrade YamlDotNet dependency [feature]
- [#1404](https://github.com/wiremock/WireMock.Net/issues/1404) - An exception occurs when adding multiple proto definitions in the TestContainer. [bug]
# 1.19.0 (12 December 2025)
- [#1391](https://github.com/wiremock/WireMock.Net/pull/1391) - Update WireMockContainerBuilder (WithImage and WithCustomImage) [feature] contributed by [StefH](https://github.com/StefH)
- [#1392](https://github.com/wiremock/WireMock.Net/pull/1392) - WireMockContainerBuilder: allow all docker images named wiremock [feature] contributed by [StefH](https://github.com/StefH)
- [#1390](https://github.com/wiremock/WireMock.Net/issues/1390) - Unable to build WireMockContainerBuilder with custom image [feature]
# 1.18.0 (09 December 2025)
- [#1388](https://github.com/wiremock/WireMock.Net/pull/1388) - Add WithBodyAsType to RequestMatcher [feature] contributed by [StefH](https://github.com/StefH)
# 1.17.0 (07 December 2025)
- [#1383](https://github.com/wiremock/WireMock.Net/pull/1383) - Aspire: Add WithProtoDefinition to support proto definition at server level [feature] contributed by [StefH](https://github.com/StefH)
- [#1386](https://github.com/wiremock/WireMock.Net/pull/1386) - Fix random delay in mapping json file [bug] contributed by [StefH](https://github.com/StefH)
- [#1274](https://github.com/wiremock/WireMock.Net/issues/1274) - .WithMappings to mount volume is not working for GRPC [bug]
- [#1381](https://github.com/wiremock/WireMock.Net/issues/1381) - Downstream dependencies missing after 1.16.0 release [bug]
- [#1382](https://github.com/wiremock/WireMock.Net/issues/1382) - Does Aspire support enabling HTTP/2? [feature]
- [#1385](https://github.com/wiremock/WireMock.Net/issues/1385) - Do delays and probabilities show in saved static mappings? [bug]
- [#1387](https://github.com/wiremock/WireMock.Net/issues/1387) - Tests failing with TaskCanceledException on Windows Server 2025 Build 7171 [bug]
# 1.16.0 (18 November 2025)
- [#1366](https://github.com/wiremock/WireMock.Net/pull/1366) - WireMock.Net.OpenApiParser : support Examples [feature] contributed by [StefH](https://github.com/StefH)
- [#1375](https://github.com/wiremock/WireMock.Net/pull/1375) - Add WireMockHealthCheck in WireMock.Net.Aspire [feature] contributed by [Zguy](https://github.com/Zguy)
- [#1377](https://github.com/wiremock/WireMock.Net/pull/1377) - Check if the path is valid when using WithPath(...) [feature] contributed by [StefH](https://github.com/StefH)
- [#1380](https://github.com/wiremock/WireMock.Net/pull/1380) - Add WireMock.Net.xUnit.v3 project [feature] contributed by [StefH](https://github.com/StefH)
- [#1364](https://github.com/wiremock/WireMock.Net/issues/1364) - Choosing examples from open api specification for responses. [feature]
- [#1376](https://github.com/wiremock/WireMock.Net/issues/1376) - AdminApiMappingBuilder `WithPath` should add the starting `/` if missing [feature]
- [#1379](https://github.com/wiremock/WireMock.Net/issues/1379) - xUnit v3 [feature]
# 1.15.0 (22 October 2025)
- [#1367](https://github.com/wiremock/WireMock.Net/pull/1367) - Fix WithProbability logic [bug] contributed by [StefH](https://github.com/StefH)
- [#1370](https://github.com/wiremock/WireMock.Net/pull/1370) - Support Testcontainers 4.8.0 [bug] contributed by [MD-V](https://github.com/MD-V)

View File

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

View File

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

View File

@@ -15,48 +15,48 @@ Lightweight Http Mocking Server for .NET, inspired by WireMock.org (from the Jav
### :star: Stubbing
A core feature of WireMock.Net is the ability to return predefined HTTP responses for requests matching criteria.
See [Wiki : Stubbing](https://github.com/wiremock/WireMock.Net/wiki/Stubbing).
See [Stubbing](https://wiremock.org/dotnet/stubbing).
### :star: Request Matching
WireMock.Net support advanced request-matching logic, see [Wiki : Request Matching](https://github.com/wiremock/WireMock.Net/wiki/Request-Matching).
WireMock.Net support advanced request-matching logic, see [Request Matching](https://wiremock.org/dotnet/request-matching).
### :star: Response Templating
The response which is returned WireMock.Net can be changed using templating. This is described here [Wiki : Response Templating](https://github.com/wiremock/WireMock.Net/wiki/Response-Templating).
The response which is returned WireMock.Net can be changed using templating. This is described here [Response Templating](https://wiremock.org/dotnet/response-templating).
### :star: Admin API Reference
The WireMock admin API provides functionality to define the mappings via a http interface see [Wiki : Admin API Reference](https://github.com/StefH/WireMock.Net/wiki/Admin-API-Reference).
The WireMock admin API provides functionality to define the mappings via a http interface see [Admin API Reference](https://wiremock.org/dotnet/admin-api-reference).
### :star: Using
WireMock.Net can be used in several ways:
#### UnitTesting
You can use your favorite test framework and use WireMock within your tests, see
[Wiki : UnitTesting](https://github.com/StefH/WireMock.Net/wiki/Using-WireMock-in-UnitTests).
[UnitTesting](https://wiremock.org/dotnet/using-wiremock-in-unittests).
### Unit/Integration Testing using Testcontainers.DotNet
See [Wiki : WireMock.Net.Testcontainers](https://github.com/wiremock/WireMock.Net/wiki/Using-WireMock.Net.Testcontainers) on how to build a WireMock.Net Docker container which can be used in Unit/Integration testing.
See [WireMock.Net.Testcontainers](https://wiremock.org/dotnet/using-wiremock-net-testcontainers/) on how to build a WireMock.Net Docker container which can be used in Unit/Integration testing.
### Unit/Integration Testing using an an Aspire Distributed Application
See [Wiki : WireMock.Net.Aspire](https://github.com/wiremock/WireMock.Net/wiki/Using-WireMock.Net.Aspire) on how to use WireMock.Net as an Aspire Hosted application to do Unit/Integration testing.
See [WireMock.Net.Aspire](https://wiremock.org/dotnet/using-wiremock-net-Aspire) on how to use WireMock.Net as an Aspire Hosted application to do Unit/Integration testing.
#### As a dotnet tool
It's simple to install WireMock.Net as (global) dotnet tool, see [Wiki : dotnet tool](https://github.com/StefH/WireMock.Net/wiki/WireMock-as-dotnet-tool).
It's simple to install WireMock.Net as (global) dotnet tool, see [dotnet tool](https://wiremock.org/dotnet/wiremock-as-dotnet-tool).
#### As standalone process / console application
This is quite straight forward to launch a mock server within a console application, see [Wiki : Standalone Process](https://github.com/StefH/WireMock.Net/wiki/WireMock-as-a-standalone-process).
This is quite straight forward to launch a mock server within a console application, see [Standalone Process](https://wiremock.org/dotnet/wiremock-as-a-standalone-process).
#### As a Windows Service
You can also run WireMock.Net as a Windows Service, follow this [WireMock-as-a-Windows-Service](https://github.com/wiremock/WireMock.Net/wiki/WireMock-as-a-Windows-Service).
You can also run WireMock.Net as a Windows Service, follow this [Windows Service](https://wiremock.org/dotnet/wiremock-as-a-windows-service).
#### As a Web Job in Azure or application in IIS
See this link [WireMock-as-a-(Azure)-Web-App](https://github.com/wiremock/WireMock.Net/wiki/WireMock-as-a-(Azure)-Web-App)
See this link [WireMock-as-a-(Azure)-Web-App](https://wiremock.org/dotnet/wiremock-as-a-azure-web-app/)
#### In a docker container
There is also a Linux and Windows-Nano container available at [hub.docker.com](https://hub.docker.com/r/sheyenrath).
For more details see also [Docker](https://github.com/wiremock/WireMock.Net-docker).
#### HTTPS / SSL
More details on using HTTPS (SSL) can be found here [Wiki : HTTPS](https://github.com/wiremock/WireMock.Net/wiki/Using-HTTPS-(SSL))
More details on using HTTPS (SSL) can be found here [HTTPS](https://wiremock.org/dotnet/using-https-ssl/)
## :books: Documentation
For more info, see also this WIKI page: [What is WireMock.Net](https://github.com/wiremock/WireMock.Net/wiki/What-Is-WireMock.Net).
For more info, see also this documentation page: [What is WireMock.Net](https://wiremock.org/dotnet/what-is-wiremock-net/).

View File

@@ -1,6 +1,5 @@
# 1.15.0 (22 October 2025)
- #1367 Fix WithProbability logic [bug]
- #1370 Support Testcontainers 4.8.0 [bug]
- #1126 Request matching WithProbability strange behaviour [bug]
# 1.22.0 (02 January 2026)
- #1412 chore(testcontainers): bump up Testcontainers to version 4.10.0 [feature]
- #1411 WireMock.Net.Testcontainers isn't compatible with Testcontainers 4.10.0 [bug]
The full release notes can be found here: https://github.com/wiremock/WireMock.Net/blob/master/CHANGELOG.md

View File

@@ -3,7 +3,7 @@ A C# .NET version based on [mock4net](https://github.com/alexvictoor/mock4net) w
---
:books: <strong>Full documentation can now be found at <a href="https://wiremock.org/dotnet/" title="WireMock.Net docs">wiremock.org</a>
### :books: Full documentation can now be found at [wiremock.org](https://wiremock.org/dotnet)
---
@@ -33,7 +33,7 @@ A C# .NET version based on [mock4net](https://github.com/alexvictoor/mock4net) w
| &nbsp;&nbsp;**Issues** | [![GitHub issues](https://img.shields.io/github/issues/WireMock-Net/WireMock.Net.svg)](https://github.com/wiremock/WireMock.Net/issues) |
| | |
| ***Quality*** | &nbsp; |
| &nbsp;&nbsp;**Build Azure** | [![Build Status Azure](https://stef.visualstudio.com/WireMock.Net/_apis/build/status/WireMock.Net)](https://stef.visualstudio.com/WireMock.Net/_build/latest?definitionId=7) |
| &nbsp;&nbsp;**Build Azure** | [![Build Status Azure](https://stef.visualstudio.com/WireMock.Net/_apis/build/status/WireMock.Net)](https://stef.visualstudio.com/WireMock.Net/_build/latest?definitionId=61) |
| &nbsp;&nbsp;**Quality** | [![Sonar Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=WireMock-Net_WireMock.Net&metric=alert_status)](https://sonarcloud.io/project/issues?id=WireMock-Net_WireMock.Net) [![CodeFactor](https://www.codefactor.io/repository/github/wiremock/wiremock.net/badge)](https://www.codefactor.io/repository/github/wiremock/wiremock.net) |
| &nbsp;&nbsp;**Sonar Bugs** | [![Sonar Bugs](https://sonarcloud.io/api/project_badges/measure?project=WireMock-Net_WireMock.Net&metric=bugs)](https://sonarcloud.io/project/issues?id=WireMock-Net_WireMock.Net&resolved=false&types=BUG) [![Sonar Code Smells](https://sonarcloud.io/api/project_badges/measure?project=WireMock-Net_WireMock.Net&metric=code_smells)](https://sonarcloud.io/project/issues?id=WireMock-Net_WireMock.Net&resolved=false&types=CODE_SMELL) |
| &nbsp;&nbsp;**Coverage** | [![Sonar Coverage](https://sonarcloud.io/api/project_badges/measure?project=WireMock-Net_WireMock.Net&metric=coverage)](https://sonarcloud.io/component_measures?id=WireMock-Net_WireMock.Net&metric=coverage) [![codecov](https://codecov.io/gh/wiremock/WireMock.Net/branch/master/graph/badge.svg)](https://codecov.io/gh/wiremock/WireMock.Net)|
@@ -41,7 +41,7 @@ A C# .NET version based on [mock4net](https://github.com/alexvictoor/mock4net) w
### :package: NuGet packages
| | Official | Preview [:information_source:](https://github.com/wiremock/WireMock.Net/wiki/MyGet-preview-versions) |
| | Official | Preview [:information_source:](https://wiremock.org/dotnet/MyGet-preview-versions) |
| - | - | - |
| &nbsp;&nbsp;**WireMock.Net** | [![NuGet Badge WireMock.Net](https://img.shields.io/nuget/v/WireMock.Net)](https://www.nuget.org/packages/WireMock.Net) | [![MyGet Badge WireMock.Net](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net)
| &nbsp;&nbsp;**WireMock.Net.Minimal** 🔺| [![NuGet Badge WireMock.Net.Minimal](https://img.shields.io/nuget/v/WireMock.Net.Minimal)](https://www.nuget.org/packages/WireMock.Net.Minimal) | [![MyGet Badge WireMock.Net](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.Minimal?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.Minimal)
@@ -53,7 +53,9 @@ A C# .NET version based on [mock4net](https://github.com/alexvictoor/mock4net) w
| &nbsp;&nbsp;**WireMock.Net.AwesomeAssertions** | [![NuGet Badge WireMock.Net.AwesomeAssertions](https://img.shields.io/nuget/v/WireMock.Net.AwesomeAssertions)](https://www.nuget.org/packages/WireMock.Net.AwesomeAssertions) | [![MyGet Badge WireMock.Net.AwesomeAssertions](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.AwesomeAssertions?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.AwesomeAssertions)
| &nbsp;&nbsp;**WireMock.Net.FluentAssertions** | [![NuGet Badge WireMock.Net.FluentAssertions](https://img.shields.io/nuget/v/WireMock.Net.FluentAssertions)](https://www.nuget.org/packages/WireMock.Net.FluentAssertions) | [![MyGet Badge WireMock.Net.FluentAssertions](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.FluentAssertions?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.FluentAssertions)
| &nbsp;&nbsp;**WireMock.Net.xUnit** | [![NuGet Badge WireMock.Net.xUnit](https://img.shields.io/nuget/v/WireMock.Net.xUnit)](https://www.nuget.org/packages/WireMock.Net.xUnit) | [![MyGet Badge WireMock.Net.xUnit](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.xUnit?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.xUnit)
| &nbsp;&nbsp;**WireMock.Net.xUnit.v3** | [![NuGet Badge WireMock.Net.xUnit](https://img.shields.io/nuget/v/WireMock.Net.xUnit.v3)](https://www.nuget.org/packages/WireMock.Net.xUnit.v3) | [![MyGet Badge WireMock.Net.xUnit](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.xUnit.v3?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.xUnit.v3)
| &nbsp;&nbsp;**WireMock.Net.TUnit** | [![NuGet Badge WireMock.Net.TUnit](https://img.shields.io/nuget/v/WireMock.Net.TUnit)](https://www.nuget.org/packages/WireMock.Net.TUnit) | [![MyGet Badge WireMock.Net.TUnit](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.TUnit?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.TUnit)
| &nbsp;&nbsp;**WireMock.Net.NUnit** | [![NuGet Badge WireMock.Net.NUnit](https://img.shields.io/nuget/v/WireMock.Net.NUnit)](https://www.nuget.org/packages/WireMock.Net.NUnit) | [![MyGet Badge WireMock.Net.TUnit](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.NUnit?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.NUnit)
| | | |
| &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)
@@ -93,52 +95,55 @@ To still enable this feature, you need to add the `Environment` category to the
---
## :memo: Development
For the supported frameworks and build information, see [this](https://github.com/wiremock/WireMock.Net/wiki/Development-Information) page.
For the supported frameworks and build information, see [this](https://wiremock.org/dotnet/development-information) page.
## :star: Stubbing
A core feature of WireMock.Net is the ability to return predefined HTTP responses for requests matching criteria.
See [Wiki : Stubbing](https://github.com/wiremock/WireMock.Net/wiki/Stubbing).
See [Stubbing](https://wiremock.org/dotnet/stubbing).
## :star: Request Matching
WireMock.Net support advanced request-matching logic, see [Wiki : Request Matching](https://github.com/wiremock/WireMock.Net/wiki/Request-Matching).
WireMock.Net support advanced request-matching logic, see [Request Matching](https://wiremock.org/dotnet/request-matching).
## :star: Response Templating
The response which is returned WireMock.Net can be changed using templating. This is described here [Wiki : Response Templating](https://github.com/wiremock/WireMock.Net/wiki/Response-Templating).
The response which is returned WireMock.Net can be changed using templating. This is described here [Response Templating](https://wiremock.org/dotnet/response-templating).
## :star: Admin API Reference
The WireMock admin API provides functionality to define the mappings via a http interface see [Wiki : Admin API Reference](https://github.com/StefH/WireMock.Net/wiki/Admin-API-Reference).
The WireMock admin API provides functionality to define the mappings via a http interface see [Admin API Reference](https://wiremock.org/dotnet/admin-api-reference).
## :star: Using
WireMock.Net can be used in several ways:
### UnitTesting
You can use your favorite test framework and use WireMock within your tests, see
[Wiki : UnitTesting](https://github.com/StefH/WireMock.Net/wiki/Using-WireMock-in-UnitTests).
[UnitTesting](https://wiremock.org/dotnet/using-wiremock-in-unittests).
### Unit/Integration Testing using Testcontainers.DotNet
See [Wiki : WireMock.Net.Testcontainers](https://github.com/wiremock/WireMock.Net/wiki/Using-WireMock.Net.Testcontainers) on how to build a WireMock.Net Docker container which can be used in Unit/Integration testing.
See [WireMock.Net.Testcontainers](https://wiremock.org/dotnet/using-wiremock-net-testcontainers/) on how to build a WireMock.Net Docker container which can be used in Unit/Integration testing.
### Unit/Integration Testing using an an Aspire Distributed Application
See [Wiki : WireMock.Net.Aspire](https://github.com/wiremock/WireMock.Net/wiki/Using-WireMock.Net.Aspire) on how to use WireMock.Net as an Aspire Hosted application to do Unit/Integration testing.
See [WireMock.Net.Aspire](https://wiremock.org/dotnet/using-wiremock-net-Aspire) on how to use WireMock.Net as an Aspire Hosted application to do Unit/Integration testing.
### As a dotnet tool
It's simple to install WireMock.Net as (global) dotnet tool, see [Wiki : dotnet tool](https://github.com/StefH/WireMock.Net/wiki/WireMock-as-dotnet-tool).
It's simple to install WireMock.Net as (global) dotnet tool, see [dotnet tool](https://wiremock.org/dotnet/wiremock-as-dotnet-tool).
### As standalone process / console application
This is quite straight forward to launch a mock server within a console application, see [Wiki : Standalone Process](https://github.com/StefH/WireMock.Net/wiki/WireMock-as-a-standalone-process).
This is quite straight forward to launch a mock server within a console application, see [Standalone Process](https://wiremock.org/dotnet/wiremock-as-a-standalone-process).
### As a Windows Service
You can also run WireMock.Net as a Windows Service, follow this [WireMock-as-a-Windows-Service](https://github.com/wiremock/WireMock.Net/wiki/WireMock-as-a-Windows-Service).
You can also run WireMock.Net as a Windows Service, follow this [Windows Service](https://wiremock.org/dotnet/wiremock-as-a-windows-service).
### As a Web Job in Azure or application in IIS
See this link [WireMock-as-a-(Azure)-Web-App](https://github.com/wiremock/WireMock.Net/wiki/WireMock-as-a-(Azure)-Web-App)
See this link [WireMock-as-a-(Azure)-Web-App](https://wiremock.org/dotnet/wiremock-as-a-azure-web-app/)
### In a docker container
There is also a Linux and Windows-Nano container available at [hub.docker.com](https://hub.docker.com/r/sheyenrath).
For more details see also [Docker](https://github.com/wiremock/WireMock.Net-docker).
#### HTTPS / SSL
More details on using HTTPS (SSL) can be found here [Wiki : HTTPS](https://github.com/wiremock/WireMock.Net/wiki/Using-HTTPS-(SSL))
### HTTPS / SSL
More details on using HTTPS (SSL) can be found here [HTTPS](https://wiremock.org/dotnet/using-https-ssl/)
## :books: Documentation
For more info, see also this documentation page: [What is WireMock.Net](https://wiremock.org/dotnet/what-is-wiremock-net/).
---

View File

@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31521.260
# Visual Studio Version 18
VisualStudioVersion = 18.0.11205.157
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8F890C6F-9ACC-438D-928A-AD61CDA862F2}"
EndProject
@@ -144,6 +144,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.Extensions.Rou
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.ProtoBuf", "src\WireMock.Net.ProtoBuf\WireMock.Net.ProtoBuf.csproj", "{B47413AA-55D3-49A7-896A-17ADBFF72407}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.xUnit.v3", "src\WireMock.Net.xUnit.v3\WireMock.Net.xUnit.v3.csproj", "{4F46BD02-BEBC-4B2D-B857-4169AD1FB067}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.NUnit", "src\WireMock.Net.NUnit\WireMock.Net.NUnit.csproj", "{2DBBD70D-8051-441F-92BB-FF9B8B4B4982}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -350,6 +354,14 @@ Global
{B47413AA-55D3-49A7-896A-17ADBFF72407}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B47413AA-55D3-49A7-896A-17ADBFF72407}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B47413AA-55D3-49A7-896A-17ADBFF72407}.Release|Any CPU.Build.0 = Release|Any CPU
{4F46BD02-BEBC-4B2D-B857-4169AD1FB067}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4F46BD02-BEBC-4B2D-B857-4169AD1FB067}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4F46BD02-BEBC-4B2D-B857-4169AD1FB067}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4F46BD02-BEBC-4B2D-B857-4169AD1FB067}.Release|Any CPU.Build.0 = Release|Any CPU
{2DBBD70D-8051-441F-92BB-FF9B8B4B4982}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2DBBD70D-8051-441F-92BB-FF9B8B4B4982}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2DBBD70D-8051-441F-92BB-FF9B8B4B4982}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2DBBD70D-8051-441F-92BB-FF9B8B4B4982}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -407,6 +419,8 @@ Global
{3FCBCA9C-9DB0-4A96-B47E-30470764CC9C} = {0BB8B634-407A-4610-A91F-11586990767A}
{1E874C8F-08A2-493B-8421-619F9A6E9E77} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{B47413AA-55D3-49A7-896A-17ADBFF72407} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{4F46BD02-BEBC-4B2D-B857-4169AD1FB067} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{2DBBD70D-8051-441F-92BB-FF9B8B4B4982} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458}

View File

@@ -100,7 +100,7 @@ jobs:
- job: Windows_Build_Test
pool:
vmImage: 'windows-2022'
vmImage: 'windows-2025'
steps:
- task: UseDotNet@2
@@ -141,9 +141,13 @@ jobs:
dependsOn: Windows_Build_Test
pool:
vmImage: 'windows-2022'
vmImage: 'windows-2025'
steps:
- script: |
echo "BuildId = $(buildId)"
displayName: 'Print buildId'
- task: UseDotNet@2
displayName: Use .NET 8.0
inputs:

View File

@@ -1,5 +1,5 @@
pool:
vmImage: 'windows-2022'
vmImage: 'windows-2025'
variables:
Prerelease: ''

View File

@@ -21,4 +21,13 @@
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.2.0" />
</ItemGroup>
</Project>
<ItemGroup>
<None Update="__admin\mappings\*.proto">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="__admin\mappings\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -4,12 +4,25 @@ var builder = DistributedApplication.CreateBuilder(args);
// IResourceBuilder<ProjectResource> apiService = builder.AddProject<Projects.AspireApp1_ApiService>("apiservice");
var mappingsPath = Path.Combine(Directory.GetCurrentDirectory(), "WireMockMappings");
var mappingsPath = Path.Combine(Directory.GetCurrentDirectory(), "__admin", "mappings");
IResourceBuilder<WireMockServerResource> apiService = builder
.AddWireMock("apiservice", WireMockServerArguments.DefaultPort)
//IResourceBuilder<WireMockServerResource> apiService1 = builder
// //.AddWireMock("apiservice", WireMockServerArguments.DefaultPort)
// .AddWireMock("apiservice1", "http://*:8081", "grpc://*:9091")
// .AsHttp2Service()
// .WithMappingsPath(mappingsPath)
// .WithReadStaticMappings()
// .WithWatchStaticMappings()
// .WithApiMappingBuilder(WeatherForecastApiMock.BuildAsync);
IResourceBuilder<WireMockServerResource> apiService2 = builder
.AddWireMock("apiservice", async args =>
{
args.WithAdditionalUrls("http://*:8081", "grpc://*:9093");
args.WithProtoDefinition("my-greeter", await File.ReadAllTextAsync(Path.Combine(mappingsPath, "greet.proto")));
})
.AsHttp2Service()
.WithMappingsPath(mappingsPath)
.WithReadStaticMappings()
.WithWatchStaticMappings()
.WithApiMappingBuilder(WeatherForecastApiMock.BuildAsync);
@@ -45,6 +58,7 @@ IResourceBuilder<WireMockServerResource> apiService = builder
builder.AddProject<Projects.AspireApp1_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(apiService);
.WithReference(apiService2)
.WaitFor(apiService2);
builder.Build().Run();
await builder.Build().RunAsync();

View File

@@ -0,0 +1,21 @@
syntax = "proto3";
package greet;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
enum PhoneType {
none = 0;
mobile = 1;
home = 2;
}
PhoneType phoneType = 2;
}

View File

@@ -0,0 +1,40 @@
{
"Guid": "351f0240-bba0-4bcb-93c6-1feba0fe0004",
"Title": "ProtoBuf Mapping 4",
"Request": {
"Path": {
"Matchers": [
{
"Name": "WildcardMatcher",
"Pattern": "/greet.Greeter/SayHello",
"IgnoreCase": false
}
]
},
"Methods": [
"POST"
],
"Body": {
"Matcher": {
"Name": "ProtoBufMatcher",
"ProtoBufMessageType": "greet.HelloRequest"
}
}
},
"Response": {
"BodyAsJson": {
"message": "hello {{request.BodyAsJson.name}} {{request.method}}"
},
"UseTransformer": true,
"TransformerType": "Handlebars",
"TransformerReplaceNodeOptions": "EvaluateAndTryToConvert",
"Headers": {
"Content-Type": "application/grpc"
},
"TrailingHeaders": {
"grpc-status": "0"
},
"ProtoBufMessageType": "greet.HelloReply"
},
"ProtoDefinition": "my-greeter"
}

View File

@@ -22,7 +22,7 @@ public class MatcherModel
public object? Pattern { get; set; }
/// <summary>
/// Gets or sets the patterns. Can be array of strings (default) or an array of objects.
/// Gets or sets the patterns. Can be an array of strings (default) or an array of objects.
/// </summary>
public object[]? Patterns { get; set; }

View File

@@ -32,7 +32,7 @@ public class ResponseModel
public object? BodyAsJson { get; set; }
/// <summary>
/// Gets or sets a value indicating whether child objects to be indented according to the Newtonsoft.Json.JsonTextWriter.Indentation and Newtonsoft.Json.JsonTextWriter.IndentChar settings.
/// Gets or sets a value indicating whether the Json Body String needs to be indented.
/// </summary>
public bool? BodyAsJsonIndented { get; set; }

View File

@@ -24,4 +24,13 @@ public class StatusModel
/// The error message.
/// </summary>
public string? Error { get; set; }
/// <summary>
/// Returns a string that represents the current status model, including its unique identifier, status, and error information.
/// </summary>
/// <returns>A string containing the values of the Guid, Status, and Error properties formatted for display.</returns>
public override string ToString()
{
return $"StatusModel [Guid={Guid}, Status={Status}, Error={Error}]";
}
}

View File

@@ -1,6 +1,7 @@
// Copyright © WireMock.Net
using System;
using WireMock.Validators;
// ReSharper disable once CheckNamespace
namespace WireMock.Admin.Mappings;
@@ -94,9 +95,14 @@ public partial class RequestModelBuilder
}
/// <summary>
/// Set the Path.
/// Set the Path. Must start with a forward slash (/).
/// </summary>
public RequestModelBuilder WithPath(string value) => WithPath(() => value);
public RequestModelBuilder WithPath(string value)
{
PathValidator.ValidateAndThrow(value);
return WithPath(() => value);
}
/// <summary>
/// Set the Path.

View File

@@ -37,7 +37,7 @@ public interface IBodyData
object? BodyAsJson { get; set; }
/// <summary>
/// Gets or sets a value indicating whether child objects to be indented according to the Newtonsoft.Json.JsonTextWriter.Indentation and Newtonsoft.Json.JsonTextWriter.IndentChar settings.
/// Gets or sets a value indicating whether the Json Body String needs to be indented.
/// </summary>
bool? BodyAsJsonIndented { get; set; }

View File

@@ -0,0 +1,19 @@
// Copyright © WireMock.Net
using System;
namespace WireMock.Validators;
public static class PathValidator
{
/// <summary>
/// A valid path must start with a '/' and cannot be null, empty or whitespace.
/// </summary>
public static void ValidateAndThrow(string? path, string? paramName = null)
{
if (string.IsNullOrWhiteSpace(path) || path?.StartsWith("/") == false)
{
throw new ArgumentException("Path must start with a '/' and cannot be null, empty or whitespace.", paramName ?? nameof(path));
}
}
}

View File

@@ -30,7 +30,10 @@
</ItemGroup>
<ItemGroup>
<Compile Include="..\WireMock.Net.Minimal\Util\EnhancedFileSystemWatcher.cs" Link="Utils\EnhancedFileSystemWatcher.cs" />
<Compile Include="..\WireMock.Net.Minimal\Util\EnhancedFileSystemWatcher.cs" Link="Util\EnhancedFileSystemWatcher.cs" />
<Compile Include="..\WireMock.Net.Minimal\Constants\WireMockConstants.cs" Link="Constants\WireMockConstants.cs" />
<Compile Include="..\WireMock.Net.Shared\Constants\RegexConstants.cs" Link="Constants\RegexConstants.cs" />
<Compile Include="..\WireMock.Net.Minimal\Util\PortUtils.cs" Link="Util\PortUtils.cs" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug - Sonar'">

View File

@@ -0,0 +1,44 @@
// Copyright © WireMock.Net
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using WireMock.Client;
namespace WireMock.Net.Aspire;
/// <summary>
/// WireMockHealthCheck
/// </summary>
public class WireMockHealthCheck(WireMockServerResource resource) : IHealthCheck
{
private const string HealthStatusHealthy = "Healthy";
/// <inheritdoc />
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
if (!await IsHealthyAsync(resource.AdminApi.Value, cancellationToken))
{
return HealthCheckResult.Unhealthy("WireMock.Net is not healthy");
}
if (resource.ApiMappingState == WireMockMappingState.NotSubmitted)
{
return HealthCheckResult.Unhealthy("WireMock.Net has not received mappings");
}
return HealthCheckResult.Healthy();
}
private static async Task<bool> IsHealthyAsync(IWireMockAdminApi adminApi, CancellationToken cancellationToken)
{
try
{
var status = await adminApi.GetHealthAsync(cancellationToken);
return string.Equals(status, HealthStatusHealthy, StringComparison.OrdinalIgnoreCase);
}
catch
{
return false;
}
}
}

View File

@@ -0,0 +1,12 @@
// Copyright © WireMock.Net
namespace WireMock.Net.Aspire;
internal enum WireMockMappingState
{
NoMappings = 0,
NotSubmitted = 1,
Submitted = 2
}

View File

@@ -1,7 +1,9 @@
// Copyright © WireMock.Net
using System.Diagnostics.CodeAnalysis;
using Stef.Validation;
using WireMock.Client.Builders;
using WireMock.Util;
// ReSharper disable once CheckNamespace
namespace Aspire.Hosting;
@@ -21,10 +23,15 @@ public class WireMockServerArguments
private const string DefaultLogger = "WireMockConsoleLogger";
/// <summary>
/// The HTTP port where WireMock.Net is listening.
/// The HTTP ports where WireMock.Net is listening on.
/// If not defined, .NET Aspire automatically assigns a random port.
/// </summary>
public int? HttpPort { get; set; }
public List<int> HttpPorts { get; set; } = [];
/// <summary>
/// Additional Urls on which WireMock listens.
/// </summary>
public List<string> AdditionalUrls { get; set; } = [];
/// <summary>
/// The admin username.
@@ -67,6 +74,42 @@ public class WireMockServerArguments
/// </summary>
public Func<AdminApiMappingBuilder, CancellationToken, Task>? ApiMappingBuilder { get; set; }
/// <summary>
/// Grpc ProtoDefinitions.
/// </summary>
public Dictionary<string, string[]> ProtoDefinitions { get; set; } = [];
/// <summary>
/// Add an additional Urls on which WireMock should listen.
/// </summary>
/// <param name="additionalUrls">The additional urls which the WireMock Server should listen on.</param>
public void WithAdditionalUrls(params string[] additionalUrls)
{
foreach (var url in additionalUrls)
{
if (!PortUtils.TryExtract(Guard.NotNullOrEmpty(url), out _, out _, out _, out _, out var port))
{
throw new ArgumentException($"The URL '{url}' is not valid.");
}
AdditionalUrls.Add(Guard.NotNullOrWhiteSpace(url));
HttpPorts.Add(port);
}
}
/// <summary>
/// Add a Grpc ProtoDefinition at server-level.
/// </summary>
/// <param name="id">Unique identifier for the ProtoDefinition.</param>
/// <param name="protoDefinitions">The ProtoDefinition as text.</param>
public void WithProtoDefinition(string id, params string[] protoDefinitions)
{
Guard.NotNullOrWhiteSpace(id);
Guard.NotNullOrEmpty(protoDefinitions);
ProtoDefinitions[id] = protoDefinitions;
}
/// <summary>
/// Converts the current instance's properties to an array of command-line arguments for starting the WireMock.Net server.
/// </summary>
@@ -95,6 +138,11 @@ public class WireMockServerArguments
Add(args, "--WatchStaticMappingsInSubdirectories", "true");
}
if (AdditionalUrls.Count > 0)
{
Add(args, "--Urls", $"http://*:{HttpContainerPort} {string.Join(' ', AdditionalUrls)}");
}
return args
.SelectMany(k => new[] { k.Key, k.Value })
.ToArray();

View File

@@ -3,10 +3,12 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.WireMock;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Stef.Validation;
using WireMock.Client.Builders;
using WireMock.Net.Aspire;
using WireMock.Util;
// ReSharper disable once CheckNamespace
namespace Aspire.Hosting;
@@ -33,9 +35,31 @@ public static class WireMockServerBuilderExtensions
Guard.NotNullOrWhiteSpace(name);
Guard.Condition(port, p => p is null or > 0 and <= ushort.MaxValue);
return builder.AddWireMock(name, callback =>
return builder.AddWireMock(name, serverArguments =>
{
callback.HttpPort = port;
if (port != null)
{
serverArguments.HttpPorts = [port.Value];
}
});
}
/// <summary>
/// Adds a WireMock.Net Server resource to the application model.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <param name="additionalUrls">The additional urls which the WireMock Server should listen on.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, params string[] additionalUrls)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
Guard.NotNull(additionalUrls);
return builder.AddWireMock(name, serverArguments =>
{
serverArguments.WithAdditionalUrls(additionalUrls);
});
}
@@ -53,13 +77,50 @@ public static class WireMockServerBuilderExtensions
Guard.NotNull(arguments);
var wireMockContainerResource = new WireMockServerResource(name, arguments);
var healthCheckKey = $"{name}_check";
var healthCheckRegistration = new HealthCheckRegistration(
healthCheckKey,
_ => new WireMockHealthCheck(wireMockContainerResource),
failureStatus: null,
tags: null);
builder.Services.AddHealthChecks().Add(healthCheckRegistration);
var resourceBuilder = builder
.AddResource(wireMockContainerResource)
.WithImage(DefaultLinuxImage)
.WithEnvironment(ctx => ctx.EnvironmentVariables.Add("DOTNET_USE_POLLING_FILE_WATCHER", "1")) // https://khalidabuhakmeh.com/aspnet-docker-gotchas-and-workarounds#configuration-reloads-and-filesystemwatcher
.WithHttpEndpoint(port: arguments.HttpPort, targetPort: WireMockServerArguments.HttpContainerPort)
.WithHealthCheck(healthCheckKey)
.WithWireMockInspectorCommand();
if (arguments.HttpPorts.Count == 0)
{
resourceBuilder = resourceBuilder.WithHttpEndpoint(port: null, targetPort: WireMockServerArguments.HttpContainerPort);
}
else if (arguments.HttpPorts.Count == 1)
{
resourceBuilder = resourceBuilder.WithHttpEndpoint(port: arguments.HttpPorts[0], targetPort: WireMockServerArguments.HttpContainerPort);
}
else
{
// Required for the default admin endpoint and health checks
resourceBuilder = resourceBuilder.WithHttpEndpoint(port: null, targetPort: WireMockServerArguments.HttpContainerPort);
var anyIsHttp2 = false;
foreach (var url in arguments.AdditionalUrls)
{
PortUtils.TryExtract(url, out _, out var isHttp2, out var scheme, out _, out var httpPort);
anyIsHttp2 |= isHttp2;
resourceBuilder = resourceBuilder.WithEndpoint(port: httpPort, targetPort: httpPort, scheme: scheme, name: $"{scheme}-{httpPort}");
}
if (anyIsHttp2)
{
resourceBuilder = resourceBuilder.AsHttp2Service();
}
}
if (!string.IsNullOrEmpty(arguments.MappingsPath))
{
resourceBuilder = resourceBuilder.WithBindMount(arguments.MappingsPath, DefaultLinuxMappingsPath);
@@ -73,6 +134,9 @@ public static class WireMockServerBuilderExtensions
}
});
// Always add the lifecycle hook to support dynamic mappings and proto definitions
resourceBuilder.ApplicationBuilder.Services.TryAddLifecycleHook<WireMockServerLifecycleHook>();
return resourceBuilder;
}
@@ -83,7 +147,10 @@ public static class WireMockServerBuilderExtensions
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <param name="callback">A callback that allows for setting the <see cref="WireMockServerArguments"/>.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, Action<WireMockServerArguments> callback)
public static IResourceBuilder<WireMockServerResource> AddWireMock(
this IDistributedApplicationBuilder builder,
string name,
Action<WireMockServerArguments> callback)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
@@ -154,7 +221,7 @@ public static class WireMockServerBuilderExtensions
/// </summary>
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
/// <param name="configure">Delegate that will be invoked to configure the WireMock.Net resource.</param>
/// <returns></returns>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithApiMappingBuilder(this IResourceBuilder<WireMockServerResource> wiremock, Func<AdminApiMappingBuilder, Task> configure)
{
return wiremock.WithApiMappingBuilder((adminApiMappingBuilder, _) => configure.Invoke(adminApiMappingBuilder));
@@ -165,13 +232,27 @@ public static class WireMockServerBuilderExtensions
/// </summary>
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
/// <param name="configure">Delegate that will be invoked to configure the WireMock.Net resource.</param>
/// <returns></returns>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithApiMappingBuilder(this IResourceBuilder<WireMockServerResource> wiremock, Func<AdminApiMappingBuilder, CancellationToken, Task> configure)
{
Guard.NotNull(wiremock);
wiremock.ApplicationBuilder.Services.TryAddLifecycleHook<WireMockServerLifecycleHook>();
wiremock.Resource.Arguments.ApiMappingBuilder = configure;
wiremock.Resource.ApiMappingState = WireMockMappingState.NotSubmitted;
return wiremock;
}
/// <summary>
/// Add a Grpc ProtoDefinition at server-level.
/// </summary>
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
/// <param name="id">Unique identifier for the ProtoDefinition.</param>
/// <param name="protoDefinitions">The ProtoDefinition as text.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithProtoDefinition(this IResourceBuilder<WireMockServerResource> wiremock, string id, params string[] protoDefinitions)
{
Guard.NotNull(wiremock).Resource.Arguments.WithProtoDefinition(id, protoDefinitions);
return wiremock;
}
@@ -183,11 +264,11 @@ public static class WireMockServerBuilderExtensions
/// dotnet tool install WireMockInspector --global --no-cache --ignore-failed-sources
/// </code>
/// </summary>
/// <param name="builder">The <see cref="IResourceBuilder{WireMockNetResource}"/>.</param>
/// <returns></returns>
public static IResourceBuilder<WireMockServerResource> WithWireMockInspectorCommand(this IResourceBuilder<WireMockServerResource> builder)
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockNetResource}"/>.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithWireMockInspectorCommand(this IResourceBuilder<WireMockServerResource> wiremock)
{
Guard.NotNull(builder);
Guard.NotNull(wiremock);
CommandOptions commandOptions = new()
{
@@ -197,13 +278,13 @@ public static class WireMockServerBuilderExtensions
IconVariant = IconVariant.Filled
};
builder.WithCommand(
wiremock.WithCommand(
name: "wiremock-inspector",
displayName: "WireMock Inspector",
executeCommand: _ => OnRunOpenInspectorCommandAsync(builder),
executeCommand: _ => OnRunOpenInspectorCommandAsync(wiremock),
commandOptions: commandOptions);
return builder;
return wiremock;
}
private static Task<ExecuteCommandResult> OnRunOpenInspectorCommandAsync(IResourceBuilder<WireMockServerResource> builder)

View File

@@ -1,5 +1,6 @@
// Copyright © WireMock.Net
using System.Diagnostics;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.Logging;
@@ -10,32 +11,49 @@ internal class WireMockServerLifecycleHook(ILoggerFactory loggerFactory) : IDist
{
private readonly CancellationTokenSource _shutdownCts = new();
public async Task AfterResourcesCreatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
private CancellationTokenSource? _linkedCts;
private Task? _mappingTask;
public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
var cts = CancellationTokenSource.CreateLinkedTokenSource(_shutdownCts.Token, cancellationToken);
_linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_shutdownCts.Token, cancellationToken);
var wireMockServerResources = appModel.Resources
.OfType<WireMockServerResource>()
.ToArray();
foreach (var wireMockServerResource in wireMockServerResources)
_mappingTask = Task.Run(async () =>
{
wireMockServerResource.SetLogger(loggerFactory.CreateLogger<WireMockServerResource>());
var wireMockServerResources = appModel.Resources
.OfType<WireMockServerResource>()
.ToArray();
var endpoint = wireMockServerResource.GetEndpoint();
if (endpoint.IsAllocated)
foreach (var wireMockServerResource in wireMockServerResources)
{
await wireMockServerResource.WaitForHealthAsync(cts.Token);
wireMockServerResource.SetLogger(loggerFactory.CreateLogger<WireMockServerResource>());
await wireMockServerResource.CallApiMappingBuilderActionAsync(cts.Token);
var endpoint = wireMockServerResource.GetEndpoint();
Debug.Assert(endpoint.IsAllocated);
wireMockServerResource.StartWatchingStaticMappings(cts.Token);
await wireMockServerResource.WaitForHealthAsync(_linkedCts.Token);
await wireMockServerResource.CallAddProtoDefinitionsAsync(_linkedCts.Token);
await wireMockServerResource.CallApiMappingBuilderActionAsync(_linkedCts.Token);
wireMockServerResource.StartWatchingStaticMappings(_linkedCts.Token);
}
}
}, _linkedCts.Token);
return Task.CompletedTask;
}
public async ValueTask DisposeAsync()
{
await _shutdownCts.CancelAsync();
_linkedCts?.Dispose();
_shutdownCts.Dispose();
if (_mappingTask is not null)
{
await _mappingTask;
}
}
}

View File

@@ -5,6 +5,7 @@ using RestEase;
using Stef.Validation;
using WireMock.Client;
using WireMock.Client.Extensions;
using WireMock.Net.Aspire;
using WireMock.Util;
// ReSharper disable once CheckNamespace
@@ -19,6 +20,7 @@ public class WireMockServerResource : ContainerResource, IResourceWithServiceDis
internal WireMockServerArguments Arguments { get; }
internal Lazy<IWireMockAdminApi> AdminApi => new(CreateWireMockAdminApi);
internal WireMockMappingState ApiMappingState { get; set; } = WireMockMappingState.NoMappings;
private ILogger? _logger;
private EnhancedFileSystemWatcher? _enhancedFileSystemWatcher;
@@ -64,6 +66,36 @@ public class WireMockServerResource : ContainerResource, IResourceWithServiceDis
var mappingBuilder = AdminApi.Value.GetMappingBuilder();
await Arguments.ApiMappingBuilder.Invoke(mappingBuilder, cancellationToken);
ApiMappingState = WireMockMappingState.Submitted;
}
internal async Task CallAddProtoDefinitionsAsync(CancellationToken cancellationToken)
{
_logger?.LogInformation("Calling AdminApi to add GRPC ProtoDefinition at server level to WireMock.Net");
foreach (var (id, protoDefinitions) in Arguments.ProtoDefinitions)
{
_logger?.LogInformation("Adding ProtoDefinition {Id}", id);
foreach (var protoDefinition in protoDefinitions)
{
try
{
var status = await AdminApi.Value.AddProtoDefinitionAsync(id, protoDefinition, cancellationToken);
_logger?.LogInformation("ProtoDefinition '{Id}' added with status: {Status}.", id, status.Status);
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Error adding ProtoDefinition '{Id}'.", id);
}
}
}
// Force a reload of static mappings when ProtoDefinitions are added at server-level to fix #1382
if (Arguments.ProtoDefinitions.Count > 0)
{
await ReloadStaticMappingsAsync(default);
}
}
internal void StartWatchingStaticMappings(CancellationToken cancellationToken)
@@ -109,10 +141,17 @@ public class WireMockServerResource : ContainerResource, IResourceWithServiceDis
private async void FileCreatedChangedOrDeleted(object sender, FileSystemEventArgs args)
{
_logger?.LogInformation("MappingFile created, changed or deleted: '{0}'. Triggering ReloadStaticMappings.", args.FullPath);
_logger?.LogInformation("MappingFile created, changed or deleted: '{FullPath}'. Triggering ReloadStaticMappings.", args.FullPath);
await ReloadStaticMappingsAsync(default);
}
private async Task ReloadStaticMappingsAsync(CancellationToken cancellationToken)
{
try
{
await AdminApi.Value.ReloadStaticMappingsAsync();
var status = await AdminApi.Value.ReloadStaticMappingsAsync(cancellationToken);
_logger?.LogInformation("ReloadStaticMappings called with status: {Status}.", status);
}
catch (Exception ex)
{

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>WireMock.Net.Routing extends WireMock.Net with modern, minimal-API-style routing for .NET</Description>
<Authors>Gennadii Saltyshchak</Authors>
@@ -25,7 +25,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.7.0" />
<PackageReference Include="JsonConverter.Abstractions" Version="0.8.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -3,7 +3,6 @@
using System;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Stef.Validation;
using WireMock.Admin.Mappings;
using WireMock.Matchers.Request;
@@ -164,8 +163,8 @@ public class MappingBuilder : IMappingBuilder
}
}
private static string ToJson(object value)
private string ToJson(object value)
{
return JsonConvert.SerializeObject(value, JsonSerializationConstants.JsonSerializerSettingsDefault);
return _settings.DefaultJsonSerializer.Serialize(value, JsonSerializationConstants.JsonConverterOptionsDefault);
}
}

View File

@@ -0,0 +1,60 @@
// Copyright © WireMock.Net
using System;
using Newtonsoft.Json.Linq;
using Stef.Validation;
namespace WireMock.Matchers.Request;
/// <summary>
/// The request body matcher.
/// </summary>
public class RequestMessageBodyMatcher<T> : IRequestMatcher
{
/// <summary>
/// The body data function for type T
/// </summary>
public Func<T?, bool>? Func { get; }
/// <summary>
/// The <see cref="MatchOperator"/>
/// </summary>
public MatchOperator MatchOperator { get; } = MatchOperator.Or;
/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageBodyMatcher"/> class.
/// </summary>
/// <param name="func">The function.</param>
public RequestMessageBodyMatcher(Func<T?, bool> func)
{
Func = Guard.NotNull(func);
}
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{
var (score, exception) = CalculateMatchScore(requestMessage).Expand();
return requestMatchResult.AddScore(GetType(), score, exception);
}
private MatchResult CalculateMatchScore(IRequestMessage requestMessage)
{
if (Func != null)
{
if (requestMessage.BodyData?.BodyAsJson is JObject jsonObject)
{
try
{
var bodyAsT = jsonObject.ToObject<T>();
return MatchScores.ToScore(Func(bodyAsT));
}
catch (Exception ex)
{
return new MatchResult(ex);
}
}
}
return default;
}
}

View File

@@ -34,13 +34,6 @@ public partial class Request
return this;
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsJson(object body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
var matcher = body as IMatcher ?? new JsonMatcher(matchBehaviour, body);
return WithBody([matcher]);
}
/// <inheritdoc />
public IRequestBuilder WithBody(IMatcher matcher)
{
@@ -98,4 +91,20 @@ public partial class Request
_requestMatchers.Add(new RequestMessageBodyMatcher(Guard.NotNull(func)));
return this;
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsJson(object body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
var matcher = body as IMatcher ?? new JsonMatcher(matchBehaviour, body);
return WithBody([matcher]);
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsType<T>(Func<T?, bool> func)
{
Guard.NotNull(func);
_requestMatchers.Add(new RequestMessageBodyMatcher<T>(func));
return this;
}
}

View File

@@ -4,6 +4,7 @@ using System;
using Stef.Validation;
using WireMock.Matchers;
using WireMock.Matchers.Request;
using WireMock.Validators;
namespace WireMock.RequestBuilders;
@@ -34,6 +35,10 @@ public partial class Request
public IRequestBuilder WithPath(MatchOperator matchOperator, params string[] paths)
{
Guard.NotNullOrEmpty(paths);
foreach (var path in paths)
{
PathValidator.ValidateAndThrow(path, nameof(paths));
}
_requestMatchers.Add(new RequestMessagePathMatcher(MatchBehaviour.AcceptOnMatch, matchOperator, paths));
return this;

View File

@@ -29,7 +29,7 @@ public partial class Request : RequestMessageCompositeMatcher, IRequestBuilder
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
public static IRequestBuilder Create()
{
return new Request(new List<IRequestMatcher>());
return new Request([]);
}
/// <summary>

View File

@@ -226,14 +226,14 @@ internal class MappingConverter(MatcherMapper mapper)
}
}
if (response.Delay is { })
{
sb.AppendLine($" .WithDelay({response.Delay.Value.TotalMilliseconds})");
}
else if (response is { MinimumDelayMilliseconds: > 0, MaximumDelayMilliseconds: > 0 })
if (response is { MinimumDelayMilliseconds: > 0, MaximumDelayMilliseconds: > 0 })
{
sb.AppendLine($" .WithRandomDelay({response.MinimumDelayMilliseconds}, {response.MaximumDelayMilliseconds})");
}
else if (response.Delay is { })
{
sb.AppendLine($" .WithDelay({response.Delay.Value.TotalMilliseconds})");
}
if (response.UseTransformer)
{

View File

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

View File

@@ -2,7 +2,8 @@
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using JsonConverter.Abstractions;
using JsonConverter.Newtonsoft.Json;
using Stef.Validation;
using WireMock.Settings;
@@ -12,12 +13,15 @@ internal class MappingToFileSaver
{
private readonly WireMockServerSettings _settings;
private readonly MappingConverter _mappingConverter;
private readonly IJsonConverter _jsonConverter;
private readonly MappingFileNameSanitizer _fileNameSanitizer;
public MappingToFileSaver(WireMockServerSettings settings, MappingConverter mappingConverter)
{
_settings = Guard.NotNull(settings);
_mappingConverter = Guard.NotNull(mappingConverter);
_jsonConverter = settings.DefaultJsonSerializer ?? new NewtonsoftJsonConverter();
_fileNameSanitizer = new MappingFileNameSanitizer(settings);
}
@@ -56,6 +60,8 @@ internal class MappingToFileSaver
{
_settings.Logger.Info("Saving Mapping file {0}", path);
_settings.FileSystemHandler.WriteMappingFile(path, JsonConvert.SerializeObject(value, JsonSerializationConstants.JsonSerializerSettingsDefault));
var json = _jsonConverter.Serialize(value, JsonSerializationConstants.JsonConverterOptionsDefault);
_settings.FileSystemHandler.WriteMappingFile(path, json);
}
}

View File

@@ -7,7 +7,7 @@ using System.Linq;
using System.Net;
using System.Text;
using JetBrains.Annotations;
using Newtonsoft.Json;
using JsonConverter.Abstractions;
using Newtonsoft.Json.Linq;
using Stef.Validation;
using WireMock.Admin.Mappings;
@@ -236,7 +236,7 @@ public partial class WireMockServer
if (FileHelper.TryReadMappingFileWithRetryAndDelay(_settings.FileSystemHandler, path, out var value))
{
var mappingModels = DeserializeJsonToArray<MappingModel>(value);
var mappingModels = _mappingSerializer.DeserializeJsonToArray<MappingModel>(value);
if (mappingModels.Length == 1 && Guid.TryParse(filenameWithoutExtension, out var guidFromFilename))
{
ConvertMappingAndRegisterAsRespondProvider(mappingModels[0], guidFromFilename, path);
@@ -826,25 +826,31 @@ public partial class WireMockServer
}
}
private static Encoding? ToEncoding(EncodingModel? encodingModel)
private ResponseMessage ToJson<T>(T result, bool keepNullValues = false, object? statusCode = null)
{
return encodingModel != null ? Encoding.GetEncoding(encodingModel.CodePage) : null;
}
var jsonOptions = new JsonConverterOptions
{
WriteIndented = true,
IgnoreNullValues = !keepNullValues
};
private static ResponseMessage ToJson<T>(T result, bool keepNullValues = false, object? statusCode = null)
{
return new ResponseMessage
{
BodyData = new BodyData
{
DetectedBodyType = BodyType.String,
BodyAsString = JsonConvert.SerializeObject(result, keepNullValues ? JsonSerializationConstants.JsonSerializerSettingsIncludeNullValues : JsonSerializationConstants.JsonSerializerSettingsDefault)
BodyAsString = _settings.DefaultJsonSerializer.Serialize(result!, jsonOptions)
},
StatusCode = statusCode ?? (int)HttpStatusCode.OK,
Headers = new Dictionary<string, WireMockList<string>> { { HttpKnownHeaderNames.ContentType, new WireMockList<string>(WireMockConstants.ContentTypeJson) } }
};
}
private static Encoding? ToEncoding(EncodingModel? encodingModel)
{
return encodingModel != null ? Encoding.GetEncoding(encodingModel.CodePage) : null;
}
private static ResponseMessage ToResponseMessage(string text)
{
return new ResponseMessage
@@ -859,6 +865,18 @@ public partial class WireMockServer
};
}
private static 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 T DeserializeObject<T>(IRequestMessage requestMessage) where T : new()
{
switch (requestMessage.BodyData?.DetectedBodyType)
@@ -874,32 +892,4 @@ public partial class WireMockServer
throw new NotSupportedException();
}
}
private static T[] DeserializeRequestMessageToArray<T>(IRequestMessage requestMessage)
{
if (requestMessage.BodyData?.DetectedBodyType == BodyType.Json && requestMessage.BodyData.BodyAsJson != null)
{
var bodyAsJson = requestMessage.BodyData.BodyAsJson;
return DeserializeObjectToArray<T>(bodyAsJson);
}
throw new NotSupportedException();
}
private static T[] DeserializeJsonToArray<T>(string value)
{
return DeserializeObjectToArray<T>(JsonUtils.DeserializeObject(value));
}
private static T[] DeserializeObjectToArray<T>(object value)
{
if (value is JArray jArray)
{
return jArray.ToObject<T[]>()!;
}
var singleResult = ((JObject)value).ToObject<T>();
return new[] { singleResult! };
}
}

View File

@@ -31,7 +31,7 @@ public partial class WireMockServer
if (FileHelper.TryReadMappingFileWithRetryAndDelay(_settings.FileSystemHandler, path, out var value))
{
var mappings = DeserializeJsonToArray<OrgMapping>(value);
var mappings = _mappingSerializer.DeserializeJsonToArray<OrgMapping>(value);
foreach (var mapping in mappings)
{
if (mappings.Length == 1 && Guid.TryParse(filenameWithoutExtension, out var guidFromFilename))

View File

@@ -11,6 +11,7 @@ using System.Net.Http;
using System.Threading;
using AnyOfTypes;
using JetBrains.Annotations;
using JsonConverter.Newtonsoft.Json;
using Newtonsoft.Json;
using Stef.Validation;
using WireMock.Admin.Mappings;
@@ -47,6 +48,7 @@ public partial class WireMockServer : IWireMockServer
private readonly MappingBuilder _mappingBuilder;
private readonly IGuidUtils _guidUtils = new GuidUtils();
private readonly IDateTimeUtils _dateTimeUtils = new DateTimeUtils();
private readonly MappingSerializer _mappingSerializer;
/// <inheritdoc />
[PublicAPI]
@@ -357,6 +359,8 @@ public partial class WireMockServer : IWireMockServer
{
_settings = Guard.NotNull(settings);
_mappingSerializer = new MappingSerializer(settings.DefaultJsonSerializer ?? new NewtonsoftJsonConverter());
// Set default values if not provided
_settings.Logger = settings.Logger ?? new WireMockNullLogger();
_settings.FileSystemHandler = settings.FileSystemHandler ?? new LocalFileSystemHandler();
@@ -639,7 +643,7 @@ public partial class WireMockServer : IWireMockServer
[PublicAPI]
public IWireMockServer WithMapping(string mappings)
{
var mappingModels = DeserializeJsonToArray<MappingModel>(mappings);
var mappingModels = _mappingSerializer.DeserializeJsonToArray<MappingModel>(mappings);
foreach (var mappingModel in mappingModels)
{
ConvertMappingAndRegisterAsRespondProvider(mappingModel, mappingModel.Guid ?? Guid.NewGuid());

View File

@@ -55,7 +55,7 @@ internal class SimpleSettingsParser
// Now also parse environment
if (environment != null)
{
foreach (string key in environment.Keys)
foreach (var key in environment.Keys.OfType<string>())
{
if (key.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase) && environment.TryGetStringValue(key, out var value))
{

View File

@@ -36,7 +36,7 @@ public static class WireMockServerSettingsParser
if (parser.GetBoolSwitchValue("help"))
{
(logger ?? new WireMockConsoleLogger()).Info("See https://github.com/wiremock/WireMock.Net/wiki/WireMock-commandline-parameters for details on all commandline options.");
(logger ?? new WireMockConsoleLogger()).Info("See https://wiremock.org/dotnet/wiremock-commandline-parameters/ for details on all commandline options.");
settings = null;
return false;
}
@@ -153,7 +153,7 @@ public static class WireMockServerSettingsParser
}
else if (settings.HostingScheme is null)
{
settings.Urls = parser.GetValues("Urls", ["http://*:9091/"]);
settings.Urls = parser.GetValues(nameof(WireMockServerSettings.Urls), defaultValue: ["http://*:9091/"]);
}
}

View File

@@ -84,22 +84,22 @@ internal static class PortUtils
}
/// <summary>
/// Extract the isHttps, isHttp2, protocol, host and port from a URL.
/// Extract the isHttps, isHttp2, scheme, host and port from a URL.
/// </summary>
public static bool TryExtract(string url, out bool isHttps, out bool isHttp2, [NotNullWhen(true)] out string? protocol, [NotNullWhen(true)] out string? host, out int port)
public static bool TryExtract(string url, out bool isHttps, out bool isHttp2, [NotNullWhen(true)] out string? scheme, [NotNullWhen(true)] out string? host, out int port)
{
isHttps = false;
isHttp2 = false;
protocol = null;
scheme = null;
host = null;
port = 0;
var match = UrlDetailsRegex.Match(url);
if (match.Success)
{
protocol = match.Groups["proto"].Value;
isHttps = protocol.StartsWith("https", StringComparison.OrdinalIgnoreCase) || protocol.StartsWith("grpcs", StringComparison.OrdinalIgnoreCase);
isHttp2 = protocol.StartsWith("grpc", StringComparison.OrdinalIgnoreCase);
scheme = match.Groups["proto"].Value;
isHttps = scheme.StartsWith("https", StringComparison.OrdinalIgnoreCase) || scheme.StartsWith("grpcs", StringComparison.OrdinalIgnoreCase);
isHttp2 = scheme.StartsWith("grpc", StringComparison.OrdinalIgnoreCase);
host = match.Groups["host"].Value;
return int.TryParse(match.Groups["port"].Value, out port);

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Minimal version from the lightweight Http Mocking Server for .NET</Description>
<AssemblyTitle>WireMock.Net.Minimal</AssemblyTitle>
@@ -57,8 +57,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="JsonConverter.Abstractions" Version="0.7.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<!--<PackageReference Include="JsonConverter.Abstractions" Version="0.8.0" />-->
<!--<PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.8.0" />-->
<PackageReference Include="NJsonSchema.Extensions" Version="0.1.0" />
<PackageReference Include="NSwag.Core" Version="13.16.1" />
<PackageReference Include="SimMetrics.Net" Version="1.0.5" />
@@ -118,11 +118,13 @@
<!-- https://github.com/wiremock/WireMock.Net/issues/507 -->
<PackageReference Include="Microsoft.AspNetCore.Server.IIS" Version="2.2.6" />
<PackageReference Include="JsonConverter.System.Text.Json" Version="0.8.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp3.1' or '$(TargetFramework)' == 'net5.0' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' or '$(TargetFramework)' == 'net8.0'">
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Scriban.Signed" Version="5.5.0" />
<PackageReference Include="JsonConverter.System.Text.Json" Version="0.8.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp3.1' ">

View File

@@ -0,0 +1,71 @@
// Copyright © WireMock.Net
using System;
using JsonConverter.Abstractions;
using JsonConverter.Newtonsoft.Json;
using NUnit.Framework;
using WireMock.Admin.Requests;
using WireMock.Logging;
namespace WireMock.Net.NUnit;
/// <summary>
/// When using NUnit, this class enables to log the output from WireMock.Net using the <see cref="TestContext"/>.
/// </summary>
public sealed class TestContextWireMockLogger(IJsonConverter? jsonConverter = null) : IWireMockLogger
{
private readonly JsonConverterOptions _jsonConverterOptions = new() { WriteIndented = true, IgnoreNullValues = true };
private readonly IJsonConverter _jsonConverter = jsonConverter ?? new NewtonsoftJsonConverter();
/// <inheritdoc />
public void Debug(string formatString, params object[] args)
{
TestContext.WriteLine(Format("Debug", formatString, args));
}
/// <inheritdoc />
public void Info(string formatString, params object[] args)
{
TestContext.WriteLine(Format("Info", formatString, args));
}
/// <inheritdoc />
public void Warn(string formatString, params object[] args)
{
TestContext.WriteLine(Format("Warning", formatString, args));
}
/// <inheritdoc />
public void Error(string formatString, params object[] args)
{
TestContext.WriteLine(Format("Error", formatString, args));
}
/// <inheritdoc />
public void Error(string message, Exception exception)
{
TestContext.WriteLine(Format("Error", $"{message} {{0}}", exception));
if (exception is AggregateException ae)
{
ae.Handle(ex =>
{
TestContext.WriteLine(Format("Error", "Exception {0}", ex));
return true;
});
}
}
/// <inheritdoc />
public void DebugRequestResponse(LogEntryModel logEntryModel, bool isAdminRequest)
{
var message = _jsonConverter.Serialize(logEntryModel, _jsonConverterOptions);
TestContext.WriteLine(Format("DebugRequestResponse", "Admin[{0}] {1}", isAdminRequest, message));
}
private static string Format(string level, string formatString, params object[] args)
{
var message = args.Length > 0 ? string.Format(formatString, args) : formatString;
return $"{DateTime.UtcNow} [{level}] : {message}";
}
}

View File

@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Some extensions for NUnit</Description>
<AssemblyTitle>WireMock.Net.NUnit</AssemblyTitle>
<Authors>Stef Heyenrath</Authors>
<TargetFrameworks>net462;net6.0;net8.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AssemblyName>WireMock.Net.NUnit</AssemblyName>
<RootNamespace>WireMock.Net.NUnit</RootNamespace>
<PackageId>WireMock.Net.NUnit</PackageId>
<PackageTags>tdd;wiremock;test;unittest;nunit</PackageTags>
<ProjectGuid>{2DBBD70D-8051-441F-92BB-3F9B8B4B4983}</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>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.8.0" />
<PackageReference Include="NUnit" Version="4.4.0" />
<PackageReference Include="Stef.Validation" Version="0.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WireMock.Net.Abstractions\WireMock.Net.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@@ -5,9 +5,7 @@ using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.Interfaces;
using Microsoft.OpenApi;
using WireMock.Net.OpenApiParser.Types;
namespace WireMock.Net.OpenApiParser.Extensions;

View File

@@ -6,8 +6,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.Interfaces;
using Microsoft.OpenApi;
using Newtonsoft.Json;
using Stef.Validation;
using WireMock.Admin.Mappings;
@@ -19,18 +18,12 @@ using SystemTextJsonSerializer = System.Text.Json.JsonSerializer;
namespace WireMock.Net.OpenApiParser.Mappers;
internal class OpenApiPathsMapper
internal class OpenApiPathsMapper(WireMockOpenApiParserSettings settings)
{
private const string HeaderContentType = "Content-Type";
private readonly WireMockOpenApiParserSettings _settings;
private readonly ExampleValueGenerator _exampleValueGenerator;
public OpenApiPathsMapper(WireMockOpenApiParserSettings settings)
{
_settings = Guard.NotNull(settings);
_exampleValueGenerator = new ExampleValueGenerator(settings);
}
private readonly WireMockOpenApiParserSettings _settings = Guard.NotNull(settings);
private readonly ExampleValueGenerator _exampleValueGenerator = new(settings);
public IReadOnlyList<MappingModel> ToMappingModels(OpenApiPaths? paths, IList<OpenApiServer> servers)
{
@@ -41,7 +34,7 @@ internal class OpenApiPathsMapper
.ToArray() ?? [];
}
private IReadOnlyList<MappingModel> MapPath(string path, IOpenApiPathItem pathItem, IList<OpenApiServer> servers)
private MappingModel[] MapPath(string path, IOpenApiPathItem pathItem, IList<OpenApiServer> servers)
{
return pathItem.Operations?.Select(o => MapOperationToMappingModel(path, o.Key.ToString().ToUpperInvariant(), o.Value, servers)).ToArray() ?? [];
}
@@ -50,35 +43,7 @@ internal class OpenApiPathsMapper
{
var queryParameters = operation.Parameters?.Where(p => p.In == ParameterLocation.Query) ?? [];
var pathParameters = operation.Parameters?.Where(p => p.In == ParameterLocation.Path) ?? [];
var headers = operation.Parameters?.Where(p => p.In == ParameterLocation.Header) ?? [];
var response = operation.Responses?.FirstOrDefault() ?? new KeyValuePair<string, IOpenApiResponse>();
TryGetContent(response.Value?.Content, out OpenApiMediaType? responseContent, out var responseContentType);
var responseSchema = response.Value?.Content?.FirstOrDefault().Value?.Schema;
var responseExample = responseContent?.Example;
var responseSchemaExample = responseContent?.Schema?.Example;
var responseBody = responseExample ?? responseSchemaExample ?? MapSchemaToObject(responseSchema);
var requestBodyModel = new BodyModel();
if (operation.RequestBody != null && operation.RequestBody.Content != null && operation.RequestBody.Required)
{
var request = operation.RequestBody.Content;
TryGetContent(request, out var requestContent, out _);
var requestBodySchema = operation.RequestBody.Content.First().Value?.Schema;
var requestBodyExample = requestContent!.Example;
var requestBodySchemaExample = requestContent.Schema?.Example;
var requestBodyMapped = requestBodyExample ?? requestBodySchemaExample ?? MapSchemaToObject(requestBodySchema);
requestBodyModel = MapRequestBody(requestBodyMapped);
}
if (!int.TryParse(response.Key, out var httpStatusCode))
{
httpStatusCode = 200;
}
var requestHeaders = operation.Parameters?.Where(p => p.In == ParameterLocation.Header) ?? [];
return new MappingModel
{
@@ -88,15 +53,94 @@ internal class OpenApiPathsMapper
Methods = [httpMethod],
Path = PathUtils.Combine(MapBasePath(servers), MapPathWithParameters(path, pathParameters)),
Params = MapQueryParameters(queryParameters),
Headers = MapRequestHeaders(headers),
Body = requestBodyModel
Headers = MapRequestHeaders(requestHeaders),
Body = GetRequestBodyModel(operation.RequestBody)
},
Response = new ResponseModel
{
StatusCode = httpStatusCode,
Headers = MapHeaders(responseContentType, response.Value?.Headers),
BodyAsJson = responseBody != null ? JsonConvert.DeserializeObject(SystemTextJsonSerializer.Serialize(responseBody)) : null
}
Response = GetResponseModel(operation.Responses?.FirstOrDefault())
};
}
private BodyModel GetRequestBodyModel(IOpenApiRequestBody? openApiRequestBody)
{
if (openApiRequestBody is not { Content: not null, Required: true })
{
return new BodyModel();
}
var content = openApiRequestBody.Content;
TryGetContent(content, out var requestContent, out _);
var requestExample = requestContent?.Example;
var requestExamples = requestContent?.Examples;
var requestSchemaExample = requestContent?.Schema?.Example;
var requestSchemaExamples = requestContent?.Schema?.Examples;
JsonNode? request;
if (requestExample != null)
{
request = requestExample;
}
else if (requestSchemaExample != null)
{
request = requestSchemaExample;
}
else if (requestExamples != null)
{
request = requestExamples.FirstOrDefault().Value.Value;
}
else if (requestSchemaExamples != null)
{
request = requestSchemaExamples.FirstOrDefault();
}
else
{
var requestSchema = content?.FirstOrDefault().Value.Schema;
request = MapSchemaToObject(requestSchema);
}
return MapRequestBody(request) ?? new BodyModel();
}
private ResponseModel GetResponseModel(KeyValuePair<string, IOpenApiResponse>? openApiResponse)
{
var content = openApiResponse?.Value.Content;
TryGetContent(content, out var responseContent, out var contentType);
var responseExample = responseContent?.Example;
var responseExamples = responseContent?.Examples;
var responseSchemaExample = responseContent?.Schema?.Example;
var responseSchemaExamples = responseContent?.Schema?.Examples;
JsonNode? response;
if (responseExample != null)
{
response = responseExample;
}
else if (responseSchemaExample != null)
{
response = responseSchemaExample;
}
else if (responseExamples != null)
{
response = responseExamples.FirstOrDefault().Value.Value;
}
else if (responseSchemaExamples != null)
{
response = responseSchemaExamples.FirstOrDefault();
}
else
{
var responseSchema = content?.FirstOrDefault().Value?.Schema;
response = MapSchemaToObject(responseSchema);
}
return new ResponseModel
{
StatusCode = int.TryParse(openApiResponse?.Key, out var httpStatusCode) ? httpStatusCode : 200,
Headers = MapHeaders(contentType, openApiResponse?.Value.Headers),
BodyAsJson = response != null ? JsonConvert.DeserializeObject(SystemTextJsonSerializer.Serialize(response)) : null
};
}

View File

@@ -1,7 +1,7 @@
// Copyright © WireMock.Net
using System;
using Microsoft.OpenApi.Models.Interfaces;
using Microsoft.OpenApi;
namespace WireMock.Net.OpenApiParser.Settings;

View File

@@ -1,7 +1,7 @@
// Copyright © WireMock.Net
using System;
using Microsoft.OpenApi.Models.Interfaces;
using Microsoft.OpenApi;
using RandomDataGenerator.FieldOptions;
using RandomDataGenerator.Randomizers;

View File

@@ -1,8 +1,7 @@
// Copyright © WireMock.Net
using System;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.Interfaces;
using Microsoft.OpenApi;
namespace WireMock.Net.OpenApiParser.Settings;

View File

@@ -3,8 +3,7 @@
using System;
using System.Linq;
using System.Text.Json.Nodes;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.Interfaces;
using Microsoft.OpenApi;
using Stef.Validation;
using WireMock.Net.OpenApiParser.Extensions;
using WireMock.Net.OpenApiParser.Settings;

View File

@@ -27,22 +27,22 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="RamlToOpenApiConverter.SourceOnly" Version="0.8.0" />
<PackageReference Include="YamlDotNet" Version="8.1.0" />
<PackageReference Include="RamlToOpenApiConverter.SourceOnly" Version="0.11.0" />
<PackageReference Include="YamlDotNet" Version="16.3.0" />
<PackageReference Include="RandomDataGenerator.Net" Version="1.0.19" />
<PackageReference Include="Stef.Validation" Version="0.1.1" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<PackageReference Include="Microsoft.OpenApi.YamlReader" Version="2.0.0-preview.17" />
<PackageReference Include="Microsoft.OpenApi.YamlReader" Version="2.3.0" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Release'">
<PackageReference Include="ILRepack.Lib.MSBuild.Task" Version="2.0.40" PrivateAssets="All" />
<PackageReference Include="Microsoft.OpenApi" Version="2.0.0-preview.17" PrivateAssets="All" />
<PackageReference Include="Microsoft.OpenApi.YamlReader" Version="2.0.0-preview.17" PrivateAssets="All" />
<PackageReference Include="Microsoft.OpenApi" Version="2.3.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.OpenApi.YamlReader" Version="2.3.0" PrivateAssets="All" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="SharpYaml" Version="2.1.1" />
<PackageReference Include="SharpYaml" Version="2.1.3" />
</ItemGroup>
<ItemGroup>

View File

@@ -5,7 +5,7 @@ using System.Collections.Generic;
using System.IO;
using System.Text;
using JetBrains.Annotations;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Reader;
using Microsoft.OpenApi.YamlReader;
using RamlToOpenApiConverter;

View File

@@ -96,14 +96,14 @@ public class ProtoBufMatcher : IProtoBufMatcher
}
var protoDefinitions = ProtoDefinition().Texts;
var resolver = new WireMockProtoFileResolver(protoDefinitions);
var request = new ConvertToObjectRequest(protoDefinitions[0], MessageType, input)
.WithProtoFileResolver(resolver);
try
{
return await ProtoBufToJsonConverter.ConvertAsync(request, cancellationToken).ConfigureAwait(false);
return await ProtoBufToJsonConverter.ConvertAsync(request, cancellationToken);
}
catch
{

View File

@@ -26,7 +26,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ProtoBufJsonConverter" Version="0.10.0" />
<PackageReference Include="ProtoBufJsonConverter" Version="0.11.0" />
</ItemGroup>
<ItemGroup>

View File

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

View File

@@ -80,6 +80,14 @@ public interface IBodyRequestBuilder : IMultiPartRequestBuilder
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBody(Func<object?, bool> func);
/// <summary>
/// WithBody: func (type)
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="func">The function.</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsType<T>(Func<T?, bool> func);
/// <summary>
/// WithBody: func (BodyData object)
/// </summary>

View File

@@ -1,5 +1,6 @@
// Copyright © WireMock.Net
using JsonConverter.Abstractions;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
@@ -7,24 +8,30 @@ namespace WireMock.Serialization;
internal static class JsonSerializationConstants
{
public static readonly JsonSerializerSettings JsonSerializerSettingsDefault = new()
internal static readonly JsonConverterOptions JsonConverterOptionsDefault = new()
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore
WriteIndented = true,
IgnoreNullValues = true
};
public static readonly JsonSerializerSettings JsonSerializerSettingsIncludeNullValues = new()
//internal static readonly JsonSerializerSettings JsonSerializerSettingsDefault = new()
//{
// Formatting = Formatting.Indented,
// NullValueHandling = NullValueHandling.Ignore
//};
internal static readonly JsonSerializerSettings JsonSerializerSettingsIncludeNullValues = new()
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Include
};
public static readonly JsonSerializerSettings JsonDeserializerSettingsWithDateParsingNone = new()
internal static readonly JsonSerializerSettings JsonDeserializerSettingsWithDateParsingNone = new()
{
DateParseHandling = DateParseHandling.None
};
public static readonly JsonSerializerSettings JsonSerializerSettingsPact = new()
internal static readonly JsonSerializerSettings JsonSerializerSettingsPact = new()
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore,

View File

@@ -14,6 +14,8 @@ using WireMock.RegularExpressions;
using WireMock.Types;
using System.Globalization;
using WireMock.Models;
using JsonConverter.Abstractions;
using JsonConverter.Newtonsoft.Json;
#if USE_ASPNETCORE
using Microsoft.Extensions.DependencyInjection;
@@ -338,4 +340,14 @@ public class WireMockServerSettings
/// </summary>
[PublicAPI]
public HandlebarsSettings? HandlebarsSettings { 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.
/// Default is <see cref="NewtonsoftJsonConverter"/>.
/// </remarks>
[PublicAPI]
public IJsonConverter DefaultJsonSerializer { get; set; } = new NewtonsoftJsonConverter();
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Shared interfaces, models, enumerations and types.</Description>
<Authors>Stef Heyenrath</Authors>
@@ -48,9 +48,10 @@
</PackageReference>
<PackageReference Include="Stef.Validation" Version="0.1.1" />
<PackageReference Include="AnyOf" Version="0.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<!--<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />-->
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="1.1.0" />
<PackageReference Include="JsonConverter.Abstractions" Version="0.7.0" />
<!--<PackageReference Include="JsonConverter.Abstractions" Version="0.8.0" />-->
<PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.8.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,18 @@
// Copyright © WireMock.Net
using WireMock.Net.Testcontainers;
namespace DotNet.Testcontainers.Configurations;
internal static class HttpWaitStrategyExtensions
{
internal static HttpWaitStrategy WithBasicAuthentication(this HttpWaitStrategy strategy, WireMockConfiguration configuration)
{
if (configuration.HasBasicAuthentication)
{
return strategy.WithBasicAuthentication(configuration.Username, configuration.Password);
}
return strategy;
}
}

View File

@@ -0,0 +1,20 @@
// Copyright © WireMock.Net
using System.Collections.Generic;
using System.Linq;
namespace WireMock.Net.Testcontainers.Utils;
internal static class CombineUtils
{
internal static List<T> Combine<T>(List<T> oldValue, List<T> newValue)
{
return oldValue.Union(newValue).ToList();
}
internal static Dictionary<TKey, TValue> Combine<TKey, TValue>(Dictionary<TKey, TValue> oldValue, Dictionary<TKey, TValue> newValue)
where TKey : notnull
{
return oldValue.Union(newValue).ToDictionary(item => item.Key, item => item.Value);
}
}

View File

@@ -39,7 +39,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Stef.Validation" Version="0.1.1" />
<PackageReference Include="Testcontainers" Version="4.8.0" />
<PackageReference Include="Testcontainers" Version="4.10.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,12 +1,12 @@
// Copyright © WireMock.Net
using System.Collections.Generic;
using System.Linq;
using Docker.DotNet.Models;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Configurations;
using JetBrains.Annotations;
using Stef.Validation;
using WireMock.Net.Testcontainers.Utils;
namespace WireMock.Net.Testcontainers;
@@ -77,8 +77,8 @@ public sealed class WireMockConfiguration : ContainerConfiguration
StaticMappingsPath = BuildConfiguration.Combine(oldValue.StaticMappingsPath, newValue.StaticMappingsPath);
WatchStaticMappings = BuildConfiguration.Combine(oldValue.WatchStaticMappings, newValue.WatchStaticMappings);
WatchStaticMappingsInSubdirectories = BuildConfiguration.Combine(oldValue.WatchStaticMappingsInSubdirectories, newValue.WatchStaticMappingsInSubdirectories);
AdditionalUrls = Combine(oldValue.AdditionalUrls, newValue.AdditionalUrls);
ProtoDefinitions = Combine(oldValue.ProtoDefinitions, newValue.ProtoDefinitions);
AdditionalUrls = CombineUtils.Combine(oldValue.AdditionalUrls, newValue.AdditionalUrls);
ProtoDefinitions = CombineUtils.Combine(oldValue.ProtoDefinitions, newValue.ProtoDefinitions);
}
/// <summary>
@@ -130,16 +130,4 @@ public sealed class WireMockConfiguration : ContainerConfiguration
return this;
}
private static List<T> Combine<T>(List<T> oldValue, List<T> newValue)
{
return oldValue.Concat(newValue).ToList();
}
private static Dictionary<TKey, TValue> Combine<TKey, TValue>(Dictionary<TKey, TValue> oldValue, Dictionary<TKey, TValue> newValue)
{
return newValue
.Concat(oldValue.Where(item => !newValue.Keys.Contains(item.Key)))
.ToDictionary(item => item.Key, item => item.Value);
}
}

View File

@@ -24,28 +24,21 @@ namespace WireMock.Net.Testcontainers;
/// <summary>
/// A container for running WireMock in a docker environment.
/// </summary>
public sealed class WireMockContainer : DockerContainer
/// <remarks>
/// Initializes a new instance of the <see cref="WireMockContainer" /> class.
/// </remarks>
/// <param name="configuration">The container configuration.</param>
public sealed class WireMockContainer(WireMockConfiguration configuration) : DockerContainer(configuration)
{
private const int EnhancedFileSystemWatcherTimeoutMs = 2000;
internal const int ContainerPort = 80;
private readonly WireMockConfiguration _configuration;
private readonly WireMockConfiguration _configuration = Guard.NotNull(configuration);
private IWireMockAdminApi? _adminApi;
private EnhancedFileSystemWatcher? _enhancedFileSystemWatcher;
private IDictionary<int, Uri>? _publicUris;
/// <summary>
/// Initializes a new instance of the <see cref="WireMockContainer" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
public WireMockContainer(WireMockConfiguration configuration) : base(configuration)
{
_configuration = Guard.NotNull(configuration);
Started += async (sender, eventArgs) => await WireMockContainerStartedAsync(sender, eventArgs);
}
/// <summary>
/// Gets the public Url.
/// </summary>
@@ -156,14 +149,29 @@ public sealed class WireMockContainer : DockerContainer
try
{
await _adminApi.ReloadStaticMappingsAsync(cancellationToken);
var result = await _adminApi.ReloadStaticMappingsAsync(cancellationToken);
Logger.LogInformation("WireMock.Net -> ReloadStaticMappings result: {Result}", result);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error calling /__admin/mappings/reloadStaticMappings");
Logger.LogWarning(ex, "WireMock.Net -> Error calling /__admin/mappings/reloadStaticMappings");
}
}
/// <summary>
/// Performs additional actions after the container is ready.
/// </summary>
public Task CallAdditionalActionsAfterReadyAsync()
{
Logger.LogInformation("WireMock.Net -> Calling additional actions.");
_adminApi = CreateWireMockAdminClient();
RegisterEnhancedFileSystemWatcher();
return AddProtoDefinitionsAsync();
}
/// <inheritdoc />
protected override ValueTask DisposeAsyncCore()
{
@@ -196,15 +204,6 @@ public sealed class WireMockContainer : DockerContainer
}
}
private async Task WireMockContainerStartedAsync(object sender, EventArgs e)
{
_adminApi = CreateWireMockAdminClient();
RegisterEnhancedFileSystemWatcher();
await CallAdditionalActionsAfterStartedAsync();
}
private void RegisterEnhancedFileSystemWatcher()
{
if (!_configuration.WatchStaticMappings || string.IsNullOrEmpty(_configuration.StaticMappingsPath))
@@ -222,23 +221,31 @@ public sealed class WireMockContainer : DockerContainer
_enhancedFileSystemWatcher.EnableRaisingEvents = true;
}
private async Task CallAdditionalActionsAfterStartedAsync()
private async Task AddProtoDefinitionsAsync()
{
foreach (var kvp in _configuration.ProtoDefinitions)
{
Logger.LogInformation("Adding ProtoDefinition {Id}", kvp.Key);
Logger.LogInformation("WireMock.Net -> Adding ProtoDefinition '{Id}'", kvp.Key);
foreach (var protoDefinition in kvp.Value)
{
try
{
await _adminApi!.AddProtoDefinitionAsync(kvp.Key, protoDefinition);
var result = await _adminApi!.AddProtoDefinitionAsync(kvp.Key, protoDefinition);
Logger.LogInformation("WireMock.Net -> AddProtoDefinition '{Id}' result: {Result}", kvp.Key, result);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error adding ProtoDefinition '{Id}'.", kvp.Key);
Logger.LogWarning(ex, "WireMock.Net -> Error adding ProtoDefinition '{Id}'.", kvp.Key);
}
}
}
// Force a reload of static mappings when ProtoDefinitions are added at server-level to fix #1382
if (_configuration.ProtoDefinitions.Count > 0)
{
await ReloadStaticMappingsAsync();
}
}
private async void FileCreatedChangedOrDeleted(object sender, FileSystemEventArgs args)
@@ -246,16 +253,17 @@ public sealed class WireMockContainer : DockerContainer
try
{
await ReloadStaticMappingsAsync(args.FullPath);
Logger.LogInformation("WireMock.Net -> ReloadStaticMappings triggered from file change: '{FullPath}'.", args.FullPath);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error reloading static mappings from '{FullPath}'.", args.FullPath);
Logger.LogWarning(ex, "WireMock.Net -> Error reloading static mappings from '{FullPath}'.", args.FullPath);
}
}
private async Task ReloadStaticMappingsAsync(string path, CancellationToken cancellationToken = default)
{
Logger.LogInformation("MappingFile created, changed or deleted: '{Path}'. Triggering ReloadStaticMappings.", path);
Logger.LogInformation("WireMock.Net -> MappingFile created, changed or deleted: '{Path}'. Triggering ReloadStaticMappings.", path);
await ReloadStaticMappingsAsync(cancellationToken);
}

View File

@@ -2,6 +2,8 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using Docker.DotNet.Models;
using DotNet.Testcontainers.Builders;
@@ -62,6 +64,29 @@ public sealed class WireMockContainerBuilder : ContainerBuilder<WireMockContaine
return WithImage(OSPlatform.Windows);
}
/// <summary>
/// Sets a custom WireMock.Net image for which to create the container.
/// </summary>
/// <param name="image">The image name.</param>
/// <returns>A configured instance of <see cref="WireMockContainerBuilder"/></returns>
[PublicAPI]
public new WireMockContainerBuilder WithImage(string image)
{
return WithCustomImage(image);
}
/// <summary>
/// Sets a custom WireMock.Net image for which to create the container.
/// </summary>
/// <param name="image">The image name.</param>
/// <returns>A configured instance of <see cref="WireMockContainerBuilder"/></returns>
[PublicAPI]
public WireMockContainerBuilder WithCustomImage(string image)
{
_imageOS ??= TestcontainersUtils.GetImageOSAsync.Value.GetAwaiter().GetResult();
return base.WithImage(image);
}
/// <summary>
/// Set the admin username and password for the container (basic authentication).
/// </summary>
@@ -112,6 +137,7 @@ public sealed class WireMockContainerBuilder : ContainerBuilder<WireMockContaine
{
DockerResourceConfiguration.WithWatchStaticMappings(includeSubDirectories);
return
WithCommand("--ReadStaticMappings true").
WithCommand("--WatchStaticMappings true").
WithCommand("--WatchStaticMappingsInSubdirectories", includeSubDirectories);
}
@@ -129,9 +155,7 @@ public sealed class WireMockContainerBuilder : ContainerBuilder<WireMockContaine
DockerResourceConfiguration.WithStaticMappingsPath(path);
return
WithReadStaticMappings().
WithCommand("--WatchStaticMappingsInSubdirectories", includeSubDirectories);
return WithWatchStaticMappings(includeSubDirectories);
}
/// <summary>
@@ -208,9 +232,9 @@ public sealed class WireMockContainerBuilder : ContainerBuilder<WireMockContaine
// In case the _imageOS is not set, determine it from the Image FullName.
if (_imageOS == null)
{
if (builder.DockerResourceConfiguration.Image.FullName.IndexOf("wiremock.net", StringComparison.OrdinalIgnoreCase) < 0)
if (builder.DockerResourceConfiguration.Image.FullName.IndexOf("wiremock", StringComparison.OrdinalIgnoreCase) < 0)
{
throw new InvalidOperationException();
throw new InvalidOperationException("It's only possible to use a wiremock docker image.");
}
_imageOS = builder.DockerResourceConfiguration.Image.FullName.IndexOf("windows", StringComparison.OrdinalIgnoreCase) >= 0 ? OSPlatform.Windows : OSPlatform.Linux;
@@ -228,6 +252,25 @@ public sealed class WireMockContainerBuilder : ContainerBuilder<WireMockContaine
builder.Validate();
var waitForContainerOS = _imageOS == OSPlatform.Windows ? Wait.ForWindowsContainer() : Wait.ForUnixContainer();
builder = builder
.WithWaitStrategy(waitForContainerOS
.UntilMessageIsLogged("WireMock.Net server running", waitStrategy => waitStrategy.WithTimeout(TimeSpan.FromSeconds(30)))
.UntilHttpRequestIsSucceeded(httpWaitStrategy => httpWaitStrategy
.ForPort(WireMockContainer.ContainerPort)
.WithMethod(HttpMethod.Get)
.WithBasicAuthentication(DockerResourceConfiguration)
.ForPath("/__admin/health")
.ForStatusCode(HttpStatusCode.OK)
.ForResponseMessageMatching(async httpResponseMessage =>
{
var content = await httpResponseMessage.Content.ReadAsStringAsync();
return content?.Contains("Healthy") == true;
})
)
.AddCustomWaitStrategy(new WireMockWaitStrategy())
);
return new WireMockContainer(builder.DockerResourceConfiguration);
}
@@ -236,11 +279,9 @@ public sealed class WireMockContainerBuilder : ContainerBuilder<WireMockContaine
{
var builder = base.Init();
var waitForContainerOS = _imageOS == OSPlatform.Windows ? Wait.ForWindowsContainer() : Wait.ForUnixContainer();
return builder
.WithPortBinding(WireMockContainer.ContainerPort, true)
.WithCommand($"--WireMockLogger {DefaultLogger}")
.WithWaitStrategy(waitForContainerOS.UntilMessageIsLogged("WireMock.Net server running"));
.WithCommand($"--WireMockLogger {DefaultLogger}");
}
/// <inheritdoc />

View File

@@ -0,0 +1,23 @@
// Copyright © WireMock.Net
using System;
using System.Threading.Tasks;
using DotNet.Testcontainers.Configurations;
using DotNet.Testcontainers.Containers;
namespace WireMock.Net.Testcontainers;
internal class WireMockWaitStrategy : IWaitUntil
{
public async Task<bool> UntilAsync(IContainer container)
{
if (container is not WireMockContainer wireMockContainer)
{
throw new InvalidOperationException("The passed container is not a WireMockContainer.");
}
await wireMockContainer.CallAdditionalActionsAfterReadyAsync();
return true;
}
}

View File

@@ -0,0 +1,79 @@
// Copyright © WireMock.Net
using System;
using Newtonsoft.Json;
using Stef.Validation;
using WireMock.Admin.Requests;
using WireMock.Logging;
using Xunit;
namespace WireMock.Net.Xunit;
/// <summary>
/// When using xUnit, this class enables to log the output from WireMock.Net to the <see cref="ITestOutputHelper"/>.
/// </summary>
public sealed class TestOutputHelperWireMockLogger : IWireMockLogger
{
private readonly ITestOutputHelper _testOutputHelper;
/// <summary>
/// Create a new instance on the <see cref="TestOutputHelperWireMockLogger"/>.
/// </summary>
/// <param name="testOutputHelper">Represents a class which can be used to provide test output.</param>
public TestOutputHelperWireMockLogger(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = Guard.NotNull(testOutputHelper);
}
/// <inheritdoc />
public void Debug(string formatString, params object[] args)
{
_testOutputHelper.WriteLine(Format("Debug", formatString, args));
}
/// <inheritdoc />
public void Info(string formatString, params object[] args)
{
_testOutputHelper.WriteLine(Format("Info", formatString, args));
}
/// <inheritdoc />
public void Warn(string formatString, params object[] args)
{
_testOutputHelper.WriteLine(Format("Warning", formatString, args));
}
/// <inheritdoc />
public void Error(string formatString, params object[] args)
{
_testOutputHelper.WriteLine(Format("Error", formatString, args));
}
/// <inheritdoc />
public void Error(string message, Exception exception)
{
_testOutputHelper.WriteLine(Format("Error", $"{message} {{0}}", exception));
if (exception is AggregateException ae)
{
ae.Handle(ex =>
{
_testOutputHelper.WriteLine(Format("Error", "Exception {0}", ex));
return true;
});
}
}
/// <inheritdoc />
public void DebugRequestResponse(LogEntryModel logEntryModel, bool isAdminRequest)
{
var message = JsonConvert.SerializeObject(logEntryModel, Formatting.Indented);
_testOutputHelper.WriteLine(Format("DebugRequestResponse", "Admin[{0}] {1}", isAdminRequest, message));
}
private static string Format(string level, string formatString, params object[] args)
{
var message = args.Length > 0 ? string.Format(formatString, args) : formatString;
return $"{DateTime.UtcNow} [{level}] : {message}";
}
}

View File

@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Some extensions for xUnit (ITestOutputHelper)</Description>
<AssemblyTitle>WireMock.Net.xUnit.v3</AssemblyTitle>
<Authors>Stef Heyenrath</Authors>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>tdd;wiremock;test;unittest;xunit</PackageTags>
<ProjectGuid>{4F46BD02-BEBC-4B2D-B857-4169AD222267}</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>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Stef.Validation" Version="0.2.0" />
<PackageReference Include="xunit.v3.extensibility.core" Version="3.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WireMock.Net.Abstractions\WireMock.Net.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="JetBrains.Annotations" Version="2025.2.2" />
</ItemGroup>
</Project>

View File

@@ -19,6 +19,7 @@ public class IntegrationTests(ITestOutputHelper output)
var appHostBuilder = await DistributedApplicationTestingBuilder.CreateAsync<WireMock_Net_Aspire_TestAppHost>();
await using var app = await appHostBuilder.BuildAsync();
await app.StartAsync();
await app.ResourceNotifications.WaitForResourceHealthyAsync("wiremock-service");
using var httpClient = app.CreateHttpClient("wiremock-service");
@@ -46,6 +47,7 @@ public class IntegrationTests(ITestOutputHelper output)
var appHostBuilder = await DistributedApplicationTestingBuilder.CreateAsync<WireMock_Net_Aspire_TestAppHost>();
await using var app = await appHostBuilder.BuildAsync();
await app.StartAsync();
await app.ResourceNotifications.WaitForResourceHealthyAsync("wiremock-service");
var adminClient = app.CreateWireMockAdminClient("wiremock-service");

View File

@@ -13,7 +13,7 @@ public class WireMockServerArgumentsTests
var args = new WireMockServerArguments();
// Assert
args.HttpPort.Should().BeNull();
args.HttpPorts.Should().BeEmpty();
args.AdminUsername.Should().BeNull();
args.AdminPassword.Should().BeNull();
args.ReadStaticMappings.Should().BeFalse();

View File

@@ -3,6 +3,7 @@
using System.Net.Sockets;
using FluentAssertions;
using Moq;
using WireMock.Util;
namespace WireMock.Net.Aspire.Tests;
@@ -40,7 +41,21 @@ public class WireMockServerBuilderExtensionsTests
}
[Fact]
public void AddWireMock()
public void AddWireMock_WithInvalidAdditionalUrls_ShouldThrowArgumentException()
{
// Arrange
string[] invalidUrls = { "err" };
var builder = Mock.Of<IDistributedApplicationBuilder>();
// Act
Action act = () => builder.AddWireMock("ValidName", invalidUrls);
// Assert
act.Should().Throw<ArgumentException>().WithMessage("The URL 'err' is not valid.");
}
[Fact]
public void AddWireMockWithPort()
{
// Arrange
var name = $"apiservice{Guid.NewGuid()}";
@@ -65,9 +80,9 @@ public class WireMockServerBuilderExtensionsTests
ReadStaticMappings = true,
WatchStaticMappings = false,
MappingsPath = null,
HttpPort = port
HttpPorts = [port]
});
wiremock.Resource.Annotations.Should().HaveCount(5);
wiremock.Resource.Annotations.Should().HaveCount(6);
var containerImageAnnotation = wiremock.Resource.Annotations.OfType<ContainerImageAnnotation>().FirstOrDefault();
containerImageAnnotation.Should().BeEquivalentTo(new ContainerImageAnnotation
@@ -90,9 +105,90 @@ public class WireMockServerBuilderExtensionsTests
));
wiremock.Resource.Annotations.OfType<EnvironmentCallbackAnnotation>().FirstOrDefault().Should().NotBeNull();
wiremock.Resource.Annotations.OfType<CommandLineArgsCallbackAnnotation>().FirstOrDefault().Should().NotBeNull();
wiremock.Resource.Annotations.OfType<ResourceCommandAnnotation>().FirstOrDefault().Should().NotBeNull();
}
[Fact]
public void AddWireMockWithAdditionalUrls()
{
// Arrange
var name = $"apiservice{Guid.NewGuid()}";
var freePorts = PortUtils.FindFreeTcpPorts(2).ToList();
string[] additionalUrls = { $"http://*:{freePorts[0]}", $"grpc://*:{freePorts[1]}" };
const string username = "admin";
const string password = "test";
var builder = DistributedApplication.CreateBuilder();
// Act
var wiremock = builder
.AddWireMock(name, additionalUrls)
.WithAdminUserNameAndPassword(username, password)
.WithReadStaticMappings();
// Assert
wiremock.Resource.Should().NotBeNull();
wiremock.Resource.Name.Should().Be(name);
wiremock.Resource.Arguments.Should().BeEquivalentTo(new WireMockServerArguments
{
AdminPassword = password,
AdminUsername = username,
ReadStaticMappings = true,
WatchStaticMappings = false,
MappingsPath = null,
HttpPorts = freePorts,
AdditionalUrls = additionalUrls.ToList()
});
wiremock.Resource.Annotations.Should().HaveCount(9);
var containerImageAnnotation = wiremock.Resource.Annotations.OfType<ContainerImageAnnotation>().FirstOrDefault();
containerImageAnnotation.Should().BeEquivalentTo(new ContainerImageAnnotation
{
Image = "sheyenrath/wiremock.net-alpine",
Registry = null,
Tag = "latest"
});
var endpointAnnotations = wiremock.Resource.Annotations.OfType<EndpointAnnotation>().ToArray();
endpointAnnotations.Should().HaveCount(3);
var endpointAnnotationForHttp80 = endpointAnnotations[0];
endpointAnnotationForHttp80.Should().BeEquivalentTo(new EndpointAnnotation(
protocol: ProtocolType.Tcp,
uriScheme: "http",
transport: null,
name: null,
port: null,
targetPort: 80,
isExternal: null,
isProxied: true
));
var endpointAnnotationForHttpFreePort = endpointAnnotations[1];
endpointAnnotationForHttpFreePort.Should().BeEquivalentTo(new EndpointAnnotation(
protocol: ProtocolType.Tcp,
uriScheme: "http",
transport: null,
name: $"http-{freePorts[0]}",
port: freePorts[0],
targetPort: freePorts[0],
isExternal: null,
isProxied: true
));
var endpointAnnotationForGrpcFreePort = endpointAnnotations[2];
endpointAnnotationForGrpcFreePort.Should().BeEquivalentTo(new EndpointAnnotation(
protocol: ProtocolType.Tcp,
uriScheme: "grpc",
transport: null,
name: $"grpc-{freePorts[1]}",
port: freePorts[1],
targetPort: freePorts[1],
isExternal: null,
isProxied: true
));
wiremock.Resource.Annotations.OfType<EnvironmentCallbackAnnotation>().FirstOrDefault().Should().NotBeNull();
wiremock.Resource.Annotations.OfType<CommandLineArgsCallbackAnnotation>().FirstOrDefault().Should().NotBeNull();
wiremock.Resource.Annotations.OfType<ResourceCommandAnnotation>().FirstOrDefault().Should().NotBeNull();
}
}

View File

@@ -190,5 +190,92 @@
BodyDestination: SameAsSource,
Body: Buy milk
}
},
{
Guid: 98fae52e-76df-47d9-876f-2ee32e931006,
UpdatedAt: 2023-01-14 15:16:17,
Request: {
Path: {
Matchers: [
{
Name: WildcardMatcher,
Pattern: /delay,
IgnoreCase: false
}
]
},
Methods: [
GET
]
},
Response: {
Delay: 1000
}
},
{
Guid: 98fae52e-76df-47d9-876f-2ee32e931007,
UpdatedAt: 2023-01-14 15:16:17,
Request: {
Path: {
Matchers: [
{
Name: WildcardMatcher,
Pattern: /random-delay,
IgnoreCase: false
}
]
},
Methods: [
GET
]
},
Response: {
MinimumRandomDelay: 1234,
MaximumRandomDelay: 60000
}
},
{
Guid: 98fae52e-76df-47d9-876f-2ee32e931008,
UpdatedAt: 2023-01-14 15:16:17,
Request: {
Path: {
Matchers: [
{
Name: WildcardMatcher,
Pattern: /prob,
IgnoreCase: false
}
]
},
Methods: [
GET
]
},
Response: {
StatusCode: 300
},
Probability: 0.1
},
{
Guid: 98fae52e-76df-47d9-876f-2ee32e931009,
UpdatedAt: 2023-01-14 15:16:17,
Request: {
Path: {
Matchers: [
{
Name: WildcardMatcher,
Pattern: /prob,
IgnoreCase: false
}
]
},
Methods: [
GET
]
},
Response: {
StatusCode: 201
},
Probability: 0.9
}
]

View File

@@ -78,3 +78,45 @@ builder
.WithBody("Buy milk")
);
builder
.Given(Request.Create()
.UsingMethod("GET")
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "/delay", false, WireMock.Matchers.MatchOperator.Or))
)
.WithGuid("98fae52e-76df-47d9-876f-2ee32e931006")
.RespondWith(Response.Create()
.WithDelay(1000)
);
builder
.Given(Request.Create()
.UsingMethod("GET")
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "/random-delay", false, WireMock.Matchers.MatchOperator.Or))
)
.WithGuid("98fae52e-76df-47d9-876f-2ee32e931007")
.RespondWith(Response.Create()
.WithRandomDelay(1234, 60000)
);
builder
.Given(Request.Create()
.UsingMethod("GET")
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "/prob", false, WireMock.Matchers.MatchOperator.Or))
)
.WithGuid("98fae52e-76df-47d9-876f-2ee32e931008")
.WithProbability(0.1)
.RespondWith(Response.Create()
.WithStatusCode(300)
);
builder
.Given(Request.Create()
.UsingMethod("GET")
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "/prob", false, WireMock.Matchers.MatchOperator.Or))
)
.WithGuid("98fae52e-76df-47d9-876f-2ee32e931009")
.WithProbability(0.9)
.RespondWith(Response.Create()
.WithStatusCode(201)
);

View File

@@ -78,3 +78,45 @@ server
.WithBody("Buy milk")
);
server
.Given(Request.Create()
.UsingMethod("GET")
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "/delay", false, WireMock.Matchers.MatchOperator.Or))
)
.WithGuid("98fae52e-76df-47d9-876f-2ee32e931006")
.RespondWith(Response.Create()
.WithDelay(1000)
);
server
.Given(Request.Create()
.UsingMethod("GET")
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "/random-delay", false, WireMock.Matchers.MatchOperator.Or))
)
.WithGuid("98fae52e-76df-47d9-876f-2ee32e931007")
.RespondWith(Response.Create()
.WithRandomDelay(1234, 60000)
);
server
.Given(Request.Create()
.UsingMethod("GET")
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "/prob", false, WireMock.Matchers.MatchOperator.Or))
)
.WithGuid("98fae52e-76df-47d9-876f-2ee32e931008")
.WithProbability(0.1)
.RespondWith(Response.Create()
.WithStatusCode(300)
);
server
.Given(Request.Create()
.UsingMethod("GET")
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "/prob", false, WireMock.Matchers.MatchOperator.Or))
)
.WithGuid("98fae52e-76df-47d9-876f-2ee32e931009")
.WithProbability(0.9)
.RespondWith(Response.Create()
.WithStatusCode(201)
);

View File

@@ -186,5 +186,92 @@
BodyDestination: SameAsSource,
Body: Buy milk
}
},
{
Guid: 98fae52e-76df-47d9-876f-2ee32e931006,
UpdatedAt: 2023-01-14T15:16:17,
Request: {
Path: {
Matchers: [
{
Name: WildcardMatcher,
Pattern: /delay,
IgnoreCase: false
}
]
},
Methods: [
GET
]
},
Response: {
Delay: 1000
}
},
{
Guid: 98fae52e-76df-47d9-876f-2ee32e931007,
UpdatedAt: 2023-01-14T15:16:17,
Request: {
Path: {
Matchers: [
{
Name: WildcardMatcher,
Pattern: /random-delay,
IgnoreCase: false
}
]
},
Methods: [
GET
]
},
Response: {
MinimumRandomDelay: 1234,
MaximumRandomDelay: 60000
}
},
{
Guid: 98fae52e-76df-47d9-876f-2ee32e931008,
UpdatedAt: 2023-01-14T15:16:17,
Request: {
Path: {
Matchers: [
{
Name: WildcardMatcher,
Pattern: /prob,
IgnoreCase: false
}
]
},
Methods: [
GET
]
},
Response: {
StatusCode: 300
},
Probability: 0.1
},
{
Guid: 98fae52e-76df-47d9-876f-2ee32e931009,
UpdatedAt: 2023-01-14T15:16:17,
Request: {
Path: {
Matchers: [
{
Name: WildcardMatcher,
Pattern: /prob,
IgnoreCase: false
}
]
},
Methods: [
GET
]
},
Response: {
StatusCode: 201
},
Probability: 0.9
}
]

View File

@@ -2,6 +2,7 @@
#if !(NET452 || NET461 || NETCOREAPP3_1)
using System;
using System.Net;
using System.Threading.Tasks;
using Moq;
using VerifyTests;
@@ -118,6 +119,35 @@ public class MappingBuilderTests
.RespondWith(Response.Create()
.WithBody("Buy milk"));
_sut.Given(Request.Create()
.WithPath("/delay")
.UsingGet()
).RespondWith(Response.Create()
.WithDelay(1000)
);
_sut.Given(Request.Create()
.WithPath("/random-delay")
.UsingGet()
).RespondWith(Response.Create()
.WithRandomDelay(1234)
);
_sut.Given(Request.Create()
.WithPath("/prob")
.UsingGet()
).WithProbability(0.1)
.RespondWith(Response.Create()
.WithStatusCode(HttpStatusCode.Ambiguous)
);
_sut.Given(Request.Create()
.WithPath("/prob")
.UsingGet()
).WithProbability(0.9)
.RespondWith(Response.Create()
.WithStatusCode(HttpStatusCode.Created)
);
_numMappings = _sut.GetMappings().Length;
}

View File

@@ -1,11 +1,12 @@
// Copyright © WireMock.Net
using System;
using FluentAssertions;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentAssertions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NFluent;
using WireMock.Matchers;
using WireMock.Matchers.Request;
@@ -72,15 +73,17 @@ public class RequestBuilderWithBodyTests
}
[Fact]
public void Request_WithBody_FuncJson()
public void Request_WithBody_FuncObject()
{
// Assign
var requestBuilder = Request.Create().UsingAnyMethod().WithBody(b => b != null);
var requestBuilder = Request.Create()
.UsingAnyMethod()
.WithBody(b => b != null);
// Act
var body = new BodyData
{
BodyAsJson = 123,
BodyAsJson = JObject.Parse("""{ "X": 123, "Y": "a" }"""),
DetectedBodyType = BodyType.Json
};
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body);
@@ -90,6 +93,57 @@ public class RequestBuilderWithBodyTests
Check.That(requestBuilder.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
}
[Theory]
[InlineData("""{ "X": 123, "Y": "a" }""", 1.0)]
[InlineData("""{ "X": 123, "Y": "b" }""", 0.0)]
public void Request_WithBodyAsType_Func(string json, double expected)
{
// Assign
var requestBuilder = Request.Create()
.UsingAnyMethod()
.WithBodyAsType<FuncType>(ft => ft != null && ft.X == 123 && ft.Y == "a");
// Act
var body = new BodyData
{
BodyAsJson = JObject.Parse(json),
DetectedBodyType = BodyType.Json
};
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body);
// Assert
var requestMatchResult = new RequestMatchResult();
Check.That(requestBuilder.GetMatchingScore(request, requestMatchResult)).IsEqualTo(expected);
}
[Fact]
public void Request_WithBodyAsType_Func_IncorrectType()
{
// Assign
var requestBuilder = Request.Create()
.UsingAnyMethod()
.WithBodyAsType<Version>(ft => ft != null);
// Act
var body = new BodyData
{
BodyAsJson = JObject.Parse("""{ "X": 123, "Y": "a" }"""),
DetectedBodyType = BodyType.Json
};
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body);
// Assert
var requestMatchResult = new RequestMatchResult();
Check.That(requestBuilder.GetMatchingScore(request, requestMatchResult)).IsEqualTo(0.0);
}
private class FuncType
{
public int X { get; set; } = 42;
public string Y { get; set; } = string.Empty;
}
[Fact]
public void Request_WithBody_FuncFormUrlEncoded()
{

View File

@@ -4,6 +4,7 @@ using System;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
using JsonConverter.Newtonsoft.Json;
using Moq;
using Newtonsoft.Json.Linq;
using NFluent;
@@ -316,12 +317,11 @@ public class ResponseWithBodyTests
var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request1, _settings).ConfigureAwait(false);
Check.That(response.Message.StatusCode).IsEqualTo(200);
Check.That(response.Message.BodyData.BodyAsString).Contains("File deleted.");
Check.That(response.Message.BodyData?.BodyAsString).Contains("File deleted.");
}
#if !(NET451 || NET452)
[Fact]
public async Task Response_ProvideResponse_WithBody_IJsonConverter_SystemTextJson()
public async Task Response_ProvideResponse_WithBody_NewtonsoftJsonConverter()
{
// Arrange
var requestBody = new BodyData
@@ -331,13 +331,34 @@ public class ResponseWithBodyTests
};
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, requestBody);
var responseBuilder = Response.Create().WithBody(new { foo = "bar", n = 42 }, new JsonConverter.System.Text.Json.SystemTextJsonConverter());
var responseBuilder = Response.Create().WithBody(new { foo = "< > & ' 😀 👍 ❤️", n = 42 }, new NewtonsoftJsonConverter());
// Act
var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false);
// Assert
response.Message.BodyData!.BodyAsString.Should().Be(@"{""foo"":""bar"",""n"":42}");
response.Message.BodyData!.BodyAsString.Should().Be("""{"foo":"< > & ' 😀 👍 ","n":42}""");
}
#if !(NET451 || NET452 || NET461)
[Fact]
public async Task Response_ProvideResponse_WithBody_SystemTextJsonConverter()
{
// Arrange
var requestBody = new BodyData
{
DetectedBodyType = BodyType.String,
BodyAsString = "abc"
};
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, requestBody);
var responseBuilder = Response.Create().WithBody(new { foo = "< > & ' 😀 👍 ❤️", n = 42 }, new JsonConverter.System.Text.Json.SystemTextJsonConverter());
// Act
var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false);
// Assert
response.Message.BodyData!.BodyAsString.Should().Be("""{"foo":"\u003C \u003E \u0026 \u0027 \uD83D\uDE00 \uD83D\uDC4D \u2764\uFE0F","n":42}""");
}
#endif
}

View File

@@ -109,7 +109,7 @@ public partial class MappingConverterTests
var guid = new Guid("8e7b9ab7-e18e-4502-8bc9-11e6679811cc");
var request = Request.Create()
.UsingGet()
.WithPath("test_path")
.WithPath("/test_path")
.WithParam("q", "42")
.WithClientIP("112.123.100.99")
.WithHeader("h-key", "h-value")

View File

@@ -1,7 +1,7 @@
builder
.Given(Request.Create()
.UsingMethod("GET")
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "test_path", false, WireMock.Matchers.MatchOperator.Or))
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "/test_path", false, WireMock.Matchers.MatchOperator.Or))
.WithParam("q", new ExactMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, false, WireMock.Matchers.MatchOperator.And, "42"))
.WithClientIP("112.123.100.99")
.WithHeader("h-key", "h-value", true)

View File

@@ -2,7 +2,7 @@
builder
.Given(Request.Create()
.UsingMethod("GET")
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "test_path", false, WireMock.Matchers.MatchOperator.Or))
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "/test_path", false, WireMock.Matchers.MatchOperator.Or))
.WithParam("q", new ExactMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, false, WireMock.Matchers.MatchOperator.And, "42"))
.WithClientIP("112.123.100.99")
.WithHeader("h-key", "h-value", true)

View File

@@ -1,7 +1,7 @@
server
.Given(Request.Create()
.UsingMethod("GET")
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "test_path", false, WireMock.Matchers.MatchOperator.Or))
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "/test_path", false, WireMock.Matchers.MatchOperator.Or))
.WithParam("q", new ExactMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, false, WireMock.Matchers.MatchOperator.And, "42"))
.WithClientIP("112.123.100.99")
.WithHeader("h-key", "h-value", true)

View File

@@ -2,7 +2,7 @@
server
.Given(Request.Create()
.UsingMethod("GET")
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "test_path", false, WireMock.Matchers.MatchOperator.Or))
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "/test_path", false, WireMock.Matchers.MatchOperator.Or))
.WithParam("q", new ExactMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, false, WireMock.Matchers.MatchOperator.And, "42"))
.WithClientIP("112.123.100.99")
.WithHeader("h-key", "h-value", true)

View File

@@ -0,0 +1,326 @@
// Copyright © WireMock.Net
using System;
using FluentAssertions;
using JsonConverter.Newtonsoft.Json;
using WireMock.Admin.Mappings;
using WireMock.Serialization;
using Xunit;
#if NET8_0_OR_GREATER
using JsonConverter.System.Text.Json;
#endif
namespace WireMock.Net.Tests.Serialization;
public class MappingSerializerTests
{
private const string SingleMappingJson =
"""
{
"Guid": "12345678-1234-1234-1234-123456789012",
"Priority": 1,
"Request": {
"Path": "/test"
},
"Response": {
"StatusCode": 200
}
}
""";
private const string ArrayMappingJson =
"""
[
{
"Guid": "12345678-1234-1234-1234-123456789012",
"Priority": 1,
"Request": {
"Path": "/test1"
},
"Response": {
"StatusCode": 200
}
},
{
"Guid": "87654321-4321-4321-4321-210987654321",
"Priority": 2,
"Request": {
"Path": "/test2"
},
"Response": {
"StatusCode": 404
}
}
]
""";
[Fact]
public void MappingSerializer_DeserializeJsonToArray_WithNewtonsoftJson_SingleObject_ShouldReturnArray()
{
// Arrange
var jsonConverter = new NewtonsoftJsonConverter();
var serializer = new MappingSerializer(jsonConverter);
// Act
var result = serializer.DeserializeJsonToArray<MappingModel>(SingleMappingJson);
// Assert
result.Should().NotBeNull();
result.Should().HaveCount(1);
result[0].Guid.Should().Be(Guid.Parse("12345678-1234-1234-1234-123456789012"));
result[0].Priority.Should().Be(1);
}
[Fact]
public void MappingSerializer_DeserializeJsonToArray_WithNewtonsoftJson_Array_ShouldReturnArray()
{
// Arrange
var jsonConverter = new NewtonsoftJsonConverter();
var serializer = new MappingSerializer(jsonConverter);
// Act
var result = serializer.DeserializeJsonToArray<MappingModel>(ArrayMappingJson);
// Assert
result.Should().NotBeNull();
result.Should().HaveCount(2);
result[0].Guid.Should().Be(Guid.Parse("12345678-1234-1234-1234-123456789012"));
result[0].Priority.Should().Be(1);
result[1].Guid.Should().Be(Guid.Parse("87654321-4321-4321-4321-210987654321"));
result[1].Priority.Should().Be(2);
}
[Fact]
public void MappingSerializer_DeserializeJsonToArray_WithNewtonsoftJson_EmptyArray_ShouldReturnEmptyArray()
{
// Arrange
var jsonConverter = new NewtonsoftJsonConverter();
var serializer = new MappingSerializer(jsonConverter);
var emptyArrayJson = "[]";
// Act
var result = serializer.DeserializeJsonToArray<MappingModel>(emptyArrayJson);
// Assert
result.Should().NotBeNull();
result.Should().BeEmpty();
}
[Fact]
public void MappingSerializer_DeserializeJsonToArray_WithNewtonsoftJson_InvalidJson_ShouldThrowException()
{
// Arrange
var jsonConverter = new NewtonsoftJsonConverter();
var serializer = new MappingSerializer(jsonConverter);
var invalidJson = "not valid json";
// Act
Action act = () => serializer.DeserializeJsonToArray<MappingModel>(invalidJson);
// Assert
act.Should().Throw<Exception>();
}
[Fact]
public void MappingSerializer_DeserializeJsonToArray_WithNewtonsoftJson_ComplexMapping_ShouldDeserializeCorrectly()
{
// Arrange
var jsonConverter = new NewtonsoftJsonConverter();
var serializer = new MappingSerializer(jsonConverter);
var complexJson =
"""
{
"Guid": "12345678-1234-1234-1234-123456789012",
"Title": "Test Mapping",
"Description": "A test mapping",
"Priority": 10,
"Request": {
"Path": "/api/test",
"Methods": ["GET", "POST"]
},
"Response": {
"StatusCode": 201,
"Body": "Test Response"
}
}
""";
// Act
var result = serializer.DeserializeJsonToArray<MappingModel>(complexJson);
// Assert
result.Should().NotBeNull();
result.Should().HaveCount(1);
result[0].Guid.Should().Be(Guid.Parse("12345678-1234-1234-1234-123456789012"));
result[0].Title.Should().Be("Test Mapping");
result[0].Description.Should().Be("A test mapping");
result[0].Priority.Should().Be(10);
}
[Fact]
public void MappingSerializer_DeserializeJsonToArray_WithNewtonsoftJson_NullValue_ShouldThrowException()
{
// Arrange
var jsonConverter = new NewtonsoftJsonConverter();
var serializer = new MappingSerializer(jsonConverter);
var nullJson = "null";
// Act
Action act = () => serializer.DeserializeJsonToArray<MappingModel>(nullJson);
// Assert
act.Should().Throw<Exception>();
}
[Fact]
public void MappingSerializer_DeserializeJsonToArray_WithNewtonsoftJson_PrimitiveValue_ShouldThrowInvalidOperationException()
{
// Arrange
var jsonConverter = new NewtonsoftJsonConverter();
var serializer = new MappingSerializer(jsonConverter);
var primitiveJson = "\"string value\"";
// Act
Action act = () => serializer.DeserializeJsonToArray<MappingModel>(primitiveJson);
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("Cannot deserialize the provided value to an array or object.");
}
#if NET8_0_OR_GREATER
[Fact]
public void MappingSerializer_DeserializeJsonToArray_WithSystemTextJson_SingleObject_ShouldReturnArray()
{
// Arrange
var jsonConverter = new SystemTextJsonConverter();
var serializer = new MappingSerializer(jsonConverter);
// Act
var result = serializer.DeserializeJsonToArray<MappingModel>(SingleMappingJson);
// Assert
result.Should().NotBeNull();
result.Should().HaveCount(1);
result[0].Guid.Should().Be(Guid.Parse("12345678-1234-1234-1234-123456789012"));
result[0].Priority.Should().Be(1);
}
[Fact]
public void MappingSerializer_DeserializeJsonToArray_WithSystemTextJson_Array_ShouldReturnArray()
{
// Arrange
var jsonConverter = new SystemTextJsonConverter();
var serializer = new MappingSerializer(jsonConverter);
// Act
var result = serializer.DeserializeJsonToArray<MappingModel>(ArrayMappingJson);
// Assert
result.Should().NotBeNull();
result.Should().HaveCount(2);
result[0].Guid.Should().Be(Guid.Parse("12345678-1234-1234-1234-123456789012"));
result[0].Priority.Should().Be(1);
result[1].Guid.Should().Be(Guid.Parse("87654321-4321-4321-4321-210987654321"));
result[1].Priority.Should().Be(2);
}
[Fact]
public void MappingSerializer_DeserializeJsonToArray_WithSystemTextJson_EmptyArray_ShouldReturnEmptyArray()
{
// Arrange
var jsonConverter = new SystemTextJsonConverter();
var serializer = new MappingSerializer(jsonConverter);
var emptyArrayJson = "[]";
// Act
var result = serializer.DeserializeJsonToArray<MappingModel>(emptyArrayJson);
// Assert
result.Should().NotBeNull();
result.Should().BeEmpty();
}
[Fact]
public void MappingSerializer_DeserializeJsonToArray_WithSystemTextJson_InvalidJson_ShouldThrowException()
{
// Arrange
var jsonConverter = new SystemTextJsonConverter();
var serializer = new MappingSerializer(jsonConverter);
var invalidJson = "not valid json";
// Act
Action act = () => serializer.DeserializeJsonToArray<MappingModel>(invalidJson);
// Assert
act.Should().Throw<Exception>();
}
[Fact]
public void MappingSerializer_DeserializeJsonToArray_WithSystemTextJson_ComplexMapping_ShouldDeserializeCorrectly()
{
// Arrange
var jsonConverter = new SystemTextJsonConverter();
var serializer = new MappingSerializer(jsonConverter);
var complexJson =
"""
{
"Guid": "12345678-1234-1234-1234-123456789012",
"Title": "Test Mapping",
"Description": "A test mapping",
"Priority": 10,
"Request": {
"Path": "/api/test",
"Methods": ["GET", "POST"]
},
"Response": {
"StatusCode": 201,
"Body": "Test Response"
}
}
""";
// Act
var result = serializer.DeserializeJsonToArray<MappingModel>(complexJson);
// Assert
result.Should().NotBeNull();
result.Should().HaveCount(1);
result[0].Guid.Should().Be(Guid.Parse("12345678-1234-1234-1234-123456789012"));
result[0].Title.Should().Be("Test Mapping");
result[0].Description.Should().Be("A test mapping");
result[0].Priority.Should().Be(10);
}
[Fact]
public void MappingSerializer_DeserializeJsonToArray_WithSystemTextJson_NullValue_ShouldThrowException()
{
// Arrange
var jsonConverter = new SystemTextJsonConverter();
var serializer = new MappingSerializer(jsonConverter);
var nullJson = "null";
// Act
Action act = () => serializer.DeserializeJsonToArray<MappingModel>(nullJson);
// Assert
act.Should().Throw<Exception>();
}
[Fact]
public void MappingSerializer_DeserializeJsonToArray_WithSystemTextJson_PrimitiveValue_ShouldThrowInvalidOperationException()
{
// Arrange
var jsonConverter = new SystemTextJsonConverter();
var serializer = new MappingSerializer(jsonConverter);
var primitiveJson = "\"string value\"";
// Act
Action act = () => serializer.DeserializeJsonToArray<MappingModel>(primitiveJson);
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("Cannot deserialize the provided value to an array or object.");
}
#endif
}

View File

@@ -9,7 +9,7 @@
Matchers: [
{
Name: WildcardMatcher,
Pattern: x,
Pattern: /x,
IgnoreCase: false
}
]

View File

@@ -56,7 +56,7 @@ public class ProxyMappingConverterTests
var request = Request.Create()
.UsingPost()
.WithPath("x")
.WithPath("/x")
.WithParam("p1", "p1-v")
.WithParam("p2", "p2-v")
.WithHeader("Content-Type", new ContentTypeMatcher("text/plain"))

View File

@@ -0,0 +1,161 @@
// Copyright © WireMock.Net
using System.Collections.Generic;
using FluentAssertions;
using WireMock.Net.Testcontainers.Utils;
using Xunit;
namespace WireMock.Net.Tests.Testcontainers;
public class CombineUtilsTests
{
[Fact]
public void Combine_Lists_WithBothEmpty_ReturnsEmptyList()
{
// Arrange
var oldValue = new List<string>();
var newValue = new List<string>();
// Act
var result = CombineUtils.Combine(oldValue, newValue);
// Assert
result.Should().BeEmpty();
}
[Fact]
public void Combine_Lists_WithEmptyOldValue_ReturnsNewValue()
{
// Arrange
var oldValue = new List<string>();
var newValue = new List<string> { "item1", "item2" };
// Act
var result = CombineUtils.Combine(oldValue, newValue);
// Assert
result.Should().Equal("item1", "item2");
}
[Fact]
public void Combine_Lists_WithEmptyNewValue_ReturnsOldValue()
{
// Arrange
var oldValue = new List<string> { "item1", "item2" };
var newValue = new List<string>();
// Act
var result = CombineUtils.Combine(oldValue, newValue);
// Assert
result.Should().Equal("item1", "item2");
}
[Fact]
public void Combine_Lists_WithBothPopulated_ReturnsConcatenatedList()
{
// Arrange
var oldValue = new List<int> { 1, 2, 3 };
var newValue = new List<int> { 4, 5, 6 };
// Act
var result = CombineUtils.Combine(oldValue, newValue);
// Assert
result.Should().Equal(1, 2, 3, 4, 5, 6);
}
[Fact]
public void Combine_Lists_WithDuplicates_RemovesDuplicates()
{
// Arrange
var oldValue = new List<string> { "a", "b", "c" };
var newValue = new List<string> { "b", "c", "d" };
// Act
var result = CombineUtils.Combine(oldValue, newValue);
// Assert
result.Should().Equal("a", "b", "c", "d");
}
[Fact]
public void Combine_Dictionaries_WithBothEmpty_ReturnsEmptyDictionary()
{
// Arrange
var oldValue = new Dictionary<string, int>();
var newValue = new Dictionary<string, int>();
// Act
var result = CombineUtils.Combine(oldValue, newValue);
// Assert
result.Should().BeEmpty();
}
[Fact]
public void Combine_Dictionaries_WithEmptyOldValue_ReturnsNewValue()
{
// Arrange
var oldValue = new Dictionary<string, int>();
var newValue = new Dictionary<string, int>
{
{ "key1", 1 },
{ "key2", 2 }
};
// Act
var result = CombineUtils.Combine(oldValue, newValue);
// Assert
result.Should().HaveCount(2);
result["key1"].Should().Be(1);
result["key2"].Should().Be(2);
}
[Fact]
public void Combine_Dictionaries_WithEmptyNewValue_ReturnsOldValue()
{
// Arrange
var oldValue = new Dictionary<string, int>
{
{ "key1", 1 },
{ "key2", 2 }
};
var newValue = new Dictionary<string, int>();
// Act
var result = CombineUtils.Combine(oldValue, newValue);
// Assert
result.Should().HaveCount(2);
result["key1"].Should().Be(1);
result["key2"].Should().Be(2);
}
[Fact]
public void Combine_Dictionaries_WithNoOverlappingKeys_ReturnsMergedDictionary()
{
// Arrange
var oldValue = new Dictionary<string, string>
{
{ "key1", "value1" },
{ "key2", "value2" }
};
var newValue = new Dictionary<string, string>
{
{ "key3", "value3" },
{ "key4", "value4" }
};
// Act
var result = CombineUtils.Combine(oldValue, newValue);
// Assert
result.Should().HaveCount(4);
result["key1"].Should().Be("value1");
result["key2"].Should().Be("value2");
result["key3"].Should().Be("value3");
result["key4"].Should().Be("value4");
}
}

View File

@@ -7,6 +7,8 @@ using System.Threading.Tasks;
using DotNet.Testcontainers.Builders;
using FluentAssertions;
using FluentAssertions.Execution;
using Meziantou.Extensions.Logging.Xunit;
using Microsoft.Extensions.Logging;
using WireMock.Net.Testcontainers;
using WireMock.Net.Testcontainers.Utils;
using WireMock.Net.Tests.Facts;
@@ -15,8 +17,14 @@ using Xunit.Abstractions;
namespace WireMock.Net.Tests.Testcontainers;
public partial class TestcontainersTests(ITestOutputHelper testOutputHelper)
public class TestcontainersTests(ITestOutputHelper testOutputHelper)
{
private readonly ILogger _logger = new XUnitLogger(testOutputHelper, new LoggerExternalScopeProvider(), nameof(TestcontainersTests), new XUnitLoggerOptions
{
IncludeCategory = true,
TimestampFormat = "yyy-MM-dd HH:mm:ss.fff"
});
[Fact]
public async Task WireMockContainer_Build_And_StartAsync_and_StopAsync()
{
@@ -24,6 +32,7 @@ public partial class TestcontainersTests(ITestOutputHelper testOutputHelper)
var adminUsername = $"username_{Guid.NewGuid()}";
var adminPassword = $"password_{Guid.NewGuid()}";
var wireMockContainer = new WireMockContainerBuilder()
.WithLogger(_logger)
.WithAdminUserNameAndPassword(adminUsername, adminPassword)
.WithAutoRemove(true)
.WithCleanUp(true)
@@ -43,6 +52,7 @@ public partial class TestcontainersTests(ITestOutputHelper testOutputHelper)
.Build();
var wireMockContainer = new WireMockContainerBuilder()
.WithLogger(_logger)
.WithNetwork(dummyNetwork)
.WithWatchStaticMappings(true)
.Build();
@@ -58,6 +68,7 @@ public partial class TestcontainersTests(ITestOutputHelper testOutputHelper)
var adminUsername = $"username_{Guid.NewGuid()}";
var adminPassword = $"password_{Guid.NewGuid()}";
var wireMockContainerBuilder = new WireMockContainerBuilder()
.WithLogger(_logger)
.WithAdminUserNameAndPassword(adminUsername, adminPassword);
var imageOS = await TestcontainersUtils.GetImageOSAsync.Value;
@@ -83,6 +94,7 @@ public partial class TestcontainersTests(ITestOutputHelper testOutputHelper)
var adminUsername = $"username_{Guid.NewGuid()}";
var adminPassword = $"password_{Guid.NewGuid()}";
var wireMockContainerBuilder = new WireMockContainerBuilder()
.WithLogger(_logger)
.WithAdminUserNameAndPassword(adminUsername, adminPassword);
var imageOS = await TestcontainersUtils.GetImageOSAsync.Value;
@@ -92,7 +104,7 @@ public partial class TestcontainersTests(ITestOutputHelper testOutputHelper)
}
else
{
wireMockContainerBuilder = wireMockContainerBuilder.WithImage("sheyenrath/wiremock.net");
wireMockContainerBuilder = wireMockContainerBuilder.WithImage("sheyenrath/wiremock.net-alpine");
}
var wireMockContainer = wireMockContainerBuilder.Build();

View File

@@ -3,6 +3,7 @@
#if NET6_0_OR_GREATER
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
@@ -10,30 +11,45 @@ using FluentAssertions;
using FluentAssertions.Execution;
using Greet;
using Grpc.Net.Client;
using Meziantou.Extensions.Logging.Xunit;
using Microsoft.Extensions.Logging;
using WireMock.Constants;
using WireMock.Net.Testcontainers;
using WireMock.Util;
using Xunit;
using Xunit.Abstractions;
namespace WireMock.Net.Tests.Testcontainers;
public partial class TestcontainersTests
[Collection("Grpc")]
public class TestcontainersTestsGrpc(ITestOutputHelper testOutputHelper)
{
private readonly ILogger _logger = new XUnitLogger(testOutputHelper, new LoggerExternalScopeProvider(), nameof(TestcontainersTestsGrpc), new XUnitLoggerOptions
{
IncludeCategory = true,
TimestampFormat = "yyy-MM-dd HH:mm:ss.fff"
});
[Fact]
public async Task WireMockContainer_Build_Grpc_TestPortsAndUrls1()
{
// Act
// Arrange
var adminUsername = $"username_{Guid.NewGuid()}";
var adminPassword = $"password_{Guid.NewGuid()}";
var port = PortUtils.FindFreeTcpPort();
// Act
var wireMockContainer = new WireMockContainerBuilder()
.WithLogger(_logger)
.WithAdminUserNameAndPassword(adminUsername, adminPassword)
.WithCommand("--UseHttp2")
.WithCommand("--Urls", "http://*:80 grpc://*:9090")
.WithPortBinding(9090, true)
.WithCommand("--Urls", $"http://*:80 grpc://*:{port}")
.WithPortBinding(port, true)
.Build();
try
{
await wireMockContainer.StartAsync().ConfigureAwait(false);
await wireMockContainer.StartAsync();
// Assert
using (new AssertionScope())
@@ -53,10 +69,10 @@ public partial class TestcontainersTests
var httpUrl = wireMockContainer.GetMappedPublicUrl(80);
httpUrl.Should().StartWith("http://");
var grpcPort = wireMockContainer.GetMappedPublicPort(9090);
var grpcPort = wireMockContainer.GetMappedPublicPort(port);
grpcPort.Should().BeGreaterThan(0);
var grpcUrl = wireMockContainer.GetMappedPublicUrl(80);
var grpcUrl = wireMockContainer.GetMappedPublicUrl(port);
grpcUrl.Should().StartWith("http://");
var adminClient = wireMockContainer.CreateWireMockAdminClient();
@@ -74,19 +90,23 @@ public partial class TestcontainersTests
[Fact]
public async Task WireMockContainer_Build_Grpc_TestPortsAndUrls2()
{
// Act
// Arrange
var adminUsername = $"username_{Guid.NewGuid()}";
var adminPassword = $"password_{Guid.NewGuid()}";
var ports = PortUtils.FindFreeTcpPorts(3);
// Act
var wireMockContainer = new WireMockContainerBuilder()
.WithLogger(_logger)
.WithAdminUserNameAndPassword(adminUsername, adminPassword)
.AddUrl("http://*:8080")
.AddUrl("grpc://*:9090")
.AddUrl("grpc://*:9091")
.AddUrl($"http://*:{ports[0]}")
.AddUrl($"grpc://*:{ports[1]}")
.AddUrl($"grpc://*:{ports[2]}")
.Build();
try
{
await wireMockContainer.StartAsync().ConfigureAwait(false);
await wireMockContainer.StartAsync();
// Assert
using (new AssertionScope())
@@ -100,7 +120,7 @@ public partial class TestcontainersTests
var urls = wireMockContainer.GetPublicUrls();
urls.Should().HaveCount(4);
foreach (var internalPort in new[] { 80, 8080, 9090, 9091 })
foreach (var internalPort in new[] { ports[0], ports[1], ports[2], 80 })
{
var publicPort = wireMockContainer.GetMappedPublicPort(internalPort);
publicPort.Should().BeGreaterThan(0);
@@ -149,10 +169,75 @@ public partial class TestcontainersTests
await StopAsync(wireMockContainer);
}
private static async Task<WireMockContainer> Given_WireMockContainerIsStartedForHttpAndGrpcAsync()
[Fact]
public async Task WireMockContainer_Build_Grpc_ProtoDefinitionAtServerLevel_UsingGrpcGeneratedClient_AndWithWatchStaticMappings()
{
var wireMockContainer = await Given_WireMockContainerWithProtoDefinitionAtServerLevelWithWatchStaticMappingsIsStartedForHttpAndGrpcAsync();
var reply = await When_GrpcClient_Calls_SayHelloAsync(wireMockContainer);
Then_ReplyMessage_Should_BeCorrect(reply);
await StopAsync(wireMockContainer);
}
private async Task<HelloReply> When_GrpcClient_Calls_SayHelloAsync(WireMockContainer wireMockContainer)
{
var address = wireMockContainer.GetPublicUrls().First(x => x.Key != 80).Value;
var channel = GrpcChannel.ForAddress(address);
var client = new Greeter.GreeterClient(channel);
try
{
return await client.SayHelloAsync(new HelloRequest { Name = "stef" });
}
catch (Exception ex)
{
testOutputHelper.WriteLine("Exception during GrpcClient Call to {0}. Exception = {1}.", address, ex);
testOutputHelper.WriteLine("Dumping WireMock.Net logs:");
var (stdOut, stdError) = await wireMockContainer.GetLogsAsync(DateTime.MinValue);
testOutputHelper.WriteLine("Out :\r\n{0}", stdOut);
testOutputHelper.WriteLine("Error:\r\n{0}", stdError);
testOutputHelper.WriteLine("Dumping WireMock.Net mappings:");
using var httpClient = wireMockContainer.CreateClient();
using var response = await httpClient.GetAsync("/__admin/mappings");
var mappings = await response.Content.ReadAsStringAsync();
testOutputHelper.WriteLine("Mappings:\r\n{0}", mappings);
throw;
}
}
private async Task StopAsync(WireMockContainer wireMockContainer)
{
try
{
await wireMockContainer.StopAsync();
}
catch (Exception ex)
{
// Sometimes we get this exception, so for now ignore it.
/*
Failed WireMock.Net.Tests.Testcontainers.TestcontainersTests.WireMockContainer_Build_WithImageAsText_And_StartAsync_and_StopAsync [9 s]
Error Message:
System.NullReferenceException : Object reference not set to an instance of an object.
Stack Trace:
at DotNet.Testcontainers.Containers.DockerContainer.UnsafeStopAsync(CancellationToken ct) in /_/src/Testcontainers/Containers/DockerContainer.cs:line 567
at DotNet.Testcontainers.Containers.DockerContainer.StopAsync(CancellationToken ct) in /_/src/Testcontainers/Containers/DockerContainer.cs:line 319
*/
testOutputHelper.WriteLine($"Exception during StopAsync: {ex}");
}
}
private async Task<WireMockContainer> Given_WireMockContainerIsStartedForHttpAndGrpcAsync()
{
var port = PortUtils.FindFreeTcpPort();
var wireMockContainer = new WireMockContainerBuilder()
.AddUrl("grpc://*:9090")
.WithLogger(_logger)
.AddUrl($"grpc://*:{port}")
.Build();
await wireMockContainer.StartAsync();
@@ -160,10 +245,12 @@ public partial class TestcontainersTests
return wireMockContainer;
}
private static async Task<WireMockContainer> Given_WireMockContainerWithProtoDefinitionAtServerLevelIsStartedForHttpAndGrpcAsync()
private async Task<WireMockContainer> Given_WireMockContainerWithProtoDefinitionAtServerLevelIsStartedForHttpAndGrpcAsync()
{
var port = PortUtils.FindFreeTcpPort();
var wireMockContainer = new WireMockContainerBuilder()
.AddUrl("grpc://*:9090")
.WithLogger(_logger)
.AddUrl($"grpc://*:{port}")
.AddProtoDefinition("my-greeter", ReadFile("greet.proto"))
.Build();
@@ -172,6 +259,21 @@ public partial class TestcontainersTests
return wireMockContainer;
}
private async Task<WireMockContainer> Given_WireMockContainerWithProtoDefinitionAtServerLevelWithWatchStaticMappingsIsStartedForHttpAndGrpcAsync()
{
var port = PortUtils.FindFreeTcpPort();
var wireMockContainer = new WireMockContainerBuilder()
.WithLogger(_logger)
.AddUrl($"grpc://*:{port}")
.AddProtoDefinition("my-greeter", ReadFile("greet.proto"))
.WithMappings(Path.Combine(Directory.GetCurrentDirectory(), "__admin", "mappings"))
.Build();
await wireMockContainer.StartAsync();
return wireMockContainer;
}
private static async Task Given_ProtoBufMappingIsAddedViaAdminInterfaceAsync(WireMockContainer wireMockContainer, string filename)
{
var mappingsJson = ReadFile(filename);
@@ -182,16 +284,6 @@ public partial class TestcontainersTests
result.EnsureSuccessStatusCode();
}
private static async Task<HelloReply> When_GrpcClient_Calls_SayHelloAsync(WireMockContainer wireMockContainer)
{
var address = wireMockContainer.GetPublicUrls()[9090];
var channel = GrpcChannel.ForAddress(address);
var client = new Greeter.GreeterClient(channel);
return await client.SayHelloAsync(new HelloRequest { Name = "stef" });
}
private static void Then_ReplyMessage_Should_BeCorrect(HelloReply reply)
{
reply.Message.Should().Be("hello stef POST");

View File

@@ -15,13 +15,13 @@ public class PortUtilsTests
var url = "test";
// Act
var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port);
var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port);
// Assert
result.Should().BeFalse();
isHttps.Should().BeFalse();
isGrpc.Should().BeFalse();
proto.Should().BeNull();
scheme.Should().BeNull();
host.Should().BeNull();
port.Should().Be(default(int));
}
@@ -33,13 +33,13 @@ public class PortUtilsTests
var url = "http://0.0.0.0";
// Act
var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port);
var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port);
// Assert
result.Should().BeFalse();
isHttps.Should().BeFalse();
isGrpc.Should().BeFalse();
proto.Should().BeNull();
scheme.Should().BeNull();
host.Should().BeNull();
port.Should().Be(default(int));
}
@@ -51,13 +51,13 @@ public class PortUtilsTests
var url = "http://wiremock.net:1234";
// Act
var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port);
var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port);
// Assert
result.Should().BeTrue();
isHttps.Should().BeFalse();
isGrpc.Should().BeFalse();
proto.Should().Be("http");
scheme.Should().Be("http");
host.Should().Be("wiremock.net");
port.Should().Be(1234);
}
@@ -69,13 +69,13 @@ public class PortUtilsTests
var url = "https://wiremock.net:5000";
// Act
var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port);
var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port);
// Assert
result.Should().BeTrue();
isHttps.Should().BeTrue();
isGrpc.Should().BeFalse();
proto.Should().Be("https");
scheme.Should().Be("https");
host.Should().Be("wiremock.net");
port.Should().Be(5000);
}
@@ -87,13 +87,13 @@ public class PortUtilsTests
var url = "grpc://wiremock.net:1234";
// Act
var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port);
var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port);
// Assert
result.Should().BeTrue();
isHttps.Should().BeFalse();
isGrpc.Should().BeTrue();
proto.Should().Be("grpc");
scheme.Should().Be("grpc");
host.Should().Be("wiremock.net");
port.Should().Be(1234);
}
@@ -105,13 +105,13 @@ public class PortUtilsTests
var url = "https://0.0.0.0:5000";
// Act
var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port);
var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var scheme, out var host, out var port);
// Assert
result.Should().BeTrue();
isHttps.Should().BeTrue();
isGrpc.Should().BeFalse();
proto.Should().Be("https");
scheme.Should().Be("https");
host.Should().Be("0.0.0.0");
port.Should().Be(5000);
}

View File

@@ -0,0 +1,42 @@
// Copyright © WireMock.Net
using System;
using System.Diagnostics.CodeAnalysis;
using FluentAssertions;
using WireMock.Validators;
using Xunit;
namespace WireMock.Net.Tests.Validators;
[ExcludeFromCodeCoverage]
public class PathValidatorTests
{
[Fact]
public void ValidateAndThrow_ValidPath_DoesNotThrow()
{
Action act = () => PathValidator.ValidateAndThrow("/valid/path");
act.Should().NotThrow();
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("\r")]
[InlineData("\n")]
[InlineData("\t")]
public void ValidateAndThrow_InvalidPath_ThrowsArgumentException_WithDefaultParamName(string? path)
{
Action act = () => PathValidator.ValidateAndThrow(path);
var ex = act.Should().Throw<ArgumentException>().Which;
ex.Message.Should().StartWith("Path must start with a '/' and cannot be null, empty or whitespace.");
ex.ParamName.Should().Be("path");
}
[Fact]
public void ValidateAndThrow_NoLeadingSlash_ThrowsArgumentException_WithProvidedParamName()
{
Action act = () => PathValidator.ValidateAndThrow("noSlash", "myParam");
var ex = act.Should().Throw<ArgumentException>().Which;
ex.ParamName.Should().Be("myParam");
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors>Stef Heyenrath</Authors>
@@ -38,6 +38,10 @@
<Compile Remove="Util\JsonUtilsTests.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\src\WireMock.Net.Testcontainers\Utils\CombineUtils.cs" Link="Testcontainers\CombineUtils.cs" />
</ItemGroup>
<ItemGroup>
<!-- https://stackoverflow.com/questions/59406201/filenesting-not-working-for-class-or-shared-library-projects -->
<ProjectCapability Include="ConfigurableFileNesting" />
@@ -103,7 +107,7 @@
<ItemGroup Condition="'$(TargetFramework)' != 'net452'">
<PackageReference Include="System.Net.Http.Json" Version="8.0.1" />
<PackageReference Include="JsonConverter.System.Text.Json" Version="0.7.0" />
<!--<PackageReference Include="JsonConverter.System.Text.Json" Version="0.8.0" />-->
<PackageReference Include="Google.Protobuf" Version="3.25.1" />
<PackageReference Include="Grpc.Net.Client" Version="2.60.0" />
<PackageReference Include="Grpc.Tools" Version="2.60.0">
@@ -117,6 +121,7 @@
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' or '$(TargetFramework)' == 'net8.0'">
<ProjectReference Include="..\..\src\WireMock.Net.Testcontainers\WireMock.Net.Testcontainers.csproj" />
<PackageReference Include="Meziantou.Extensions.Logging.Xunit" Version="1.0.21" />
</ItemGroup>
<ItemGroup>

View File

@@ -4,21 +4,21 @@
using System.Net;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using FluentAssertions;
using System.Security.Cryptography.X509Certificates;
using WireMock.Net.Tests.Facts;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
using WireMock.Settings;
using WireMock.Types;
using Xunit;
namespace WireMock.Net.Tests;
public partial class WireMockServerTests
{
[Fact]
[IgnoreOnContinuousIntegrationFact]
public async Task WireMockServer_WithRequiredClientCertificates_Should_Work_Correct()
{
// Arrange
@@ -57,5 +57,4 @@ public partial class WireMockServerTests
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
}
#endif

View File

@@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
@@ -34,11 +33,10 @@ public partial class WireMockServerTests
Request.Create()
.WithPath("/a")
.WithBody(
new IMatcher[]
{
[
new JmesPathMatcher("requestId == '1'"),
new JmesPathMatcher("value == 'A'")
},
],
MatchOperator.And
)
.UsingPost()
@@ -49,11 +47,10 @@ public partial class WireMockServerTests
Request.Create()
.WithPath("/a")
.WithBody(
new IMatcher[]
{
[
new JmesPathMatcher("requestId == '2'"),
new JmesPathMatcher("value == 'A'")
},
],
MatchOperator.And
)
.UsingPost()
@@ -81,12 +78,11 @@ public partial class WireMockServerTests
Request.Create()
.WithPath("/a")
.WithBody(
new IMatcher[]
{
[
new JmesPathMatcher("extra == 'X'"),
new JmesPathMatcher("requestId == '1'"),
new JmesPathMatcher("value == 'A'")
},
],
MatchOperator.And
)
.UsingPost()
@@ -98,11 +94,10 @@ public partial class WireMockServerTests
Request.Create()
.WithPath("/a")
.WithBody(
new IMatcher[]
{
[
new JmesPathMatcher("requestId == '1'"),
new JmesPathMatcher("value == 'A'")
},
],
MatchOperator.And
)
.UsingPost()
@@ -188,6 +183,7 @@ public partial class WireMockServerTests
{"jsonrpc":"2.0","id":"{{request.bodyAsJson.id}}","result":{"protocolVersion":"2024-11-05","capabilities":{"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"ExampleServer","version":"1.0.0"}}}
""")
.WithStatusCode(200)
.WithTransformer(true)
);
// Act
@@ -199,15 +195,47 @@ public partial class WireMockServerTests
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var responseText = await response.RequestMessage!.Content!.ReadAsStringAsync();
var responseText = await response.Content.ReadAsStringAsync();
responseText.Should().Contain("ec475f56d4694b48bc737500ba575b35-1");
}
#if NET6_0_OR_GREATER
[Fact]
public async Task WireMockServer_WithBodyAsJson_Using_PostAsync_And_JsonPartialWildcardMatcher_And_SystemTextJson_ShouldMatch()
{
// Arrange
using var server = WireMockServer.Start(x => x.DefaultJsonSerializer = new JsonConverter.System.Text.Json.SystemTextJsonConverter() );
var matcher = new JsonPartialWildcardMatcher(new { id = "^[a-f0-9]{32}-[0-9]$" }, ignoreCase: true, regex: true);
server.Given(Request.Create()
.WithHeader("Content-Type", "application/json*")
.UsingPost()
.WithPath("/system-text-json")
.WithBody(matcher)
)
.RespondWith(Response.Create()
.WithBody("OK")
);
// Act
var content = """{"id":"ec475f56d4694b48bc737500ba575b35-1"}""";
var response = await new HttpClient()
.PostAsync($"{server.Url}/system-text-json", new StringContent(content, Encoding.UTF8, "application/json"))
.ConfigureAwait(false);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var responseText = await response.Content.ReadAsStringAsync();
responseText.Should().Contain("OK");
}
#endif
[Fact]
public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithFunc()
{
// Arrange
var server = WireMockServer.Start();
using var server = WireMockServer.Start();
server.Given(
Request.Create()
.UsingPost()
@@ -219,7 +247,7 @@ public partial class WireMockServerTests
);
// Act
var content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("key1", "value1") });
var content = new FormUrlEncodedContent([new KeyValuePair<string, string>("key1", "value1")]);
var response = await new HttpClient()
.PostAsync($"{server.Url}/foo", content)
.ConfigureAwait(false);

View File

@@ -235,7 +235,7 @@ public partial class WireMockServerTests
}
#if NET8_0_OR_GREATER
[IgnoreOnContinuousIntegrationFact]
[Fact(Skip = "Does not work on local and pipeline")]
public async Task WireMockServer_WithUrl0000_Should_Listen_On_All_IPs_IPv6()
{
// Arrange