mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-04-28 11:17:02 +02:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bf42904ab | ||
|
|
0a48b40021 | ||
|
|
1962437dcd | ||
|
|
85d61a1877 | ||
|
|
885911203b | ||
|
|
1e591d5f8a | ||
|
|
02b7e3744e | ||
|
|
6e2a4d7e04 | ||
|
|
479bb0b8ec | ||
|
|
a453e00fdb |
4
.github/copilot-instructions.md
vendored
4
.github/copilot-instructions.md
vendored
@@ -1,4 +0,0 @@
|
||||
# Copilot Instructions
|
||||
|
||||
## Project Guidelines
|
||||
- All new byte[xx] calls should use using var data = ArrayPool<byte>.Shared.Lease(xx); instead of directly allocating byte arrays
|
||||
15
.github/instructions/instructions.md
vendored
Normal file
15
.github/instructions/instructions.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
description: 'Guidelines for this solution'
|
||||
applyTo: '**/*.csproj, **/*.cs'
|
||||
---
|
||||
|
||||
# Multi .NET Framework Targeting
|
||||
|
||||
## Instructions
|
||||
- The main project "WireMock.Net.Minimal" targets `netstandard2.0` and `net8.0`. Ensure that any new code or dependencies are compatible with these frameworks.
|
||||
|
||||
|
||||
# C# Guidelines
|
||||
|
||||
## Instructions
|
||||
- When a new ByteArray is needed, do not use `var data = new byte[bufferSize];`. Always use `var data = ArrayPool<byte>.Shared.Lease(bufferSize);`.
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,3 +1,19 @@
|
||||
# 2.4.0 (24 April 2026)
|
||||
- [#1437](https://github.com/wiremock/WireMock.Net/pull/1437) - Added feature to enable and disable mappings [feature] contributed by [jayaraman-venkatesan](https://github.com/jayaraman-venkatesan)
|
||||
- [#1450](https://github.com/wiremock/WireMock.Net/pull/1450) - Bump OpenTelemetry.Exporter.OpenTelemetryProtocol from 1.14.0 to 1.15.x [dependencies, .NET] contributed by [dependabot[bot]](https://github.com/apps/dependabot)
|
||||
- [#1421](https://github.com/wiremock/WireMock.Net/issues/1421) - Deactivate mapping without deleting it [feature]
|
||||
|
||||
# 2.3.0 (20 April 2026)
|
||||
- [#1436](https://github.com/wiremock/WireMock.Net/pull/1436) - Moving Scenario state change before global response delay is set [feature] contributed by [jayaraman-venkatesan](https://github.com/jayaraman-venkatesan)
|
||||
- [#1440](https://github.com/wiremock/WireMock.Net/pull/1440) - Bump log4net from 2.0.15 to 3.3.0 [dependencies] contributed by [dependabot[bot]](https://github.com/apps/dependabot)
|
||||
- [#1443](https://github.com/wiremock/WireMock.Net/pull/1443) - Fix ExactMatcher and JsonMatcher not working for ISO dates as string [bug] contributed by [StefH](https://github.com/StefH)
|
||||
- [#1444](https://github.com/wiremock/WireMock.Net/pull/1444) - Update instructions.md [refactor] contributed by [StefH](https://github.com/StefH)
|
||||
- [#1448](https://github.com/wiremock/WireMock.Net/pull/1448) - Use DefaultJsonSerializer for BodyAsJson-Response [bug] contributed by [StefH](https://github.com/StefH)
|
||||
- [#1205](https://github.com/wiremock/WireMock.Net/issues/1205) - System.Private.Uri 4.3.0 Blackduck security High vulnerability [bug]
|
||||
- [#1260](https://github.com/wiremock/WireMock.Net/issues/1260) - Scenario state change before a response delay timeout ends [bug]
|
||||
- [#1441](https://github.com/wiremock/WireMock.Net/issues/1441) - ExactMatcher and JsonMatcher not working for ISO dates in v2.2.0 [bug]
|
||||
- [#1446](https://github.com/wiremock/WireMock.Net/issues/1446) - WithBodyAsJson does not return correctly formatted json when using SystemTextJsonConverter [bug]
|
||||
|
||||
# 2.2.0 (30 March 2026)
|
||||
- [#1433](https://github.com/wiremock/WireMock.Net/pull/1433) - Add comments for ScenarioStateStore related code [refactor] contributed by [StefH](https://github.com/StefH)
|
||||
- [#1434](https://github.com/wiremock/WireMock.Net/pull/1434) - Upgrade Scriban.Signed [security] contributed by [StefH](https://github.com/StefH)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionPrefix>2.2.0</VersionPrefix>
|
||||
<VersionPrefix>2.4.0</VersionPrefix>
|
||||
<PackageIcon>WireMock.Net-Logo.png</PackageIcon>
|
||||
<PackageProjectUrl>https://github.com/wiremock/WireMock.Net</PackageProjectUrl>
|
||||
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
rem https://github.com/StefH/GitHubReleaseNotes
|
||||
|
||||
SET version=2.2.0
|
||||
SET version=2.4.0
|
||||
|
||||
GitHubReleaseNotes --output CHANGELOG.md --skip-empty-releases --exclude-labels wontfix test question invalid doc duplicate example environment --version %version% --token %GH_TOKEN%
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# 2.2.0 (30 March 2026)
|
||||
- #1433 Add comments for ScenarioStateStore related code [refactor]
|
||||
- #1434 Upgrade Scriban.Signed [security]
|
||||
# 2.4.0 (24 April 2026)
|
||||
- #1437 Added feature to enable and disable mappings [feature]
|
||||
- #1450 Bump OpenTelemetry.Exporter.OpenTelemetryProtocol from 1.14.0 to 1.15.x [dependencies, .NET]
|
||||
- #1421 Deactivate mapping without deleting it [feature]
|
||||
|
||||
The full release notes can be found here: https://github.com/wiremock/WireMock.Net/blob/master/CHANGELOG.md
|
||||
@@ -12,11 +12,11 @@
|
||||
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.4.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="8.0.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.1" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.8.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\WireMock.Net\WireMock.Net.csproj" />
|
||||
<PackageReference Include="log4net" Version="2.0.15" />
|
||||
<PackageReference Include="log4net" Version="3.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.15.3" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WireMock.Net.WebApplication;
|
||||
|
||||
public class App : IHostedService
|
||||
public class App(IWireMockService service) : IHostedService
|
||||
{
|
||||
private readonly IWireMockService _service;
|
||||
|
||||
public App(IWireMockService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_service.Start();
|
||||
service.Start();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_service.Stop();
|
||||
service.Stop();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WireMock.Settings;
|
||||
|
||||
namespace WireMock.Net.WebApplication;
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\WireMock.Net.Abstractions\WireMock.Net.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\src\WireMock.Net\WireMock.Net.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using WireMock.Admin.Requests;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"WireMockServerSettings": {
|
||||
"StartAdminInterface": true,
|
||||
"Urls": [
|
||||
"http://localhost"
|
||||
"http://localhost:0"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<!--
|
||||
<!--
|
||||
Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380
|
||||
-->
|
||||
<system.webServer>
|
||||
<handlers>
|
||||
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
|
||||
</handlers>
|
||||
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" />
|
||||
</system.webServer>
|
||||
-->
|
||||
<system.webServer>
|
||||
<handlers>
|
||||
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
|
||||
</handlers>
|
||||
<httpProtocol>
|
||||
<customHeaders>
|
||||
<add name="X-Frame-Options" value="SAMEORIGIN" />
|
||||
</customHeaders>
|
||||
</httpProtocol>
|
||||
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" />
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
@@ -55,12 +55,17 @@ public class MappingModel
|
||||
/// In case the value is null state will not be changed.
|
||||
/// </summary>
|
||||
public string? SetStateTo { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The number of times this match should be matched before the state will be changed to the specified one.
|
||||
/// </summary>
|
||||
public int? TimesInSameState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Value to determine if the mapping is disabled. Defaults to <c>null</c> (not disabled).
|
||||
/// </summary>
|
||||
public bool? IsDisabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The request model.
|
||||
/// </summary>
|
||||
@@ -100,7 +105,7 @@ public class MappingModel
|
||||
/// </summary>
|
||||
public object? Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <summary>
|
||||
/// The probability when this request should be matched. Value is between 0 and 1. [Optional]
|
||||
/// </summary>
|
||||
public double? Probability { get; set; }
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JsonConverter.Abstractions" Version="0.8.0" />
|
||||
<PackageReference Include="JsonConverter.Abstractions" Version="0.9.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -62,6 +62,9 @@ public class Mapping : IMapping
|
||||
/// <inheritdoc />
|
||||
public bool IsProxy => Provider is ProxyAsyncResponseProvider;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsDisabled { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool LogMapping => Provider is not (DynamicResponseProvider or DynamicAsyncResponseProvider);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using JsonConverter.Abstractions;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using WireMock.Handlers;
|
||||
@@ -99,4 +100,12 @@ internal interface IWireMockMiddlewareOptions
|
||||
/// WebSocket settings.
|
||||
/// </summary>
|
||||
WebSocketSettings? WebSocketSettings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default JSON converter used for serialization.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Set this property to customize how objects are serialized to and deserialized from JSON during mapping.
|
||||
/// </remarks>
|
||||
IJsonConverter DefaultJsonSerializer { get; set; }
|
||||
}
|
||||
@@ -3,249 +3,242 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using JsonConverter.Abstractions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
using RandomDataGenerator.FieldOptions;
|
||||
using RandomDataGenerator.Randomizers;
|
||||
using Stef.Validation;
|
||||
using WireMock.Http;
|
||||
using WireMock.ResponseBuilders;
|
||||
using WireMock.ResponseProviders;
|
||||
using WireMock.Types;
|
||||
using WireMock.Util;
|
||||
|
||||
namespace WireMock.Owin.Mappers
|
||||
namespace WireMock.Owin.Mappers;
|
||||
|
||||
/// <summary>
|
||||
/// OwinResponseMapper
|
||||
/// </summary>
|
||||
internal class OwinResponseMapper(IWireMockMiddlewareOptions options) : IOwinResponseMapper
|
||||
{
|
||||
/// <summary>
|
||||
/// OwinResponseMapper
|
||||
/// </summary>
|
||||
internal class OwinResponseMapper : IOwinResponseMapper
|
||||
private readonly IRandomizerNumber<double> _randomizerDouble = RandomizerFactory.GetRandomizer(new FieldOptionsDouble { Min = 0, Max = 1 });
|
||||
private readonly IRandomizerBytes _randomizerBytes = RandomizerFactory.GetRandomizer(new FieldOptionsBytes { Min = 100, Max = 200 });
|
||||
private readonly Encoding _utf8NoBom = new UTF8Encoding(false);
|
||||
|
||||
// https://msdn.microsoft.com/en-us/library/78h415ay(v=vs.110).aspx
|
||||
private static readonly IDictionary<string, Action<HttpResponse, bool, WireMockList<string>>> ResponseHeadersToFix =
|
||||
new Dictionary<string, Action<HttpResponse, bool, WireMockList<string>>>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ HttpKnownHeaderNames.ContentType, (r, _, v) => r.ContentType = v.FirstOrDefault() },
|
||||
{ HttpKnownHeaderNames.ContentLength, (r, hasBody, v) =>
|
||||
{
|
||||
// Only set the Content-Length header if the response does not have a body
|
||||
if (!hasBody && long.TryParse(v.FirstOrDefault(), out var contentLength))
|
||||
{
|
||||
r.ContentLength = contentLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task MapAsync(IResponseMessage? responseMessage, HttpResponse response)
|
||||
{
|
||||
private readonly IRandomizerNumber<double> _randomizerDouble = RandomizerFactory.GetRandomizer(new FieldOptionsDouble { Min = 0, Max = 1 });
|
||||
private readonly IRandomizerBytes _randomizerBytes = RandomizerFactory.GetRandomizer(new FieldOptionsBytes { Min = 100, Max = 200 });
|
||||
private readonly IWireMockMiddlewareOptions _options;
|
||||
private readonly Encoding _utf8NoBom = new UTF8Encoding(false);
|
||||
|
||||
// https://msdn.microsoft.com/en-us/library/78h415ay(v=vs.110).aspx
|
||||
private static readonly IDictionary<string, Action<HttpResponse, bool, WireMockList<string>>> ResponseHeadersToFix =
|
||||
new Dictionary<string, Action<HttpResponse, bool, WireMockList<string>>>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ HttpKnownHeaderNames.ContentType, (r, _, v) => r.ContentType = v.FirstOrDefault() },
|
||||
{ HttpKnownHeaderNames.ContentLength, (r, hasBody, v) =>
|
||||
{
|
||||
// Only set the Content-Length header if the response does not have a body
|
||||
if (!hasBody && long.TryParse(v.FirstOrDefault(), out var contentLength))
|
||||
{
|
||||
r.ContentLength = contentLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="options">The IWireMockMiddlewareOptions.</param>
|
||||
public OwinResponseMapper(IWireMockMiddlewareOptions options)
|
||||
if (responseMessage == null || responseMessage is WebSocketHandledResponse)
|
||||
{
|
||||
_options = Guard.NotNull(options);
|
||||
return;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task MapAsync(IResponseMessage? responseMessage, HttpResponse response)
|
||||
var bodyData = responseMessage.BodyData;
|
||||
if (bodyData?.GetDetectedBodyType() == BodyType.SseString)
|
||||
{
|
||||
if (responseMessage == null || responseMessage is WebSocketHandledResponse)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await HandleSseStringAsync(responseMessage, response, bodyData);
|
||||
return;
|
||||
}
|
||||
|
||||
var bodyData = responseMessage.BodyData;
|
||||
if (bodyData?.GetDetectedBodyType() == BodyType.SseString)
|
||||
{
|
||||
await HandleSseStringAsync(responseMessage, response, bodyData);
|
||||
return;
|
||||
}
|
||||
byte[]? bytes;
|
||||
switch (responseMessage.FaultType)
|
||||
{
|
||||
case FaultType.EMPTY_RESPONSE:
|
||||
bytes = IsFault(responseMessage) ? [] : await GetNormalBodyAsync(responseMessage).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
byte[]? bytes;
|
||||
switch (responseMessage.FaultType)
|
||||
{
|
||||
case FaultType.EMPTY_RESPONSE:
|
||||
bytes = IsFault(responseMessage) ? [] : await GetNormalBodyAsync(responseMessage).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case FaultType.MALFORMED_RESPONSE_CHUNK:
|
||||
bytes = await GetNormalBodyAsync(responseMessage).ConfigureAwait(false) ?? [];
|
||||
if (IsFault(responseMessage))
|
||||
{
|
||||
bytes = bytes.Take(bytes.Length / 2).Union(_randomizerBytes.Generate()).ToArray();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
bytes = await GetNormalBodyAsync(responseMessage).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
|
||||
if (responseMessage.StatusCode is HttpStatusCode or int)
|
||||
{
|
||||
response.StatusCode = MapStatusCode((int) responseMessage.StatusCode);
|
||||
}
|
||||
else if (responseMessage.StatusCode is string statusCodeAsString)
|
||||
{
|
||||
// Note: this case will also match on null
|
||||
_ = int.TryParse(statusCodeAsString, out var statusCodeTypeAsInt);
|
||||
response.StatusCode = MapStatusCode(statusCodeTypeAsInt);
|
||||
}
|
||||
|
||||
SetResponseHeaders(responseMessage, bytes != null, response);
|
||||
|
||||
if (bytes != null)
|
||||
{
|
||||
try
|
||||
case FaultType.MALFORMED_RESPONSE_CHUNK:
|
||||
bytes = await GetNormalBodyAsync(responseMessage).ConfigureAwait(false) ?? [];
|
||||
if (IsFault(responseMessage))
|
||||
{
|
||||
await response.Body.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
|
||||
bytes = bytes.Take(bytes.Length / 2).Union(_randomizerBytes.Generate()).ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
break;
|
||||
|
||||
default:
|
||||
bytes = await GetNormalBodyAsync(responseMessage).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
|
||||
if (responseMessage.StatusCode is HttpStatusCode or int)
|
||||
{
|
||||
response.StatusCode = MapStatusCode((int)responseMessage.StatusCode);
|
||||
}
|
||||
else if (responseMessage.StatusCode is string statusCodeAsString)
|
||||
{
|
||||
// Note: this case will also match on null
|
||||
_ = int.TryParse(statusCodeAsString, out var statusCodeTypeAsInt);
|
||||
response.StatusCode = MapStatusCode(statusCodeTypeAsInt);
|
||||
}
|
||||
|
||||
SetResponseHeaders(responseMessage, bytes != null, response);
|
||||
|
||||
if (bytes != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await response.Body.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
options.Logger.Warn("Error writing response body. Exception : {0}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
SetResponseTrailingHeaders(responseMessage, response);
|
||||
}
|
||||
|
||||
private static async Task HandleSseStringAsync(IResponseMessage responseMessage, HttpResponse response, IBodyData bodyData)
|
||||
{
|
||||
if (bodyData.SseStringQueue == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetResponseHeaders(responseMessage, true, response);
|
||||
|
||||
string? text;
|
||||
do
|
||||
{
|
||||
if (bodyData.SseStringQueue.TryRead(out text))
|
||||
{
|
||||
await response.WriteAsync(text);
|
||||
await response.Body.FlushAsync();
|
||||
}
|
||||
} while (text != null);
|
||||
}
|
||||
|
||||
private int MapStatusCode(int code)
|
||||
{
|
||||
if (options.AllowOnlyDefinedHttpStatusCodeInResponse == true && !Enum.IsDefined(typeof(HttpStatusCode), code))
|
||||
{
|
||||
return (int)HttpStatusCode.OK;
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
private bool IsFault(IResponseMessage responseMessage)
|
||||
{
|
||||
return responseMessage.FaultPercentage == null || _randomizerDouble.Generate() <= responseMessage.FaultPercentage;
|
||||
}
|
||||
|
||||
private async Task<byte[]?> GetNormalBodyAsync(IResponseMessage responseMessage)
|
||||
{
|
||||
var bodyData = responseMessage.BodyData;
|
||||
switch (bodyData?.GetDetectedBodyType())
|
||||
{
|
||||
case BodyType.String:
|
||||
case BodyType.FormUrlEncoded:
|
||||
return (bodyData.Encoding ?? _utf8NoBom).GetBytes(bodyData.BodyAsString!);
|
||||
|
||||
case BodyType.Json:
|
||||
var jsonConverterOptions = new JsonConverterOptions
|
||||
{
|
||||
_options.Logger.Warn("Error writing response body. Exception : {0}", ex);
|
||||
}
|
||||
}
|
||||
WriteIndented = bodyData.BodyAsJsonIndented == true,
|
||||
IgnoreNullValues = true
|
||||
};
|
||||
|
||||
SetResponseTrailingHeaders(responseMessage, response);
|
||||
}
|
||||
var jsonBody = options.DefaultJsonSerializer.Serialize(bodyData.BodyAsJson!, jsonConverterOptions);
|
||||
return (bodyData.Encoding ?? _utf8NoBom).GetBytes(jsonBody);
|
||||
|
||||
private static async Task HandleSseStringAsync(IResponseMessage responseMessage, HttpResponse response, IBodyData bodyData)
|
||||
{
|
||||
if (bodyData.SseStringQueue == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetResponseHeaders(responseMessage, true, response);
|
||||
|
||||
string? text;
|
||||
do
|
||||
{
|
||||
if (bodyData.SseStringQueue.TryRead(out text))
|
||||
case BodyType.ProtoBuf:
|
||||
if (TypeLoader.TryLoadStaticInstance<IProtoBufUtils>(out var protoBufUtils))
|
||||
{
|
||||
await response.WriteAsync(text);
|
||||
await response.Body.FlushAsync();
|
||||
var protoDefinitions = bodyData.ProtoDefinition?.Invoke().Texts;
|
||||
return await protoBufUtils.GetProtoBufMessageWithHeaderAsync(protoDefinitions, bodyData.ProtoBufMessageType, bodyData.BodyAsJson).ConfigureAwait(false);
|
||||
}
|
||||
} while (text != null);
|
||||
break;
|
||||
|
||||
case BodyType.Bytes:
|
||||
return bodyData.BodyAsBytes;
|
||||
|
||||
case BodyType.File:
|
||||
return options.FileSystemHandler?.ReadResponseBodyAsFile(bodyData.BodyAsFile!);
|
||||
|
||||
case BodyType.MultiPart:
|
||||
options.Logger.Warn("MultiPart body type is not handled!");
|
||||
break;
|
||||
|
||||
case BodyType.None:
|
||||
break;
|
||||
}
|
||||
|
||||
private int MapStatusCode(int code)
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void SetResponseHeaders(IResponseMessage responseMessage, bool hasBody, HttpResponse response)
|
||||
{
|
||||
// Force setting the Date header (#577)
|
||||
AppendResponseHeader(
|
||||
response,
|
||||
HttpKnownHeaderNames.Date,
|
||||
[DateTime.UtcNow.ToString(CultureInfo.InvariantCulture.DateTimeFormat.RFC1123Pattern, CultureInfo.InvariantCulture)]
|
||||
);
|
||||
|
||||
// Set other headers
|
||||
foreach (var item in responseMessage.Headers!)
|
||||
{
|
||||
if (_options.AllowOnlyDefinedHttpStatusCodeInResponse == true && !Enum.IsDefined(typeof(HttpStatusCode), code))
|
||||
var headerName = item.Key;
|
||||
var value = item.Value;
|
||||
if (ResponseHeadersToFix.TryGetValue(headerName, out var action))
|
||||
{
|
||||
return (int)HttpStatusCode.OK;
|
||||
action.Invoke(response, hasBody, value);
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
private bool IsFault(IResponseMessage responseMessage)
|
||||
{
|
||||
return responseMessage.FaultPercentage == null || _randomizerDouble.Generate() <= responseMessage.FaultPercentage;
|
||||
}
|
||||
|
||||
private async Task<byte[]?> GetNormalBodyAsync(IResponseMessage responseMessage)
|
||||
{
|
||||
var bodyData = responseMessage.BodyData;
|
||||
switch (bodyData?.GetDetectedBodyType())
|
||||
else
|
||||
{
|
||||
case BodyType.String:
|
||||
case BodyType.FormUrlEncoded:
|
||||
return (bodyData.Encoding ?? _utf8NoBom).GetBytes(bodyData.BodyAsString!);
|
||||
|
||||
case BodyType.Json:
|
||||
var formatting = bodyData.BodyAsJsonIndented == true ? Formatting.Indented : Formatting.None;
|
||||
var jsonBody = JsonConvert.SerializeObject(bodyData.BodyAsJson, new JsonSerializerSettings { Formatting = formatting, NullValueHandling = NullValueHandling.Ignore });
|
||||
return (bodyData.Encoding ?? _utf8NoBom).GetBytes(jsonBody);
|
||||
|
||||
case BodyType.ProtoBuf:
|
||||
if (TypeLoader.TryLoadStaticInstance<IProtoBufUtils>(out var protoBufUtils))
|
||||
{
|
||||
var protoDefinitions = bodyData.ProtoDefinition?.Invoke().Texts;
|
||||
return await protoBufUtils.GetProtoBufMessageWithHeaderAsync(protoDefinitions, bodyData.ProtoBufMessageType, bodyData.BodyAsJson).ConfigureAwait(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case BodyType.Bytes:
|
||||
return bodyData.BodyAsBytes;
|
||||
|
||||
case BodyType.File:
|
||||
return _options.FileSystemHandler?.ReadResponseBodyAsFile(bodyData.BodyAsFile!);
|
||||
|
||||
case BodyType.MultiPart:
|
||||
_options.Logger.Warn("MultiPart body type is not handled!");
|
||||
break;
|
||||
|
||||
case BodyType.None:
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void SetResponseHeaders(IResponseMessage responseMessage, bool hasBody, HttpResponse response)
|
||||
{
|
||||
// Force setting the Date header (#577)
|
||||
AppendResponseHeader(
|
||||
response,
|
||||
HttpKnownHeaderNames.Date,
|
||||
[DateTime.UtcNow.ToString(CultureInfo.InvariantCulture.DateTimeFormat.RFC1123Pattern, CultureInfo.InvariantCulture)]
|
||||
);
|
||||
|
||||
// Set other headers
|
||||
foreach (var item in responseMessage.Headers!)
|
||||
{
|
||||
var headerName = item.Key;
|
||||
var value = item.Value;
|
||||
if (ResponseHeadersToFix.TryGetValue(headerName, out var action))
|
||||
// Check if this response header can be added (#148, #227 and #720)
|
||||
if (!HttpKnownHeaderNames.IsRestrictedResponseHeader(headerName))
|
||||
{
|
||||
action.Invoke(response, hasBody, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check if this response header can be added (#148, #227 and #720)
|
||||
if (!HttpKnownHeaderNames.IsRestrictedResponseHeader(headerName))
|
||||
{
|
||||
AppendResponseHeader(response, headerName, value.ToArray());
|
||||
}
|
||||
AppendResponseHeader(response, headerName, value.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetResponseTrailingHeaders(IResponseMessage responseMessage, HttpResponse response)
|
||||
{
|
||||
if (responseMessage.TrailingHeaders == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#if TRAILINGHEADERS
|
||||
foreach (var (headerName, value) in responseMessage.TrailingHeaders)
|
||||
{
|
||||
if (ResponseHeadersToFix.TryGetValue(headerName, out var action))
|
||||
{
|
||||
action.Invoke(response, false, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check if this trailing header can be added to the response
|
||||
if (response.SupportsTrailers() && !HttpKnownHeaderNames.IsRestrictedResponseHeader(headerName))
|
||||
{
|
||||
response.AppendTrailer(headerName, new Microsoft.Extensions.Primitives.StringValues(value.ToArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private static void AppendResponseHeader(HttpResponse response, string headerName, string[] values)
|
||||
{
|
||||
response.Headers.Append(headerName, values);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetResponseTrailingHeaders(IResponseMessage responseMessage, HttpResponse response)
|
||||
{
|
||||
if (responseMessage.TrailingHeaders == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#if TRAILINGHEADERS
|
||||
foreach (var (headerName, value) in responseMessage.TrailingHeaders)
|
||||
{
|
||||
if (ResponseHeadersToFix.TryGetValue(headerName, out var action))
|
||||
{
|
||||
action.Invoke(response, false, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check if this trailing header can be added to the response
|
||||
if (response.SupportsTrailers() && !HttpKnownHeaderNames.IsRestrictedResponseHeader(headerName))
|
||||
{
|
||||
response.AppendTrailer(headerName, new Microsoft.Extensions.Primitives.StringValues(value.ToArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private static void AppendResponseHeader(HttpResponse response, string headerName, string[] values)
|
||||
{
|
||||
response.Headers.Append(headerName, values);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ internal class MappingMatcher(IWireMockMiddlewareOptions options, IRandomizerDou
|
||||
var possibleMappings = new List<MappingMatcherResult>();
|
||||
|
||||
var mappings = _options.Mappings.Values
|
||||
.Where(m => !m.IsDisabled)
|
||||
.Where(m => m.TimeSettings.IsValid())
|
||||
.Where(m => m.Probability is null || _randomizerDoubleBetween0And1.Generate() <= m.Probability)
|
||||
.ToArray();
|
||||
|
||||
@@ -122,6 +122,14 @@ internal class WireMockMiddleware(
|
||||
}
|
||||
}
|
||||
|
||||
// Transition scenario state immediately after matching, before any delay (global or
|
||||
// per-mapping) so that concurrent retries arriving during a delay period see the
|
||||
// updated state and match the correct next mapping instead of re-matching this one.
|
||||
if (targetMapping.Scenario != null)
|
||||
{
|
||||
UpdateScenarioState(targetMapping);
|
||||
}
|
||||
|
||||
if (!targetMapping.IsAdminInterface && options.RequestProcessingDelay > TimeSpan.Zero)
|
||||
{
|
||||
await Task.Delay(options.RequestProcessingDelay.Value).ConfigureAwait(false);
|
||||
@@ -147,11 +155,6 @@ internal class WireMockMiddleware(
|
||||
}
|
||||
}
|
||||
|
||||
if (targetMapping.Scenario != null)
|
||||
{
|
||||
UpdateScenarioState(targetMapping);
|
||||
}
|
||||
|
||||
if (!targetMapping.IsAdminInterface && targetMapping.Webhooks?.Length > 0)
|
||||
{
|
||||
await SendToWebhooksAsync(targetMapping, request, response).ConfigureAwait(false);
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using JsonConverter.Abstractions;
|
||||
using JsonConverter.Newtonsoft.Json;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using WireMock.Handlers;
|
||||
@@ -108,5 +110,9 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions
|
||||
/// <inheritdoc />
|
||||
public ConcurrentDictionary<Guid, WebSocketConnectionRegistry> WebSocketRegistries { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public WebSocketSettings? WebSocketSettings { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IJsonConverter DefaultJsonSerializer { get; set; } = new NewtonsoftJsonConverter();
|
||||
}
|
||||
@@ -275,6 +275,7 @@ internal class MappingConverter(MatcherMapper mapper)
|
||||
TimesInSameState = !string.IsNullOrWhiteSpace(mapping.NextState) ? mapping.TimesInSameState : null,
|
||||
Data = mapping.Data,
|
||||
Probability = mapping.Probability,
|
||||
IsDisabled = mapping.IsDisabled ? true : null,
|
||||
Request = new RequestModel
|
||||
{
|
||||
Headers = headerMatchers.Any() ? headerMatchers.Select(hm => new HeaderModel
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using JsonConverter.Abstractions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
#if NETSTANDARD2_0_OR_GREATER || NETCOREAPP3_1_OR_GREATER || NET6_0_OR_GREATER || NET461
|
||||
using System.Text.Json;
|
||||
@@ -10,9 +11,15 @@ namespace WireMock.Serialization;
|
||||
|
||||
internal class MappingSerializer(IJsonConverter jsonConverter)
|
||||
{
|
||||
private static readonly JsonConverterOptions JsonConverterOptions = new JsonConverterOptions
|
||||
{
|
||||
DateParseHandling = (int) DateParseHandling.None
|
||||
};
|
||||
|
||||
internal T[] DeserializeJsonToArray<T>(string value)
|
||||
{
|
||||
return DeserializeObjectToArray<T>(jsonConverter.Deserialize<object>(value)!);
|
||||
// DeserializeObject
|
||||
return DeserializeObjectToArray<T>(jsonConverter.Deserialize<object>(value, JsonConverterOptions)!);
|
||||
}
|
||||
|
||||
internal static T[] DeserializeObjectToArray<T>(object value)
|
||||
|
||||
@@ -234,6 +234,13 @@ public interface IRespondWithAProvider
|
||||
/// <returns>The <see cref="IRespondWithAProvider"/>.</returns>
|
||||
IRespondWithAProvider WithProbability(double probability);
|
||||
|
||||
/// <summary>
|
||||
/// Define whether this mapping is disabled. Defaults to <c>false</c>.
|
||||
/// </summary>
|
||||
/// <param name="isDisabled">Whether this mapping is disabled.</param>
|
||||
/// <returns>The <see cref="IRespondWithAProvider"/>.</returns>
|
||||
IRespondWithAProvider WithIsDisabled(bool isDisabled);
|
||||
|
||||
/// <summary>
|
||||
/// Define a Grpc ProtoDefinition which is used for the request and the response.
|
||||
/// This can be a ProtoDefinition as a string, or an id when the ProtoDefinitions are defined at the WireMockServer.
|
||||
|
||||
@@ -37,6 +37,7 @@ internal class RespondWithAProvider : IRespondWithAProvider
|
||||
private int _timesInSameState = 1;
|
||||
private bool? _useWebhookFireAndForget;
|
||||
private double? _probability;
|
||||
private bool _isDisabled = false;
|
||||
private GraphQLSchemaDetails? _graphQLSchemaDetails; // Future Use.
|
||||
|
||||
public Guid Guid { get; private set; }
|
||||
@@ -108,6 +109,11 @@ internal class RespondWithAProvider : IRespondWithAProvider
|
||||
mapping.WithProbability(_probability.Value);
|
||||
}
|
||||
|
||||
if (_isDisabled)
|
||||
{
|
||||
mapping.IsDisabled = true;
|
||||
}
|
||||
|
||||
if (ProtoDefinition != null)
|
||||
{
|
||||
mapping.WithProtoDefinition(ProtoDefinition.Value);
|
||||
@@ -354,6 +360,13 @@ internal class RespondWithAProvider : IRespondWithAProvider
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRespondWithAProvider WithIsDisabled(bool isDisabled)
|
||||
{
|
||||
_isDisabled = isDisabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRespondWithAProvider WithProtoDefinition(params string[] protoDefinitionOrId)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
@@ -26,9 +25,6 @@ using WireMock.Util;
|
||||
|
||||
namespace WireMock.Server;
|
||||
|
||||
/// <summary>
|
||||
/// The fluent mock server.
|
||||
/// </summary>
|
||||
public partial class WireMockServer
|
||||
{
|
||||
private const int EnhancedFileSystemWatcherTimeoutMs = 1000;
|
||||
@@ -61,6 +57,8 @@ public partial class WireMockServer
|
||||
public string OpenApi => $"{_prefix}/openapi";
|
||||
|
||||
public RegexMatcher MappingsGuidPathMatcher => new($"^{_prefixEscaped}\\/mappings\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
|
||||
public RegexMatcher MappingsGuidEnablePathMatcher => new($"^{_prefixEscaped}\\/mappings\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})\\/enable$");
|
||||
public RegexMatcher MappingsGuidDisablePathMatcher => new($"^{_prefixEscaped}\\/mappings\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})\\/disable$");
|
||||
public RegexMatcher MappingsCodeGuidPathMatcher => new($"^{_prefixEscaped}\\/mappings\\/code\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
|
||||
public RegexMatcher RequestsGuidPathMatcher => new($"^{_prefixEscaped}\\/requests\\/([0-9A-Fa-f]{{8}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{4}}[-][0-9A-Fa-f]{{12}})$");
|
||||
public RegexMatcher ScenariosNameMatcher => new($"^{_prefixEscaped}\\/scenarios\\/.+$");
|
||||
@@ -104,6 +102,12 @@ public partial class WireMockServer
|
||||
Given(Request.Create().WithPath(_adminPaths.MappingsGuidPathMatcher).UsingPut().WithHeader(HttpKnownHeaderNames.ContentType, AdminRequestContentTypeJson)).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingPut));
|
||||
Given(Request.Create().WithPath(_adminPaths.MappingsGuidPathMatcher).UsingDelete()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingDelete));
|
||||
|
||||
// __admin/mappings/{guid}/enable
|
||||
Given(Request.Create().WithPath(_adminPaths.MappingsGuidEnablePathMatcher).UsingPut()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingEnable));
|
||||
|
||||
// __admin/mappings/{guid}/disable
|
||||
Given(Request.Create().WithPath(_adminPaths.MappingsGuidDisablePathMatcher).UsingPut()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingDisable));
|
||||
|
||||
// __admin/mappings/code/{guid}
|
||||
Given(Request.Create().WithPath(_adminPaths.MappingsCodeGuidPathMatcher).UsingGet()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(MappingCodeGet));
|
||||
|
||||
@@ -430,6 +434,47 @@ public partial class WireMockServer
|
||||
var lastPart = requestMessage.Path.Split('/').LastOrDefault();
|
||||
return Guid.TryParse(lastPart, out guid);
|
||||
}
|
||||
|
||||
private static bool TryParseGuidFromSecondToLastSegment(IRequestMessage requestMessage, out Guid guid)
|
||||
{
|
||||
var parts = requestMessage.Path.Split('/');
|
||||
if (parts.Length >= 2 && Guid.TryParse(parts[parts.Length - 2], out guid))
|
||||
return true;
|
||||
guid = Guid.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private IResponseMessage MappingEnable(HttpContext _, IRequestMessage requestMessage)
|
||||
{
|
||||
if (TryParseGuidFromSecondToLastSegment(requestMessage, out var guid))
|
||||
{
|
||||
var mapping = Mappings.FirstOrDefault(m => !m.IsAdminInterface && m.Guid == guid);
|
||||
if (mapping != null)
|
||||
{
|
||||
mapping.IsDisabled = false;
|
||||
return ResponseMessageBuilder.Create(HttpStatusCode.OK, "Mapping enabled", guid);
|
||||
}
|
||||
}
|
||||
|
||||
_settings.Logger.Warn("HttpStatusCode set to 404 : Mapping not found");
|
||||
return ResponseMessageBuilder.Create(HttpStatusCode.NotFound, "Mapping not found");
|
||||
}
|
||||
|
||||
private IResponseMessage MappingDisable(HttpContext _, IRequestMessage requestMessage)
|
||||
{
|
||||
if (TryParseGuidFromSecondToLastSegment(requestMessage, out var guid))
|
||||
{
|
||||
var mapping = Mappings.FirstOrDefault(m => !m.IsAdminInterface && m.Guid == guid);
|
||||
if (mapping != null)
|
||||
{
|
||||
mapping.IsDisabled = true;
|
||||
return ResponseMessageBuilder.Create(HttpStatusCode.OK, "Mapping disabled", guid);
|
||||
}
|
||||
}
|
||||
|
||||
_settings.Logger.Warn("HttpStatusCode set to 404 : Mapping not found");
|
||||
return ResponseMessageBuilder.Create(HttpStatusCode.NotFound, "Mapping not found");
|
||||
}
|
||||
#endregion Mapping/{guid}
|
||||
|
||||
#region Mappings
|
||||
|
||||
@@ -120,6 +120,11 @@ public partial class WireMockServer
|
||||
respondProvider.WithProbability(mappingModel.Probability.Value);
|
||||
}
|
||||
|
||||
if (mappingModel.IsDisabled == true)
|
||||
{
|
||||
respondProvider.WithIsDisabled(true);
|
||||
}
|
||||
|
||||
// ProtoDefinition is defined at Mapping level
|
||||
if (mappingModel.ProtoDefinition != null)
|
||||
{
|
||||
|
||||
@@ -414,6 +414,7 @@ public partial class WireMockServer : IWireMockServer
|
||||
_options.CorsPolicyOptions = _settings.CorsPolicyOptions;
|
||||
_options.ClientCertificateMode = (Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode)_settings.ClientCertificateMode;
|
||||
_options.AcceptAnyClientCertificate = _settings.AcceptAnyClientCertificate;
|
||||
_options.DefaultJsonSerializer = _settings.DefaultJsonSerializer;
|
||||
|
||||
_httpServer = new AspNetCoreSelfHost(_options, urlOptions);
|
||||
var startTask = _httpServer.StartAsync();
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.8.0" />
|
||||
<PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.9.0" />
|
||||
<PackageReference Include="NUnit" Version="4.4.0" />
|
||||
<PackageReference Include="Stef.Validation" Version="0.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.14.0" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -163,6 +163,22 @@ public interface IWireMockAdminApi
|
||||
[Header("Content-Type", "application/json")]
|
||||
Task<StatusModel> PutMappingAsync([Path] Guid guid, [Body] MappingModel mapping, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Enable a mapping based on the guid.
|
||||
/// </summary>
|
||||
/// <param name="guid">The Guid.</param>
|
||||
/// <param name="cancellationToken">The optional cancellationToken.</param>
|
||||
[Put("mappings/{guid}/enable")]
|
||||
Task<StatusModel> EnableMappingAsync([Path] Guid guid, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Disable a mapping based on the guid.
|
||||
/// </summary>
|
||||
/// <param name="guid">The Guid.</param>
|
||||
/// <param name="cancellationToken">The optional cancellationToken.</param>
|
||||
[Put("mappings/{guid}/disable")]
|
||||
Task<StatusModel> DisableMappingAsync([Path] Guid guid, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Delete a mapping based on the guid
|
||||
/// </summary>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.8.0" />
|
||||
<PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.9.0" />
|
||||
<PackageReference Include="RestEase" Version="1.6.4" />
|
||||
<PackageReference Include="Stef.Validation" Version="0.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -108,6 +108,14 @@ public interface IMapping
|
||||
/// </value>
|
||||
bool IsProxy { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this mapping is disabled.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if this mapping is disabled; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
bool IsDisabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this mapping to be logged.
|
||||
/// </summary>
|
||||
@@ -135,7 +143,7 @@ public interface IMapping
|
||||
/// </summary>
|
||||
object? Data { get; }
|
||||
|
||||
/// <summary>
|
||||
/// <summary>
|
||||
/// The probability when this request should be matched. Value is between 0 and 1. [Optional]
|
||||
/// </summary>
|
||||
double? Probability { get; }
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<PackageReference Include="Stef.Validation" Version="0.2.0" />
|
||||
<PackageReference Include="AnyOf" Version="0.5.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.8.0" />
|
||||
<PackageReference Include="JsonConverter.Newtonsoft.Json" Version="0.9.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright © WireMock.Net
|
||||
|
||||
using RestEase;
|
||||
using WireMock.Admin.Mappings;
|
||||
using WireMock.Client;
|
||||
using WireMock.Server;
|
||||
|
||||
namespace WireMock.Net.Tests.AdminApi;
|
||||
|
||||
public partial class WireMockAdminApiTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task IWireMockAdminApi_PostMappingAsync_WithIsDisabledTrue_DoesNotMatchRequests()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.StartWithAdminInterface();
|
||||
var api = RestClient.For<IWireMockAdminApi>(server.Urls[0]);
|
||||
var httpClient = server.CreateClient();
|
||||
|
||||
var model = new MappingModel
|
||||
{
|
||||
Request = new RequestModel { Path = "/foo", Methods = ["GET"] },
|
||||
Response = new ResponseModel { Body = "hello", StatusCode = 200 },
|
||||
IsDisabled = true
|
||||
};
|
||||
|
||||
// Act — POST the disabled mapping
|
||||
var postResult = await api.PostMappingAsync(model, ct);
|
||||
postResult.Should().NotBeNull();
|
||||
|
||||
// Assert — request should not be matched (404)
|
||||
var response = await httpClient.GetAsync("/foo", ct);
|
||||
((int)response.StatusCode).Should().Be(404);
|
||||
|
||||
// Assert — mapping exists but IsDisabled is true
|
||||
server.Mappings.Where(m => !m.IsAdminInterface).Should().ContainSingle(m => m.IsDisabled == true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IWireMockAdminApi_DisableMappingAsync_PreventsMatching()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.StartWithAdminInterface();
|
||||
var api = RestClient.For<IWireMockAdminApi>(server.Urls[0]);
|
||||
var httpClient = server.CreateClient();
|
||||
|
||||
var model = new MappingModel
|
||||
{
|
||||
Request = new RequestModel { Path = "/bar", Methods = ["GET"] },
|
||||
Response = new ResponseModel { Body = "world", StatusCode = 200 }
|
||||
};
|
||||
var postResult = await api.PostMappingAsync(model, ct);
|
||||
var guid = postResult.Guid!.Value;
|
||||
|
||||
// Assert — mapping matches before disable
|
||||
var before = await httpClient.GetAsync("/bar", ct);
|
||||
((int)before.StatusCode).Should().Be(200);
|
||||
|
||||
// Act — disable
|
||||
var disableResult = await api.DisableMappingAsync(guid, ct);
|
||||
disableResult.Status.Should().Be("Mapping disabled");
|
||||
|
||||
// Assert — no match after disable
|
||||
var after = await httpClient.GetAsync("/bar", ct);
|
||||
((int)after.StatusCode).Should().Be(404);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IWireMockAdminApi_EnableMappingAsync_ResumesMatching()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.StartWithAdminInterface();
|
||||
var api = RestClient.For<IWireMockAdminApi>(server.Urls[0]);
|
||||
var httpClient = server.CreateClient();
|
||||
|
||||
var model = new MappingModel
|
||||
{
|
||||
Request = new RequestModel { Path = "/baz", Methods = ["GET"] },
|
||||
Response = new ResponseModel { Body = "re-enabled", StatusCode = 200 },
|
||||
IsDisabled = true
|
||||
};
|
||||
var postResult = await api.PostMappingAsync(model, ct);
|
||||
var guid = postResult.Guid!.Value;
|
||||
|
||||
// Assert — no match while disabled
|
||||
var before = await httpClient.GetAsync("/baz", ct);
|
||||
((int)before.StatusCode).Should().Be(404);
|
||||
|
||||
// Act — enable
|
||||
var enableResult = await api.EnableMappingAsync(guid, ct);
|
||||
enableResult.Status.Should().Be("Mapping enabled");
|
||||
|
||||
// Assert — mapping matches after enable
|
||||
var after = await httpClient.GetAsync("/baz", ct);
|
||||
((int)after.StatusCode).Should().Be(200);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IWireMockAdminApi_GetMappingAsync_ReturnsIsDisabledTrue_WhenDisabled()
|
||||
{
|
||||
// Arrange
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.StartWithAdminInterface();
|
||||
var api = RestClient.For<IWireMockAdminApi>(server.Urls[0]);
|
||||
|
||||
var disabledModel = new MappingModel
|
||||
{
|
||||
Request = new RequestModel { Path = "/check-disabled" },
|
||||
Response = new ResponseModel { Body = "x", StatusCode = 200 },
|
||||
IsDisabled = true
|
||||
};
|
||||
var enabledModel = new MappingModel
|
||||
{
|
||||
Request = new RequestModel { Path = "/check-enabled" },
|
||||
Response = new ResponseModel { Body = "y", StatusCode = 200 }
|
||||
};
|
||||
|
||||
var disabledPost = await api.PostMappingAsync(disabledModel, ct);
|
||||
var enabledPost = await api.PostMappingAsync(enabledModel, ct);
|
||||
|
||||
// Act
|
||||
var disabledGot = await api.GetMappingAsync(disabledPost.Guid!.Value, ct);
|
||||
var enabledGot = await api.GetMappingAsync(enabledPost.Guid!.Value, ct);
|
||||
|
||||
// Assert — disabled mapping serializes IsDisabled = true
|
||||
disabledGot.IsDisabled.Should().BeTrue();
|
||||
|
||||
// Assert — enabled mapping omits IsDisabled (null = default not disabled)
|
||||
enabledGot.IsDisabled.Should().BeNull();
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,5 @@ internal static class Constants
|
||||
{
|
||||
internal const int NumStaticMappings = 10;
|
||||
|
||||
internal const int NumAdminMappings = 37;
|
||||
internal const int NumAdminMappings = 39;
|
||||
}
|
||||
@@ -9,6 +9,8 @@ using WireMock.Util;
|
||||
using WireMock.Owin;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using JsonConverter.Newtonsoft.Json;
|
||||
using JsonConverter.System.Text.Json;
|
||||
|
||||
namespace WireMock.Net.Tests.Owin.Mappers;
|
||||
|
||||
@@ -34,6 +36,7 @@ public class OwinResponseMapperTests
|
||||
_optionsMock = new Mock<IWireMockMiddlewareOptions>();
|
||||
_optionsMock.SetupAllProperties();
|
||||
_optionsMock.SetupGet(o => o.FileSystemHandler).Returns(_fileSystemHandlerMock.Object);
|
||||
_optionsMock.SetupGet(o => o.DefaultJsonSerializer).Returns(new NewtonsoftJsonConverter());
|
||||
|
||||
_headers = new Mock<IHeaderDictionary>();
|
||||
_headers.SetupAllProperties();
|
||||
@@ -186,7 +189,7 @@ public class OwinResponseMapperTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OwinResponseMapper_MapAsync_BodyAsJson()
|
||||
public async Task OwinResponseMapper_MapAsync_BodyAsJson_UsingNewtonsoftJson()
|
||||
{
|
||||
// Arrange
|
||||
var json = new { t = "x", i = (string?)null };
|
||||
@@ -203,6 +206,25 @@ public class OwinResponseMapperTests
|
||||
_stream.Verify(s => s.WriteAsync(new byte[] { 123, 34, 116, 34, 58, 34, 120, 34, 125 }, 0, 9, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OwinResponseMapper_MapAsync_BodyAsJson_UsingSystemTextJson()
|
||||
{
|
||||
// Arrange
|
||||
var json = new { t = "x", i = (string?)null };
|
||||
var responseMessage = new ResponseMessage
|
||||
{
|
||||
Headers = new Dictionary<string, WireMockList<string>>(),
|
||||
BodyData = new BodyData { DetectedBodyType = BodyType.Json, BodyAsJson = json, BodyAsJsonIndented = false }
|
||||
};
|
||||
_optionsMock.SetupGet(o => o.DefaultJsonSerializer).Returns(new SystemTextJsonConverter());
|
||||
|
||||
// Act
|
||||
await _sut.MapAsync(responseMessage, _responseMock.Object);
|
||||
|
||||
// Assert
|
||||
_stream.Verify(s => s.WriteAsync(new byte[] { 123, 34, 116, 34, 58, 34, 120, 34, 125 }, 0, 9, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OwinResponseMapper_MapAsync_SetResponseHeaders()
|
||||
{
|
||||
|
||||
@@ -56,6 +56,7 @@ public class MappingMatcherTests
|
||||
{
|
||||
// Assign
|
||||
var mappingMock = new Mock<IMapping>();
|
||||
mappingMock.SetupGet(m => m.IsDisabled).Returns(false);
|
||||
mappingMock.Setup(m => m.GetRequestMatchResult(It.IsAny<RequestMessage>(), It.IsAny<string>())).Throws<Exception>();
|
||||
|
||||
var mappings = new ConcurrentDictionary<Guid, IMapping>();
|
||||
@@ -229,6 +230,35 @@ public class MappingMatcherTests
|
||||
result.Match!.Mapping.Guid.Should().Be(withProbability);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MappingMatcher_FindBestMatch_WhenMappingIsDisabled_ShouldReturnNull()
|
||||
{
|
||||
// Assign
|
||||
var guid = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||
var mappingMock = new Mock<IMapping>();
|
||||
mappingMock.SetupGet(m => m.Guid).Returns(guid);
|
||||
mappingMock.SetupGet(m => m.IsDisabled).Returns(true);
|
||||
mappingMock.SetupGet(m => m.Probability).Returns((double?)null);
|
||||
|
||||
var matchResult = new RequestMatchResult();
|
||||
matchResult.AddScore(typeof(object), 1.0, null);
|
||||
mappingMock.Setup(m => m.GetRequestMatchResult(It.IsAny<RequestMessage>(), It.IsAny<string>())).Returns(matchResult);
|
||||
|
||||
var mappings = new ConcurrentDictionary<Guid, IMapping>();
|
||||
mappings.TryAdd(guid, mappingMock.Object);
|
||||
_optionsMock.Setup(o => o.Mappings).Returns(mappings);
|
||||
|
||||
var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1");
|
||||
|
||||
// Act
|
||||
var result = _sut.FindBestMatch(request);
|
||||
|
||||
// Assert
|
||||
result.Match.Should().BeNull();
|
||||
result.Partial.Should().BeNull();
|
||||
mappingMock.Verify(m => m.GetRequestMatchResult(It.IsAny<RequestMessage>(), It.IsAny<string>()), Times.Never);
|
||||
}
|
||||
|
||||
private static ConcurrentDictionary<Guid, IMapping> InitMappings(params (Guid guid, double[] scores, double? probability)[] matches)
|
||||
{
|
||||
var mappings = new ConcurrentDictionary<Guid, IMapping>();
|
||||
@@ -237,6 +267,7 @@ public class MappingMatcherTests
|
||||
{
|
||||
var mappingMock = new Mock<IMapping>();
|
||||
mappingMock.SetupGet(m => m.Guid).Returns(match.guid);
|
||||
mappingMock.SetupGet(m => m.IsDisabled).Returns(false);
|
||||
|
||||
var requestMatchResult = new RequestMatchResult();
|
||||
foreach (var score in match.scores)
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
using JsonConverter.Newtonsoft.Json;
|
||||
using WireMock.Admin.Mappings;
|
||||
using WireMock.Serialization;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
using JsonConverter.System.Text.Json;
|
||||
#endif
|
||||
@@ -319,5 +321,92 @@ public class MappingSerializerTests
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("Cannot deserialize the provided value to an array or object.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MappingSerializer_DeserializeJsonToArray_WithNewtonsoftJson_DateTimeStringInQueryParamExactMatcherPattern_ShouldPreservePatternAsString()
|
||||
{
|
||||
// Arrange
|
||||
var jsonConverter = new NewtonsoftJsonConverter();
|
||||
var serializer = new MappingSerializer(jsonConverter);
|
||||
var mappingJson =
|
||||
"""
|
||||
{
|
||||
"Guid": "12345678-1234-1234-1234-aaaaaaaaaaaa",
|
||||
"Request": {
|
||||
"Path": "/api/report",
|
||||
"Methods": ["GET"],
|
||||
"Params": [
|
||||
{
|
||||
"Name": "asOfDate",
|
||||
"Matchers": [
|
||||
{
|
||||
"Name": "ExactMatcher",
|
||||
"Pattern": "2021-11-10T13:39:13.705"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"Response": {
|
||||
"StatusCode": 200
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = serializer.DeserializeJsonToArray<MappingModel>(mappingJson);
|
||||
|
||||
// Assert
|
||||
result.Should().HaveCount(1);
|
||||
var matcher = result[0].Request!.Params![0].Matchers![0];
|
||||
matcher.Name.Should().Be("ExactMatcher");
|
||||
matcher.Pattern.Should().BeOfType<string>()
|
||||
.Which.Should().Be("2021-11-10T13:39:13.705",
|
||||
"datetime-format strings in ExactMatcher Pattern fields must survive deserialization as strings, " +
|
||||
"not be auto-converted to DateTime by Newtonsoft.Json's DateParseHandling.DateTime");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MappingSerializer_DeserializeJsonToArray_WithNewtonsoftJson_DateTimeStringInJsonMatcherBodyPattern_ShouldPreservePatternAsString()
|
||||
{
|
||||
// Arrange
|
||||
var jsonConverter = new NewtonsoftJsonConverter();
|
||||
var serializer = new MappingSerializer(jsonConverter);
|
||||
// Pattern is an INLINE JSON object (not a string) - this is how WireMock mapping files store
|
||||
// JsonMatcher patterns when recorded. Newtonsoft with DateParseHandling.DateTime will convert
|
||||
// the datetime value inside the JObject to JTokenType.Date during deserialization.
|
||||
var mappingJson =
|
||||
"""
|
||||
{
|
||||
"Guid": "12345678-1234-1234-1234-bbbbbbbbbbbb",
|
||||
"Request": {
|
||||
"Path": "/api/report",
|
||||
"Methods": ["POST"],
|
||||
"Body": {
|
||||
"Matcher": {
|
||||
"Name": "JsonMatcher",
|
||||
"Pattern": {"Date": "2021-09-30T00:00:00Z", "Names": ["Cash"]}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Response": {
|
||||
"StatusCode": 200
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
// Act
|
||||
var result = serializer.DeserializeJsonToArray<MappingModel>(mappingJson);
|
||||
|
||||
// Assert - datetime values inside the JObject pattern must remain JTokenType.String.
|
||||
result.Should().HaveCount(1);
|
||||
var matcher = result[0].Request!.Body!.Matcher!;
|
||||
matcher.Name.Should().Be("JsonMatcher");
|
||||
var patternJObject = matcher.Pattern.Should().BeOfType<JObject>().Subject;
|
||||
patternJObject["Date"]!.Type.Should().Be(JTokenType.String,
|
||||
"datetime-format strings inside an inline JsonMatcher body pattern must retain JTokenType.String " +
|
||||
"after deserialization; if DateParseHandling.DateTime auto-converts them to JTokenType.Date, " +
|
||||
"JToken.DeepEquals will fail against incoming request bodies parsed with DateParseHandling.None");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -373,7 +373,7 @@ public class StatefulBehaviorTests
|
||||
// Act and Assert
|
||||
server.SetScenarioState(scenario, "Buy milk");
|
||||
server.Scenarios.First(s => s.Name == scenario).Should().BeEquivalentTo(new { Name = scenario, NextState = "Buy milk" });
|
||||
|
||||
|
||||
var getResponse1 = await client.GetStringAsync("/todo/items", cancelationToken);
|
||||
getResponse1.Should().Be("Buy milk");
|
||||
|
||||
@@ -413,6 +413,120 @@ public class StatefulBehaviorTests
|
||||
action.Should().ThrowAsync<HttpRequestException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Scenarios_FirstRequestWithDelay_StateTransitions_BeforeDelayCompletes()
|
||||
{
|
||||
// Arrange
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var path = $"/foo_{Guid.NewGuid()}";
|
||||
using var server = WireMockServer.Start();
|
||||
|
||||
// Mapping 1: start state, has a 500 ms delay
|
||||
server
|
||||
.Given(Request.Create().WithPath(path).UsingGet())
|
||||
.InScenario("1260")
|
||||
.WillSetStateTo("State1")
|
||||
.RespondWith(Response.Create()
|
||||
.WithBody("delayed response")
|
||||
.WithDelay(TimeSpan.FromMilliseconds(500)));
|
||||
|
||||
// Mapping 2: only matches after state has transitioned to "State1"
|
||||
server
|
||||
.Given(Request.Create().WithPath(path).UsingGet())
|
||||
.InScenario("1260")
|
||||
.WhenStateIs("State1")
|
||||
.RespondWith(Response.Create().WithBody("immediate response"));
|
||||
|
||||
var client = new HttpClient();
|
||||
|
||||
// Act: fire request 1 but don't await it yet — it will sit in a 500 ms delay
|
||||
var request1Task = client.GetStringAsync(server.Url + path, cancellationToken);
|
||||
|
||||
// Give the server a moment to match & transition state before the delay completes
|
||||
await Task.Delay(100, cancellationToken);
|
||||
|
||||
// Request 2 is sent while request 1 is still being delayed.
|
||||
// After the fix the state has already transitioned, so request 2 matches Mapping 2.
|
||||
var response2 = await client.GetStringAsync(server.Url + path, cancellationToken);
|
||||
|
||||
var response1 = await request1Task;
|
||||
|
||||
// Assert
|
||||
response1.Should().Be("delayed response");
|
||||
response2.Should().Be("immediate response");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Scenarios_WithGlobalRequestProcessingDelay_StateTransitions_BeforeDelayCompletes()
|
||||
{
|
||||
// Arrange: use the global RequestProcessingDelay instead of a per-mapping delay
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var path = $"/foo_{Guid.NewGuid()}";
|
||||
using var server = WireMockServer.Start();
|
||||
server.AddGlobalProcessingDelay(TimeSpan.FromMilliseconds(500));
|
||||
|
||||
server
|
||||
.Given(Request.Create().WithPath(path).UsingGet())
|
||||
.InScenario("s")
|
||||
.WillSetStateTo("State1")
|
||||
.RespondWith(Response.Create().WithBody("delayed response"));
|
||||
|
||||
server
|
||||
.Given(Request.Create().WithPath(path).UsingGet())
|
||||
.InScenario("s")
|
||||
.WhenStateIs("State1")
|
||||
.RespondWith(Response.Create().WithBody("immediate response"));
|
||||
|
||||
var client = new HttpClient();
|
||||
|
||||
// Act
|
||||
var request1Task = client.GetStringAsync(server.Url + path, cancellationToken);
|
||||
await Task.Delay(100, cancellationToken);
|
||||
var response2 = await client.GetStringAsync(server.Url + path, cancellationToken);
|
||||
var response1 = await request1Task;
|
||||
|
||||
// Assert
|
||||
response1.Should().Be("delayed response");
|
||||
response2.Should().Be("immediate response");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Scenarios_WithDelay_And_TimesInSameState_Should_Transition_After_Required_Hits()
|
||||
{
|
||||
// Arrange
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var path = $"/foo_{Guid.NewGuid()}";
|
||||
using var server = WireMockServer.Start();
|
||||
|
||||
// Mapping 1: requires 2 hits before transitioning; has a short delay
|
||||
server
|
||||
.Given(Request.Create().WithPath(path).UsingGet())
|
||||
.InScenario("s")
|
||||
.WillSetStateTo("State1", 2)
|
||||
.RespondWith(Response.Create()
|
||||
.WithBody("first")
|
||||
.WithDelay(TimeSpan.FromMilliseconds(50)));
|
||||
|
||||
// Mapping 2: matches after state is "State1"
|
||||
server
|
||||
.Given(Request.Create().WithPath(path).UsingGet())
|
||||
.InScenario("s")
|
||||
.WhenStateIs("State1")
|
||||
.RespondWith(Response.Create().WithBody("second"));
|
||||
|
||||
var client = new HttpClient();
|
||||
|
||||
// Act
|
||||
var response1 = await client.GetStringAsync(server.Url + path, cancellationToken);
|
||||
var response2 = await client.GetStringAsync(server.Url + path, cancellationToken);
|
||||
var response3 = await client.GetStringAsync(server.Url + path, cancellationToken);
|
||||
|
||||
// Assert: state only transitions after 2 hits, so request 3 is the first to match Mapping 2
|
||||
response1.Should().Be("first");
|
||||
response2.Should().Be("first");
|
||||
response3.Should().Be("second");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Scenarios_Should_process_request_if_equals_state_and_multiple_state_defined()
|
||||
{
|
||||
|
||||
@@ -4,10 +4,13 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using JsonConverter.System.Text.Json;
|
||||
using WireMock.Matchers;
|
||||
using WireMock.RequestBuilders;
|
||||
using WireMock.ResponseBuilders;
|
||||
using WireMock.Server;
|
||||
using WireMock.Settings;
|
||||
|
||||
namespace WireMock.Net.Tests;
|
||||
|
||||
@@ -18,11 +21,41 @@ public partial class WireMockServerTests
|
||||
public string? Hi { get; set; }
|
||||
}
|
||||
|
||||
public class Person
|
||||
{
|
||||
[JsonPropertyName("first_name")]
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("last_name")]
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WireMockServer_WithBodyAsJson_UsingWireMockServerSettings_SystemTextJsonConverter_ShouldReturnCorrectResponse()
|
||||
{
|
||||
// Arange
|
||||
var person = new Person { FirstName = "John", LastName = "Smith" };
|
||||
|
||||
using var server = WireMockServer.Start(new WireMockServerSettings
|
||||
{
|
||||
DefaultJsonSerializer = new SystemTextJsonConverter()
|
||||
});
|
||||
|
||||
// Act
|
||||
server
|
||||
.Given(Request.Create().UsingAnyMethod())
|
||||
.RespondWith(Response.Create().WithBodyAsJson(person));
|
||||
|
||||
var response = await server.CreateClient().GetStringAsync("/", _ct);
|
||||
|
||||
// Assert
|
||||
response.Should().BeEquivalentTo("{\"first_name\":\"John\",\"last_name\":\"Smith\"}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WireMockServer_WithBodyAsJson_Using_PostAsJsonAsync_And_MultipleJmesPathMatchers_ShouldMatch()
|
||||
{
|
||||
// Arrange
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.Start();
|
||||
server.Given(
|
||||
Request.Create()
|
||||
@@ -56,7 +89,7 @@ public partial class WireMockServerTests
|
||||
var requestUri = new Uri($"http://localhost:{server.Port}/a");
|
||||
|
||||
var json = new { requestId = "1", value = "A" };
|
||||
var response = await server.CreateClient().PostAsJsonAsync(requestUri, json, cancellationToken);
|
||||
var response = await server.CreateClient().PostAsJsonAsync(requestUri, json, _ct);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
@@ -68,7 +101,6 @@ public partial class WireMockServerTests
|
||||
public async Task WireMockServer_WithBodyAsJson_Using_PostAsJsonAsync_And_MultipleJmesPathMatchers_ShouldMatch_BestMatching()
|
||||
{
|
||||
// Arrange
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.Start();
|
||||
server.Given(
|
||||
Request.Create()
|
||||
@@ -105,7 +137,7 @@ public partial class WireMockServerTests
|
||||
var requestUri = new Uri($"http://localhost:{server.Port}/a");
|
||||
|
||||
var json = new { extra = "X", requestId = "1", value = "A" };
|
||||
var response = await server.CreateClient().PostAsJsonAsync(requestUri, json, cancellationToken);
|
||||
var response = await server.CreateClient().PostAsJsonAsync(requestUri, json, _ct);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
@@ -117,7 +149,6 @@ public partial class WireMockServerTests
|
||||
public async Task WireMockServer_WithBodyAsJson_Using_PostAsJsonAsync_And_WildcardMatcher_ShouldMatch()
|
||||
{
|
||||
// Arrange
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.Start();
|
||||
server.Given(
|
||||
Request.Create().UsingPost().WithPath("/foo").WithBody(new WildcardMatcher("*Hello*"))
|
||||
@@ -132,7 +163,7 @@ public partial class WireMockServerTests
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await new HttpClient().PostAsJsonAsync("http://localhost:" + server.Ports[0] + "/foo", jsonObject, cancellationToken);
|
||||
var response = await new HttpClient().PostAsJsonAsync("http://localhost:" + server.Ports[0] + "/foo", jsonObject, _ct);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
@@ -144,7 +175,6 @@ public partial class WireMockServerTests
|
||||
public async Task WireMockServer_WithBodyAsJson_Using_PostAsync_And_WildcardMatcher_ShouldMatch()
|
||||
{
|
||||
// Arrange
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.Start();
|
||||
server.Given(
|
||||
Request.Create().UsingPost().WithPath("/foo").WithBody(new WildcardMatcher("*Hello*"))
|
||||
@@ -154,7 +184,7 @@ public partial class WireMockServerTests
|
||||
);
|
||||
|
||||
// Act
|
||||
var response = await new HttpClient().PostAsync("http://localhost:" + server.Ports[0] + "/foo", new StringContent("{ Hi = \"Hello World\" }"), cancellationToken);
|
||||
var response = await new HttpClient().PostAsync("http://localhost:" + server.Ports[0] + "/foo", new StringContent("{ Hi = \"Hello World\" }"), _ct);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
@@ -166,7 +196,6 @@ public partial class WireMockServerTests
|
||||
public async Task WireMockServer_WithBodyAsJson_Using_PostAsync_And_JsonPartialWildcardMatcher_ShouldMatch()
|
||||
{
|
||||
// Arrange
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.Start();
|
||||
|
||||
var matcher = new JsonPartialWildcardMatcher(new { method = "initialize", id = "^[a-f0-9]{32}-[0-9]$" }, ignoreCase: true, regex: true);
|
||||
@@ -188,13 +217,13 @@ public partial class WireMockServerTests
|
||||
// Act
|
||||
var content = "{\"jsonrpc\":\"2.0\",\"id\":\"ec475f56d4694b48bc737500ba575b35-1\",\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"GitHub Test\",\"version\":\"1.0.0\"}}}";
|
||||
var response = await new HttpClient()
|
||||
.PostAsync($"{server.Url}/foo", new StringContent(content, Encoding.UTF8, "application/json"), cancellationToken)
|
||||
.PostAsync($"{server.Url}/foo", new StringContent(content, Encoding.UTF8, "application/json"), _ct)
|
||||
;
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var responseText = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
var responseText = await response.Content.ReadAsStringAsync(_ct);
|
||||
responseText.Should().Contain("ec475f56d4694b48bc737500ba575b35-1");
|
||||
}
|
||||
|
||||
@@ -203,8 +232,7 @@ public partial class WireMockServerTests
|
||||
public async Task WireMockServer_WithBodyAsJson_Using_PostAsync_And_JsonPartialWildcardMatcher_And_SystemTextJson_ShouldMatch()
|
||||
{
|
||||
// Arrange
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.Start(x => x.DefaultJsonSerializer = new JsonConverter.System.Text.Json.SystemTextJsonConverter() );
|
||||
using var server = WireMockServer.Start(x => x.DefaultJsonSerializer = new SystemTextJsonConverter());
|
||||
|
||||
var matcher = new JsonPartialWildcardMatcher(new { id = "^[a-f0-9]{32}-[0-9]$" }, ignoreCase: true, regex: true);
|
||||
server.Given(Request.Create()
|
||||
@@ -220,12 +248,12 @@ public partial class WireMockServerTests
|
||||
// Act
|
||||
var content = """{"id":"ec475f56d4694b48bc737500ba575b35-1"}""";
|
||||
using var httpClient = new HttpClient();
|
||||
var response = await httpClient.PostAsync($"{server.Url}/system-text-json", new StringContent(content, Encoding.UTF8, "application/json"), cancellationToken);
|
||||
var response = await httpClient.PostAsync($"{server.Url}/system-text-json", new StringContent(content, Encoding.UTF8, "application/json"), _ct);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
|
||||
var responseText = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
var responseText = await response.Content.ReadAsStringAsync(_ct);
|
||||
responseText.Should().Contain("OK");
|
||||
}
|
||||
#endif
|
||||
@@ -234,7 +262,6 @@ public partial class WireMockServerTests
|
||||
public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithFunc()
|
||||
{
|
||||
// Arrange
|
||||
var cancelationToken = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.Start();
|
||||
server.Given(
|
||||
Request.Create()
|
||||
@@ -249,7 +276,7 @@ public partial class WireMockServerTests
|
||||
// Act
|
||||
var content = new FormUrlEncodedContent([new KeyValuePair<string, string>("key1", "value1")]);
|
||||
var response = await new HttpClient()
|
||||
.PostAsync($"{server.Url}/foo", content, cancelationToken);
|
||||
.PostAsync($"{server.Url}/foo", content, _ct);
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
@@ -261,7 +288,6 @@ public partial class WireMockServerTests
|
||||
public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithExactMatcher()
|
||||
{
|
||||
// Arrange
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.Start();
|
||||
server.Given(
|
||||
Request.Create()
|
||||
@@ -282,7 +308,7 @@ public partial class WireMockServerTests
|
||||
new KeyValuePair<string, string>("email", "johndoe@example.com")
|
||||
]);
|
||||
using var httpClient = new HttpClient();
|
||||
var response = await httpClient.PostAsync($"{server.Url}/foo", content, cancellationToken)
|
||||
var response = await httpClient.PostAsync($"{server.Url}/foo", content, _ct)
|
||||
;
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||
@@ -294,7 +320,6 @@ public partial class WireMockServerTests
|
||||
public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithFormUrlEncodedMatcher()
|
||||
{
|
||||
// Arrange
|
||||
var cancelationToken = TestContext.Current.CancellationToken;
|
||||
var matcher = new FormUrlEncodedMatcher(["email=johndoe@example.com", "name=John Doe"]);
|
||||
using var server = WireMockServer.Start();
|
||||
server.Given(
|
||||
@@ -326,7 +351,7 @@ public partial class WireMockServerTests
|
||||
new KeyValuePair<string, string>("email", "johndoe@example.com")
|
||||
]);
|
||||
var responseOrdered = await new HttpClient()
|
||||
.PostAsync($"{server.Url}/foo", contentOrdered, cancelationToken)
|
||||
.PostAsync($"{server.Url}/foo", contentOrdered, _ct)
|
||||
;
|
||||
|
||||
// Assert 1
|
||||
@@ -340,7 +365,7 @@ public partial class WireMockServerTests
|
||||
new KeyValuePair<string, string>("name", "John Doe"),
|
||||
]);
|
||||
var responseUnordered = await new HttpClient()
|
||||
.PostAsync($"{server.Url}/bar", contentUnordered, cancelationToken)
|
||||
.PostAsync($"{server.Url}/bar", contentUnordered, _ct)
|
||||
;
|
||||
|
||||
// Assert 2
|
||||
@@ -353,7 +378,6 @@ public partial class WireMockServerTests
|
||||
public async Task WireMockServer_WithSseBody()
|
||||
{
|
||||
// Arrange
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
using var server = WireMockServer.Start();
|
||||
server
|
||||
.WhenRequest(r => r
|
||||
@@ -387,9 +411,8 @@ public partial class WireMockServerTests
|
||||
using var client = new HttpClient();
|
||||
|
||||
// Act 1
|
||||
var normal = await client.GetAsync(server.Url, cancellationToken)
|
||||
;
|
||||
(await normal.Content.ReadAsStringAsync(cancellationToken)).Should().Be("normal");
|
||||
var normal = await client.GetAsync(server.Url, _ct);
|
||||
(await normal.Content.ReadAsStringAsync(_ct)).Should().Be("normal");
|
||||
|
||||
// Act 2
|
||||
using var response = await client.GetStreamAsync($"{server.Url}/sse", _ct);
|
||||
|
||||
Reference in New Issue
Block a user