Compare commits

...

32 Commits
1.7.4 ... 1.8.6

Author SHA1 Message Date
Stef Heyenrath
9b5801f828 1.8.6 2025-05-15 20:16:17 +02:00
Stef Heyenrath
61b6eb8752 Content-Type multipart/form-data header should also be proxied (#1296) 2025-05-15 18:21:21 +02:00
Stef Heyenrath
baa33552e9 1.8.5 2025-05-14 07:34:04 +02:00
Stef Heyenrath
492f01ade1 Add more tests for WireMockOpenApiParser (#1294) 2025-05-13 22:11:25 +02:00
Stef Heyenrath
7596967fcc Grpc: Fix parsing null value for google.protobuf.Timestamp (#1293)
* Add another example for Grpc client + mapping

* <PackageReference Include="ProtoBufJsonConverter" Version="0.9.0" />
2025-05-10 12:53:18 +02:00
Stef Heyenrath
56c058fe24 Cleanup old WireMock.Net.OpenApiParser 2025-05-10 08:10:20 +02:00
Stef
b43be28b5f 1.8.4 2025-05-08 20:15:23 +02:00
Stef Heyenrath
5ed09d84a3 Use ILRepack to include Microsoft.OpenApi as internal (#1290)
* .

* Use ILRepack to include Microsoft.OpenApi as internal

* ...

* OpenApiSpecificationVersion

* .

* 080

* 4
2025-05-08 20:11:41 +02:00
Stef
cfcc55d2dd 1.8.3 2025-05-06 21:47:29 +02:00
Stef Heyenrath
249b3562ab Update AzureADAuthenticationMatcher to support V2 Azure AAD tokens (#1288)
* Update AzureADAuthenticationMatcher to support V2 Azure AAD tokens

* fix ;-)

* add tests

* Update test/WireMock.Net.Tests/Authentication/MockJwtSecurityTokenHandler.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* .

* WIREMOCK_AAD_TENANT

* update logging

* throw new SecurityTokenInvalidIssuerException($"tenant {extractedTenant} does not match {_tenant}.");

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-06 18:49:21 +02:00
Stef
cfc13b2449 1.8.2 2025-05-05 10:23:14 +02:00
Stef Heyenrath
4303706530 Update ProtoBufJsonConverter to fix conflict for 'MessageOptions' (#1291)
* Update ProtoBufJsonConverter to fix conflict for 'MessageOptions'

* <PackageReference Include="ProtoBufJsonConverter" Version="0.8.0" />
2025-05-05 10:21:15 +02:00
Stef Heyenrath
5c8105b50d move 'WireMock.Net.OpenApiParser.Preview' to ''src-preview' folder 2025-05-02 09:15:59 +02:00
Stef Heyenrath
630ffab56e Do not generate NuGet for WireMock.Net.OpenApiParser.Preview 2025-05-02 08:50:32 +02:00
Stef Heyenrath
c181d0286a Do not generate NuGet for WireMock.Net.OpenApiParser.Preview 2025-05-02 08:41:51 +02:00
Stef Heyenrath
16dab99175 1.8.1 2025-05-02 07:29:11 +02:00
Stef Heyenrath
cf0dcf5855 Revert changes to WireMock.Net.OpenApiParser (#1289)
* Revert changes to WireMock.Net.OpenApiParser

* revert
2025-05-02 07:18:35 +02:00
Stef
e7310fbc7b 1.8.0 2025-04-28 19:35:48 +02:00
Jonathan Mezach
8a07286b89 Add an launch inspector command to Aspire Dashboard (#1283)
* Upgrade to Aspire 9.2.0

Signed-off-by: Jonathan Mezach <jonathan.mezach@rr-wfm.com>

* Remove workload installs from CI pipeline

Signed-off-by: Jonathan Mezach <jonathan.mezach@rr-wfm.com>

* Missed package upgrade

Signed-off-by: Jonathan Mezach <jonathan.mezach@rr-wfm.com>

* Fix usings

Signed-off-by: Jonathan Mezach <jonathan.mezach@rr-wfm.com>

* Add Open Inspector command

Signed-off-by: Jonathan Mezach <jonathan.mezach@rr-wfm.com>

* Fix broken test

Signed-off-by: Jonathan Mezach <jonathan.mezach@rr-wfm.com>

* PR comments

Signed-off-by: Jonathan Mezach <jonathan.mezach@rr-wfm.com>

* More PR comments

Signed-off-by: Jonathan Mezach <jonathan.mezach@rr-wfm.com>

---------

Signed-off-by: Jonathan Mezach <jonathan.mezach@rr-wfm.com>
2025-04-25 20:23:19 +02:00
Emil Tang Kristensen
9392069f8a Enable support for WireMock Middleware in Hosted Services (#1285) 2025-04-25 16:35:55 +02:00
Stef Heyenrath
0fd190b5a3 Fix Changelog 2025-04-24 20:17:15 +02:00
Stef Heyenrath
4368e3cde6 1.7.x (#1268)
* Fix construction of path in OpenApiParser (#1265)

* Server-Sent Events (#1269)

* Server Side Events

* fixes

* await HandleSseStringAsync(responseMessage, response, bodyData);

* 1.7.5-preview-01

* IBlockingQueue

* 1.7.5-preview-02 (03 April 2025)

* IBlockingQueue

* ...

* Support OpenApi V31 (#1279)

* Support OpenApi V31

* Update src/WireMock.Net.OpenApiParser/Extensions/OpenApiSchemaExtensions.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fx

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add ProtoDefinitionHelper.FromDirectory (#1263)

* Add ProtoDefinitionHelper.FromDirectory

* .

* unix-windows

* move test

* imports in the proto files indeed should use a forward slash

* updates

* .

* private Func<IdOrTexts> ProtoDefinitionFunc()

* OpenTelemetry

* .

* fix path utils

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-23 11:51:44 +02:00
Stef Heyenrath
fc0f82db33 Add HandlebarsSettings (#1271)
* Add HandlebarsSettings class

* DefaultAllowedHandlebarsHelpers

* HB - 2.5.0-preview-01

* readme

* fix

* readme

* Handlebars.Net.Helpers Version="2.5.0"
2025-04-23 07:47:37 +02:00
Stef Heyenrath
beabba4064 Add extra unit tests for PR #1278 2025-04-23 07:47:07 +02:00
jollyjoyce1995
d9a7e80360 changed null check in JSONPathMatcher and JmesPathMatcher to ensure that the body is not an empty string (the json parse would throw an exception at this point) (#1278) 2025-04-23 07:30:52 +02:00
Stef Heyenrath
66a048a487 update readme for WireMock.Net.AwesomeAssertions 2025-04-23 07:30:21 +02:00
Francesco Venturoli
04d53f3a9e feat(awesome-assertions): Added new project WireMock.Net.AwesomeAssertions (#1273)
* feat(awesome-assertions): Added new project WireMock.Net.AwesomeAssertions

* feat(awesome-assertions): Applied dotnet naming convention for private readonly fields

---------

Co-authored-by: Francesco Venturoli <f.venturoli@crif.com>
2025-04-22 22:51:40 +02:00
Stef Heyenrath
a8562fda32 Use vmImage ubuntu-22.04 and install aspire workload in Azure DevOps CI-CD pipeline (#1282)
* Install aspire workload in Azure DevOps CI-CD pipeline

* vmImage: 'ubuntu-22.04'
2025-04-22 22:47:43 +02:00
Stef Heyenrath
5abb424d3c Add extra JsonPartialWildcardMatcher Tests (#1267)
* Add extra JsonPartialWildcardMatcher Tests

* responseText.Should().Contain
2025-04-02 08:57:44 +02:00
Stef Heyenrath
db158bcc7e Update readme 2025-03-04 18:17:09 +01:00
Stef Heyenrath
0effda3cfa 1.8.0-prview-01 (Update the usage from the custom Handlebars.Net File helper) 2025-03-04 18:11:13 +01:00
Stef Heyenrath
ff36c1ee6f Merge commit from fork 2025-03-04 17:58:38 +01:00
140 changed files with 7202 additions and 2647 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,13 @@
</PropertyGroup>
<PropertyGroup>
<VersionPrefix>1.7.4</VersionPrefix>
<VersionPrefix>1.8.6</VersionPrefix>
<PackageIcon>WireMock.Net-Logo.png</PackageIcon>
<PackageProjectUrl>https://github.com/WireMock-Net/WireMock.Net</PackageProjectUrl>
<PackageProjectUrl>https://github.com/wiremock/WireMock.Net</PackageProjectUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../../PackageReleaseNotes.txt"))</PackageReleaseNotes>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/WireMock-Net/WireMock.Net</RepositoryUrl>
<RepositoryUrl>https://github.com/wiremock/WireMock.Net</RepositoryUrl>
<ApplicationIcon>../../resources/WireMock.Net-Logo.ico</ApplicationIcon>
<PackageReadmeFile>PackageReadme.md</PackageReadmeFile>
<LangVersion>12.0</LangVersion>

View File

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

View File

@@ -15,13 +15,13 @@ 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-Net/WireMock.Net/wiki/Stubbing).
See [Wiki : Stubbing](https://github.com/wiremock/WireMock.Net/wiki/Stubbing).
### :star: Request Matching
WireMock.Net support advanced request-matching logic, see [Wiki : Request Matching](https://github.com/WireMock-Net/WireMock.Net/wiki/Request-Matching).
WireMock.Net support advanced request-matching logic, see [Wiki : Request Matching](https://github.com/wiremock/WireMock.Net/wiki/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-Net/WireMock.Net/wiki/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).
### :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).
@@ -34,10 +34,10 @@ 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).
### Unit/Integration Testing using Testcontainers.DotNet
See [Wiki : WireMock.Net.Testcontainers](https://github.com/WireMock-Net/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 [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.
### Unit/Integration Testing using an an Aspire Distributed Application
See [Wiki : WireMock.Net.Aspire](https://github.com/WireMock-Net/WireMock.Net/wiki/Using-WireMock.Net.Aspire) on how to use WireMock.Net as an Aspire Hosted application to do Unit/Integration testing.
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.
#### 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).
@@ -46,17 +46,17 @@ It's simple to install WireMock.Net as (global) dotnet tool, see [Wiki : dotnet
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).
#### 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-Net/WireMock.Net/wiki/WireMock-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).
#### As a Web Job in Azure or application in IIS
See this link [WireMock-as-a-(Azure)-Web-App](https://github.com/WireMock-Net/WireMock.Net/wiki/WireMock-as-a-(Azure)-Web-App)
See this link [WireMock-as-a-(Azure)-Web-App](https://github.com/wiremock/WireMock.Net/wiki/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-Net/WireMock.Net-docker).
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-Net/WireMock.Net/wiki/Using-HTTPS-(SSL))
More details on using HTTPS (SSL) can be found here [Wiki : HTTPS](https://github.com/wiremock/WireMock.Net/wiki/Using-HTTPS-(SSL))
## :books: Documentation
For more info, see also this WIKI page: [What is WireMock.Net](https://github.com/WireMock-Net/WireMock.Net/wiki/What-Is-WireMock.Net).
For more info, see also this WIKI page: [What is WireMock.Net](https://github.com/wiremock/WireMock.Net/wiki/What-Is-WireMock.Net).

View File

@@ -3,4 +3,4 @@
- #{{Number}} {{Title}}{{#if Labels}} [{{join Labels ", "}}]{{/if}}
{{/each}}
The full release notes can be found here: https://github.com/WireMock-Net/WireMock.Net/blob/master/CHANGELOG.md
The full release notes can be found here: https://github.com/wiremock/WireMock.Net/blob/master/CHANGELOG.md

View File

@@ -1,5 +1,5 @@
# 1.7.4 (27 February 2025)
- #1256 Add ToArray() to ConcurrentObservableCollection [bug]
- #1254 FindLogEntries exception 'Destination array was not long enough' [bug]
# 1.8.6 (15 May 2025)
- #1296 Content-Type multipart/form-data header should also be proxied [bug]
- #1295 Content-Type `multipart/form-data` header not proxied [bug]
The full release notes can be found here: https://github.com/WireMock-Net/WireMock.Net/blob/master/CHANGELOG.md
The full release notes can be found here: https://github.com/wiremock/WireMock.Net/blob/master/CHANGELOG.md

View File

@@ -1,7 +1,7 @@
# WireMock.Net
A C# .NET version based on [mock4net](https://github.com/alexvictoor/mock4net) which mimics the functionality from the JAVA based [WireMock](http://WireMock.org).
A C# .NET version based on [mock4net](https://github.com/alexvictoor/mock4net) which mimics the functionality from the JAVA based [WireMock](http://wiremock.org).
For more info, see also this WIKI page: [What is WireMock.Net](https://github.com/WireMock-Net/WireMock.Net/wiki/What-Is-WireMock.Net).
For more info, see also this WIKI page: [What is WireMock.Net](https://github.com/wiremock/WireMock.Net/wiki/What-Is-WireMock.Net).
## :star: Key Features
* HTTP response stubbing, matchable on URL/Path, headers, cookies and body content patterns
@@ -26,7 +26,7 @@ For more info, see also this WIKI page: [What is WireMock.Net](https://github.co
| --- | --- |
| ***Project*** | &nbsp; |
| &nbsp;&nbsp;**Chat** | [![Slack](https://badgen.net/badge/icon/slack?icon=slack&label)](https://slack.wiremock.org/) [![Gitter](https://img.shields.io/gitter/room/wiremock_dotnet/Lobby.svg)](https://gitter.im/wiremock_dotnet/Lobby) |
| &nbsp;&nbsp;**Issues** | [![GitHub issues](https://img.shields.io/github/issues/WireMock-Net/WireMock.Net.svg)](https://github.com/WireMock-Net/WireMock.Net/issues) |
| &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) |
@@ -37,7 +37,7 @@ For more info, see also this WIKI page: [What is WireMock.Net](https://github.co
### :package: NuGet packages
| | Official | Preview [:information_source:](https://github.com/WireMock-Net/WireMock.Net/wiki/MyGet-preview-versions) |
| | Official | Preview [:information_source:](https://github.com/wiremock/WireMock.Net/wiki/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.StandAlone** | [![NuGet Badge WireMock.Net](https://img.shields.io/nuget/v/WireMock.Net.StandAlone)](https://www.nuget.org/packages/WireMock.Net.StandAlone) | [![MyGet Badge WireMock.Net.StandAlone](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.StandAlone?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.StandAlone)
@@ -45,6 +45,7 @@ For more info, see also this WIKI page: [What is WireMock.Net](https://github.co
| &nbsp;&nbsp;**WireMock.Net.Aspire** | [![NuGet Badge WireMock.Net.Aspire](https://img.shields.io/nuget/v/WireMock.Net.Aspire)](https://www.nuget.org/packages/WireMock.Net.Aspire) | [![MyGet Badge WireMock.Net.Aspire](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.Aspire?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.Aspire)
| &nbsp;&nbsp;**WireMock.Net.AspNetCore.Middleware** | [![NuGet Badge WireMock.Net.AspNetCore.Middleware](https://img.shields.io/nuget/v/WireMock.Net.AspNetCore.Middleware)](https://www.nuget.org/packages/WireMock.Net.AspNetCore.Middleware) | [![MyGet Badge WireMock.Net.AspNetCore.Middleware](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.AspNetCore.Middleware?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.AspNetCore.Middleware)
| | | |
| &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.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)
@@ -64,20 +65,31 @@ A breaking change is introduced which is related to System.Linq.Dynamic.Core Dyn
- The `LinqMatcher` is not allowed.
- The [Handlebars.Net.Helpers.DynamicLinq](https://www.nuget.org/packages/Handlebars.Net.Helpers.DynamicLinq) package is not included anymore.
### 1.8.0
Some breaking changes are introduced in this version:
#### Handlebars.Net `File`-helper
By default, the internal Handlebars.Net `File`-helper is not allowed anymore because of potential security issues.
To still enable this feature, you need to set the `AllowedCustomHandlebarHelpers` property to `File` in the `HandlebarsSettings` property in `WireMockServerSettings`.
#### Handlebars.Net `Environment`-helper
By default, the Handlebars.Net `Environment`-helper is not automatically allowed anymore because of potential security issues.
To still enable this feature, you need to add the `Environment` category to the `AllowedHandlebarsHelpers` list-property in the `HandlebarsSettings` property in `WireMockServerSettings`.
---
## :memo: Development
For the supported frameworks and build information, see [this](https://github.com/WireMock-Net/WireMock.Net/wiki/Development-Information) page.
For the supported frameworks and build information, see [this](https://github.com/wiremock/WireMock.Net/wiki/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-Net/WireMock.Net/wiki/Stubbing).
See [Wiki : Stubbing](https://github.com/wiremock/WireMock.Net/wiki/Stubbing).
## :star: Request Matching
WireMock.Net support advanced request-matching logic, see [Wiki : Request Matching](https://github.com/WireMock-Net/WireMock.Net/wiki/Request-Matching).
WireMock.Net support advanced request-matching logic, see [Wiki : Request Matching](https://github.com/wiremock/WireMock.Net/wiki/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-Net/WireMock.Net/wiki/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).
## :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).
@@ -90,10 +102,10 @@ 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).
### Unit/Integration Testing using Testcontainers.DotNet
See [Wiki : WireMock.Net.Testcontainers](https://github.com/WireMock-Net/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 [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.
### Unit/Integration Testing using an an Aspire Distributed Application
See [Wiki : WireMock.Net.Aspire](https://github.com/WireMock-Net/WireMock.Net/wiki/Using-WireMock.Net.Aspire) on how to use WireMock.Net as an Aspire Hosted application to do Unit/Integration testing.
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.
### 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).
@@ -102,14 +114,14 @@ It's simple to install WireMock.Net as (global) dotnet tool, see [Wiki : dotnet
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).
### 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-Net/WireMock.Net/wiki/WireMock-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).
### As a Web Job in Azure or application in IIS
See this link [WireMock-as-a-(Azure)-Web-App](https://github.com/WireMock-Net/WireMock.Net/wiki/WireMock-as-a-(Azure)-Web-App)
See this link [WireMock-as-a-(Azure)-Web-App](https://github.com/wiremock/WireMock.Net/wiki/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-Net/WireMock.Net-docker).
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-Net/WireMock.Net/wiki/Using-HTTPS-(SSL))
More details on using HTTPS (SSL) can be found here [Wiki : HTTPS](https://github.com/wiremock/WireMock.Net/wiki/Using-HTTPS-(SSL))

View File

@@ -46,8 +46,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.Console.Net452
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Console.RequestLogTest", "examples\WireMock.Net.Console.RequestLogTest\WireMock.Net.Console.RequestLogTest.csproj", "{A9D039B9-7509-4CF1-9EFD-87EB82998575}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.OpenApiParser", "src\WireMock.Net.OpenApiParser\WireMock.Net.OpenApiParser.csproj", "{D3804228-91F4-4502-9595-39584E5AADAD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.OpenApiParser.ConsoleApp", "examples\WireMock.Net.OpenApiParser.ConsoleApp\WireMock.Net.OpenApiParser.ConsoleApp.csproj", "{5C09FB93-1535-4F92-AF26-21E8A061EE4A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.FluentAssertions", "src\WireMock.Net.FluentAssertions\WireMock.Net.FluentAssertions.csproj", "{B6269AAC-170A-4346-8B9A-579DED3D9A95}"
@@ -128,6 +126,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.TestWebApplica
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Middleware.Tests", "test\WireMock.Net.Middleware.Tests\WireMock.Net.Middleware.Tests.csproj", "{A5FEF4F7-7DA2-4962-89A8-16BA942886E5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.AwesomeAssertions", "src\WireMock.Net.AwesomeAssertions\WireMock.Net.AwesomeAssertions.csproj", "{7753670F-7C7F-44BF-8BC7-08325588E60C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.OpenApiParser", "src\WireMock.Net.OpenApiParser\WireMock.Net.OpenApiParser.csproj", "{E5B03EEF-822C-4295-952B-4479AD30082B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -174,10 +176,6 @@ Global
{A9D039B9-7509-4CF1-9EFD-87EB82998575}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9D039B9-7509-4CF1-9EFD-87EB82998575}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A9D039B9-7509-4CF1-9EFD-87EB82998575}.Release|Any CPU.Build.0 = Release|Any CPU
{D3804228-91F4-4502-9595-39584E5AADAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D3804228-91F4-4502-9595-39584E5AADAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D3804228-91F4-4502-9595-39584E5AADAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D3804228-91F4-4502-9595-39584E5AADAD}.Release|Any CPU.Build.0 = Release|Any CPU
{5C09FB93-1535-4F92-AF26-21E8A061EE4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5C09FB93-1535-4F92-AF26-21E8A061EE4A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C09FB93-1535-4F92-AF26-21E8A061EE4A}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -302,6 +300,14 @@ Global
{A5FEF4F7-7DA2-4962-89A8-16BA942886E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A5FEF4F7-7DA2-4962-89A8-16BA942886E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A5FEF4F7-7DA2-4962-89A8-16BA942886E5}.Release|Any CPU.Build.0 = Release|Any CPU
{7753670F-7C7F-44BF-8BC7-08325588E60C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7753670F-7C7F-44BF-8BC7-08325588E60C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7753670F-7C7F-44BF-8BC7-08325588E60C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7753670F-7C7F-44BF-8BC7-08325588E60C}.Release|Any CPU.Build.0 = Release|Any CPU
{E5B03EEF-822C-4295-952B-4479AD30082B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5B03EEF-822C-4295-952B-4479AD30082B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5B03EEF-822C-4295-952B-4479AD30082B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E5B03EEF-822C-4295-952B-4479AD30082B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -317,7 +323,6 @@ Global
{7F0B2446-0363-4720-AF46-F47F83B557DC} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
{668F689E-57B4-422E-8846-C0FF643CA268} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
{A9D039B9-7509-4CF1-9EFD-87EB82998575} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
{D3804228-91F4-4502-9595-39584E5AADAD} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{5C09FB93-1535-4F92-AF26-21E8A061EE4A} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
{B6269AAC-170A-4346-8B9A-579DED3D9A95} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{40BF24B5-12E6-4610-9489-138798632E28} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
@@ -351,6 +356,8 @@ Global
{B6269AAC-170A-4346-8B9A-579DED3D9A13} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{6B30AA9F-DA04-4EB5-B03C-45A8EF272ECE} = {0BB8B634-407A-4610-A91F-11586990767A}
{A5FEF4F7-7DA2-4962-89A8-16BA942886E5} = {0BB8B634-407A-4610-A91F-11586990767A}
{7753670F-7C7F-44BF-8BC7-08325588E60C} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{E5B03EEF-822C-4295-952B-4479AD30082B} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458}

View File

@@ -35,6 +35,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Jmes/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Levenstein/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=openapi/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=opentelemetry/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pacticipant/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=protobuf/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Raml/@EntryIndexedValue">True</s:Boolean>

View File

@@ -1,181 +1,176 @@
variables:
Prerelease: 'ci'
buildId: "1$(Build.BuildId)"
buildProjects: '**/src/**/*.csproj'
jobs:
- job: Linux_Build_Test_SonarCloud
pool:
vmImage: 'Ubuntu-latest'
steps:
- script: |
echo "BuildId = $(buildId)"
displayName: 'Print buildId'
- task: CmdLine@2
displayName: 'Install .NET Aspire workload'
inputs:
script: 'dotnet workload install aspire'
- script: |
dotnet tool install --global dotnet-sonarscanner
dotnet tool install --global dotnet-coverage
displayName: 'Install dotnet tools'
- task: PowerShell@2
displayName: "Use JDK17 by default"
inputs:
targetType: 'inline'
script: |
$jdkPath = $env:JAVA_HOME_17_X64
Write-Host "##vso[task.setvariable variable=JAVA_HOME]$jdkPath"
- script: |
dotnet dev-certs https --trust || true
displayName: 'dotnet dev-certs https'
# See: https://docs.sonarsource.com/sonarcloud/enriching/test-coverage/dotnet-test-coverage
- script: |
dotnet sonarscanner begin /k:"WireMock-Net_WireMock.Net" /o:"wiremock-net" /d:sonar.branch.name=$(Build.SourceBranchName) /d:sonar.host.url="https://sonarcloud.io" /d:sonar.token="$(SONAR_TOKEN)" /d:sonar.pullrequest.provider=github /d:sonar.cs.vscoveragexml.reportsPaths=**/wiremock-coverage-*.xml /d:sonar.verbose=true
displayName: 'Begin analysis on SonarCloud'
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
- task: DotNetCoreCLI@2
displayName: 'Build Unit tests'
inputs:
command: 'build'
projects: '**/test/**/*.csproj'
arguments: '--configuration Debug --framework net8.0'
- task: CmdLine@2
inputs:
script: |
dotnet-coverage collect "dotnet test ./test/WireMock.Net.Tests/WireMock.Net.Tests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-xunit.xml"
displayName: 'WireMock.Net.Tests with Coverage'
- task: CmdLine@2
inputs:
script: |
dotnet-coverage collect "dotnet test ./test/WireMock.Net.TUnitTests/WireMock.Net.TUnitTests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-tunit.xml"
displayName: 'WireMock.Net.TUnitTests with Coverage'
- task: CmdLine@2
inputs:
script: |
dotnet-coverage collect "dotnet test ./test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-middleware.xml"
displayName: 'WireMock.Net.Middleware.Tests with Coverage'
- task: CmdLine@2
inputs:
script: |
dotnet-coverage collect "dotnet test ./test/WireMock.Net.Aspire.Tests/WireMock.Net.Aspire.Tests.csproj --configuration Debug --no-build" -f xml -o "wiremock-coverage-aspire.xml"
displayName: 'WireMock.Net.Aspire.Tests with Coverage'
- task: CmdLine@2
displayName: 'Merge coverage files'
inputs:
script: 'dotnet coverage merge **/wiremock-coverage-*.xml --output ./test/wiremock-coverage.xml --output-format xml'
- script: |
dotnet sonarscanner end /d:sonar.token="$(SONAR_TOKEN)"
displayName: 'End analysis on SonarCloud'
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
- task: whitesource.ws-bolt.bolt.wss.WhiteSource Bolt@19
displayName: 'WhiteSource Bolt'
condition: and(succeeded(), eq(variables['RUN_WHITESOURCE'], 'yes'))
- script: |
bash <(curl https://codecov.io/bash) -t $(CODECOV_TOKEN) -f ./test/wiremock-coverage.xml
displayName: 'Upload coverage results to codecov'
- task: PublishTestResults@2
condition: and(succeeded(), eq(variables['PUBLISH_TESTRESULTS'], 'yes'))
inputs:
testRunner: VSTest
testResultsFiles: '**/*.trx'
- task: PublishBuildArtifacts@1
displayName: Publish coverage files
inputs:
PathtoPublish: './test/WireMock.Net.Tests/coverage.net8.0.opencover.xml'
- job: Windows_Build_Test
pool:
vmImage: 'windows-2022'
steps:
- task: UseDotNet@2
displayName: Use .NET 8.0
inputs:
packageType: 'sdk'
version: '8.0.x'
- task: DotNetCoreCLI@2
displayName: 'WireMock.Net.Tests with Coverage'
inputs:
command: 'test'
projects: './test/WireMock.Net.Tests/WireMock.Net.Tests.csproj'
arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx'
- task: DotNetCoreCLI@2
displayName: 'WireMock.Net.TUnitTests with Coverage'
inputs:
command: 'test'
projects: './test/WireMock.Net.TUnitTests/WireMock.Net.TUnitTests.csproj'
arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx'
- task: DotNetCoreCLI@2
displayName: 'WireMock.Net.Middleware.Tests with Coverage'
inputs:
command: 'test'
projects: './test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj'
arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx'
- job: Windows_Release_to_MyGet
dependsOn: Windows_Build_Test
pool:
vmImage: 'windows-2022'
steps:
- task: UseDotNet@2
displayName: Use .NET 8.0
inputs:
packageType: 'sdk'
version: '8.0.x'
- task: DotNetCoreCLI@2
displayName: Build Release
inputs:
command: 'build'
arguments: /p:Configuration=Release
projects: $(buildProjects)
- task: DotNetCoreCLI@2
displayName: Pack
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
inputs:
command: pack
configuration: 'Release'
packagesToPack: $(buildProjects)
nobuild: true
packDirectory: '$(Build.ArtifactStagingDirectory)/packages'
verbosityPack: 'normal'
- task: PublishBuildArtifacts@1
displayName: Publish Artifacts
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
- task: DotNetCoreCLI@2
displayName: Push to MyGet
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
inputs:
command: custom
custom: nuget
variables:
Prerelease: 'ci'
buildId: "1$(Build.BuildId)"
buildProjects: '**/src/**/*.csproj'
jobs:
- job: Linux_Build_Test_SonarCloud
pool:
vmImage: 'ubuntu-22.04'
steps:
- script: |
echo "BuildId = $(buildId)"
displayName: 'Print buildId'
- script: |
dotnet tool install --global dotnet-sonarscanner
dotnet tool install --global dotnet-coverage
displayName: 'Install dotnet tools'
- task: PowerShell@2
displayName: "Use JDK17 by default"
inputs:
targetType: 'inline'
script: |
$jdkPath = $env:JAVA_HOME_17_X64
Write-Host "##vso[task.setvariable variable=JAVA_HOME]$jdkPath"
- script: |
dotnet dev-certs https --trust || true
displayName: 'dotnet dev-certs https'
# See: https://docs.sonarsource.com/sonarcloud/enriching/test-coverage/dotnet-test-coverage
- script: |
dotnet sonarscanner begin /k:"WireMock-Net_WireMock.Net" /o:"wiremock-net" /d:sonar.branch.name=$(Build.SourceBranchName) /d:sonar.host.url="https://sonarcloud.io" /d:sonar.token="$(SONAR_TOKEN)" /d:sonar.pullrequest.provider=github /d:sonar.cs.vscoveragexml.reportsPaths=**/wiremock-coverage-*.xml /d:sonar.verbose=true
displayName: 'Begin analysis on SonarCloud'
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
- task: DotNetCoreCLI@2
displayName: 'Build Unit tests'
inputs:
command: 'build'
projects: '**/test/**/*.csproj'
arguments: '--configuration Debug --framework net8.0'
- task: CmdLine@2
inputs:
script: |
dotnet-coverage collect "dotnet test ./test/WireMock.Net.Tests/WireMock.Net.Tests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-xunit.xml"
displayName: 'WireMock.Net.Tests with Coverage'
- task: CmdLine@2
inputs:
script: |
dotnet-coverage collect "dotnet test ./test/WireMock.Net.TUnitTests/WireMock.Net.TUnitTests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-tunit.xml"
displayName: 'WireMock.Net.TUnitTests with Coverage'
- task: CmdLine@2
inputs:
script: |
dotnet-coverage collect "dotnet test ./test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj --configuration Debug --no-build --framework net8.0" -f xml -o "wiremock-coverage-middleware.xml"
displayName: 'WireMock.Net.Middleware.Tests with Coverage'
- task: CmdLine@2
inputs:
script: |
dotnet-coverage collect "dotnet test ./test/WireMock.Net.Aspire.Tests/WireMock.Net.Aspire.Tests.csproj --configuration Debug --no-build" -f xml -o "wiremock-coverage-aspire.xml"
displayName: 'WireMock.Net.Aspire.Tests with Coverage'
- task: CmdLine@2
displayName: 'Merge coverage files'
inputs:
script: 'dotnet coverage merge **/wiremock-coverage-*.xml --output ./test/wiremock-coverage.xml --output-format xml'
- script: |
dotnet sonarscanner end /d:sonar.token="$(SONAR_TOKEN)"
displayName: 'End analysis on SonarCloud'
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
- task: whitesource.ws-bolt.bolt.wss.WhiteSource Bolt@19
displayName: 'WhiteSource Bolt'
condition: and(succeeded(), eq(variables['RUN_WHITESOURCE'], 'yes'))
- script: |
bash <(curl https://codecov.io/bash) -t $(CODECOV_TOKEN) -f ./test/wiremock-coverage.xml
displayName: 'Upload coverage results to codecov'
- task: PublishTestResults@2
condition: and(succeeded(), eq(variables['PUBLISH_TESTRESULTS'], 'yes'))
inputs:
testRunner: VSTest
testResultsFiles: '**/*.trx'
- task: PublishBuildArtifacts@1
displayName: Publish coverage files
inputs:
PathtoPublish: './test/WireMock.Net.Tests/coverage.net8.0.opencover.xml'
- job: Windows_Build_Test
pool:
vmImage: 'windows-2022'
steps:
- task: UseDotNet@2
displayName: Use .NET 8.0
inputs:
packageType: 'sdk'
version: '8.0.x'
- task: DotNetCoreCLI@2
displayName: 'WireMock.Net.Tests with Coverage'
inputs:
command: 'test'
projects: './test/WireMock.Net.Tests/WireMock.Net.Tests.csproj'
arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx'
- task: DotNetCoreCLI@2
displayName: 'WireMock.Net.TUnitTests with Coverage'
inputs:
command: 'test'
projects: './test/WireMock.Net.TUnitTests/WireMock.Net.TUnitTests.csproj'
arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx'
- task: DotNetCoreCLI@2
displayName: 'WireMock.Net.Middleware.Tests with Coverage'
inputs:
command: 'test'
projects: './test/WireMock.Net.Middleware.Tests/WireMock.Net.Middleware.Tests.csproj'
arguments: '--configuration Debug --framework net8.0 --collect:"XPlat Code Coverage" --logger trx'
- job: Windows_Release_to_MyGet
dependsOn: Windows_Build_Test
pool:
vmImage: 'windows-2022'
steps:
- task: UseDotNet@2
displayName: Use .NET 8.0
inputs:
packageType: 'sdk'
version: '8.0.x'
- task: DotNetCoreCLI@2
displayName: Build Release
inputs:
command: 'build'
arguments: /p:Configuration=Release
projects: $(buildProjects)
- task: DotNetCoreCLI@2
displayName: Pack
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
inputs:
command: pack
configuration: 'Release'
packagesToPack: $(buildProjects)
nobuild: true
packDirectory: '$(Build.ArtifactStagingDirectory)/packages'
verbosityPack: 'normal'
- task: PublishBuildArtifacts@1
displayName: Publish Artifacts
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
- task: DotNetCoreCLI@2
displayName: Push to MyGet
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) # Do not run for PullRequests
inputs:
command: custom
custom: nuget
arguments: push $(Build.ArtifactStagingDirectory)\packages\*.nupkg -n -s https://www.myget.org/F/wiremock-net/api/v3/index.json -k $(MyGetKey)

View File

@@ -1,23 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AspireApp1.ApiService\AspireApp1.ApiService.csproj" />
<ProjectReference Include="..\AspireApp1.Web\AspireApp1.Web.csproj" />
<!-- https://learn.microsoft.com/en-us/dotnet/aspire/extensibility/custom-resources?tabs=windows#create-library-for-resource-extension -->
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="8.2.0" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.2.0" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AspireApp1.ApiService\AspireApp1.ApiService.csproj" />
<ProjectReference Include="..\AspireApp1.Web\AspireApp1.Web.csproj" />
<!-- https://learn.microsoft.com/en-us/dotnet/aspire/extensibility/custom-resources?tabs=windows#create-library-for-resource-extension -->
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.2.0" />
</ItemGroup>
</Project>

View File

@@ -1,20 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AspireApp1.ApiService\AspireApp1.ApiService.csproj" />
<ProjectReference Include="..\AspireApp1.Web\AspireApp1.Web.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="8.2.0" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.2.0" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AspireApp1.ApiService\AspireApp1.ApiService.csproj" />
<ProjectReference Include="..\AspireApp1.Web\AspireApp1.Web.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.2.0" />
</ItemGroup>
</Project>

View File

@@ -2,22 +2,42 @@
using Greet;
using Grpc.Net.Client;
using Policy2;
namespace WireMock.Net.Console.GrpcClient;
await TestPolicyAsync();
// await TestGreeterAsync();
return;
internal class Program
async Task TestGreeterAsync()
{
static async Task Main(string[] args)
var channel = GrpcChannel.ForAddress("http://localhost:9093/grpc3", new GrpcChannelOptions
{
var channel = GrpcChannel.ForAddress("http://localhost:9093/grpc3", new GrpcChannelOptions
Credentials = Grpc.Core.ChannelCredentials.Insecure
});
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(new HelloRequest { Name = "stef" });
Console.WriteLine("Greeting: " + reply.Message);
}
async Task TestPolicyAsync()
{
var channel = GrpcChannel.ForAddress("http://localhost:9093/grpc-policy", new GrpcChannelOptions
{
Credentials = Grpc.Core.ChannelCredentials.Insecure
});
var client = new PolicyService2.PolicyService2Client(channel);
var reply = await client.GetCancellationDetailAsync(new GetCancellationDetailRequest
{
Client = new Client
{
Credentials = Grpc.Core.ChannelCredentials.Insecure
});
CorrelationId = "abc"
}
});
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(new HelloRequest { Name = "stef" });
System.Console.WriteLine("Greeting: " + reply.Message);
}
Console.WriteLine("PolicyService2:reply.CancellationName " + reply.CancellationName);
}

View File

@@ -5,6 +5,7 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
@@ -18,6 +19,7 @@
<ItemGroup>
<Protobuf Include="greet.proto" GrpcServices="Client" />
<Protobuf Include="policy.proto" GrpcServices="Client" />
</ItemGroup>
</Project>
</Project>

View File

@@ -0,0 +1,64 @@
syntax = "proto3";
import "google/protobuf/timestamp.proto";
// option csharp_namespace = "NarrowIntegrationTest.Lookup";
package Policy2;
service PolicyService2 {
rpc GetCancellationDetail (GetCancellationDetailRequest) returns (GetCancellationDetailResponse);
}
message GetCancellationDetailRequest {
Client Client = 1;
LegacyPolicyKey LegacyPolicyKey = 2;
}
message GetCancellationDetailResponse {
ResponseStatus Status = 1;
string CancellationCode = 2;
string CancellationName = 3;
string CancellationDescription = 4;
google.protobuf.Timestamp CancellationEffDate = 5;
string NonRenewalCode = 6;
string NonRenewalName = 7;
string NonRenewalDescription = 8;
google.protobuf.Timestamp NonRenewalEffDate = 9;
google.protobuf.Timestamp LastReinstatementDate = 10;
}
message LegacyPolicyKey {
string Group = 1;
int32 UnitNumber = 2;
int32 Year = 3;
string Suffix = 4;
}
message ResponseStatus {
bool HasErrors = 1;
bool HasWarnings = 2;
repeated string Errors = 3;
repeated string Warnings = 4;
string CorrelationId = 5;
}
message Client {
string CorrelationId = 1;
enum Clients {
Unknown = 0;
QMS = 1;
BillingCenter = 2;
PAS = 3;
Payroll = 4;
Portal = 5;
SFO = 6;
QuoteAndBind = 7;
LegacyConversion = 8;
BindNow = 9;
PaymentPortal = 10 ;
PricingEngine = 11;
}
Clients ClientName = 2;
}

View File

@@ -6,7 +6,6 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using Newtonsoft.Json;
using WireMock.Logging;
@@ -45,57 +44,169 @@ namespace WireMock.Net.ConsoleApplication
public static class MainApp
{
private const string ProtoDefinition = @"
syntax = ""proto3"";
private const string ProtoDefinitionGreeter =
"""
syntax = "proto3";
package greet;
package greet;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
";
message HelloReply {
string message = 1;
}
private const string TestSchema = @"
scalar DateTime
scalar MyCustomScalar
""";
input MessageInput {
content: String
author: String
}
private const string ProtoDefinitionPolicy =
"""
syntax = "proto3";
type Message {
id: ID!
content: String
author: String
}
import "google/protobuf/timestamp.proto";
type Mutation {
createMessage(input: MessageInput): Message
createAnotherMessage(x: MyCustomScalar, dt: DateTime): Message
updateMessage(id: ID!, input: MessageInput): Message
}
// option csharp_namespace = "NarrowIntegrationTest.Lookup";
type Query {
greeting:String
students:[Student]
studentById(id:ID!):Student
}
package Policy2;
type Student {
id:ID!
firstName:String
lastName:String
fullName:String
}";
service PolicyService2 {
rpc GetCancellationDetail (GetCancellationDetailRequest) returns (GetCancellationDetailResponse);
}
message GetCancellationDetailRequest {
Client Client = 1;
LegacyPolicyKey LegacyPolicyKey = 2;
}
message GetCancellationDetailResponse {
ResponseStatus Status = 1;
string CancellationCode = 2;
string CancellationName = 3;
string CancellationDescription = 4;
google.protobuf.Timestamp CancellationEffDate = 5;
string NonRenewalCode = 6;
string NonRenewalName = 7;
string NonRenewalDescription = 8;
google.protobuf.Timestamp NonRenewalEffDate = 9;
google.protobuf.Timestamp LastReinstatementDate = 10; // Always send the last reinstatement date if present on the policy term.
}
message LegacyPolicyKey {
string Group = 1;
int32 UnitNumber = 2;
int32 Year = 3;
string Suffix = 4;
}
message ResponseStatus {
bool HasErrors = 1;
bool HasWarnings = 2;
repeated string Errors = 3;
repeated string Warnings = 4;
string CorrelationId = 5;
}
message Client {
string CorrelationId = 1;
enum Clients {
Unknown = 0;
QMS = 1;
BillingCenter = 2;
PAS = 3;
Payroll = 4;
Portal = 5;
SFO = 6;
QuoteAndBind = 7;
LegacyConversion = 8;
BindNow = 9;
PaymentPortal = 10 ;
PricingEngine = 11;
}
Clients ClientName = 2;
}
""";
private const string TestSchema =
"""
scalar DateTime
scalar MyCustomScalar
input MessageInput {
content: String
author: String
}
type Message {
id: ID!
content: String
author: String
}
type Mutation {
createMessage(input: MessageInput): Message
createAnotherMessage(x: MyCustomScalar, dt: DateTime): Message
updateMessage(id: ID!, input: MessageInput): Message
}
type Query {
greeting:String
students:[Student]
studentById(id:ID!):Student
}
type Student {
id:ID!
firstName:String
lastName:String
fullName:String
}
""";
private static void RunSse()
{
var server = WireMockServer.Start(new WireMockServerSettings
{
Port = 9091,
StartAdminInterface = true,
Logger = new WireMockConsoleLogger()
});
server
.WhenRequest(r => r
.UsingGet()
.WithPath("/sse")
)
.ThenRespondWith(r => r
.WithHeader("Content-Type", "text/event-stream")
.WithHeader("Cache-Control", "no-cache")
.WithHeader("Connection", "keep-alive")
.WithSseBody(async (_, q) =>
{
for (var i = 0; i < 5; i++)
{
q.Write("test " + i + "\r\n");
await Task.Delay(5000);
}
q.Close();
})
);
server
.WhenRequest(r => r
.UsingGet()
)
.ThenRespondWith(r => r
.WithBody("normal")
);
System.Console.ReadKey();
}
private static void RunOnLocal()
{
@@ -136,7 +247,8 @@ message HelloReply {
public static void Run()
{
RunOnLocal();
//RunSse();
//RunOnLocal();
var mappingBuilder = new MappingBuilder();
mappingBuilder
@@ -227,8 +339,8 @@ message HelloReply {
});
System.Console.WriteLine("WireMockServer listening at {0}", string.Join(",", server.Urls));
server.SetBasicAuthentication("a", "b");
//server.SetAzureADAuthentication("6c2a4722-f3b9-4970-b8fc-fac41e29stef", "8587fde1-7824-42c7-8592-faf92b04stef");
//server.SetBasicAuthentication("a", "b");
server.SetAzureADAuthentication(Environment.GetEnvironmentVariable("WIREMOCK_AAD_TENANT")!, "api://e083d51a-01a6-446c-8ad5-0c5c7f002208");
//var http = new HttpClient();
//var response = await http.GetAsync($"{_wireMockServer.Url}/pricing");
@@ -241,11 +353,11 @@ message HelloReply {
.UsingPost()
.WithHttpVersion("2")
.WithPath("/grpc/greet.Greeter/SayHello")
.WithBodyAsProtoBuf(ProtoDefinition, "greet.HelloRequest", protoBufJsonMatcher)
.WithBodyAsProtoBuf(ProtoDefinitionGreeter, "greet.HelloRequest", protoBufJsonMatcher)
)
.RespondWith(Response.Create()
.WithHeader("Content-Type", "application/grpc")
.WithBodyAsProtoBuf(ProtoDefinition, "greet.HelloReply",
.WithBodyAsProtoBuf(ProtoDefinitionGreeter, "greet.HelloReply",
new
{
message = "hello {{request.BodyAsJson.name}}"
@@ -262,7 +374,7 @@ message HelloReply {
.WithPath("/grpc2/greet.Greeter/SayHello")
.WithBodyAsProtoBuf("greet.HelloRequest", protoBufJsonMatcher)
)
.WithProtoDefinition(ProtoDefinition)
.WithProtoDefinition(ProtoDefinitionGreeter)
.RespondWith(Response.Create()
.WithHeader("Content-Type", "application/grpc")
.WithBodyAsProtoBuf("greet.HelloReply",
@@ -276,7 +388,7 @@ message HelloReply {
);
server
.AddProtoDefinition("my-greeter", ProtoDefinition)
.AddProtoDefinition("my-greeter", ProtoDefinitionGreeter)
.Given(Request.Create()
.UsingPost()
.WithPath("/grpc3/greet.Greeter/SayHello")
@@ -294,6 +406,25 @@ message HelloReply {
.WithTrailingHeader("grpc-status", "0")
.WithTransformer()
);
var protoBufJsonMatcherForGetCancellationDetailRequest = new JsonPartialWildcardMatcher("{\"Client\":{\"CorrelationId\":\"*\"}}", false, true);
var getCancellationDetailResponseAsJsonObject = JsonConvert.DeserializeObject(
"""{"Status":{"HasErrors":false,"HasWarnings":false,"Errors":[],"Warnings":[],"CorrelationId":"b8ad0d04-ed2f-42e1-ac85-339d91dc9855"},"CancellationCode":"cc123","CancellationName":"cn123","CancellationDescription":"","CancellationEffDate":null,"NonRenewalCode":"","NonRenewalName":"","NonRenewalDescription":"","NonRenewalEffDate":null,"LastReinstatementDate":null}"""
)!;
server
.AddProtoDefinition("grpc-policy", ProtoDefinitionPolicy)
.Given(Request.Create()
.UsingPost()
.WithPath("/Policy2.PolicyService2/GetCancellationDetail")
.WithBodyAsProtoBuf("Policy2.GetCancellationDetailRequest", protoBufJsonMatcherForGetCancellationDetailRequest)
)
.WithProtoDefinition("grpc-policy")
.RespondWith(Response.Create()
.WithHeader("Content-Type", "application/grpc")
.WithBodyAsProtoBuf("Policy2.GetCancellationDetailResponse", getCancellationDetailResponseAsJsonObject)
.WithTrailingHeader("grpc-status", "0")
.WithTransformer()
);
#endif
#if GRAPHQL
@@ -555,9 +686,9 @@ message HelloReply {
.WithHeader("Content-Type", "application/json")
.WithBodyAsJson(new { result = "data:headers posted with 201" }));
if (!System.IO.File.Exists(@"c:\temp\x.json"))
if (!File.Exists(@"c:\temp\x.json"))
{
System.IO.File.WriteAllText(@"c:\temp\x.json", "{ \"hello\": \"world\", \"answer\": 42 }");
File.WriteAllText(@"c:\temp\x.json", "{ \"hello\": \"world\", \"answer\": 42 }");
}
server
@@ -884,7 +1015,7 @@ message HelloReply {
BodyData = new BodyData
{
BodyAsString = "random200or505:" + code + ", HeadersFromRequest = " + string.Join(",", request.Headers),
DetectedBodyType = Types.BodyType.String,
DetectedBodyType = BodyType.String,
},
StatusCode = code
};
@@ -900,7 +1031,7 @@ message HelloReply {
return new ResponseMessage
{
BodyData = new BodyData { BodyAsString = "random200or505async:" + code, DetectedBodyType = Types.BodyType.String },
BodyData = new BodyData { BodyAsString = "random200or505async:" + code, DetectedBodyType = BodyType.String },
StatusCode = code
};
}));

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -0,0 +1,30 @@
// Copyright © WireMock.Net
using System.Diagnostics.CodeAnalysis;
namespace WireMock.Models;
/// <summary>
/// A simple implementation for a Blocking Queue.
/// </summary>
/// <typeparam name="T">Specifies the type of elements in the queue.</typeparam>
public interface IBlockingQueue<T>
{
/// <summary>
/// Writes an item to the queue and signals that an item is available.
/// </summary>
/// <param name="item">The item to be added to the queue.</param>
void Write(T item);
/// <summary>
/// Tries to read an item from the queue. Waits until an item is available or the timeout occurs.
/// </summary>
/// <param name="item">The item read from the queue, or default if the timeout occurs.</param>
/// <returns>True if an item was successfully read; otherwise, false.</returns>
bool TryRead([NotNullWhen(true)] out T? item);
/// <summary>
/// Closes the queue and signals all waiting threads.
/// </summary>
public void Close();
}

View File

@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using WireMock.Models;
using WireMock.Types;
@@ -71,7 +72,7 @@ public interface IBodyData
Encoding? Encoding { get; set; }
/// <summary>
/// Defines if this BodyData is the result of a dynamically created response-string. (
/// Defines if this BodyData is the result of a dynamically created response-string.
/// </summary>
public string? IsFuncUsed { get; set; }
@@ -86,4 +87,14 @@ public interface IBodyData
/// </summary>
public string? ProtoBufMessageType { get; set; }
#endregion
/// <summary>
/// Defines the queue to use for Server-Sent Events (string).
/// </summary>
public IBlockingQueue<string?>? SseStringQueue { get; set; }
/// <summary>
/// Defines if the body is using Server-Sent Events (string).
/// </summary>
public Task? BodyAsSseStringTask { get; set; }
}

View File

@@ -45,5 +45,10 @@ public enum BodyType
/// <summary>
/// Body is a ProtoBuf Byte array
/// </summary>
ProtoBuf
ProtoBuf,
/// <summary>
/// Use Server-Sent Events (string)
/// </summary>
SseString
}

View File

@@ -0,0 +1,16 @@
using System;
namespace WireMock.Types;
/// <summary>
/// A enum defining the supported Handlebar helpers.
/// </summary>
[Flags]
public enum CustomHandlebarsHelpers
{
None = 0,
File = 1,
All = File
}

View File

@@ -61,4 +61,11 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Nullable" Version="1.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -36,7 +36,6 @@ internal class WireMockDelegationHandler : DelegatingHandler
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Guard.NotNull(request);
Guard.NotNull(_httpContextAccessor.HttpContext);
if (_settings.AlwaysRedirect || IsWireMockRedirectHeaderSetToTrue())
{
@@ -57,16 +56,30 @@ internal class WireMockDelegationHandler : DelegatingHandler
private bool IsWireMockRedirectHeaderSetToTrue()
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext is null)
{
_logger.LogDebug("HttpContext is not available in current runtime environment");
return false;
}
return
_httpContextAccessor.HttpContext!.Request.Headers.TryGetValue(AppConstants.HEADER_REDIRECT, out var values) &&
httpContext.Request.Headers.TryGetValue(AppConstants.HEADER_REDIRECT, out var values) &&
bool.TryParse(values.ToString(), out var shouldRedirectToWireMock) && shouldRedirectToWireMock;
}
private bool TryGetDelayHeaderValue(out int delayInMs)
{
delayInMs = 0;
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext is null)
{
_logger.LogDebug("HttpContext is not available in current runtime environment");
return false;
}
return
_httpContextAccessor.HttpContext!.Request.Headers.TryGetValue(AppConstants.HEADER_RESPONSE_DELAY, out var values) &&
httpContext.Request.Headers.TryGetValue(AppConstants.HEADER_RESPONSE_DELAY, out var values) &&
int.TryParse(values.ToString(), out delayInMs);
}
}

View File

@@ -1,49 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Description>Aspire extension to start a WireMock.Net server to stub an api.</Description>
<AssemblyTitle>WireMock.Net.Aspire</AssemblyTitle>
<Authors>Stef Heyenrath</Authors>
<TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AssemblyName>WireMock.Net.Aspire</AssemblyName>
<PackageId>WireMock.Net.Aspire</PackageId>
<PackageTags>dotnet;aspire;wiremock;extension</PackageTags>
<ProjectGuid>{B6269AAC-170A-4346-8B9A-579DED3D9A12}</ProjectGuid>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<CodeAnalysisRuleSet>../WireMock.Net/WireMock.Net.ruleset</CodeAnalysisRuleSet>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>WireMock.Net-LogoAspire.png</PackageIcon>
<ApplicationIcon>../../resources/WireMock.Net-LogoAspire.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<None Remove="../../resources/WireMock.Net-Logo.png" />
<None Include="../../resources/WireMock.Net-LogoAspire.png" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\WireMock.Net\Util\EnhancedFileSystemWatcher.cs" Link="Utils\EnhancedFileSystemWatcher.cs" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting" Version="8.2.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WireMock.Net.RestClient\WireMock.Net.RestClient.csproj" />
</ItemGroup>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Description>Aspire extension to start a WireMock.Net server to stub an api.</Description>
<AssemblyTitle>WireMock.Net.Aspire</AssemblyTitle>
<Authors>Stef Heyenrath</Authors>
<TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AssemblyName>WireMock.Net.Aspire</AssemblyName>
<PackageId>WireMock.Net.Aspire</PackageId>
<PackageTags>dotnet;aspire;wiremock;extension</PackageTags>
<ProjectGuid>{B6269AAC-170A-4346-8B9A-579DED3D9A12}</ProjectGuid>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<CodeAnalysisRuleSet>../WireMock.Net/WireMock.Net.ruleset</CodeAnalysisRuleSet>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>WireMock.Net-LogoAspire.png</PackageIcon>
<ApplicationIcon>../../resources/WireMock.Net-LogoAspire.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<None Remove="../../resources/WireMock.Net-Logo.png" />
<None Include="../../resources/WireMock.Net-LogoAspire.png" Pack="true" PackagePath="" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\WireMock.Net\Util\EnhancedFileSystemWatcher.cs" Link="Utils\EnhancedFileSystemWatcher.cs" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting" Version="9.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WireMock.Net.RestClient\WireMock.Net.RestClient.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,49 @@
// Copyright © WireMock.Net
using System.Diagnostics;
using System.Runtime.CompilerServices;
// ReSharper disable once CheckNamespace
namespace Aspire.Hosting.WireMock;
internal static class WireMockInspector
{
/// <summary>
/// Opens the WireMockInspector tool to inspect the WireMock server.
/// </summary>
/// <param name="wireMockUrl"></param>
/// <param name="title"></param>
/// <exception cref="InvalidOperationException"></exception>
/// <remarks>
/// Copy of <see href="https://github.com/WireMock-Net/WireMockInspector/blob/main/src/WireMock.Net.Extensions.WireMockInspector/WireMockServerExtensions.cs" />
/// without requestFilters and no call to WaitForExit() method in the process so it doesn't block the caller.
/// </remarks>
public static void Inspect(string wireMockUrl, [CallerMemberName] string title = "")
{
try
{
var arguments = $"attach --adminUrl {wireMockUrl} --autoLoad --instanceName \"{title}\"";
Process.Start(new ProcessStartInfo
{
FileName = "wiremockinspector",
Arguments = arguments,
UseShellExecute = false
});
}
catch (Exception e)
{
throw new InvalidOperationException
(
message:
"""
Cannot find installation of WireMockInspector.
Execute the following command to install WireMockInspector dotnet tool:
> dotnet tool install WireMockInspector --global --no-cache --ignore-failed-sources
To get more info please visit https://github.com/WireMock-Net/WireMockInspector
""",
innerException: e
);
}
}
}

View File

@@ -1,175 +1,222 @@
// Copyright © WireMock.Net
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using Stef.Validation;
using WireMock.Client.Builders;
using WireMock.Net.Aspire;
// ReSharper disable once CheckNamespace
namespace Aspire.Hosting;
/// <summary>
/// Provides extension methods for adding WireMock.Net Server resources to the application model.
/// </summary>
public static class WireMockServerBuilderExtensions
{
// Linux only (https://github.com/dotnet/aspire/issues/854)
private const string DefaultLinuxImage = "sheyenrath/wiremock.net-alpine";
private const string DefaultLinuxMappingsPath = "/app/__admin/mappings";
/// <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="port">The HTTP port for the WireMock Server.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, int? port = null)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
Guard.Condition(port, p => p is null or > 0 and <= ushort.MaxValue);
return builder.AddWireMock(name, callback =>
{
callback.HttpPort = port;
});
}
/// <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="arguments">The arguments to start the WireMock.Net Server.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, WireMockServerArguments arguments)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
Guard.NotNull(arguments);
var wireMockContainerResource = new WireMockServerResource(name, arguments);
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);
if (!string.IsNullOrEmpty(arguments.MappingsPath))
{
resourceBuilder = resourceBuilder.WithBindMount(arguments.MappingsPath, DefaultLinuxMappingsPath);
}
resourceBuilder = resourceBuilder.WithArgs(ctx =>
{
foreach (var arg in arguments.GetArgs())
{
ctx.Args.Add(arg);
}
});
return resourceBuilder;
}
/// <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="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)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
Guard.NotNull(callback);
var arguments = new WireMockServerArguments();
callback(arguments);
return builder.AddWireMock(name, arguments);
}
/// <summary>
/// Defines if the static mappings should be read at startup.
///
/// Default set to <c>false</c>.
/// </summary>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithReadStaticMappings(this IResourceBuilder<WireMockServerResource> wiremock)
{
Guard.NotNull(wiremock).Resource.Arguments.ReadStaticMappings = true;
return wiremock;
}
/// <summary>
/// Watch the static mapping files + folder for changes when running.
///
/// Default set to <c>false</c>.
/// </summary>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithWatchStaticMappings(this IResourceBuilder<WireMockServerResource> wiremock)
{
Guard.NotNull(wiremock).Resource.Arguments.WatchStaticMappings = true;
return wiremock;
}
/// <summary>
/// Specifies the path for the (static) mapping json files.
/// </summary>
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
/// <param name="mappingsPath">The local path.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithMappingsPath(this IResourceBuilder<WireMockServerResource> wiremock, string mappingsPath)
{
Guard.NotNullOrWhiteSpace(mappingsPath);
Guard.NotNull(wiremock).Resource.Arguments.MappingsPath = mappingsPath;
return wiremock.WithBindMount(mappingsPath, DefaultLinuxMappingsPath);
}
/// <summary>
/// Set the admin username and password for accessing the admin interface from WireMock.Net via HTTP.
/// </summary>
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
/// <param name="username">The admin username.</param>
/// <param name="password">The admin password.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithAdminUserNameAndPassword(this IResourceBuilder<WireMockServerResource> wiremock, string username, string password)
{
Guard.NotNull(wiremock);
wiremock.Resource.Arguments.AdminUsername = Guard.NotNull(username);
wiremock.Resource.Arguments.AdminPassword = Guard.NotNull(password);
return wiremock;
}
/// <summary>
/// Use WireMock Client's AdminApiMappingBuilder to configure the WireMock.Net resource.
/// </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>
public static IResourceBuilder<WireMockServerResource> WithApiMappingBuilder(this IResourceBuilder<WireMockServerResource> wiremock, Func<AdminApiMappingBuilder, Task> configure)
{
return wiremock.WithApiMappingBuilder((adminApiMappingBuilder, _) => configure.Invoke(adminApiMappingBuilder));
}
/// <summary>
/// Use WireMock Client's AdminApiMappingBuilder to configure the WireMock.Net resource.
/// </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>
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;
return wiremock;
}
// Copyright © WireMock.Net
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.WireMock;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Stef.Validation;
using WireMock.Client.Builders;
using WireMock.Net.Aspire;
// ReSharper disable once CheckNamespace
namespace Aspire.Hosting;
/// <summary>
/// Provides extension methods for adding WireMock.Net Server resources to the application model.
/// </summary>
public static class WireMockServerBuilderExtensions
{
// Linux only (https://github.com/dotnet/aspire/issues/854)
private const string DefaultLinuxImage = "sheyenrath/wiremock.net-alpine";
private const string DefaultLinuxMappingsPath = "/app/__admin/mappings";
/// <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="port">The HTTP port for the WireMock Server.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, int? port = null)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
Guard.Condition(port, p => p is null or > 0 and <= ushort.MaxValue);
return builder.AddWireMock(name, callback =>
{
callback.HttpPort = port;
});
}
/// <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="arguments">The arguments to start the WireMock.Net Server.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> AddWireMock(this IDistributedApplicationBuilder builder, string name, WireMockServerArguments arguments)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
Guard.NotNull(arguments);
var wireMockContainerResource = new WireMockServerResource(name, arguments);
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)
.WithWireMockInspectorCommand();
if (!string.IsNullOrEmpty(arguments.MappingsPath))
{
resourceBuilder = resourceBuilder.WithBindMount(arguments.MappingsPath, DefaultLinuxMappingsPath);
}
resourceBuilder = resourceBuilder.WithArgs(ctx =>
{
foreach (var arg in arguments.GetArgs())
{
ctx.Args.Add(arg);
}
});
return resourceBuilder;
}
/// <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="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)
{
Guard.NotNull(builder);
Guard.NotNullOrWhiteSpace(name);
Guard.NotNull(callback);
var arguments = new WireMockServerArguments();
callback(arguments);
return builder.AddWireMock(name, arguments);
}
/// <summary>
/// Defines if the static mappings should be read at startup.
///
/// Default set to <c>false</c>.
/// </summary>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithReadStaticMappings(this IResourceBuilder<WireMockServerResource> wiremock)
{
Guard.NotNull(wiremock).Resource.Arguments.ReadStaticMappings = true;
return wiremock;
}
/// <summary>
/// Watch the static mapping files + folder for changes when running.
///
/// Default set to <c>false</c>.
/// </summary>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithWatchStaticMappings(this IResourceBuilder<WireMockServerResource> wiremock)
{
Guard.NotNull(wiremock).Resource.Arguments.WatchStaticMappings = true;
return wiremock;
}
/// <summary>
/// Specifies the path for the (static) mapping json files.
/// </summary>
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
/// <param name="mappingsPath">The local path.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithMappingsPath(this IResourceBuilder<WireMockServerResource> wiremock, string mappingsPath)
{
Guard.NotNullOrWhiteSpace(mappingsPath);
Guard.NotNull(wiremock).Resource.Arguments.MappingsPath = mappingsPath;
return wiremock.WithBindMount(mappingsPath, DefaultLinuxMappingsPath);
}
/// <summary>
/// Set the admin username and password for accessing the admin interface from WireMock.Net via HTTP.
/// </summary>
/// <param name="wiremock">The <see cref="IResourceBuilder{WireMockServerResource}"/>.</param>
/// <param name="username">The admin username.</param>
/// <param name="password">The admin password.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{WireMockServerResource}"/>.</returns>
public static IResourceBuilder<WireMockServerResource> WithAdminUserNameAndPassword(this IResourceBuilder<WireMockServerResource> wiremock, string username, string password)
{
Guard.NotNull(wiremock);
wiremock.Resource.Arguments.AdminUsername = Guard.NotNull(username);
wiremock.Resource.Arguments.AdminPassword = Guard.NotNull(password);
return wiremock;
}
/// <summary>
/// Use WireMock Client's AdminApiMappingBuilder to configure the WireMock.Net resource.
/// </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>
public static IResourceBuilder<WireMockServerResource> WithApiMappingBuilder(this IResourceBuilder<WireMockServerResource> wiremock, Func<AdminApiMappingBuilder, Task> configure)
{
return wiremock.WithApiMappingBuilder((adminApiMappingBuilder, _) => configure.Invoke(adminApiMappingBuilder));
}
/// <summary>
/// Use WireMock Client's AdminApiMappingBuilder to configure the WireMock.Net resource.
/// </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>
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;
return wiremock;
}
/// <summary>
/// Enables the WireMockInspect, a cross-platform UI app that facilitates WireMock troubleshooting.
/// This requires installation of the WireMockInspector tool.
/// <code>
/// 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)
{
Guard.NotNull(builder);
CommandOptions commandOptions = new()
{
Description = "Requires installation of the WireMockInspector (https://github.com/WireMock-Net/WireMockInspector) tool:\ndotnet tool install WireMockInspector --global --no-cache --ignore-failed-sources",
UpdateState = OnUpdateResourceState,
IconName = "BoxSearch",
IconVariant = IconVariant.Filled
};
builder.WithCommand(
name: "wiremock-inspector",
displayName: "WireMock Inspector",
executeCommand: _ => OnRunOpenInspectorCommandAsync(builder),
commandOptions: commandOptions);
return builder;
}
private static Task<ExecuteCommandResult> OnRunOpenInspectorCommandAsync(IResourceBuilder<WireMockServerResource> builder)
{
WireMockInspector.Inspect(builder.Resource.GetEndpoint().Url);
return Task.FromResult(CommandResults.Success());
}
private static ResourceCommandState OnUpdateResourceState(UpdateCommandStateContext context)
{
return context.ResourceSnapshot.HealthStatus is HealthStatus.Healthy
? ResourceCommandState.Enabled
: ResourceCommandState.Disabled;
}
}

View File

@@ -0,0 +1,40 @@
// Copyright © WireMock.Net
using Stef.Validation;
using WireMock.Server;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
/// <summary>
/// Provides assertion methods to verify the number of calls made to a WireMock server.
/// This class is used in the context of FluentAssertions.
/// </summary>
public class WireMockANumberOfCallsAssertions
{
private readonly IWireMockServer _server;
private readonly int _callsCount;
private readonly AssertionChain _chain;
/// <summary>
/// Initializes a new instance of the <see cref="WireMockANumberOfCallsAssertions"/> class.
/// </summary>
/// <param name="server">The WireMock server to assert against.</param>
/// <param name="callsCount">The expected number of calls to assert.</param>
/// <param name="chain">The assertion chain</param>
public WireMockANumberOfCallsAssertions(IWireMockServer server, int callsCount, AssertionChain chain)
{
_server = Guard.NotNull(server);
_callsCount = callsCount;
_chain = chain;
}
/// <summary>
/// Returns an instance of <see cref="WireMockAssertions"/> which can be used to assert the expected number of calls.
/// </summary>
/// <returns>A <see cref="WireMockAssertions"/> instance for asserting the number of calls to the server.</returns>
public WireMockAssertions Calls()
{
return new WireMockAssertions(_server, _callsCount, _chain);
}
}

View File

@@ -0,0 +1,83 @@
// Copyright © WireMock.Net
using WireMock.Extensions;
using WireMock.Matchers;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
#pragma warning disable CS1591
public partial class WireMockAssertions
{
[CustomAssertion]
public AndWhichConstraint<WireMockAssertions, string> AtAbsoluteUrl(string absoluteUrl, string because = "", params object[] becauseArgs)
{
_ = AtAbsoluteUrl(new ExactMatcher(true, absoluteUrl), because, becauseArgs);
return new AndWhichConstraint<WireMockAssertions, string>(this, absoluteUrl);
}
[CustomAssertion]
public AndWhichConstraint<WireMockAssertions, IStringMatcher> AtAbsoluteUrl(IStringMatcher absoluteUrlMatcher, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(request => absoluteUrlMatcher.IsPerfectMatch(request.AbsoluteUrl));
var absoluteUrl = absoluteUrlMatcher.GetPatterns().FirstOrDefault().GetPattern();
_chain
.BecauseOf(because, becauseArgs)
.Given(() => RequestMessages)
.ForCondition(requests => CallsCount == 0 || requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but no calls were made.",
absoluteUrl
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but didn't find it among the calls to {1}.",
_ => absoluteUrl,
requests => requests.Select(request => request.AbsoluteUrl)
);
FilterRequestMessages(filter);
return new AndWhichConstraint<WireMockAssertions, IStringMatcher>(this, absoluteUrlMatcher);
}
[CustomAssertion]
public AndWhichConstraint<WireMockAssertions, string> AtUrl(string url, string because = "", params object[] becauseArgs)
{
_ = AtUrl(new ExactMatcher(true, url), because, becauseArgs);
return new AndWhichConstraint<WireMockAssertions, string>(this, url);
}
[CustomAssertion]
public AndWhichConstraint<WireMockAssertions, IStringMatcher> AtUrl(IStringMatcher urlMatcher, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(request => urlMatcher.IsPerfectMatch(request.Url));
var url = urlMatcher.GetPatterns().FirstOrDefault().GetPattern();
_chain
.BecauseOf(because, becauseArgs)
.Given(() => RequestMessages)
.ForCondition(requests => CallsCount == 0 || requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the url {0}{reason}, but no calls were made.",
url
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} to have been called at address matching the url {0}{reason}, but didn't find it among the calls to {1}.",
_ => url,
requests => requests.Select(request => request.Url)
);
FilterRequestMessages(filter);
return new AndWhichConstraint<WireMockAssertions, IStringMatcher>(this, urlMatcher);
}
}

View File

@@ -0,0 +1,35 @@
// Copyright © WireMock.Net
#pragma warning disable CS1591
using System;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
public partial class WireMockAssertions
{
[CustomAssertion]
public AndWhichConstraint<WireMockAssertions, string> FromClientIP(string clientIP, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(request => string.Equals(request.ClientIP, clientIP, StringComparison.OrdinalIgnoreCase));
_chain
.BecauseOf(because, becauseArgs)
.Given(() => RequestMessages)
.ForCondition(requests => CallsCount == 0 || requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called from client IP {0}{reason}, but no calls were made.",
clientIP
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} to have been called from client IP {0}{reason}, but didn't find it among the calls from IP(s) {1}.",
_ => clientIP, requests => requests.Select(request => request.ClientIP)
);
FilterRequestMessages(filter);
return new AndWhichConstraint<WireMockAssertions, string>(this, clientIP);
}
}

View File

@@ -0,0 +1,81 @@
// Copyright © WireMock.Net
#pragma warning disable CS1591
using System;
using WireMock.Constants;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
public partial class WireMockAssertions
{
[CustomAssertion]
public AndConstraint<WireMockAssertions> UsingConnect(string because = "", params object[] becauseArgs)
=> UsingMethod(HttpRequestMethod.CONNECT, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> UsingDelete(string because = "", params object[] becauseArgs)
=> UsingMethod(HttpRequestMethod.DELETE, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> UsingGet(string because = "", params object[] becauseArgs)
=> UsingMethod(HttpRequestMethod.GET, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> UsingHead(string because = "", params object[] becauseArgs)
=> UsingMethod(HttpRequestMethod.HEAD, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> UsingOptions(string because = "", params object[] becauseArgs)
=> UsingMethod(HttpRequestMethod.OPTIONS, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> UsingPost(string because = "", params object[] becauseArgs)
=> UsingMethod(HttpRequestMethod.POST, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> UsingPatch(string because = "", params object[] becauseArgs)
=> UsingMethod(HttpRequestMethod.PATCH, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> UsingPut(string because = "", params object[] becauseArgs)
=> UsingMethod(HttpRequestMethod.PUT, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> UsingTrace(string because = "", params object[] becauseArgs)
=> UsingMethod(HttpRequestMethod.TRACE, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> UsingAnyMethod(string because = "", params object[] becauseArgs)
=> UsingMethod(Any, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> UsingMethod(string method, string because = "", params object[] becauseArgs)
{
var any = method == Any;
Func<IRequestMessage, bool> predicate = request => (any && !string.IsNullOrEmpty(request.Method)) ||
string.Equals(request.Method, method, StringComparison.OrdinalIgnoreCase);
var (filter, condition) = BuildFilterAndCondition(predicate);
_chain
.BecauseOf(because, becauseArgs)
.Given(() => RequestMessages)
.ForCondition(requests => CallsCount == 0 || requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called using method {0}{reason}, but no calls were made.",
method
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} to have been called using method {0}{reason}, but didn't find it among the methods {1}.",
_ => method,
requests => requests.Select(request => request.Method)
);
FilterRequestMessages(filter);
return new AndConstraint<WireMockAssertions>(this);
}
}

View File

@@ -0,0 +1,147 @@
// Copyright © WireMock.Net
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using AnyOfTypes;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using WireMock.Extensions;
using WireMock.Matchers;
using WireMock.Models;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
public partial class WireMockAssertions
{
private const string MessageFormatNoCalls = "Expected {context:wiremockserver} to have been called using body {0}{reason}, but no calls were made.";
private const string MessageFormat = "Expected {context:wiremockserver} to have been called using body {0}{reason}, but didn't find it among the body/bodies {1}.";
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithBody(string body, string because = "", params object[] becauseArgs)
{
return WithBody(new WildcardMatcher(body), because, becauseArgs);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithBody(IStringMatcher matcher, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(r => r.Body, matcher);
return ExecuteAssertionWithBodyStringMatcher(matcher, because, becauseArgs, condition, filter, r => r.Body);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithBodyAsJson(object body, string because = "", params object[] becauseArgs)
{
return WithBodyAsJson(new JsonMatcher(body), because, becauseArgs);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithBodyAsJson(string body, string because = "", params object[] becauseArgs)
{
return WithBodyAsJson(new JsonMatcher(body), because, becauseArgs);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithBodyAsJson(IObjectMatcher matcher, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(r => r.BodyAsJson, matcher);
return ExecuteAssertionWithBodyAsIObjectMatcher(matcher, because, becauseArgs, condition, filter, r => r.BodyAsJson);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithBodyAsBytes(byte[] body, string because = "", params object[] becauseArgs)
{
return WithBodyAsBytes(new ExactObjectMatcher(body), because, becauseArgs);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithBodyAsBytes(ExactObjectMatcher matcher, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(r => r.BodyAsBytes, matcher);
return ExecuteAssertionWithBodyAsIObjectMatcher(matcher, because, becauseArgs, condition, filter, r => r.BodyAsBytes);
}
private AndConstraint<WireMockAssertions> ExecuteAssertionWithBodyStringMatcher(
IStringMatcher matcher,
string because,
object[] becauseArgs,
Func<IReadOnlyList<IRequestMessage>, bool> condition,
Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> filter,
Func<IRequestMessage, object?> expression
)
{
_chain
.BecauseOf(because, becauseArgs)
.Given(() => RequestMessages)
.ForCondition(requests => CallsCount == 0 || requests.Any())
.FailWith(
MessageFormatNoCalls,
FormatBody(matcher.GetPatterns())
)
.Then
.ForCondition(condition)
.FailWith(
MessageFormat,
_ => FormatBody(matcher.GetPatterns()),
requests => FormatBodies(requests.Select(expression))
);
FilterRequestMessages(filter);
return new AndConstraint<WireMockAssertions>(this);
}
private AndConstraint<WireMockAssertions> ExecuteAssertionWithBodyAsIObjectMatcher(
IObjectMatcher matcher,
string because,
object[] becauseArgs,
Func<IReadOnlyList<IRequestMessage>, bool> condition,
Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> filter,
Func<IRequestMessage, object?> expression
)
{
_chain
.BecauseOf(because, becauseArgs)
.Given(() => RequestMessages)
.ForCondition(requests => CallsCount == 0 || requests.Any())
.FailWith(
MessageFormatNoCalls,
FormatBody(matcher.Value)
)
.Then
.ForCondition(condition)
.FailWith(
MessageFormat,
_ => FormatBody(matcher.Value),
requests => FormatBodies(requests.Select(expression))
);
FilterRequestMessages(filter);
return new AndConstraint<WireMockAssertions>(this);
}
private static string? FormatBody(object? body)
{
return body switch
{
null => null,
string str => str,
AnyOf<string, StringPattern>[] stringPatterns => FormatBodies(stringPatterns.Select(p => p.GetPattern())),
byte[] bytes => $"byte[{bytes.Length}] {{...}}",
JToken jToken => jToken.ToString(Formatting.None),
_ => JToken.FromObject(body).ToString(Formatting.None)
};
}
private static string? FormatBodies(IEnumerable<object?> bodies)
{
var valueAsArray = bodies as object[] ?? bodies.ToArray();
return valueAsArray.Length == 1 ? FormatBody(valueAsArray[0]) : $"[ {string.Join(", ", valueAsArray.Select(FormatBody))} ]";
}
}

View File

@@ -0,0 +1,157 @@
// Copyright © WireMock.Net
#pragma warning disable CS1591
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
public partial class WireMockAssertions
{
[CustomAssertion]
public AndWhichConstraint<WireMockAssertions, string> WitHeaderKey(string expectedKey, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(request =>
{
return request.Headers?.Any(h => h.Key == expectedKey) == true;
});
_chain
.BecauseOf(because, becauseArgs)
.Given(() => RequestMessages)
.ForCondition(requests => CallsCount == 0 || requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called with Header {0}{reason}.",
expectedKey
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} to have been called with Header {0}{reason}, but didn't find it among the calls with Header(s) {1}.",
_ => expectedKey,
requests => requests.Select(request => request.Headers)
);
FilterRequestMessages(filter);
return new AndWhichConstraint<WireMockAssertions, string>(this, expectedKey);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithHeader(string expectedKey, string value, string because = "", params object[] becauseArgs)
=> WithHeader(expectedKey, new[] { value }, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithHeader(string expectedKey, string[] expectedValues, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(request =>
{
var headers = request.Headers?.ToArray() ?? [];
var matchingHeaderValues = headers.Where(h => h.Key == expectedKey).SelectMany(h => h.Value.ToArray()).ToArray();
if (expectedValues.Length == 1 && matchingHeaderValues.Length == 1)
{
return matchingHeaderValues[0] == expectedValues[0];
}
var trimmedHeaderValues = string.Join(",", matchingHeaderValues.Select(x => x)).Split(',').Select(x => x.Trim()).ToArray();
return expectedValues.Any(trimmedHeaderValues.Contains);
});
_chain
.BecauseOf(because, becauseArgs)
.Given(() => RequestMessages)
.ForCondition(requests => CallsCount == 0 || requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called with Header {0} and Values {1}{reason}.",
expectedKey,
expectedValues
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} to have been called with Header {0} and Values {1}{reason}, but didn't find it among the calls with Header(s) {2}.",
_ => expectedKey,
_ => expectedValues,
requests => requests.Select(request => request.Headers)
);
FilterRequestMessages(filter);
return new AndConstraint<WireMockAssertions>(this);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithoutHeaderKey(string unexpectedKey, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(request =>
{
return request.Headers?.Any(h => h.Key == unexpectedKey) != true;
});
_chain
.BecauseOf(because, becauseArgs)
.Given(() => RequestMessages)
.ForCondition(requests => CallsCount == 0 || requests.Any())
.FailWith(
"Expected {context:wiremockserver} not to have been called with Header {0}{reason}.",
unexpectedKey
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} not to have been called with Header {0}{reason}, but found it among the calls with Header(s) {1}.",
_ => unexpectedKey,
requests => requests.Select(request => request.Headers)
);
FilterRequestMessages(filter);
return new AndConstraint<WireMockAssertions>(this);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithoutHeader(string unexpectedKey, string value, string because = "", params object[] becauseArgs)
=> WithoutHeader(unexpectedKey, new[] { value }, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithoutHeader(string unexpectedKey, string[] expectedValues, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(request =>
{
var headers = request.Headers?.ToArray() ?? [];
var matchingHeaderValues = headers.Where(h => h.Key == unexpectedKey).SelectMany(h => h.Value.ToArray()).ToArray();
if (expectedValues.Length == 1 && matchingHeaderValues.Length == 1)
{
return matchingHeaderValues[0] != expectedValues[0];
}
var trimmedHeaderValues = string.Join(",", matchingHeaderValues.Select(x => x)).Split(',').Select(x => x.Trim()).ToArray();
return !expectedValues.Any(trimmedHeaderValues.Contains);
});
_chain
.BecauseOf(because, becauseArgs)
.Given(() => RequestMessages)
.ForCondition(requests => CallsCount == 0 || requests.Any())
.FailWith(
"Expected {context:wiremockserver} not to have been called with Header {0} and Values {1}{reason}.",
unexpectedKey,
expectedValues
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} not to have been called with Header {0} and Values {1}{reason}, but found it among the calls with Header(s) {2}.",
_ => unexpectedKey,
_ => expectedValues,
requests => requests.Select(request => request.Headers)
);
FilterRequestMessages(filter);
return new AndConstraint<WireMockAssertions>(this);
}
}

View File

@@ -0,0 +1,36 @@
// Copyright © WireMock.Net
#pragma warning disable CS1591
using System;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
public partial class WireMockAssertions
{
[CustomAssertion]
public AndWhichConstraint<WireMockAssertions, string> WithProxyUrl(string proxyUrl, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(request => string.Equals(request.ProxyUrl, proxyUrl, StringComparison.OrdinalIgnoreCase));
_chain
.BecauseOf(because, becauseArgs)
.Given(() => RequestMessages)
.ForCondition(requests => CallsCount == 0 || requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called with proxy url {0}{reason}, but no calls were made.",
proxyUrl
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} to have been called with proxy url {0}{reason}, but didn't find it among the calls with {1}.",
_ => proxyUrl,
requests => requests.Select(request => request.ProxyUrl)
);
FilterRequestMessages(filter);
return new AndWhichConstraint<WireMockAssertions, string>(this, proxyUrl);
}
}

View File

@@ -0,0 +1,48 @@
// Copyright © WireMock.Net
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using WireMock.Matchers;
using WireMock.Server;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
public partial class WireMockAssertions
{
public const string Any = "*";
public int? CallsCount { get; }
public IReadOnlyList<IRequestMessage> RequestMessages { get; private set; }
private readonly AssertionChain _chain;
public WireMockAssertions(IWireMockServer subject, int? callsCount, AssertionChain chain)
{
CallsCount = callsCount;
RequestMessages = subject.LogEntries.Select(logEntry => logEntry.RequestMessage).ToList();
_chain = chain;
}
public (Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> Filter, Func<IReadOnlyList<IRequestMessage>, bool> Condition) BuildFilterAndCondition(Func<IRequestMessage, bool> predicate)
{
Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> filter = requests => requests.Where(predicate).ToList();
return (filter, requests => (CallsCount is null && filter(requests).Any()) || CallsCount == filter(requests).Count);
}
public (Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> Filter, Func<IReadOnlyList<IRequestMessage>, bool> Condition) BuildFilterAndCondition(Func<IRequestMessage, string?> expression, IStringMatcher matcher)
{
return BuildFilterAndCondition(r => matcher.IsMatch(expression(r)).IsPerfect());
}
public (Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> Filter, Func<IReadOnlyList<IRequestMessage>, bool> Condition) BuildFilterAndCondition(Func<IRequestMessage, object?> expression, IObjectMatcher matcher)
{
return BuildFilterAndCondition(r => matcher.IsMatch(expression(r)).IsPerfect());
}
public void FilterRequestMessages(Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> filter)
{
RequestMessages = filter(RequestMessages).ToList();
}
}

View File

@@ -0,0 +1,56 @@
// Copyright © WireMock.Net
using FluentAssertions.Primitives;
using WireMock.Server;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
/// <summary>
/// Contains a number of methods to assert that the <see cref="IWireMockServer"/> is in the expected state.
/// </summary>
public class WireMockReceivedAssertions : ReferenceTypeAssertions<IWireMockServer, WireMockReceivedAssertions>
{
private readonly AssertionChain _chain;
/// <summary>
/// Create a WireMockReceivedAssertions.
/// </summary>
/// <param name="server">The <see cref="IWireMockServer"/>.</param>
/// <param name="chain">The assertion chain</param>
public WireMockReceivedAssertions(IWireMockServer server, AssertionChain chain) : base(server, chain)
{
_chain = chain;
}
/// <summary>
/// Asserts if <see cref="IWireMockServer"/> has received no calls.
/// </summary>
/// <returns><see cref="WireMockAssertions"/></returns>
public WireMockAssertions HaveReceivedNoCalls()
{
return new WireMockAssertions(Subject, 0, _chain);
}
/// <summary>
/// Asserts if <see cref="IWireMockServer"/> has received a call.
/// </summary>
/// <returns><see cref="WireMockAssertions"/></returns>
public WireMockAssertions HaveReceivedACall()
{
return new WireMockAssertions(Subject, null, _chain);
}
/// <summary>
/// Asserts if <see cref="IWireMockServer"/> has received n-calls.
/// </summary>
/// <param name="callsCount"></param>
/// <returns><see cref="WireMockANumberOfCallsAssertions"/></returns>
public WireMockANumberOfCallsAssertions HaveReceived(int callsCount)
{
return new WireMockANumberOfCallsAssertions(Subject, callsCount, _chain);
}
/// <inheritdoc />
protected override string Identifier => "wiremockserver";
}

View File

@@ -0,0 +1,23 @@
// Copyright © WireMock.Net
using WireMock.Server;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions
{
/// <summary>
/// Contains extension methods for custom assertions in unit tests.
/// </summary>
public static class WireMockExtensions
{
/// <summary>
/// Returns a <see cref="WireMockReceivedAssertions"/> object that can be used to assert the current <see cref="IWireMockServer"/>.
/// </summary>
/// <param name="instance">The WireMockServer</param>
/// <returns><see cref="WireMockReceivedAssertions"/></returns>
public static WireMockReceivedAssertions Should(this IWireMockServer instance)
{
return new WireMockReceivedAssertions(instance, AssertionChain.GetOrCreate());
}
}
}

View File

@@ -0,0 +1,5 @@
// Copyright © WireMock.Net
global using System.Linq;
global using FluentAssertions;
global using FluentAssertions.Execution;

View File

@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>AwesomeAssertions extensions for WireMock.Net</Description>
<AssemblyTitle>WireMock.Net.AwesomeAssertions</AssemblyTitle>
<Authors>Francesco Venturoli;Mahmoud Ali;Stef Heyenrath</Authors>
<TargetFrameworks>net47;netstandard2.0;netstandard2.1</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AssemblyName>WireMock.Net.AwesomeAssertions</AssemblyName>
<PackageId>WireMock.Net.AwesomeAssertions</PackageId>
<PackageTags>wiremock;AwesomeAssertions;UnitTest;Assert;Assertions</PackageTags>
<RootNamespace>WireMock.AwesomeAssertions</RootNamespace>
<ProjectGuid>{9565C395-FC5D-4CB1-8381-EC3D9DA74779}</ProjectGuid>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<CodeAnalysisRuleSet>../WireMock.Net/WireMock.Net.ruleset</CodeAnalysisRuleSet>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<!--<DelaySign>true</DelaySign>-->
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AwesomeAssertions" Version="8.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WireMock.Net\WireMock.Net.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,6 @@
// Copyright © WireMock.Net
#if NET46 || NETSTANDARD2_0
#if NET46 || NET47 || NETSTANDARD2_0
using System.Collections.Generic;
namespace WireMock.Net.OpenApiParser.Extensions;

View File

@@ -1,60 +1,62 @@
// Copyright © WireMock.Net
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Any;
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 WireMock.Net.OpenApiParser.Types;
namespace WireMock.Net.OpenApiParser.Extensions;
internal static class OpenApiSchemaExtensions
{
/// <summary>
/// https://stackoverflow.com/questions/48111459/how-to-define-a-property-that-can-be-string-or-null-in-openapi-swagger
/// </summary>
public static bool TryGetXNullable(this OpenApiSchema schema, out bool value)
public static bool TryGetXNullable(this IOpenApiSchema schema, out bool value)
{
value = false;
if (schema.Extensions.TryGetValue("x-nullable", out var e) && e is OpenApiBoolean openApiBoolean)
#pragma warning disable S3011
var extensionsProperty = schema.GetType().GetProperty("Extensions", BindingFlags.Instance | BindingFlags.NonPublic);
#pragma warning restore S3011
if (extensionsProperty?.GetValue(schema) is Dictionary<string, IOpenApiExtension> extensions && extensions.TryGetValue("x-nullable", out var nullExtRawValue))
{
value = openApiBoolean.Value;
return true;
var nodeProperty = nullExtRawValue.GetType().GetProperty("Node", BindingFlags.Instance | BindingFlags.Public);
if (nodeProperty?.GetValue(nullExtRawValue) is JsonNode jsonNode)
{
return jsonNode.GetValueKind() == JsonValueKind.True;
}
}
return false;
}
public static SchemaType GetSchemaType(this OpenApiSchema? schema)
public static JsonSchemaType? GetSchemaType(this IOpenApiSchema? schema, out bool isNullable)
{
isNullable = false;
if (schema == null)
{
return SchemaType.Unknown;
return null;
}
if (schema.Type == null)
{
if (schema.AllOf.Any() || schema.AnyOf.Any())
if (schema.AllOf?.Any() == true || schema.AnyOf?.Any() == true)
{
return SchemaType.Object;
return JsonSchemaType.Object;
}
}
return schema.Type switch
{
"object" => SchemaType.Object,
"array" => SchemaType.Array,
"integer" => SchemaType.Integer,
"number" => SchemaType.Number,
"boolean" => SchemaType.Boolean,
"string" => SchemaType.String,
"file" => SchemaType.File,
_ => SchemaType.Unknown
};
isNullable = (schema.Type | JsonSchemaType.Null) == JsonSchemaType.Null || (schema.TryGetXNullable(out var xNullable) && xNullable);
// Removes the Null flag from the schema.Type, ensuring the returned value represents a non-nullable type.
return schema.Type & ~JsonSchemaType.Null;
}
public static SchemaFormat GetSchemaFormat(this OpenApiSchema? schema)
public static SchemaFormat GetSchemaFormat(this IOpenApiSchema? schema)
{
switch (schema?.Format)
{

View File

@@ -1,96 +1,95 @@
// Copyright © WireMock.Net
using System.IO;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
using Stef.Validation;
using WireMock.Net.OpenApiParser.Settings;
using WireMock.Server;
namespace WireMock.Net.OpenApiParser.Extensions;
/// <summary>
/// Some extension methods for <see cref="IWireMockServer"/>.
/// </summary>
public static class WireMockServerExtensions
{
/// <summary>
/// Register the mappings via an OpenAPI (swagger) V2 or V3 file.
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="path">Path containing OpenAPI file to parse and use the mappings.</param>
/// <param name="diagnostic">Returns diagnostic object containing errors detected during parsing</param>
[PublicAPI]
public static IWireMockServer WithMappingFromOpenApiFile(this IWireMockServer server, string path, out OpenApiDiagnostic diagnostic)
{
return WithMappingFromOpenApiFile(server, path, new WireMockOpenApiParserSettings(), out diagnostic);
}
/// <summary>
/// Register the mappings via an OpenAPI (swagger) V2 or V3 file.
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="path">Path containing OpenAPI file to parse and use the mappings.</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">Returns diagnostic object containing errors detected during parsing</param>
[PublicAPI]
public static IWireMockServer WithMappingFromOpenApiFile(this IWireMockServer server, string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
{
Guard.NotNull(server);
Guard.NotNullOrEmpty(path);
var mappings = new WireMockOpenApiParser().FromFile(path, settings, out diagnostic);
return server.WithMapping(mappings.ToArray());
}
/// <summary>
/// Register the mappings via an OpenAPI (swagger) V2 or V3 stream.
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="stream">Stream containing OpenAPI description to parse and use the mappings.</param>
/// <param name="diagnostic">Returns diagnostic object containing errors detected during parsing</param>
[PublicAPI]
public static IWireMockServer WithMappingFromOpenApiStream(this IWireMockServer server, Stream stream, out OpenApiDiagnostic diagnostic)
{
return WithMappingFromOpenApiStream(server, stream, new WireMockOpenApiParserSettings(), out diagnostic);
}
/// <summary>
/// Register the mappings via an OpenAPI (swagger) V2 or V3 stream.
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="stream">Stream containing OpenAPI description to parse and use the mappings.</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">Returns diagnostic object containing errors detected during parsing</param>
[PublicAPI]
public static IWireMockServer WithMappingFromOpenApiStream(this IWireMockServer server, Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
{
Guard.NotNull(server);
Guard.NotNull(stream);
Guard.NotNull(settings);
var mappings = new WireMockOpenApiParser().FromStream(stream, settings, out diagnostic);
return server.WithMapping(mappings.ToArray());
}
/// <summary>
/// Register the mappings via an OpenAPI (swagger) V2 or V3 document.
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="document">The OpenAPI document to use as mappings.</param>
/// <param name="settings">Additional settings [optional].</param>
[PublicAPI]
public static IWireMockServer WithMappingFromOpenApiDocument(this IWireMockServer server, OpenApiDocument document, WireMockOpenApiParserSettings? settings = null)
{
Guard.NotNull(server);
Guard.NotNull(document);
var mappings = new WireMockOpenApiParser().FromDocument(document, settings);
return server.WithMapping(mappings.ToArray());
}
// Copyright © WireMock.Net
using System.IO;
using System.Linq;
using JetBrains.Annotations;
using Stef.Validation;
using WireMock.Net.OpenApiParser.Models;
using WireMock.Net.OpenApiParser.Settings;
using WireMock.Server;
namespace WireMock.Net.OpenApiParser.Extensions;
/// <summary>
/// Some extension methods for <see cref="IWireMockServer"/>.
/// </summary>
public static class WireMockServerExtensions
{
/// <summary>
/// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 file.
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="path">Path containing OpenAPI file to parse and use the mappings.</param>
/// <param name="diagnostic">Returns diagnostic object containing errors detected during parsing</param>
[PublicAPI]
public static IWireMockServer WithMappingFromOpenApiFile(this IWireMockServer server, string path, out OpenApiDiagnostic diagnostic)
{
return WithMappingFromOpenApiFile(server, path, new WireMockOpenApiParserSettings(), out diagnostic);
}
/// <summary>
/// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 file.
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="path">Path containing OpenAPI file to parse and use the mappings.</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">Returns diagnostic object containing errors detected during parsing</param>
[PublicAPI]
public static IWireMockServer WithMappingFromOpenApiFile(this IWireMockServer server, string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
{
Guard.NotNull(server);
Guard.NotNullOrEmpty(path);
var mappings = new WireMockOpenApiParser().FromFile(path, settings, out diagnostic);
return server.WithMapping(mappings.ToArray());
}
/// <summary>
/// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 stream.
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="stream">Stream containing OpenAPI description to parse and use the mappings.</param>
/// <param name="diagnostic">Returns diagnostic object containing errors detected during parsing</param>
[PublicAPI]
public static IWireMockServer WithMappingFromOpenApiStream(this IWireMockServer server, Stream stream, out OpenApiDiagnostic diagnostic)
{
return WithMappingFromOpenApiStream(server, stream, new WireMockOpenApiParserSettings(), out diagnostic);
}
/// <summary>
/// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 stream.
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="stream">Stream containing OpenAPI description to parse and use the mappings.</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">Returns diagnostic object containing errors detected during parsing</param>
[PublicAPI]
public static IWireMockServer WithMappingFromOpenApiStream(this IWireMockServer server, Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
{
Guard.NotNull(server);
Guard.NotNull(stream);
Guard.NotNull(settings);
var mappings = new WireMockOpenApiParser().FromStream(stream, settings, out diagnostic);
return server.WithMapping(mappings.ToArray());
}
/// <summary>
/// Register the mappings via an OpenAPI (swagger) V2/V3/V3.1 document.
/// </summary>
/// <param name="server">The WireMockServer instance</param>
/// <param name="document">The OpenAPI document to use as mappings.</param>
/// <param name="settings">Additional settings [optional].</param>
[PublicAPI]
public static IWireMockServer WithMappingFromOpenApiDocument(this IWireMockServer server, object document, WireMockOpenApiParserSettings? settings = null)
{
Guard.NotNull(server);
Guard.NotNull(document);
var mappings = new WireMockOpenApiParser().FromDocument(document, settings);
return server.WithMapping(mappings.ToArray());
}
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- See also https://github.com/ravibpatel/ILRepack.Lib.MSBuild.Task/issues/26 -->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="ILRepacker" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
<ItemGroup>
<InputAssemblies Include="$(OutputPath)WireMock.Net.OpenApiParser.dll" />
<InputAssemblies Include="@(ReferencePathWithRefAssemblies)" Condition="'%(filename)' == 'Microsoft.OpenApi.YamlReader'" />
<InputAssemblies Include="@(ReferencePathWithRefAssemblies)" Condition="'%(filename)' == 'Microsoft.OpenApi'" />
<LibraryPath Include="%(ReferencePathWithRefAssemblies.RelativeDir)" />
</ItemGroup>
<ItemGroup>
<DoNotInternalizeAssemblies Include="WireMock.Net.OpenApiParser" />
</ItemGroup>
<ILRepack
Parallel="true"
Internalize="true"
RenameInternalized="true"
InternalizeExclude="@(DoNotInternalizeAssemblies)"
InputAssemblies="@(InputAssemblies)"
LibraryPath="@(LibraryPath)"
TargetKind="Dll"
KeyFile="../../src/WireMock.Net/WireMock.Net.snk"
OutputFile="$(OutputPath)$(AssemblyName).dll"
/>
</Target>
</Project>

View File

@@ -1,75 +1,74 @@
// Copyright © WireMock.Net
using System.Collections.Generic;
using System.IO;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
using WireMock.Admin.Mappings;
using WireMock.Net.OpenApiParser.Settings;
namespace WireMock.Net.OpenApiParser;
/// <summary>
/// Parse a OpenApi/Swagger/V2/V3 or Raml to WireMock MappingModels.
/// </summary>
public interface IWireMockOpenApiParser
{
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a file-path.
/// </summary>
/// <param name="path">The path to read the OpenApi/Swagger/V2/V3 or Raml file.</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromFile(string path, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a file-path.
/// </summary>
/// <param name="path">The path to read the OpenApi/Swagger/V2/V3 or Raml file.</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from an <seealso cref="OpenApiDocument"/>.
/// </summary>
/// <param name="document">The source OpenApiDocument</param>
/// <param name="settings">Additional settings [optional]</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromDocument(OpenApiDocument document, WireMockOpenApiParserSettings? settings = null);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a <seealso cref="Stream"/>.
/// </summary>
/// <param name="stream">The source stream</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromStream(Stream stream, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a <seealso cref="Stream"/>.
/// </summary>
/// <param name="stream">The source stream</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a <seealso cref="string"/>.
/// </summary>
/// <param name="text">The source text</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromText(string text, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a <seealso cref="string"/>.
/// </summary>
/// <param name="text">The source text</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromText(string text, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic);
// Copyright © WireMock.Net
using System.Collections.Generic;
using System.IO;
using WireMock.Admin.Mappings;
using WireMock.Net.OpenApiParser.Models;
using WireMock.Net.OpenApiParser.Settings;
namespace WireMock.Net.OpenApiParser;
/// <summary>
/// Parse a OpenApi/Swagger/V2/V3 or Raml to WireMock MappingModels.
/// </summary>
public interface IWireMockOpenApiParser
{
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a file-path.
/// </summary>
/// <param name="path">The path to read the OpenApi/Swagger/V2/V3/V31 or Raml file.</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromFile(string path, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a file-path.
/// </summary>
/// <param name="path">The path to read the OpenApi/Swagger/V2/V3/V31 or Raml file.</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from an Microsoft.OpenApi.Models.OpenApiDocument.
/// </summary>
/// <param name="document">The source OpenApiDocument</param>
/// <param name="settings">Additional settings [optional]</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromDocument(object document, WireMockOpenApiParserSettings? settings = null);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a <seealso cref="Stream"/>.
/// </summary>
/// <param name="stream">The source stream</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromStream(Stream stream, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a <seealso cref="Stream"/>.
/// </summary>
/// <param name="stream">The source stream</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a <seealso cref="string"/>.
/// </summary>
/// <param name="text">The source text</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromText(string text, out OpenApiDiagnostic diagnostic);
/// <summary>
/// Generate <see cref="IReadOnlyList{MappingModel}"/> from a <seealso cref="string"/>.
/// </summary>
/// <param name="text">The source text</param>
/// <param name="settings">Additional settings</param>
/// <param name="diagnostic">OpenApiDiagnostic output</param>
/// <returns>MappingModel</returns>
IReadOnlyList<MappingModel> FromText(string text, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic);
}

View File

@@ -3,20 +3,19 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Any;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Writers;
using Microsoft.OpenApi.Models.Interfaces;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Stef.Validation;
using WireMock.Admin.Mappings;
using WireMock.Net.OpenApiParser.Extensions;
using WireMock.Net.OpenApiParser.Settings;
using WireMock.Net.OpenApiParser.Types;
using WireMock.Net.OpenApiParser.Utils;
using SystemTextJsonSerializer = System.Text.Json.JsonSerializer;
namespace WireMock.Net.OpenApiParser.Mappers;
@@ -39,56 +38,40 @@ internal class OpenApiPathsMapper
.OrderBy(p => p.Key)
.Select(p => MapPath(p.Key, p.Value, servers))
.SelectMany(x => x)
.ToArray() ??
Array.Empty<MappingModel>();
.ToArray() ?? [];
}
private IReadOnlyList<MappingModel> MapPaths(OpenApiPaths? paths, IList<OpenApiServer> servers)
private IReadOnlyList<MappingModel> MapPath(string path, IOpenApiPathItem pathItem, IList<OpenApiServer> servers)
{
return paths?
.OrderBy(p => p.Key)
.Select(p => MapPath(p.Key, p.Value, servers))
.SelectMany(x => x)
.ToArray() ??
Array.Empty<MappingModel>();
}
private IReadOnlyList<MappingModel> MapPath(string path, OpenApiPathItem pathItem, IList<OpenApiServer> servers)
{
return pathItem.Operations.Select(o => MapOperationToMappingModel(path, o.Key.ToString().ToUpperInvariant(), o.Value, servers)).ToArray();
return pathItem.Operations?.Select(o => MapOperationToMappingModel(path, o.Key.ToString().ToUpperInvariant(), o.Value, servers)).ToArray() ?? [];
}
private MappingModel MapOperationToMappingModel(string path, string httpMethod, OpenApiOperation operation, IList<OpenApiServer> servers)
{
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 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();
var response = operation.Responses?.FirstOrDefault() ?? new KeyValuePair<string, IOpenApiResponse>();
TryGetContent(response.Value?.Content, out OpenApiMediaType? responseContent, out string? responseContentType);
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 body = responseExample != null ? MapOpenApiAnyToJToken(responseExample) :
responseSchemaExample != null ? MapOpenApiAnyToJToken(responseSchemaExample) :
MapSchemaToObject(responseSchema);
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 OpenApiMediaType? requestContent, out _);
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 != null ? MapOpenApiAnyToJToken(requestBodyExample) :
requestBodySchemaExample != null ? MapOpenApiAnyToJToken(requestBodySchemaExample) :
MapSchemaToObject(requestBodySchema);
var requestBodyMapped = requestBodyExample ?? requestBodySchemaExample ?? MapSchemaToObject(requestBodySchema);
requestBodyModel = MapRequestBody(requestBodyMapped);
}
@@ -102,8 +85,8 @@ internal class OpenApiPathsMapper
Guid = Guid.NewGuid(),
Request = new RequestModel
{
Methods = new[] { httpMethod },
Path = MapBasePath(servers) + MapPathWithParameters(path, pathParameters),
Methods = [httpMethod],
Path = PathUtils.Combine(MapBasePath(servers), MapPathWithParameters(path, pathParameters)),
Params = MapQueryParameters(queryParameters),
Headers = MapRequestHeaders(headers),
Body = requestBodyModel
@@ -112,12 +95,12 @@ internal class OpenApiPathsMapper
{
StatusCode = httpStatusCode,
Headers = MapHeaders(responseContentType, response.Value?.Headers),
BodyAsJson = body
BodyAsJson = responseBody != null ? JsonConvert.DeserializeObject(SystemTextJsonSerializer.Serialize(responseBody)) : null
}
};
}
private BodyModel? MapRequestBody(object? requestBody)
private BodyModel? MapRequestBody(JsonNode? requestBody)
{
if (requestBody == null)
{
@@ -129,7 +112,7 @@ internal class OpenApiPathsMapper
Matcher = new MatcherModel
{
Name = "JsonMatcher",
Pattern = JsonConvert.SerializeObject(requestBody, Formatting.Indented),
Pattern = SystemTextJsonSerializer.Serialize(requestBody, new JsonSerializerOptions { WriteIndented = true }),
IgnoreCase = _settings.RequestBodyIgnoreCase
}
};
@@ -160,117 +143,103 @@ internal class OpenApiPathsMapper
return true;
}
private object? MapSchemaToObject(OpenApiSchema? schema, string? name = null)
private JsonNode? MapSchemaToObject(IOpenApiSchema? schema)
{
if (schema == null)
{
return null;
}
switch (schema.GetSchemaType())
switch (schema.GetSchemaType(out _))
{
case SchemaType.Array:
var jArray = new JArray();
for (int i = 0; i < _settings.NumberOfArrayItems; i++)
case JsonSchemaType.Array:
var array = new JsonArray();
for (var i = 0; i < _settings.NumberOfArrayItems; i++)
{
if (schema.Items.Properties.Count > 0)
if (schema.Items?.Properties?.Count > 0)
{
var arrayItem = new JObject();
var item = new JsonObject();
foreach (var property in schema.Items.Properties)
{
var objectValue = MapSchemaToObject(property.Value, property.Key);
if (objectValue is JProperty jp)
{
arrayItem.Add(jp);
}
else
{
arrayItem.Add(new JProperty(property.Key, objectValue));
}
item[property.Key] = MapSchemaToObject(property.Value);
}
jArray.Add(arrayItem);
array.Add(item);
}
else
{
var arrayItem = MapSchemaToObject(schema.Items, name: null); // Set name to null to force JObject instead of JProperty
jArray.Add(arrayItem);
var arrayItem = MapSchemaToObject(schema.Items);
array.Add(arrayItem);
}
}
if (schema.AllOf.Count > 0)
if (schema.AllOf?.Count > 0)
{
jArray.Add(MapSchemaAllOfToObject(schema));
array.Add(MapSchemaAllOfToObject(schema));
}
return jArray;
return array;
case SchemaType.Boolean:
case SchemaType.Integer:
case SchemaType.Number:
case SchemaType.String:
case JsonSchemaType.Boolean:
case JsonSchemaType.Integer:
case JsonSchemaType.Number:
case JsonSchemaType.String:
return _exampleValueGenerator.GetExampleValue(schema);
case SchemaType.Object:
var propertyAsJObject = new JObject();
foreach (var schemaProperty in schema.Properties)
case JsonSchemaType.Object:
var propertyAsJsonObject = new JsonObject();
foreach (var schemaProperty in schema.Properties ?? new Dictionary<string, IOpenApiSchema>())
{
propertyAsJObject.Add(MapPropertyAsJObject(schemaProperty.Value, schemaProperty.Key));
propertyAsJsonObject[schemaProperty.Key] = MapPropertyAsJsonNode(schemaProperty.Value);
}
if (schema.AllOf.Count > 0)
if (schema.AllOf?.Count > 0)
{
foreach (var group in schema.AllOf.SelectMany(p => p.Properties).GroupBy(x => x.Key))
foreach (var group in schema.AllOf.SelectMany(p => p.Properties ?? new Dictionary<string, IOpenApiSchema>()).GroupBy(x => x.Key))
{
propertyAsJObject.Add(MapPropertyAsJObject(group.First().Value, group.Key));
propertyAsJsonObject[group.Key] = MapPropertyAsJsonNode(group.First().Value);
}
}
return name != null ? new JProperty(name, propertyAsJObject) : propertyAsJObject;
return propertyAsJsonObject;
default:
return null;
}
}
private JObject MapSchemaAllOfToObject(OpenApiSchema schema)
private JsonObject MapSchemaAllOfToObject(IOpenApiSchema schema)
{
var arrayItem = new JObject();
foreach (var property in schema.AllOf)
var arrayItem = new JsonObject();
foreach (var property in schema.AllOf ?? [])
{
foreach (var item in property.Properties)
foreach (var item in property.Properties ?? new Dictionary<string, IOpenApiSchema>())
{
arrayItem.Add(MapPropertyAsJObject(item.Value, item.Key));
arrayItem[item.Key] = MapPropertyAsJsonNode(item.Value);
}
}
return arrayItem;
}
private object MapPropertyAsJObject(OpenApiSchema openApiSchema, string key)
private JsonNode? MapPropertyAsJsonNode(IOpenApiSchema openApiSchema)
{
if (openApiSchema.GetSchemaType() == SchemaType.Object || openApiSchema.GetSchemaType() == SchemaType.Array)
var schemaType = openApiSchema.GetSchemaType(out _);
if (schemaType is JsonSchemaType.Object or JsonSchemaType.Array)
{
var mapped = MapSchemaToObject(openApiSchema, key);
if (mapped is JProperty jp)
{
return jp;
}
return new JProperty(key, mapped);
return MapSchemaToObject(openApiSchema);
}
// bool propertyIsNullable = openApiSchema.Nullable || (openApiSchema.TryGetXNullable(out bool x) && x);
return new JProperty(key, _exampleValueGenerator.GetExampleValue(openApiSchema));
return _exampleValueGenerator.GetExampleValue(openApiSchema);
}
private string MapPathWithParameters(string path, IEnumerable<OpenApiParameter>? parameters)
private string MapPathWithParameters(string path, IEnumerable<IOpenApiParameter>? parameters)
{
if (parameters == null)
{
return path;
}
string newPath = path;
var newPath = path;
foreach (var parameter in parameters)
{
var exampleMatcherModel = GetExampleMatcherModel(parameter.Schema, _settings.PathPatternToUse);
@@ -280,93 +249,56 @@ internal class OpenApiPathsMapper
return newPath;
}
private string MapBasePath(IList<OpenApiServer>? servers)
private IDictionary<string, object>? MapHeaders(string? responseContentType, IDictionary<string, IOpenApiHeader>? headers)
{
if (servers == null || servers.Count == 0)
{
return string.Empty;
}
OpenApiServer server = servers.First();
if (Uri.TryCreate(server.Url, UriKind.RelativeOrAbsolute, out Uri uriResult))
{
return uriResult.IsAbsoluteUri ? uriResult.AbsolutePath : uriResult.ToString();
}
return string.Empty;
}
private JToken? MapOpenApiAnyToJToken(IOpenApiAny? any)
{
if (any == null)
{
return null;
}
using var outputString = new StringWriter();
var writer = new OpenApiJsonWriter(outputString);
any.Write(writer, OpenApiSpecVersion.OpenApi3_0);
if (any.AnyType == AnyType.Array)
{
return JArray.Parse(outputString.ToString());
}
return JObject.Parse(outputString.ToString());
}
private IDictionary<string, object>? MapHeaders(string? responseContentType, IDictionary<string, OpenApiHeader>? headers)
{
var mappedHeaders = headers?.ToDictionary(
item => item.Key,
_ => GetExampleMatcherModel(null, _settings.HeaderPatternToUse).Pattern!
) ?? new Dictionary<string, object>();
var mappedHeaders = headers?
.ToDictionary(item => item.Key, _ => GetExampleMatcherModel(null, _settings.HeaderPatternToUse).Pattern!) ?? new Dictionary<string, object>();
if (!string.IsNullOrEmpty(responseContentType))
{
mappedHeaders.TryAdd(HeaderContentType, responseContentType!);
mappedHeaders.TryAdd(HeaderContentType, responseContentType);
}
return mappedHeaders.Keys.Any() ? mappedHeaders : null;
}
private IList<ParamModel>? MapQueryParameters(IEnumerable<OpenApiParameter> queryParameters)
private IList<ParamModel>? MapQueryParameters(IEnumerable<IOpenApiParameter> queryParameters)
{
var list = queryParameters
.Where(req => req.Required)
.Select(qp => new ParamModel
{
Name = qp.Name,
Name = qp.Name ?? string.Empty,
IgnoreCase = _settings.QueryParameterPatternIgnoreCase,
Matchers = new[]
{
Matchers =
[
GetExampleMatcherModel(qp.Schema, _settings.QueryParameterPatternToUse)
}
]
})
.ToList();
return list.Any() ? list : null;
}
private IList<HeaderModel>? MapRequestHeaders(IEnumerable<OpenApiParameter> headers)
private IList<HeaderModel>? MapRequestHeaders(IEnumerable<IOpenApiParameter> headers)
{
var list = headers
.Where(req => req.Required)
.Select(qp => new HeaderModel
{
Name = qp.Name,
Name = qp.Name ?? string.Empty,
IgnoreCase = _settings.HeaderPatternIgnoreCase,
Matchers = new[]
{
Matchers =
[
GetExampleMatcherModel(qp.Schema, _settings.HeaderPatternToUse)
}
]
})
.ToList();
return list.Any() ? list : null;
}
private MatcherModel GetExampleMatcherModel(OpenApiSchema? schema, ExampleValueType type)
private MatcherModel GetExampleMatcherModel(IOpenApiSchema? schema, ExampleValueType type)
{
return type switch
{
@@ -385,15 +317,31 @@ internal class OpenApiPathsMapper
};
}
private string GetExampleValueAsStringForSchemaType(OpenApiSchema? schema)
private string GetExampleValueAsStringForSchemaType(IOpenApiSchema? schema)
{
var value = _exampleValueGenerator.GetExampleValue(schema);
return value switch
if (value.GetValueKind() == JsonValueKind.String)
{
string valueAsString => valueAsString,
return value.GetValue<string>();
}
_ => value.ToString(),
};
return value.ToString();
}
private static string MapBasePath(IList<OpenApiServer>? servers)
{
var server = servers?.FirstOrDefault();
if (server == null)
{
return string.Empty;
}
if (Uri.TryCreate(server.Url, UriKind.RelativeOrAbsolute, out var uriResult))
{
return uriResult.IsAbsoluteUri ? uriResult.AbsolutePath : uriResult.ToString();
}
return string.Empty;
}
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Generic;
using RamlToOpenApiConverter;
namespace WireMock.Net.OpenApiParser.Models;
/// <summary>
/// Object containing all diagnostic information related to Open API parsing.
/// </summary>
public class OpenApiDiagnostic
{
/// <summary>
/// List of all errors.
/// </summary>
public List<OpenApiError> Errors { get; set; } = [];
/// <summary>
/// List of all warnings
/// </summary>
public List<OpenApiError> Warnings { get; set; } = [];
/// <summary>
/// Open API specification version of the document parsed.
/// </summary>
public OpenApiSpecificationVersion SpecificationVersion { get; set; }
}

View File

@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
namespace WireMock.Net.OpenApiParser.Models;
/// <summary>
/// Error related to the Open API Document.
/// </summary>
public class OpenApiError
{
/// <summary>
/// Initializes the <see cref="OpenApiError"/> class.
/// </summary>
public OpenApiError(string? pointer, string message)
{
Pointer = pointer;
Message = message;
}
/// <summary>
/// Initializes a copy of an <see cref="OpenApiError"/> object
/// </summary>
public OpenApiError(OpenApiError error)
{
Pointer = error.Pointer;
Message = error.Message;
}
/// <summary>
/// Message explaining the error.
/// </summary>
public string Message { get; set; }
/// <summary>
/// Pointer to the location of the error.
/// </summary>
public string? Pointer { get; set; }
/// <summary>
/// Gets the string representation of <see cref="OpenApiError"/>.
/// </summary>
public override string ToString()
{
return Message + (!string.IsNullOrEmpty(Pointer) ? " [" + Pointer + "]" : "");
}
}

View File

@@ -0,0 +1,25 @@
// Copyright © WireMock.Net
using System.Linq;
using RamlToOpenApiConverter;
using MicrosoftOpenApiDiagnostic = Microsoft.OpenApi.Reader.OpenApiDiagnostic;
namespace WireMock.Net.OpenApiParser.Models;
internal static class OpenApiMapper
{
internal static OpenApiDiagnostic? Map(MicrosoftOpenApiDiagnostic? openApiDiagnostic)
{
if (openApiDiagnostic == null)
{
return null;
}
return new OpenApiDiagnostic
{
Errors = openApiDiagnostic.Errors.Select(e => new OpenApiError(e.Pointer, e.Message)).ToList(),
Warnings = openApiDiagnostic.Warnings.Select(e => new OpenApiError(e.Pointer, e.Message)).ToList(),
SpecificationVersion = (OpenApiSpecificationVersion)openApiDiagnostic.SpecificationVersion
};
}
}

View File

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

View File

@@ -1,62 +1,62 @@
// Copyright © WireMock.Net
using System;
using Microsoft.OpenApi.Models;
namespace WireMock.Net.OpenApiParser.Settings;
/// <summary>
/// A interface defining the example values to use for the different types.
/// </summary>
public interface IWireMockOpenApiParserExampleValues
{
/// <summary>
/// An example value for a Boolean.
/// </summary>
bool Boolean { get; }
/// <summary>
/// An example value for an Integer.
/// </summary>
int Integer { get; }
/// <summary>
/// An example value for a Float.
/// </summary>
float Float { get; }
/// <summary>
/// An example value for a Double.
/// </summary>
double Double { get; }
/// <summary>
/// An example value for a Date.
/// </summary>
Func<DateTime> Date { get; }
/// <summary>
/// An example value for a DateTime.
/// </summary>
Func<DateTime> DateTime { get; }
/// <summary>
/// An example value for Bytes.
/// </summary>
byte[] Bytes { get; }
/// <summary>
/// An example value for a Object.
/// </summary>
object Object { get; }
/// <summary>
/// An example value for a String.
/// </summary>
string String { get; }
/// <summary>
/// OpenApi Schema to generate dynamic examples more accurate
/// </summary>
OpenApiSchema? Schema { get; set; }
// Copyright © WireMock.Net
using System;
using Microsoft.OpenApi.Models.Interfaces;
namespace WireMock.Net.OpenApiParser.Settings;
/// <summary>
/// An interface defining the example values to use for the different types.
/// </summary>
public interface IWireMockOpenApiParserExampleValues
{
/// <summary>
/// An example value for a Boolean.
/// </summary>
bool Boolean { get; }
/// <summary>
/// An example value for an Integer.
/// </summary>
int Integer { get; }
/// <summary>
/// An example value for a Float.
/// </summary>
float Float { get; }
/// <summary>
/// An example value for a Decimal.
/// </summary>
decimal Decimal { get; }
/// <summary>
/// An example value for a Date.
/// </summary>
Func<DateTime> Date { get; }
/// <summary>
/// An example value for a DateTime.
/// </summary>
Func<DateTime> DateTime { get; }
/// <summary>
/// An example value for Bytes.
/// </summary>
byte[] Bytes { get; }
/// <summary>
/// An example value for a Object.
/// </summary>
object Object { get; }
/// <summary>
/// An example value for a String.
/// </summary>
string String { get; }
/// <summary>
/// OpenApi Schema to generate dynamic examples more accurate
/// </summary>
IOpenApiSchema? Schema { get; set; }
}

View File

@@ -1,44 +1,59 @@
// Copyright © WireMock.Net
using System;
using Microsoft.OpenApi.Models;
using RandomDataGenerator.FieldOptions;
using RandomDataGenerator.Randomizers;
namespace WireMock.Net.OpenApiParser.Settings;
/// <summary>
/// A class defining the random example values to use for the different types.
/// </summary>
public class WireMockOpenApiParserDynamicExampleValues : IWireMockOpenApiParserExampleValues
{
/// <inheritdoc />
public virtual bool Boolean => RandomizerFactory.GetRandomizer(new FieldOptionsBoolean()).Generate() ?? true;
/// <inheritdoc />
public virtual int Integer => RandomizerFactory.GetRandomizer(new FieldOptionsInteger()).Generate() ?? 42;
/// <inheritdoc />
public virtual float Float => RandomizerFactory.GetRandomizer(new FieldOptionsFloat()).Generate() ?? 4.2f;
/// <inheritdoc />
public virtual double Double => RandomizerFactory.GetRandomizer(new FieldOptionsDouble()).Generate() ?? 4.2d;
/// <inheritdoc />
public virtual Func<DateTime> Date => () => RandomizerFactory.GetRandomizer(new FieldOptionsDateTime()).Generate() ?? System.DateTime.UtcNow.Date;
/// <inheritdoc />
public virtual Func<DateTime> DateTime => () => RandomizerFactory.GetRandomizer(new FieldOptionsDateTime()).Generate() ?? System.DateTime.UtcNow;
/// <inheritdoc />
public virtual byte[] Bytes => RandomizerFactory.GetRandomizer(new FieldOptionsBytes()).Generate();
/// <inheritdoc />
public virtual object Object => "example-object";
/// <inheritdoc />
public virtual string String => RandomizerFactory.GetRandomizer(new FieldOptionsTextRegex { Pattern = @"^[0-9]{2}[A-Z]{5}[0-9]{2}" }).Generate() ?? "example-string";
/// <inheritdoc />
public virtual OpenApiSchema? Schema { get; set; }
// Copyright © WireMock.Net
using System;
using Microsoft.OpenApi.Models.Interfaces;
using RandomDataGenerator.FieldOptions;
using RandomDataGenerator.Randomizers;
namespace WireMock.Net.OpenApiParser.Settings;
/// <summary>
/// A class defining the random example values to use for the different types.
/// </summary>
public class WireMockOpenApiParserDynamicExampleValues : IWireMockOpenApiParserExampleValues
{
/// <inheritdoc />
public virtual bool Boolean => RandomizerFactory.GetRandomizer(new FieldOptionsBoolean()).Generate() ?? true;
/// <inheritdoc />
public virtual int Integer => RandomizerFactory.GetRandomizer(new FieldOptionsInteger()).Generate() ?? 42;
/// <inheritdoc />
public virtual float Float => RandomizerFactory.GetRandomizer(new FieldOptionsFloat()).Generate() ?? 4.2f;
/// <inheritdoc />
public virtual decimal Decimal => SafeConvertFloatToDecimal(RandomizerFactory.GetRandomizer(new FieldOptionsFloat()).Generate() ?? 4.2f);
/// <inheritdoc />
public virtual Func<DateTime> Date => () => RandomizerFactory.GetRandomizer(new FieldOptionsDateTime()).Generate() ?? System.DateTime.UtcNow.Date;
/// <inheritdoc />
public virtual Func<DateTime> DateTime => () => RandomizerFactory.GetRandomizer(new FieldOptionsDateTime()).Generate() ?? System.DateTime.UtcNow;
/// <inheritdoc />
public virtual byte[] Bytes => RandomizerFactory.GetRandomizer(new FieldOptionsBytes()).Generate();
/// <inheritdoc />
public virtual object Object => "example-object";
/// <inheritdoc />
public virtual string String => RandomizerFactory.GetRandomizer(new FieldOptionsTextRegex { Pattern = @"^[0-9]{2}[A-Z]{5}[0-9]{2}" }).Generate() ?? "example-string";
/// <inheritdoc />
public virtual IOpenApiSchema? Schema { get; set; }
/// <summary>
/// Safely converts a float to a decimal, ensuring the value stays within the bounds of a decimal.
/// </summary>
/// <param name="value">The float value to convert.</param>
/// <returns>A decimal value within the valid range of a decimal.</returns>
private static decimal SafeConvertFloatToDecimal(float value)
{
return value switch
{
< (float)decimal.MinValue => decimal.MinValue,
> (float)decimal.MaxValue => decimal.MaxValue,
_ => (decimal)value
};
}
}

View File

@@ -1,42 +1,43 @@
// Copyright © WireMock.Net
using System;
using Microsoft.OpenApi.Models;
namespace WireMock.Net.OpenApiParser.Settings;
/// <summary>
/// A class defining the example values to use for the different types.
/// </summary>
public class WireMockOpenApiParserExampleValues : IWireMockOpenApiParserExampleValues
{
/// <inheritdoc />
public virtual bool Boolean => true;
/// <inheritdoc />
public virtual int Integer => 42;
/// <inheritdoc />
public virtual float Float => 4.2f;
/// <inheritdoc />
public virtual double Double => 4.2d;
/// <inheritdoc />
public virtual Func<DateTime> Date { get; } = () => System.DateTime.UtcNow.Date;
/// <inheritdoc />
public virtual Func<DateTime> DateTime { get; } = () => System.DateTime.UtcNow;
/// <inheritdoc />
public virtual byte[] Bytes { get; } = { 48, 49, 50 };
/// <inheritdoc />
public virtual object Object => "example-object";
/// <inheritdoc />
public virtual string String => "example-string";
/// <inheritdoc />
public virtual OpenApiSchema? Schema { get; set; } = new();
// Copyright © WireMock.Net
using System;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.Interfaces;
namespace WireMock.Net.OpenApiParser.Settings;
/// <summary>
/// A class defining the example values to use for the different types.
/// </summary>
public class WireMockOpenApiParserExampleValues : IWireMockOpenApiParserExampleValues
{
/// <inheritdoc />
public virtual bool Boolean => true;
/// <inheritdoc />
public virtual int Integer => 42;
/// <inheritdoc />
public virtual float Float => 4.2f;
/// <inheritdoc />
public virtual decimal Decimal => 4.2m;
/// <inheritdoc />
public virtual Func<DateTime> Date { get; } = () => System.DateTime.UtcNow.Date;
/// <inheritdoc />
public virtual Func<DateTime> DateTime { get; } = () => System.DateTime.UtcNow;
/// <inheritdoc />
public virtual byte[] Bytes { get; } = [48, 49, 50];
/// <inheritdoc />
public virtual object Object => "example-object";
/// <inheritdoc />
public virtual string String => "example-string";
/// <inheritdoc />
public virtual IOpenApiSchema? Schema { get; set; } = new OpenApiSchema();
}

View File

@@ -1,22 +0,0 @@
// Copyright © WireMock.Net
namespace WireMock.Net.OpenApiParser.Types;
internal enum SchemaType
{
Object,
Array,
String,
Integer,
Number,
Boolean,
File,
Unknown
}

View File

@@ -7,13 +7,16 @@ namespace WireMock.Net.OpenApiParser.Utils;
internal static class DateTimeUtils
{
private const string DateFormat = "yyyy-MM-dd";
private const string DateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss.fffzzz";
public static string ToRfc3339DateTime(DateTime dateTime)
{
return dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo);
return dateTime.ToString(DateTimeFormat, DateTimeFormatInfo.InvariantInfo);
}
public static string ToRfc3339Date(DateTime dateTime)
{
return dateTime.ToString("yyyy-MM-dd", DateTimeFormatInfo.InvariantInfo);
return dateTime.ToString(DateFormat, DateTimeFormatInfo.InvariantInfo);
}
}

View File

@@ -1,8 +1,10 @@
// Copyright © WireMock.Net
using System;
using System.Linq;
using Microsoft.OpenApi.Any;
using System.Text.Json.Nodes;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Models.Interfaces;
using Stef.Validation;
using WireMock.Net.OpenApiParser.Extensions;
using WireMock.Net.OpenApiParser.Settings;
@@ -36,82 +38,66 @@ internal class ExampleValueGenerator
}
}
public object GetExampleValue(OpenApiSchema? schema)
public JsonNode GetExampleValue(IOpenApiSchema? schema)
{
var schemaExample = schema?.Example;
var schemaEnum = schema?.Enum?.FirstOrDefault();
_exampleValues.Schema = schema;
switch (schema?.GetSchemaType())
switch (schema?.GetSchemaType(out _))
{
case SchemaType.Boolean:
var exampleBoolean = schemaExample as OpenApiBoolean;
return exampleBoolean?.Value ?? _exampleValues.Boolean;
case JsonSchemaType.Boolean:
var exampleBoolean = schemaExample?.GetValue<bool>();
return exampleBoolean ?? _exampleValues.Boolean;
case SchemaType.Integer:
switch (schema?.GetSchemaFormat())
{
case SchemaFormat.Int64:
var exampleLong = schemaExample as OpenApiLong;
var enumLong = schemaEnum as OpenApiLong;
var valueLongEnumOrExample = enumLong?.Value ?? exampleLong?.Value;
return valueLongEnumOrExample ?? _exampleValues.Integer;
case JsonSchemaType.Integer:
var exampleInteger = schemaExample?.GetValue<decimal>();
var enumInteger = schemaEnum?.GetValue<decimal>();
var valueIntegerEnumOrExample = enumInteger ?? exampleInteger;
return valueIntegerEnumOrExample ?? _exampleValues.Integer;
default:
var exampleInteger = schemaExample as OpenApiInteger;
var enumInteger = schemaEnum as OpenApiInteger;
var valueIntegerEnumOrExample = enumInteger?.Value ?? exampleInteger?.Value;
return valueIntegerEnumOrExample ?? _exampleValues.Integer;
}
case SchemaType.Number:
switch (schema?.GetSchemaFormat())
case JsonSchemaType.Number:
switch (schema.GetSchemaFormat())
{
case SchemaFormat.Float:
var exampleFloat = schemaExample as OpenApiFloat;
var enumFloat = schemaEnum as OpenApiFloat;
var valueFloatEnumOrExample = enumFloat?.Value ?? exampleFloat?.Value;
var exampleFloat = schemaExample?.GetValue<float>();
var enumFloat = schemaEnum?.GetValue<float>();
var valueFloatEnumOrExample = enumFloat ?? exampleFloat;
return valueFloatEnumOrExample ?? _exampleValues.Float;
default:
var exampleDouble = schemaExample as OpenApiDouble;
var enumDouble = schemaEnum as OpenApiDouble;
var valueDoubleEnumOrExample = enumDouble?.Value ?? exampleDouble?.Value;
return valueDoubleEnumOrExample ?? _exampleValues.Double;
var exampleDecimal = schemaExample?.GetValue<decimal>();
var enumDecimal = schemaEnum?.GetValue<decimal>();
var valueDecimalEnumOrExample = enumDecimal ?? exampleDecimal;
return valueDecimalEnumOrExample ?? _exampleValues.Decimal;
}
default:
switch (schema?.GetSchemaFormat())
{
case SchemaFormat.Date:
var exampleDate = schemaExample as OpenApiDate;
var enumDate = schemaEnum as OpenApiDate;
var valueDateEnumOrExample = enumDate?.Value ?? exampleDate?.Value;
return DateTimeUtils.ToRfc3339Date(valueDateEnumOrExample ?? _exampleValues.Date());
var exampleDate = schemaExample?.GetValue<string>();
var enumDate = schemaEnum?.GetValue<string>();
var valueDateEnumOrExample = enumDate ?? exampleDate;
return valueDateEnumOrExample ?? DateTimeUtils.ToRfc3339Date(_exampleValues.Date());
case SchemaFormat.DateTime:
var exampleDateTime = schemaExample as OpenApiDateTime;
var enumDateTime = schemaEnum as OpenApiDateTime;
var valueDateTimeEnumOrExample = enumDateTime?.Value ?? exampleDateTime?.Value;
return DateTimeUtils.ToRfc3339DateTime(valueDateTimeEnumOrExample?.DateTime ?? _exampleValues.DateTime());
var exampleDateTime = schemaExample?.GetValue<string>();
var enumDateTime = schemaEnum?.GetValue<string>();
var valueDateTimeEnumOrExample = enumDateTime ?? exampleDateTime;
return valueDateTimeEnumOrExample ?? DateTimeUtils.ToRfc3339DateTime(_exampleValues.DateTime());
case SchemaFormat.Byte:
var exampleByte = schemaExample as OpenApiByte;
var enumByte = schemaEnum as OpenApiByte;
var valueByteEnumOrExample = enumByte?.Value ?? exampleByte?.Value;
return valueByteEnumOrExample ?? _exampleValues.Bytes;
case SchemaFormat.Binary:
var exampleBinary = schemaExample as OpenApiBinary;
var enumBinary = schemaEnum as OpenApiBinary;
var valueBinaryEnumOrExample = enumBinary?.Value ?? exampleBinary?.Value;
return valueBinaryEnumOrExample ?? _exampleValues.Object;
var exampleByte = schemaExample?.GetValue<byte[]>();
var enumByte = schemaEnum?.GetValue<byte[]>();
var valueByteEnumOrExample = enumByte ?? exampleByte;
return Convert.ToBase64String(valueByteEnumOrExample ?? _exampleValues.Bytes);
default:
var exampleString = schemaExample as OpenApiString;
var enumString = schemaEnum as OpenApiString;
var valueStringEnumOrExample = enumString?.Value ?? exampleString?.Value;
var exampleString = schemaExample?.GetValue<string>();
var enumString = schemaEnum?.GetValue<string>();
var valueStringEnumOrExample = enumString ?? exampleString;
return valueStringEnumOrExample ?? _exampleValues.String;
}
}

View File

@@ -0,0 +1,29 @@
// Copyright © WireMock.Net
using System.Text;
namespace WireMock.Net.OpenApiParser.Utils;
internal static class PathUtils
{
internal static string Combine(params string[] paths)
{
if (paths.Length == 0)
{
return string.Empty;
}
var result = new StringBuilder(paths[0].Trim().TrimEnd('/'));
for (var i = 1; i < paths.Length; i++)
{
var nextPath = paths[i].Trim().TrimStart('/').TrimEnd('/');
if (!string.IsNullOrEmpty(nextPath))
{
result.Append('/').Append(nextPath);
}
}
return result.ToString();
}
}

View File

@@ -1,37 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>An OpenApi (swagger) parser to generate MappingModel or mapping.json file.</Description>
<TargetFrameworks>net46;netstandard2.0;netstandard2.1</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>wiremock;openapi;OAS;raml;converter;parser;openapiparser</PackageTags>
<ProjectGuid>{D3804228-91F4-4502-9595-39584E5AADAD}</ProjectGuid>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<CodeAnalysisRuleSet>../WireMock.Net/WireMock.Net.ruleset</CodeAnalysisRuleSet>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Microsoft.OpenApi.Readers" Version="1.2.3" />
<PackageReference Include="Nullable" Version="1.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="RamlToOpenApiConverter" Version="0.6.1" />
<PackageReference Include="RandomDataGenerator.Net" Version="1.0.18" />
<PackageReference Include="Stef.Validation" Version="0.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WireMock.Net.Abstractions\WireMock.Net.Abstractions.csproj" />
</ItemGroup>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>An OpenApi (swagger) parser to generate MappingModel or mapping.json file.</Description>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>wiremock;openapi;OAS;raml;converter;parser;openapiparser</PackageTags>
<ProjectGuid>{E5B03EEF-822C-4295-952B-4479AD30082B}</ProjectGuid>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<CodeAnalysisRuleSet>../WireMock.Net/WireMock.Net.ruleset</CodeAnalysisRuleSet>
<!--<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>-->
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Nullable" Version="1.3.1">
<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="RandomDataGenerator.Net" Version="1.0.18" />
<PackageReference Include="Stef.Validation" Version="0.1.1" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<PackageReference Include="Microsoft.OpenApi.YamlReader" Version="2.0.0-preview.17" />
</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="System.Text.Json" Version="8.0.5" />
<PackageReference Include="SharpYaml" Version="2.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\WireMock.Net.Abstractions\WireMock.Net.Abstractions.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,84 +1,121 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using JetBrains.Annotations;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Readers;
using RamlToOpenApiConverter;
using WireMock.Admin.Mappings;
using WireMock.Net.OpenApiParser.Mappers;
using WireMock.Net.OpenApiParser.Settings;
namespace WireMock.Net.OpenApiParser;
/// <summary>
/// Parse a OpenApi/Swagger/V2/V3 or Raml to WireMock.Net MappingModels.
/// </summary>
public class WireMockOpenApiParser : IWireMockOpenApiParser
{
private readonly OpenApiStreamReader _reader = new();
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromFile(string path, out OpenApiDiagnostic diagnostic)
{
return FromFile(path, new WireMockOpenApiParserSettings(), out diagnostic);
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
{
OpenApiDocument document;
if (Path.GetExtension(path).EndsWith("raml", StringComparison.OrdinalIgnoreCase))
{
diagnostic = new OpenApiDiagnostic();
document = new RamlConverter().ConvertToOpenApiDocument(path);
}
else
{
var reader = new OpenApiStreamReader();
document = reader.Read(File.OpenRead(path), out diagnostic);
}
return FromDocument(document, settings);
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromDocument(OpenApiDocument document, WireMockOpenApiParserSettings? settings = null)
{
return new OpenApiPathsMapper(settings ?? new WireMockOpenApiParserSettings()).ToMappingModels(document.Paths, document.Servers);
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromStream(Stream stream, out OpenApiDiagnostic diagnostic)
{
return FromDocument(_reader.Read(stream, out diagnostic));
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
{
return FromDocument(_reader.Read(stream, out diagnostic), settings);
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromText(string text, out OpenApiDiagnostic diagnostic)
{
return FromStream(new MemoryStream(Encoding.UTF8.GetBytes(text)), out diagnostic);
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromText(string text, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
{
return FromStream(new MemoryStream(Encoding.UTF8.GetBytes(text)), settings, out diagnostic);
}
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using JetBrains.Annotations;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Reader;
using Microsoft.OpenApi.YamlReader;
using RamlToOpenApiConverter;
using WireMock.Admin.Mappings;
using WireMock.Net.OpenApiParser.Mappers;
using WireMock.Net.OpenApiParser.Models;
using WireMock.Net.OpenApiParser.Settings;
using OpenApiDiagnostic = WireMock.Net.OpenApiParser.Models.OpenApiDiagnostic;
namespace WireMock.Net.OpenApiParser;
/// <summary>
/// Parse a OpenApi/Swagger/V2/V3/V3.1 to WireMock.Net MappingModels.
/// </summary>
public class WireMockOpenApiParser : IWireMockOpenApiParser
{
private readonly OpenApiReaderSettings _readerSettings;
/// <summary>
/// Initializes a new instance of the <see cref="WireMockOpenApiParser"/> class.
/// </summary>
public WireMockOpenApiParser()
{
_readerSettings = new OpenApiReaderSettings();
_readerSettings.AddMicrosoftExtensionParsers();
_readerSettings.AddJsonReader();
_readerSettings.TryAddReader(OpenApiConstants.Yaml, new OpenApiYamlReader());
_readerSettings.TryAddReader(OpenApiConstants.Yml, new OpenApiYamlReader());
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromFile(string path, out OpenApiDiagnostic diagnostic)
{
return FromFile(path, new WireMockOpenApiParserSettings(), out diagnostic);
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromFile(string path, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
{
OpenApiDocument document;
if (Path.GetExtension(path).EndsWith("raml", StringComparison.OrdinalIgnoreCase))
{
diagnostic = new OpenApiDiagnostic();
document = new RamlConverter().ConvertToOpenApiDocument(path);
}
else
{
document = Read(File.OpenRead(path), out diagnostic);
}
return FromDocument(document, settings);
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromDocument(object document, WireMockOpenApiParserSettings? settings = null)
{
var openApiDocument = document as OpenApiDocument ?? throw new ArgumentException("The document should be a Microsoft.OpenApi.Models.OpenApiDocument", nameof(document));
return new OpenApiPathsMapper(settings ?? new WireMockOpenApiParserSettings()).ToMappingModels(openApiDocument.Paths, openApiDocument.Servers ?? []);
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromStream(Stream stream, out OpenApiDiagnostic diagnostic)
{
return FromDocument(Read(stream, out diagnostic));
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromStream(Stream stream, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
{
return FromDocument(Read(stream, out diagnostic), settings);
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromText(string text, out OpenApiDiagnostic diagnostic)
{
return FromStream(new MemoryStream(Encoding.UTF8.GetBytes(text)), out diagnostic);
}
/// <inheritdoc />
[PublicAPI]
public IReadOnlyList<MappingModel> FromText(string text, WireMockOpenApiParserSettings settings, out OpenApiDiagnostic diagnostic)
{
return FromStream(new MemoryStream(Encoding.UTF8.GetBytes(text)), settings, out diagnostic);
}
private OpenApiDocument Read(Stream stream, out OpenApiDiagnostic diagnostic)
{
if (stream is not MemoryStream memoryStream)
{
memoryStream = ReadStreamIntoMemoryStream(stream);
}
var result = OpenApiDocument.Load(memoryStream, settings: _readerSettings);
diagnostic = OpenApiMapper.Map(result.Diagnostic) ?? new OpenApiDiagnostic();
return result.Document ?? throw new InvalidOperationException("The document is null.");
}
private static MemoryStream ReadStreamIntoMemoryStream(Stream stream)
{
var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
memoryStream.Position = 0;
return memoryStream;
}
}

View File

@@ -2,7 +2,7 @@
#if !NETSTANDARD1_3
using System;
using System.Globalization;
using System.Diagnostics.CodeAnalysis;
using System.IdentityModel.Tokens.Jwt;
using System.Text.RegularExpressions;
using AnyOfTypes;
@@ -19,18 +19,24 @@ namespace WireMock.Authentication;
/// <summary>
/// https://www.c-sharpcorner.com/article/how-to-validate-azure-ad-token-using-console-application/
/// https://stackoverflow.com/questions/38684865/validation-of-an-azure-ad-bearer-token-in-a-console-application
/// https://github.com/AzureAD/microsoft-identity-web/blob/36fb5f555638787823a89e89c67f17d6a10006ed/tools/CrossPlatformValidator/CrossPlatformValidation/CrossPlatformValidation/RequestValidator.cs#L42
/// </summary>
internal class AzureADAuthenticationMatcher : IStringMatcher
{
private const string BearerPrefix = "Bearer ";
private static readonly Regex ExtractTenantIdRegex = new(@"https:\/\/(?:sts\.windows\.net|login\.microsoftonline\.com)\/([a-z0-9-]{36}|[a-zA-Z0-9\.]+)/", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private readonly JwtSecurityTokenHandler _jwtSecurityTokenHandler;
private readonly IConfigurationManager<OpenIdConnectConfiguration> _configurationManager;
private readonly string _tenant;
private readonly string _audience;
private readonly string _stsDiscoveryEndpoint;
public AzureADAuthenticationMatcher(string tenant, string audience)
public AzureADAuthenticationMatcher(JwtSecurityTokenHandler jwtSecurityTokenHandler, IConfigurationManager<OpenIdConnectConfiguration> configurationManager, string tenant, string audience)
{
_jwtSecurityTokenHandler = Guard.NotNull(jwtSecurityTokenHandler);
_configurationManager = Guard.NotNull(configurationManager);
_audience = Guard.NotNullOrEmpty(audience);
_stsDiscoveryEndpoint = string.Format(CultureInfo.InvariantCulture, "https://login.microsoftonline.com/{0}/.well-known/openid-configuration", Guard.NotNullOrEmpty(tenant));
_tenant = Guard.NotNullOrEmpty(tenant);
}
public string Name => nameof(AzureADAuthenticationMatcher);
@@ -55,19 +61,27 @@ internal class AzureADAuthenticationMatcher : IStringMatcher
try
{
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(_stsDiscoveryEndpoint, new OpenIdConnectConfigurationRetriever());
var config = configManager.GetConfigurationAsync().ConfigureAwait(false).GetAwaiter().GetResult();
var config = _configurationManager.GetConfigurationAsync(default).ConfigureAwait(false).GetAwaiter().GetResult();
var validationParameters = new TokenValidationParameters
{
ValidAudience = _audience,
ValidIssuer = config.Issuer,
IssuerValidator = (issuer, _, _) =>
{
if (TryExtractTenantId(issuer, out var extractedTenant) && string.Equals(extractedTenant, _tenant, StringComparison.OrdinalIgnoreCase))
{
return issuer;
}
throw new SecurityTokenInvalidIssuerException($"tenant {extractedTenant} does not match {_tenant}.");
},
IssuerSigningKeys = config.SigningKeys,
ValidateLifetime = true
};
// Throws an Exception as the token is invalid (expired, invalid-formatted, etc.)
new JwtSecurityTokenHandler().ValidateToken(token, validationParameters, out var _);
// Throws an Exception as the token is invalid (expired, invalid-formatted, tenant mismatch, etc.)
_jwtSecurityTokenHandler.ValidateToken(token, validationParameters, out _);
return MatchScores.Perfect;
}
@@ -82,5 +96,20 @@ internal class AzureADAuthenticationMatcher : IStringMatcher
{
throw new NotImplementedException();
}
// Handles: https://sts.windows.net/{tenant}/, https://login.microsoftonline.com/{tenant}/ or /v2.0
private static bool TryExtractTenantId(string issuer, [NotNullWhen(true)] out string? tenant)
{
var match = ExtractTenantIdRegex.Match(issuer);
if (match is { Success: true, Groups.Count: > 1 })
{
tenant = match.Groups[1].Value;
return !string.IsNullOrEmpty(tenant);
}
tenant = null;
return false;
}
}
#endif

View File

@@ -0,0 +1,31 @@
using System.Globalization;
namespace WireMock.Extensions;
internal static class StringExtensions
{
// See https://andrewlock.net/why-is-string-gethashcode-different-each-time-i-run-my-program-in-net-core/
public static string GetDeterministicHashCodeAsString(this string str)
{
unchecked
{
int hash1 = (5381 << 16) + 5381;
int hash2 = hash1;
for (int i = 0; i < str.Length; i += 2)
{
hash1 = ((hash1 << 5) + hash1) ^ str[i];
if (i == str.Length - 1)
{
break;
}
hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
}
int result = hash1 + hash2 * 1566083941;
return result.ToString(CultureInfo.InvariantCulture).Replace('-', '_');
}
}
}

View File

@@ -84,20 +84,20 @@ public class LocalFileSystemHandler : IFileSystemHandler
public virtual byte[] ReadResponseBodyAsFile(string path)
{
Guard.NotNullOrEmpty(path);
path = PathUtils.CleanPath(path)!;
path = FilePathUtils.CleanPath(path)!;
// If the file exists at the given path relative to the MappingsFolder, then return that.
// Else the path will just be as-is.
return File.ReadAllBytes(File.Exists(PathUtils.Combine(GetMappingFolder(), path)) ? PathUtils.Combine(GetMappingFolder(), path) : path);
return File.ReadAllBytes(File.Exists(FilePathUtils.Combine(GetMappingFolder(), path)) ? FilePathUtils.Combine(GetMappingFolder(), path) : path);
}
/// <inheritdoc cref="IFileSystemHandler.ReadResponseBodyAsString"/>
public virtual string ReadResponseBodyAsString(string path)
{
Guard.NotNullOrEmpty(path);
path = PathUtils.CleanPath(path)!;
path = FilePathUtils.CleanPath(path)!;
// In case the path is a filename, the path will be adjusted to the MappingFolder.
// Else the path will just be as-is.
return File.ReadAllText(File.Exists(PathUtils.Combine(GetMappingFolder(), path)) ? PathUtils.Combine(GetMappingFolder(), path) : path);
return File.ReadAllText(File.Exists(FilePathUtils.Combine(GetMappingFolder(), path)) ? FilePathUtils.Combine(GetMappingFolder(), path) : path);
}
/// <inheritdoc cref="IFileSystemHandler.FileExists"/>
@@ -124,7 +124,7 @@ public class LocalFileSystemHandler : IFileSystemHandler
Guard.NotNullOrEmpty(filename);
Guard.NotNull(bytes);
File.WriteAllBytes(PathUtils.Combine(folder, filename), bytes);
File.WriteAllBytes(FilePathUtils.Combine(folder, filename), bytes);
}
/// <inheritdoc cref="IFileSystemHandler.DeleteFile"/>

View File

@@ -2,7 +2,6 @@
using System.Net.Http;
using System.Net.Http.Headers;
using Stef.Validation;
namespace WireMock.Http;
@@ -16,8 +15,6 @@ internal static class ByteArrayContentHelper
/// <returns>ByteArrayContent</returns>
internal static ByteArrayContent Create(byte[] content, MediaTypeHeaderValue? contentType)
{
Guard.NotNull(content);
var byteContent = new ByteArrayContent(content);
if (contentType != null)
{

View File

@@ -37,10 +37,11 @@ internal static class HttpRequestMessageHelper
var bodyData = requestMessage.BodyData;
httpRequestMessage.Content = bodyData?.GetBodyType() switch
{
BodyType.Bytes => ByteArrayContentHelper.Create(bodyData!.BodyAsBytes!, contentType),
BodyType.Json => StringContentHelper.Create(JsonConvert.SerializeObject(bodyData!.BodyAsJson), contentType),
BodyType.String => StringContentHelper.Create(bodyData!.BodyAsString!, contentType),
BodyType.FormUrlEncoded => StringContentHelper.Create(bodyData!.BodyAsString!, contentType),
BodyType.Bytes => ByteArrayContentHelper.Create(bodyData.BodyAsBytes!, contentType),
BodyType.Json => StringContentHelper.Create(JsonConvert.SerializeObject(bodyData.BodyAsJson), contentType),
BodyType.String => StringContentHelper.Create(bodyData.BodyAsString!, contentType),
BodyType.FormUrlEncoded => StringContentHelper.Create(bodyData.BodyAsString!, contentType),
BodyType.MultiPart => StringContentHelper.Create(bodyData.BodyAsString!, contentType),
_ => httpRequestMessage.Content
};

View File

@@ -2,7 +2,6 @@
using System.Net.Http;
using System.Net.Http.Headers;
using Stef.Validation;
namespace WireMock.Http;
@@ -16,8 +15,6 @@ internal static class StringContentHelper
/// <returns>StringContent</returns>
internal static StringContent Create(string content, MediaTypeHeaderValue? contentType)
{
Guard.NotNull(content);
var stringContent = new StringContent(content);
stringContent.Headers.ContentType = contentType;
return stringContent;

View File

@@ -1,17 +0,0 @@
// Copyright © WireMock.Net
// Copied from https://github.com/Handlebars-Net/Handlebars.Net.Helpers/blob/master/src/Handlebars.Net.Helpers.DynamicLinq
using System.Linq.Dynamic.Core;
namespace WireMock.Json;
internal class DynamicPropertyWithValue : DynamicProperty
{
public object? Value { get; }
public DynamicPropertyWithValue(string name, object? value) : base(name, value?.GetType() ?? typeof(object))
{
Value = value;
}
}

View File

@@ -5,7 +5,7 @@
namespace WireMock.Json;
/// <summary>
/// Enum to define how to convert an Float in the Json Object.
/// Enum to define how to convert a Float in the Json Object.
/// </summary>
internal enum FloatBehavior
{

View File

@@ -1,202 +0,0 @@
// Copyright © WireMock.Net
// Copied from https://github.com/Handlebars-Net/Handlebars.Net.Helpers/blob/master/src/Handlebars.Net.Helpers.DynamicLinq which is copied from https://github.com/StefH/JsonConverter
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Reflection;
using Newtonsoft.Json.Linq;
namespace WireMock.Json;
internal static class JObjectExtensions
{
private class JTokenResolvers : Dictionary<JTokenType, Func<JToken, DynamicJsonClassOptions?, object?>>;
private static readonly JTokenResolvers Resolvers = new()
{
{ JTokenType.Array, ConvertJTokenArray },
{ JTokenType.Boolean, (jToken, _) => jToken.Value<bool>() },
{ JTokenType.Bytes, (jToken, _) => jToken.Value<byte[]>() },
{ JTokenType.Date, (jToken, _) => jToken.Value<DateTime>() },
{ JTokenType.Float, ConvertJTokenFloat },
{ JTokenType.Guid, (jToken, _) => jToken.Value<Guid>() },
{ JTokenType.Integer, ConvertJTokenInteger },
{ JTokenType.None, (_, _) => null },
{ JTokenType.Null, (_, _) => null },
{ JTokenType.Object, ConvertJObject },
{ JTokenType.Property, ConvertJTokenProperty },
{ JTokenType.String, (jToken, _) => jToken.Value<string>() },
{ JTokenType.TimeSpan, (jToken, _) => jToken.Value<TimeSpan>() },
{ JTokenType.Undefined, (_, _) => null },
{ JTokenType.Uri, (o, _) => o.Value<Uri>() },
};
internal static DynamicClass? ToDynamicJsonClass(this JObject? src, DynamicJsonClassOptions? options = null)
{
if (src == null)
{
return null;
}
var dynamicPropertyWithValues = new List<DynamicPropertyWithValue>();
foreach (var prop in src.Properties())
{
var value = Resolvers[prop.Type](prop.Value, options);
if (value != null)
{
dynamicPropertyWithValues.Add(new DynamicPropertyWithValue(prop.Name, value));
}
}
return CreateInstance(dynamicPropertyWithValues);
}
internal static IEnumerable ToDynamicClassArray(this JArray? src, DynamicJsonClassOptions? options = null)
{
if (src == null)
{
return EmptyArray<object?>.Value;
}
return ConvertJTokenArray(src, options);
}
private static object? ConvertJObject(JToken arg, DynamicJsonClassOptions? options = null)
{
if (arg is JObject asJObject)
{
return asJObject.ToDynamicJsonClass(options);
}
return GetResolverFor(arg)(arg, options);
}
private static object PassThrough(JToken arg, DynamicJsonClassOptions? options)
{
return arg;
}
private static Func<JToken, DynamicJsonClassOptions?, object?> GetResolverFor(JToken arg)
{
return Resolvers.TryGetValue(arg.Type, out var result) ? result : PassThrough;
}
private static object ConvertJTokenFloat(JToken arg, DynamicJsonClassOptions? options = null)
{
if (arg.Type != JTokenType.Float)
{
throw new InvalidOperationException($"Unable to convert {nameof(JToken)} of type: {arg.Type} to double or float.");
}
if (options?.FloatConvertBehavior == FloatBehavior.UseFloat)
{
try
{
return arg.Value<float>();
}
catch
{
return arg.Value<double>();
}
}
if (options?.FloatConvertBehavior == FloatBehavior.UseDecimal)
{
try
{
return arg.Value<decimal>();
}
catch
{
return arg.Value<double>();
}
}
return arg.Value<double>();
}
private static object ConvertJTokenInteger(JToken arg, DynamicJsonClassOptions? options = null)
{
if (arg.Type != JTokenType.Integer)
{
throw new InvalidOperationException($"Unable to convert {nameof(JToken)} of type: {arg.Type} to long or int.");
}
var longValue = arg.Value<long>();
if (options is null || options.IntegerConvertBehavior == IntegerBehavior.UseInt)
{
if (longValue is >= int.MinValue and <= int.MaxValue)
{
return Convert.ToInt32(longValue);
}
}
return longValue;
}
private static object? ConvertJTokenProperty(JToken arg, DynamicJsonClassOptions? options = null)
{
var resolver = GetResolverFor(arg);
if (resolver is null)
{
throw new InvalidOperationException($"Unable to handle {nameof(JToken)} of type: {arg.Type}.");
}
return resolver(arg, options);
}
private static IEnumerable ConvertJTokenArray(JToken arg, DynamicJsonClassOptions? options = null)
{
if (arg is not JArray array)
{
throw new InvalidOperationException($"Unable to convert {nameof(JToken)} of type: {arg.Type} to {nameof(JArray)}.");
}
var result = new List<object?>();
foreach (var item in array)
{
result.Add(ConvertJObject(item));
}
var distinctType = FindSameTypeOf(result);
return distinctType == null ? result.ToArray() : ConvertToTypedArray(result, distinctType);
}
private static Type? FindSameTypeOf(IEnumerable<object?> src)
{
var types = src.Select(o => o?.GetType()).Distinct().OfType<Type>().ToArray();
return types.Length == 1 ? types[0] : null;
}
private static IEnumerable ConvertToTypedArray(IEnumerable<object?> src, Type newType)
{
var method = ConvertToTypedArrayGenericMethod.MakeGenericMethod(newType);
return (IEnumerable)method.Invoke(null, [src])!;
}
private static readonly MethodInfo ConvertToTypedArrayGenericMethod = typeof(JObjectExtensions).GetMethod(nameof(ConvertToTypedArrayGeneric), BindingFlags.NonPublic | BindingFlags.Static)!;
private static T[] ConvertToTypedArrayGeneric<T>(IEnumerable<object> src)
{
return src.Cast<T>().ToArray();
}
public static DynamicClass CreateInstance(IList<DynamicPropertyWithValue> dynamicPropertiesWithValue, bool createParameterCtor = true)
{
var type = DynamicClassFactory.CreateType(dynamicPropertiesWithValue.Cast<DynamicProperty>().ToArray(), createParameterCtor);
var dynamicClass = (DynamicClass)Activator.CreateInstance(type)!;
foreach (var dynamicPropertyWithValue in dynamicPropertiesWithValue.Where(p => p.Value != null))
{
dynamicClass.SetDynamicPropertyValue(dynamicPropertyWithValue.Name, dynamicPropertyWithValue.Value!);
}
return dynamicClass;
}
}

View File

@@ -67,7 +67,7 @@ public class JsonPathMatcher : IStringMatcher, IObjectMatcher
var score = MatchScores.Mismatch;
Exception? exception = null;
if (input != null)
if (!string.IsNullOrWhiteSpace(input))
{
try
{
@@ -141,7 +141,7 @@ public class JsonPathMatcher : IStringMatcher, IObjectMatcher
return MatchScores.ToScore(values, MatchOperator);
}
// https://github.com/WireMock-Net/WireMock.Net/issues/965
// https://github.com/wiremock/WireMock.Net/issues/965
// https://stackoverflow.com/questions/66922188/newtonsoft-jsonpath-with-c-sharp-syntax
// Filtering using SelectToken() isn't guaranteed to work for objects inside objects -- only objects inside arrays.
// So this code checks if it's an JArray, if it's not an array, construct a new JArray.

View File

@@ -74,7 +74,7 @@ public class JmesPathMatcher : IStringMatcher, IObjectMatcher
var score = MatchScores.Mismatch;
Exception? exception = null;
if (input != null)
if (!string.IsNullOrWhiteSpace(input))
{
try
{

View File

@@ -0,0 +1,85 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
namespace WireMock.Models;
/// <inheritdoc />
internal class BlockingQueue<T>(TimeSpan? readTimeout = null) : IBlockingQueue<T>
{
private readonly TimeSpan _readTimeout = readTimeout ?? TimeSpan.FromHours(1);
private readonly Queue<T?> _queue = new();
private readonly object _lockObject = new();
private bool _isClosed;
/// <summary>
/// Writes an item to the queue and signals that an item is available.
/// </summary>
/// <param name="item">The item to be added to the queue.</param>
public void Write(T item)
{
lock (_lockObject)
{
if (_isClosed)
{
throw new InvalidOperationException("Cannot write to a closed queue.");
}
_queue.Enqueue(item);
// Signal that an item is available
Monitor.Pulse(_lockObject);
}
}
/// <summary>
/// Tries to read an item from the queue.
/// - waits until an item is available
/// - or the timeout occurs
/// - or queue is closed
/// </summary>
/// <param name="item">The item read from the queue, or default if the timeout occurs.</param>
/// <returns>True if an item was successfully read; otherwise, false.</returns>
public bool TryRead([NotNullWhen(true)] out T? item)
{
lock (_lockObject)
{
// Wait until an item is available or timeout occurs
while (_queue.Count == 0 && !_isClosed)
{
// Wait with timeout
if (!Monitor.Wait(_lockObject, _readTimeout))
{
item = default;
return false;
}
}
// After waiting, check if we have items
if (_queue.Count == 0)
{
item = default;
return false;
}
item = _queue.Dequeue();
return item != null;
}
}
/// <summary>
/// Closes the queue and signals all waiting threads.
/// </summary>
public void Close()
{
lock (_lockObject)
{
_isClosed = true;
Monitor.PulseAll(_lockObject);
}
}
}

View File

@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using WireMock.Models;
using WireMock.Types;
@@ -57,4 +58,10 @@ public class BodyData : IBodyData
/// <inheritdoc />
public string? ProtoBufMessageType { get; set; }
#endregion
/// <inheritdoc />
public IBlockingQueue<string?>? SseStringQueue { get; set; }
/// <inheritdoc />
public Task? BodyAsSseStringTask { get; set; }
}

View File

@@ -0,0 +1,39 @@
// Copyright © WireMock.Net
using System.Collections.Generic;
using System.Linq;
using Stef.Validation;
namespace WireMock.Models;
/// <summary>
/// A placeholder class for Proto Definitions.
/// </summary>
public class ProtoDefinitionData
{
private readonly IDictionary<string, string> _filenameMappedToProtoDefinition;
internal ProtoDefinitionData(IDictionary<string, string> filenameMappedToProtoDefinition)
{
_filenameMappedToProtoDefinition = filenameMappedToProtoDefinition;
}
/// <summary>
/// Get all the ProtoDefinitions.
/// Note: the main ProtoDefinition will be the first one in the list.
/// </summary>
/// <param name="mainProtoFilename">The main ProtoDefinition filename.</param>
public IReadOnlyList<string> ToList(string mainProtoFilename)
{
Guard.NotNullOrEmpty(mainProtoFilename);
if (!_filenameMappedToProtoDefinition.TryGetValue(mainProtoFilename, out var mainProtoDefinition))
{
throw new KeyNotFoundException($"The ProtoDefinition with filename '{mainProtoFilename}' was not found.");
}
var list = new List<string> { mainProtoDefinition };
list.AddRange(_filenameMappedToProtoDefinition.Where(kvp => kvp.Key != mainProtoFilename).Select(kvp => kvp.Value));
return list;
}
}

View File

@@ -53,7 +53,7 @@ internal partial class AspNetCoreSelfHost : IOwinSelfHost
{
var builder = new WebHostBuilder();
// Workaround for https://github.com/WireMock-Net/WireMock.Net/issues/292
// Workaround for https://github.com/wiremock/WireMock.Net/issues/292
// On some platforms, AppContext.BaseDirectory is null, which causes WebHostBuilder to fail if ContentRoot is not
// specified (even though we don't actually use that base path mechanism, since we have our own way of configuring
// a filesystem handler).

View File

@@ -69,6 +69,13 @@ namespace WireMock.Owin.Mappers
return;
}
var bodyData = responseMessage.BodyData;
if (bodyData?.GetBodyType() == BodyType.SseString)
{
await HandleSseStringAsync(responseMessage, response, bodyData);
return;
}
byte[]? bytes;
switch (responseMessage.FaultType)
{
@@ -104,7 +111,7 @@ namespace WireMock.Owin.Mappers
}
}
SetResponseHeaders(responseMessage, bytes, response);
SetResponseHeaders(responseMessage, bytes != null, response);
if (bytes != null)
{
@@ -121,6 +128,26 @@ namespace WireMock.Owin.Mappers
SetResponseTrailingHeaders(responseMessage, response);
}
private static async Task HandleSseStringAsync(IResponseMessage responseMessage, IResponse response, IBodyData bodyData)
{
if (bodyData.SseStringQueue == null)
{
return;
}
SetResponseHeaders(responseMessage, true, response);
string? text;
do
{
if (bodyData.SseStringQueue.TryRead(out text))
{
await response.WriteAsync(text);
await response.Body.FlushAsync();
}
} while (text != null);
}
private int MapStatusCode(int code)
{
if (_options.AllowOnlyDefinedHttpStatusCodeInResponse == true && !Enum.IsDefined(typeof(HttpStatusCode), code))
@@ -136,7 +163,8 @@ namespace WireMock.Owin.Mappers
return responseMessage.FaultPercentage == null || _randomizerDouble.Generate() <= responseMessage.FaultPercentage;
}
private async Task<byte[]?> GetNormalBodyAsync(IResponseMessage responseMessage) {
private async Task<byte[]?> GetNormalBodyAsync(IResponseMessage responseMessage)
{
var bodyData = responseMessage.BodyData;
switch (bodyData?.GetBodyType())
{
@@ -172,13 +200,13 @@ namespace WireMock.Owin.Mappers
return null;
}
private static void SetResponseHeaders(IResponseMessage responseMessage, byte[]? bytes, IResponse response)
private static void SetResponseHeaders(IResponseMessage responseMessage, bool hasBody, IResponse response)
{
// Force setting the Date header (#577)
AppendResponseHeader(
response,
HttpKnownHeaderNames.Date,
[ DateTime.UtcNow.ToString(CultureInfo.InvariantCulture.DateTimeFormat.RFC1123Pattern, CultureInfo.InvariantCulture) ]
[DateTime.UtcNow.ToString(CultureInfo.InvariantCulture.DateTimeFormat.RFC1123Pattern, CultureInfo.InvariantCulture)]
);
// Set other headers
@@ -188,7 +216,7 @@ namespace WireMock.Owin.Mappers
var value = item.Value;
if (ResponseHeadersToFix.TryGetValue(headerName, out var action))
{
action?.Invoke(response, bytes != null, value);
action?.Invoke(response, hasBody, value);
}
else
{

View File

@@ -10,11 +10,11 @@ using WireMock.Matchers;
using WireMock.Http;
using WireMock.Owin.Mappers;
using WireMock.Serialization;
using WireMock.Types;
using WireMock.ResponseBuilders;
using WireMock.Settings;
using System.Collections.Generic;
using WireMock.Constants;
using WireMock.Exceptions;
using WireMock.Util;
#if !USE_ASPNETCORE
using IContext = Microsoft.Owin.IOwinContext;
@@ -126,7 +126,7 @@ namespace WireMock.Owin
if (targetMapping == null)
{
logRequest = true;
_options.Logger.Warn("HttpStatusCode set to 404 : No matching mapping found", ctx.Request);
_options.Logger.Warn("HttpStatusCode set to 404 : No matching mapping found");
response = ResponseMessageBuilder.Create(HttpStatusCode.NotFound, WireMockConstants.NoMatchingFound);
return;
}
@@ -135,10 +135,18 @@ namespace WireMock.Owin
if (targetMapping.IsAdminInterface && _options.AuthenticationMatcher != null && request.Headers != null)
{
bool present = request.Headers.TryGetValue(HttpKnownHeaderNames.Authorization, out WireMockList<string>? authorization);
if (!present || _options.AuthenticationMatcher.IsMatch(authorization!.ToString()).Score < MatchScores.Perfect)
var authorizationHeaderPresent = request.Headers.TryGetValue(HttpKnownHeaderNames.Authorization, out var authorization);
if (!authorizationHeaderPresent)
{
_options.Logger.Error("HttpStatusCode set to 401");
_options.Logger.Error("HttpStatusCode set to 401, authorization header is missing.");
response = ResponseMessageBuilder.Create(HttpStatusCode.Unauthorized, null);
return;
}
var authorizationHeaderMatchResult = _options.AuthenticationMatcher.IsMatch(authorization!.ToString());
if (!MatchScores.IsPerfect(authorizationHeaderMatchResult.Score))
{
_options.Logger.Error("HttpStatusCode set to 401, authentication failed.", authorizationHeaderMatchResult.Exception ?? throw new WireMockException("Authentication failed"));
response = ResponseMessageBuilder.Create(HttpStatusCode.Unauthorized, null);
return;
}
@@ -156,12 +164,12 @@ namespace WireMock.Owin
if (!targetMapping.IsAdminInterface && theOptionalNewMapping != null)
{
if (responseBuilder?.ProxyAndRecordSettings?.SaveMapping == true || targetMapping.Settings?.ProxyAndRecordSettings?.SaveMapping == true)
if (responseBuilder?.ProxyAndRecordSettings?.SaveMapping == true || targetMapping.Settings.ProxyAndRecordSettings?.SaveMapping == true)
{
_options.Mappings.TryAdd(theOptionalNewMapping.Guid, theOptionalNewMapping);
}
if (responseBuilder?.ProxyAndRecordSettings?.SaveMappingToFile == true || targetMapping.Settings?.ProxyAndRecordSettings?.SaveMappingToFile == true)
if (responseBuilder?.ProxyAndRecordSettings?.SaveMappingToFile == true || targetMapping.Settings.ProxyAndRecordSettings?.SaveMappingToFile == true)
{
var matcherMapper = new MatcherMapper(targetMapping.Settings);
var mappingConverter = new MappingConverter(matcherMapper);

View File

@@ -37,13 +37,14 @@ public partial class Request
/// <inheritdoc />
public IRequestBuilder WithBodyAsJson(object body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return WithBody(new IMatcher[] { new JsonMatcher(matchBehaviour, body) });
var matcher = body as IMatcher ?? new JsonMatcher(matchBehaviour, body);
return WithBody([matcher]);
}
/// <inheritdoc />
public IRequestBuilder WithBody(IMatcher matcher)
{
return WithBody(new[] { matcher });
return WithBody([matcher]);
}
/// <inheritdoc />

View File

@@ -1,5 +1,6 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using WireMock.Matchers;
using WireMock.Matchers.Request;
@@ -12,7 +13,7 @@ public partial class Request
/// <inheritdoc />
public IRequestBuilder WithBodyAsProtoBuf(string protoDefinition, string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return WithBodyAsProtoBuf([ protoDefinition ], messageType, matchBehaviour);
return WithBodyAsProtoBuf([protoDefinition], messageType, matchBehaviour);
}
/// <inheritdoc />
@@ -36,12 +37,25 @@ public partial class Request
/// <inheritdoc />
public IRequestBuilder WithBodyAsProtoBuf(string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, () => Mapping.ProtoDefinition!.Value, messageType));
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, ProtoDefinitionFunc(), messageType));
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsProtoBuf(string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, () => Mapping.ProtoDefinition!.Value, messageType, matcher));
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, ProtoDefinitionFunc(), messageType, matcher));
}
private Func<IdOrTexts> ProtoDefinitionFunc()
{
return () =>
{
if (Mapping.ProtoDefinition == null)
{
throw new InvalidOperationException($"No ProtoDefinition defined on mapping '{Mapping.Guid}'. Please use the WireMockServerSettings to define ProtoDefinitions.");
}
return Mapping.ProtoDefinition.Value;
};
}
}

View File

@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using JsonConverter.Abstractions;
using WireMock.Models;
namespace WireMock.ResponseBuilders;
@@ -32,7 +33,7 @@ public interface IBodyResponseBuilder : IFaultResponseBuilder
IResponseBuilder WithBody(Func<IRequestMessage, string> bodyFactory, string? destination = BodyDestinationFormat.SameAsSource, Encoding? encoding = null);
/// <summary>
/// WithBody : Create a ... response based on a async callback function.
/// WithBody : Create a ... response based on an async callback function.
/// </summary>
/// <param name="bodyFactory">The async delegate to build the body.</param>
/// <param name="destination">The Body Destination format (SameAsSource, String or Bytes).</param>
@@ -40,6 +41,14 @@ public interface IBodyResponseBuilder : IFaultResponseBuilder
/// <returns>A <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithBody(Func<IRequestMessage, Task<string>> bodyFactory, string? destination = BodyDestinationFormat.SameAsSource, Encoding? encoding = null);
/// <summary>
/// WithBody : Create a ... response based on an async callback function.
/// </summary>
/// <param name="bodyFactory">The async delegate to build the body.</param>
/// <param name="timeout">The timeout to wait on new items in the queue. Default value is <c>1</c> hour.</param>
/// <returns>A <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithSseBody(Func<IRequestMessage, IBlockingQueue<string?>, Task> bodyFactory, TimeSpan? timeout = null);
/// <summary>
/// WithBody : Create a ... response based on a bytearray.
/// </summary>

View File

@@ -51,6 +51,26 @@ public partial class Response
});
}
/// <inheritdoc />
public IResponseBuilder WithSseBody(Func<IRequestMessage, IBlockingQueue<string?>, Task> bodyFactory, TimeSpan? timeout = null)
{
Guard.NotNull(bodyFactory);
var queue = new BlockingQueue<string?>(timeout);
return WithCallbackInternal(true, req => new ResponseMessage
{
BodyData = new BodyData
{
DetectedBodyType = BodyType.SseString,
SseStringQueue = queue,
BodyAsSseStringTask = bodyFactory(req, queue),
Encoding = Encoding.UTF8,
IsFuncUsed = "Func<IRequestMessage, BlockingQueue<string?>, Task>"
}
});
}
/// <inheritdoc />
public IResponseBuilder WithBody(byte[] body, string? destination = BodyDestinationFormat.SameAsSource, Encoding? encoding = null)
{

View File

@@ -356,9 +356,12 @@ internal class RespondWithAProvider : IRespondWithAProvider
{
Guard.NotNull(protoDefinitionOrId);
#if PROTOBUF
ProtoDefinition = ProtoDefinitionHelper.GetIdOrTexts(_settings, protoDefinitionOrId);
return this;
#else
throw new NotSupportedException("The WithProtoDefinition method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower.");
#endif
}
/// <inheritdoc />

View File

@@ -25,7 +25,7 @@ public partial class WireMockServer
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, e.Message);
}
#else
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, "Not supported for .NETStandard 1.3 and .NET 4.5.2 or lower.");
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, "Not supported for .NETStandard 1.3 and .NET 4.6.x or lower.");
#endif
}
@@ -50,7 +50,7 @@ public partial class WireMockServer
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, e.Message);
}
#else
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, "Not supported for .NETStandard 1.3 and .NET 4.5.2 or lower.");
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, "Not supported for .NETStandard 1.3 and .NET 4.6.x or lower.");
#endif
}
}

View File

@@ -335,7 +335,7 @@ public partial class WireMockServer : IWireMockServer
_settings.Logger = settings.Logger ?? new WireMockNullLogger();
_settings.FileSystemHandler = settings.FileSystemHandler ?? new LocalFileSystemHandler();
_settings.Logger.Info("By Stef Heyenrath (https://github.com/WireMock-Net/WireMock.Net)");
_settings.Logger.Info("By Stef Heyenrath (https://github.com/wiremock/WireMock.Net)");
_settings.Logger.Debug("Server settings {0}", JsonConvert.SerializeObject(settings, Formatting.Indented));
HostUrlOptions urlOptions;
@@ -514,7 +514,11 @@ public partial class WireMockServer : IWireMockServer
#if NETSTANDARD1_3
throw new NotSupportedException("AzureADAuthentication is not supported for NETStandard 1.3");
#else
_options.AuthenticationMatcher = new AzureADAuthenticationMatcher(tenant, audience);
_options.AuthenticationMatcher = new AzureADAuthenticationMatcher(
new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler(),
new Microsoft.IdentityModel.Protocols.ConfigurationManager<Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfiguration>($"https://login.microsoftonline.com/{tenant}/.well-known/openid-configuration", new Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfigurationRetriever()),
tenant,
audience);
#endif
}

View File

@@ -0,0 +1,50 @@
// Copyright © WireMock.Net
using HandlebarsDotNet.Helpers.Enums;
using JetBrains.Annotations;
using WireMock.Types;
namespace WireMock.Settings;
/// <summary>
/// HandlebarsSettings
/// </summary>
[PublicAPI]
public class HandlebarsSettings
{
internal static readonly Category[] DefaultAllowedHandlebarsHelpers =
[
Category.Boolean,
Category.Constants,
Category.DateTime,
Category.Enumerable,
Category.Humanizer,
Category.JsonPath,
Category.Math,
Category.Object,
Category.Random,
Category.Regex,
Category.String,
Category.Url,
Category.Xeger,
Category.XPath,
Category.Xslt
];
/// <summary>
/// Defines the allowed custom HandlebarsHelpers which can be used. Possible values are:
/// - <see cref="CustomHandlebarsHelpers.None"/> (Default)
/// - <see cref="CustomHandlebarsHelpers.File"/>
/// - <see cref="CustomHandlebarsHelpers.All"/>
/// </summary>
[PublicAPI]
public CustomHandlebarsHelpers AllowedCustomHandlebarsHelpers { get; set; } = CustomHandlebarsHelpers.None;
/// <summary>
/// Defines the allowed HandlebarHelpers which can be used.
///
/// By default, all categories except <see cref="Category.DynamicLinq"/> and <see cref="Category.Environment"/> are registered.
/// </summary>
[PublicAPI]
public Category[] AllowedHandlebarsHelpers { get; set; } = DefaultAllowedHandlebarsHelpers;
}

View File

@@ -98,7 +98,7 @@ public class ProxyAndRecordSettings : HttpClientSettings
public bool UseDefinedRequestMatchers { get; set; }
/// <summary>
/// Append an unique GUID to the filename from the saved mapping file.
/// Append a unique GUID to the filename from the saved mapping file.
/// </summary>
public bool AppendGuidToSavedMappingFile { get; set; }

View File

@@ -18,7 +18,7 @@ public class ProxyUrlReplaceSettings
public string NewValue { get; set; } = null!;
/// <summary>
/// Defines if the case should be ignore when replacing.
/// Defines if the case should be ignored when replacing.
/// </summary>
public bool IgnoreCase { get; set; }
}

View File

@@ -70,6 +70,11 @@ internal class SimpleSettingsParser
return Arguments.ContainsKey(name);
}
public bool ContainsAny(params string[] names)
{
return names.Any(Arguments.ContainsKey);
}
public string[] GetValues(string name, string[] defaultValue)
{
return Contains(name) ? Arguments[name] : defaultValue;
@@ -142,6 +147,28 @@ internal class SimpleSettingsParser
}, defaultValue);
}
public TEnum[] GetEnumValues<TEnum>(string name, TEnum[] defaultValues)
where TEnum : struct
{
var values = GetValues(name);
if (values == null)
{
return defaultValues;
}
var enums = new List<TEnum>();
foreach (var value in values)
{
if (Enum.TryParse<TEnum>(value, true, out var enumValue))
{
enums.Add(enumValue);
}
}
return enums.ToArray();
}
public string GetStringValue(string name, string defaultValue)
{
return GetValue(name, values => values.FirstOrDefault() ?? defaultValue, defaultValue);

View File

@@ -16,13 +16,13 @@ public class WebProxySettings
public string Address { get; set; } = null!;
/// <summary>
/// The user name associated with the credentials.
/// The username associated with the credentials.
/// </summary>
[PublicAPI]
public string? UserName { get; set; }
/// <summary>
/// The password for the user name associated with the credentials.
/// The password for the username associated with the credentials.
/// </summary>
[PublicAPI]
public string? Password { get; set; }

View File

@@ -329,4 +329,10 @@ public class WireMockServerSettings
/// </summary>
[PublicAPI]
public string? AdminPath { get; set; }
/// <summary>
/// Defines the additional Handlebars Settings.
/// </summary>
[PublicAPI]
public HandlebarsSettings? HandlebarsSettings { get; set; }
}

View File

@@ -36,7 +36,7 @@ public static class WireMockServerSettingsParser
if (parser.GetBoolSwitchValue("help"))
{
(logger ?? new WireMockConsoleLogger()).Info("See https://github.com/WireMock-Net/WireMock.Net/wiki/WireMock-commandline-parameters for details on all commandline options.");
(logger ?? new WireMockConsoleLogger()).Info("See https://github.com/wiremock/WireMock.Net/wiki/WireMock-commandline-parameters for details on all commandline options.");
settings = null;
return false;
}
@@ -84,6 +84,7 @@ public static class WireMockServerSettingsParser
ParsePortSettings(settings, parser);
ParseProxyAndRecordSettings(settings, parser);
ParseCertificateSettings(settings, parser);
ParseHandlebarsSettings(settings, parser);
return true;
}
@@ -152,7 +153,7 @@ public static class WireMockServerSettingsParser
}
else if (settings.HostingScheme is null)
{
settings.Urls = parser.GetValues("Urls", new[] { "http://*:9091/" });
settings.Urls = parser.GetValues("Urls", ["http://*:9091/"]);
}
}
@@ -166,12 +167,25 @@ public static class WireMockServerSettingsParser
X509CertificateFilePath = parser.GetStringValue("X509CertificateFilePath"),
X509CertificatePassword = parser.GetStringValue("X509CertificatePassword")
};
if (certificateSettings.IsDefined)
{
settings.CertificateSettings = certificateSettings;
}
}
private static void ParseHandlebarsSettings(WireMockServerSettings settings, SimpleSettingsParser parser)
{
if (parser.ContainsAny(nameof(HandlebarsSettings.AllowedCustomHandlebarsHelpers), nameof(HandlebarsSettings.AllowedHandlebarsHelpers)))
{
settings.HandlebarsSettings = new HandlebarsSettings
{
AllowedCustomHandlebarsHelpers = parser.GetEnumValue(nameof(HandlebarsSettings.AllowedCustomHandlebarsHelpers), CustomHandlebarsHelpers.None),
AllowedHandlebarsHelpers = parser.GetEnumValues(nameof(HandlebarsSettings.AllowedHandlebarsHelpers), HandlebarsSettings.DefaultAllowedHandlebarsHelpers)
};
}
}
private static void ParseWebProxyAddressSettings(ProxyAndRecordSettings settings, SimpleSettingsParser parser)
{
string? proxyAddress = parser.GetStringValue("WebProxyAddress");

View File

@@ -4,26 +4,30 @@ using HandlebarsDotNet;
using HandlebarsDotNet.Helpers.Attributes;
using HandlebarsDotNet.Helpers.Enums;
using HandlebarsDotNet.Helpers.Helpers;
using HandlebarsDotNet.Helpers.Options;
using Stef.Validation;
using WireMock.Handlers;
using WireMock.Settings;
namespace WireMock.Transformers.Handlebars;
internal class FileHelpers : BaseHelpers, IHelpers
{
internal const string Name = "File";
private readonly IFileSystemHandler _fileSystemHandler;
public FileHelpers(IHandlebars context, IFileSystemHandler fileSystemHandler) : base(context)
public FileHelpers(IHandlebars context, WireMockServerSettings settings) : base(context, new HandlebarsHelpersOptions())
{
_fileSystemHandler = Guard.NotNull(fileSystemHandler);
_fileSystemHandler = Guard.NotNull(settings.FileSystemHandler);
}
[HandlebarsWriter(WriterType.String, usage: HelperUsage.Both, passContext: true, name: "File")]
[HandlebarsWriter(WriterType.String, usage: HelperUsage.Both, passContext: true, name: Name)]
public string Read(Context context, string path)
{
var templateFunc = Context.Compile(path);
var transformed = templateFunc(context.Value);
return _fileSystemHandler.ReadResponseBodyAsString(transformed);
var transformedPath = templateFunc(context.Value);
return _fileSystemHandler.ReadResponseBodyAsString(transformedPath);
}
public Category Category => Category.Custom;

View File

@@ -23,7 +23,7 @@ internal class HandlebarsContextFactory : ITransformerContextFactory
};
var handlebars = HandlebarsDotNet.Handlebars.Create(config);
WireMockHandlebarsHelpers.Register(handlebars, _settings.FileSystemHandler);
WireMockHandlebarsHelpers.Register(handlebars, _settings);
_settings.HandlebarsRegistrationCallback?.Invoke(handlebars, _settings.FileSystemHandler);

View File

@@ -2,21 +2,20 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using HandlebarsDotNet;
using HandlebarsDotNet.Helpers;
using HandlebarsDotNet.Helpers.Helpers;
using WireMock.Handlers;
using WireMock.Settings;
using WireMock.Types;
namespace WireMock.Transformers.Handlebars;
internal static class WireMockHandlebarsHelpers
{
public static void Register(IHandlebars handlebarsContext, IFileSystemHandler fileSystemHandler)
internal static void Register(IHandlebars handlebarsContext, WireMockServerSettings settings)
{
// Register https://github.com/StefH/Handlebars.Net.Helpers
// Register https://github.com/Handlebars.Net/Handlebars.Net.Helpers
HandlebarsHelpers.Register(handlebarsContext, o =>
{
var paths = new List<string>
@@ -33,17 +32,18 @@ internal static class WireMockHandlebarsHelpers
customHelperPaths.Add(path!);
}
}
Add(Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location), paths);
Add(Path.GetDirectoryName(Assembly.GetCallingAssembly().Location), paths);
Add(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), paths);
Add(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule?.FileName), paths);
Add(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly()?.Location), paths);
Add(Path.GetDirectoryName(System.Reflection.Assembly.GetCallingAssembly().Location), paths);
Add(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), paths);
Add(Path.GetDirectoryName(System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName), paths);
#endif
o.CustomHelperPaths = paths;
o.CustomHelpers = new Dictionary<string, IHelpers>
o.CustomHelpers = new Dictionary<string, IHelpers>();
if (settings.HandlebarsSettings?.AllowedCustomHandlebarsHelpers.HasFlag(CustomHandlebarsHelpers.File) == true)
{
{ "File", new FileHelpers(handlebarsContext, fileSystemHandler) }
};
o.CustomHelpers.Add(FileHelpers.Name, new FileHelpers(handlebarsContext, settings));
}
});
}

View File

@@ -0,0 +1,86 @@
// Copyright © WireMock.Net
using System.IO;
using Stef.Validation;
namespace WireMock.Util;
internal static class FilePathUtils
{
/// <summary>
/// Robust handling of the user defined path.
/// Also supports Unix and Windows platforms
/// </summary>
/// <param name="path">The path to clean</param>
public static string? CleanPath(string? path)
{
return path?.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
}
/// <summary>
/// Removes leading directory separator chars from the filepath, which could break Path.Combine
/// </summary>
/// <param name="path">The path to remove the loading DirectorySeparatorChars</param>
public static string? RemoveLeadingDirectorySeparators(string? path)
{
return path?.TrimStart(Path.DirectorySeparatorChar);
}
/// <summary>
/// Combine two paths
/// </summary>
/// <param name="root">The root path</param>
/// <param name="path">The path</param>
public static string Combine(string root, string? path)
{
Guard.NotNull(root);
var result = RemoveLeadingDirectorySeparators(path);
return result == null ? root : Path.Combine(root, result);
}
/// <summary>
/// Returns a relative path from one path to another.
/// </summary>
/// <param name="relativeTo">The source path the result should be relative to. This path is always considered to be a directory..</param>
/// <param name="path">The destination path.</param>
/// <returns>The relative path, or path if the paths don't share the same root.</returns>
public static string GetRelativePath(string relativeTo, string path)
{
#if NETCOREAPP3_1 || NET5_0_OR_GREATER || NETSTANDARD2_1
return Path.GetRelativePath(relativeTo, path);
#else
Guard.NotNull(relativeTo);
Guard.NotNull(path);
static string AppendDirectorySeparatorChar(string path)
{
// Append a slash only if the path is a directory and does not have a slash.
if (!Path.HasExtension(path) && !path.EndsWith(Path.DirectorySeparatorChar.ToString()))
{
return path + Path.DirectorySeparatorChar;
}
return path;
}
var fromUri = new System.Uri(AppendDirectorySeparatorChar(relativeTo));
var toUri = new System.Uri(AppendDirectorySeparatorChar(path));
if (fromUri.Scheme != toUri.Scheme)
{
return path;
}
var relativeUri = fromUri.MakeRelativeUri(toUri);
var relativePath = System.Uri.UnescapeDataString(relativeUri.ToString());
if (string.Equals(toUri.Scheme, "FILE", System.StringComparison.OrdinalIgnoreCase))
{
relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
}
return relativePath;
#endif
}
}

View File

@@ -1,41 +0,0 @@
// Copyright © WireMock.Net
using System.IO;
using Stef.Validation;
namespace WireMock.Util;
internal static class PathUtils
{
/// <summary>
/// Robust handling of the user defined path.
/// Also supports Unix and Windows platforms
/// </summary>
/// <param name="path">The path to clean</param>
public static string? CleanPath(string? path)
{
return path?.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
}
/// <summary>
/// Removes leading directory separator chars from the filepath, which could break Path.Combine
/// </summary>
/// <param name="path">The path to remove the loading DirectorySeparatorChars</param>
public static string? RemoveLeadingDirectorySeparators(string? path)
{
return path?.TrimStart(new[] { Path.DirectorySeparatorChar });
}
/// <summary>
/// Combine two paths
/// </summary>
/// <param name="root">The root path</param>
/// <param name="path">The path</param>
public static string Combine(string root, string? path)
{
Guard.NotNull(root);
var result = RemoveLeadingDirectorySeparators(path);
return result == null ? root : Path.Combine(root, result);
}
}

View File

@@ -1,12 +1,85 @@
// Copyright © WireMock.Net
#if PROTOBUF
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ProtoBufJsonConverter;
using ProtoBufJsonConverter.Models;
using Stef.Validation;
using WireMock.Models;
using WireMock.Settings;
namespace WireMock.Util;
internal static class ProtoDefinitionHelper
/// <summary>
/// Some helper methods for Proto Definitions.
/// </summary>
public static class ProtoDefinitionHelper
{
/// <summary>
/// Builds a dictionary of ProtoDefinitions from a directory.
/// - The key will be the filename without extension.
/// - The value will be the ProtoDefinition with an extra comment with the relative path to each <c>.proto</c> file so it can be used by the WireMockProtoFileResolver.
/// </summary>
/// <param name="directory">The directory to start from.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <c>System.Threading.CancellationToken.None</c>.</param>
public static async Task<ProtoDefinitionData> FromDirectory(string directory, CancellationToken cancellationToken = default)
{
Guard.NotNullOrEmpty(directory);
var fileNameMappedToProtoDefinition = new Dictionary<string, string>();
var filePaths = Directory.EnumerateFiles(directory, "*.proto", SearchOption.AllDirectories);
foreach (var filePath in filePaths)
{
// Get the relative path to the directory (note that this will be OS specific).
var relativePath = FilePathUtils.GetRelativePath(directory, filePath);
// Make it a valid proto import path
var protoRelativePath = relativePath.Replace(Path.DirectorySeparatorChar, '/');
// Build comment and get content from file.
var comment = $"// {protoRelativePath}";
#if NETSTANDARD2_0
var content = File.ReadAllText(filePath);
#else
var content = await File.ReadAllTextAsync(filePath, cancellationToken);
#endif
// Only add the comment if it's not already defined.
var modifiedContent = !content.StartsWith(comment) ? $"{comment}\n{content}" : content;
var key = Path.GetFileNameWithoutExtension(filePath);
fileNameMappedToProtoDefinition.Add(key, modifiedContent);
}
var converter = SingletonFactory<Converter>.GetInstance();
var resolver = new WireMockProtoFileResolver(fileNameMappedToProtoDefinition.Values);
var messageTypeMappedToWithProtoDefinition = new Dictionary<string, string>();
foreach (var protoDefinition in fileNameMappedToProtoDefinition.Values)
{
var infoRequest = new GetInformationRequest(protoDefinition, resolver);
try
{
var info = await converter.GetInformationAsync(infoRequest, cancellationToken);
foreach (var messageType in info.MessageTypes)
{
messageTypeMappedToWithProtoDefinition[messageType.Key] = protoDefinition;
}
}
catch
{
// Ignore
}
}
return new ProtoDefinitionData(fileNameMappedToProtoDefinition);
}
internal static IdOrTexts GetIdOrTexts(WireMockServerSettings settings, params string[] protoDefinitionOrId)
{
switch (protoDefinitionOrId.Length)
@@ -19,9 +92,10 @@ internal static class ProtoDefinitionHelper
}
return new(null, protoDefinitionOrId);
default:
return new(null, protoDefinitionOrId);
}
}
}
}
#endif

View File

@@ -25,7 +25,7 @@ internal static class TypeLoader
return foundType;
}
throw new DllNotFoundException($"No dll found which implements Interface '{key}'.");
throw new DllNotFoundException($"No dll found which implements interface '{key}'.");
});
return (TInterface)Activator.CreateInstance(pluginType, args)!;

View File

@@ -7,18 +7,19 @@ using System.IO;
using System.Linq;
using ProtoBufJsonConverter;
using Stef.Validation;
using WireMock.Extensions;
namespace WireMock.Util;
/// <summary>
/// This resolver is used to resolve the extra ProtoDefinition files.
///
/// It assumes that:
/// - the first ProtoDefinition file is the main ProtoDefinition file.
/// - the first commented line of each extra ProtoDefinition file is the filename which is used in the import of the other ProtoDefinition file(s).
/// - The first commented line of each ProtoDefinition file is the filepath which is used in the import of the other ProtoDefinition file(s).
/// </summary>
internal class WireMockProtoFileResolver : IProtoFileResolver
{
private readonly Dictionary<string, string> _files = new();
private readonly Dictionary<string, string> _files = [];
public WireMockProtoFileResolver(IReadOnlyCollection<string> protoDefinitions)
{
@@ -27,12 +28,19 @@ internal class WireMockProtoFileResolver : IProtoFileResolver
return;
}
foreach (var extraProtoDefinition in protoDefinitions.Skip(1))
foreach (var extraProtoDefinition in protoDefinitions)
{
var firstNonEmptyLine = extraProtoDefinition.Split(['\r', '\n']).FirstOrDefault(l => !string.IsNullOrEmpty(l));
if (firstNonEmptyLine != null && TryGetValidFileName(firstNonEmptyLine.TrimStart(['/', ' ']), out var validFileName))
if (firstNonEmptyLine != null)
{
_files.Add(validFileName, extraProtoDefinition);
if (TryGetValidPath(firstNonEmptyLine.TrimStart(['/', ' ']), out var validPath))
{
_files.Add(validPath, extraProtoDefinition);
}
else
{
_files.Add(extraProtoDefinition.GetDeterministicHashCodeAsString(), extraProtoDefinition);
}
}
}
}
@@ -52,15 +60,15 @@ internal class WireMockProtoFileResolver : IProtoFileResolver
throw new FileNotFoundException($"The ProtoDefinition '{path}' was not found.");
}
private static bool TryGetValidFileName(string fileName, [NotNullWhen(true)] out string? validFileName)
private static bool TryGetValidPath(string path, [NotNullWhen(true)] out string? validPath)
{
if (!fileName.Any(c => Path.GetInvalidFileNameChars().Contains(c)))
if (!path.Any(c => Path.GetInvalidPathChars().Contains(c)))
{
validFileName = fileName;
validPath = path;
return true;
}
validFileName = null;
validPath = null;
return false;
}
}

View File

@@ -46,7 +46,7 @@
<DefineConstants>$(DefineConstants);USE_ASPNETCORE;NET46</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452'">
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452' and '$(TargetFramework)' != 'net46'">
<DefineConstants>$(DefineConstants);OPENAPIPARSER</DefineConstants>
</PropertyGroup>
@@ -59,8 +59,6 @@
</PropertyGroup>
<ItemGroup>
<Compile Remove="Json\DynamicPropertyWithValue.cs" />
<Compile Remove="Json\JObjectExtensions.cs" />
<Compile Remove="Matchers\LinqMatcher.cs" />
</ItemGroup>
@@ -114,7 +112,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- https://github.com/WireMock-Net/WireMock.Net/issues/507 -->
<!-- https://github.com/wiremock/WireMock.Net/issues/507 -->
<PackageReference Include="Microsoft.AspNetCore.Server.IIS" Version="2.2.6" />
</ItemGroup>
@@ -140,7 +138,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- https://github.com/WireMock-Net/WireMock.Net/issues/507 -->
<!-- https://github.com/wiremock/WireMock.Net/issues/507 -->
<PackageReference Include="Microsoft.AspNetCore.Server.IIS" Version="2.2.6" />
</ItemGroup>
@@ -152,7 +150,7 @@
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452' and '$(TargetFramework)' != 'net46' and '$(TargetFramework)' != 'net461'">
<PackageReference Include="GraphQL.NewtonsoftJson" Version="8.2.1" />
<PackageReference Include="MimeKitLite" Version="4.1.0.1" />
<PackageReference Include="ProtoBufJsonConverter" Version="0.7.0" />
<PackageReference Include="ProtoBufJsonConverter" Version="0.9.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp3.1' ">
@@ -182,30 +180,30 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Handlebars.Net.Helpers" Version="2.4.10" />
<!--<PackageReference Include="Handlebars.Net.Helpers.DynamicLinq" Version="2.4.10" />-->
<PackageReference Include="Handlebars.Net.Helpers.Humanizer" Version="2.4.10" />
<PackageReference Include="Handlebars.Net.Helpers.Json" Version="2.4.10" />
<PackageReference Include="Handlebars.Net.Helpers.Random" Version="2.4.10" />
<PackageReference Include="Handlebars.Net.Helpers.Xeger" Version="2.4.10" />
<PackageReference Include="Handlebars.Net.Helpers.XPath" Version="2.4.10" />
<PackageReference Include="Handlebars.Net.Helpers" Version="2.5.0" />
<!--<PackageReference Include="Handlebars.Net.Helpers.DynamicLinq" Version="2.5.0" />-->
<PackageReference Include="Handlebars.Net.Helpers.Humanizer" Version="2.5.0" />
<PackageReference Include="Handlebars.Net.Helpers.Json" Version="2.5.0" />
<PackageReference Include="Handlebars.Net.Helpers.Random" Version="2.5.0" />
<PackageReference Include="Handlebars.Net.Helpers.Xeger" Version="2.5.0" />
<PackageReference Include="Handlebars.Net.Helpers.XPath" Version="2.5.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452' ">
<PackageReference Include="Handlebars.Net.Helpers.Xslt" Version="2.4.6" />
</ItemGroup>
<ItemGroup>
<!-- CVE-2021-26701 and https://github.com/WireMock-Net/WireMock.Net/issues/697 -->
<!--<ItemGroup>
--><!-- CVE-2021-26701 and https://github.com/wiremock/WireMock.Net/issues/697 --><!--
<PackageReference Include="System.Text.Encodings.Web" Version="4.7.2" />
</ItemGroup>
</ItemGroup>-->
<ItemGroup>
<ProjectReference Include="..\WireMock.Net.Abstractions\WireMock.Net.Abstractions.csproj" />
<ProjectReference Include="..\WireMock.Org.Abstractions\WireMock.Org.Abstractions.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452'">
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452' and '$(TargetFramework)' != 'net46'">
<ProjectReference Include="..\WireMock.Net.OpenApiParser\WireMock.Net.OpenApiParser.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,30 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../../src/WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
</PropertyGroup>
<ItemGroup>
<!-- https://learn.microsoft.com/en-us/dotnet/aspire/extensibility/custom-resources?tabs=windows#create-library-for-resource-extension -->
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="8.2.2" />
</ItemGroup>
<ItemGroup>
<None Update="WireMockMappings\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.2.0" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../../src/WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
</PropertyGroup>
<ItemGroup>
<!-- https://learn.microsoft.com/en-us/dotnet/aspire/extensibility/custom-resources?tabs=windows#create-library-for-resource-extension -->
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.2.0" />
</ItemGroup>
<ItemGroup>
<None Update="WireMockMappings\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -1,46 +1,48 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<SonarQubeExclude>true</SonarQubeExclude>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../../src/WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="8.2.2" />
<PackageReference Include="Codecov" Version="1.13.0" />
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\WireMock.Net.Aspire.TestAppHost\WireMock.Net.Aspire.TestAppHost.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Aspire.Hosting.Testing" />
<Using Include="Xunit" />
</ItemGroup>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<SonarQubeExclude>true</SonarQubeExclude>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../../src/WireMock.Net/WireMock.Net.snk</AssemblyOriginatorKeyFile>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="9.2.0" />
<PackageReference Include="Codecov" Version="1.13.0" />
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\WireMock.Net.Aspire\WireMock.Net.Aspire.csproj" IsAspireProjectResource="false" />
<ProjectReference Include="..\WireMock.Net.Aspire.TestAppHost\WireMock.Net.Aspire.TestAppHost.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Aspire.Hosting" />
<Using Include="Aspire.Hosting.ApplicationModel" />
<Using Include="Aspire.Hosting.Testing" />
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@@ -1,96 +1,98 @@
// Copyright © WireMock.Net
using System.Net.Sockets;
using FluentAssertions;
using Moq;
namespace WireMock.Net.Aspire.Tests;
public class WireMockServerBuilderExtensionsTests
{
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("\t")]
public void AddWireMock_WithNullOrWhiteSpaceName_ShouldThrowException(string? name)
{
// Arrange
var builder = Mock.Of<IDistributedApplicationBuilder>();
// Act
Action act = () => builder.AddWireMock(name!, 12345);
// Assert
act.Should().Throw<Exception>();
}
[Fact]
public void AddWireMock_WithInvalidPort_ShouldThrowArgumentOutOfRangeException()
{
// Arrange
const int invalidPort = -1;
var builder = Mock.Of<IDistributedApplicationBuilder>();
// Act
Action act = () => builder.AddWireMock("ValidName", invalidPort);
// Assert
act.Should().Throw<ArgumentOutOfRangeException>().WithMessage("Specified argument was out of the range of valid values. (Parameter 'port')");
}
[Fact]
public void AddWireMock()
{
// Arrange
var name = $"apiservice{Guid.NewGuid()}";
const int port = 12345;
const string username = "admin";
const string password = "test";
var builder = DistributedApplication.CreateBuilder();
// Act
var wiremock = builder
.AddWireMock(name, port)
.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,
HttpPort = port
});
wiremock.Resource.Annotations.Should().HaveCount(4);
var containerImageAnnotation = wiremock.Resource.Annotations.OfType<ContainerImageAnnotation>().FirstOrDefault();
containerImageAnnotation.Should().BeEquivalentTo(new ContainerImageAnnotation
{
Image = "sheyenrath/wiremock.net-alpine",
Registry = null,
Tag = "latest"
});
var endpointAnnotation = wiremock.Resource.Annotations.OfType<EndpointAnnotation>().FirstOrDefault();
endpointAnnotation.Should().BeEquivalentTo(new EndpointAnnotation(
protocol: ProtocolType.Tcp,
uriScheme: "http",
transport: null,
name: null,
port: port,
targetPort: 80,
isExternal: null,
isProxied: true
));
wiremock.Resource.Annotations.OfType<EnvironmentCallbackAnnotation>().FirstOrDefault().Should().NotBeNull();
wiremock.Resource.Annotations.OfType<CommandLineArgsCallbackAnnotation>().FirstOrDefault().Should().NotBeNull();
}
// Copyright © WireMock.Net
using System.Net.Sockets;
using FluentAssertions;
using Moq;
namespace WireMock.Net.Aspire.Tests;
public class WireMockServerBuilderExtensionsTests
{
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("\t")]
public void AddWireMock_WithNullOrWhiteSpaceName_ShouldThrowException(string? name)
{
// Arrange
var builder = Mock.Of<IDistributedApplicationBuilder>();
// Act
Action act = () => builder.AddWireMock(name!, 12345);
// Assert
act.Should().Throw<Exception>();
}
[Fact]
public void AddWireMock_WithInvalidPort_ShouldThrowArgumentOutOfRangeException()
{
// Arrange
const int invalidPort = -1;
var builder = Mock.Of<IDistributedApplicationBuilder>();
// Act
Action act = () => builder.AddWireMock("ValidName", invalidPort);
// Assert
act.Should().Throw<ArgumentOutOfRangeException>().WithMessage("Specified argument was out of the range of valid values. (Parameter 'port')");
}
[Fact]
public void AddWireMock()
{
// Arrange
var name = $"apiservice{Guid.NewGuid()}";
const int port = 12345;
const string username = "admin";
const string password = "test";
var builder = DistributedApplication.CreateBuilder();
// Act
var wiremock = builder
.AddWireMock(name, port)
.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,
HttpPort = port
});
wiremock.Resource.Annotations.Should().HaveCount(5);
var containerImageAnnotation = wiremock.Resource.Annotations.OfType<ContainerImageAnnotation>().FirstOrDefault();
containerImageAnnotation.Should().BeEquivalentTo(new ContainerImageAnnotation
{
Image = "sheyenrath/wiremock.net-alpine",
Registry = null,
Tag = "latest"
});
var endpointAnnotation = wiremock.Resource.Annotations.OfType<EndpointAnnotation>().FirstOrDefault();
endpointAnnotation.Should().BeEquivalentTo(new EndpointAnnotation(
protocol: ProtocolType.Tcp,
uriScheme: "http",
transport: null,
name: null,
port: port,
targetPort: 80,
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();
}
}

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