Compare commits

..

8 Commits
2.3.0 ... 2.5.0

Author SHA1 Message Date
Stef Heyenrath
4bb378bdce 2.5.0 2026-05-04 18:52:29 +02:00
Stef Heyenrath
f9df0d0ee9 Fix name in RequestMessageHeaderMatcher 2026-05-03 09:41:25 +02:00
Степан
f8d3b51fbc Feature/early mismatch (#1451)
* feat(request matchers): Add support for early mismatch in mapping processing

* test(request matchers): Add unit test for early mismatch functionality

* test(grpc): Add test for grpc requests early mismatch and error logging (Issue #1442)

* feat(request matchers): RequestMatcherType

Add `RequestMatcherType` to request matchers for improved type
identification

Closes #1442

* refactor(request matchers): Request

Replace `EarlyMatcherSelector` with `EarlyMatcherType` for improved
clarity and consistency

Closes #1442

* feat(request): conversion

Add EarlyMatcherType support in request models and mapping conversion

Closes #1442

* test(mapping): new tests

add unit tests for EarlyMatcherType in mapping conversion and
serialization

Closes #1442

* refactor(request matchers): RequestMessageEarlyMatcher

Replaced inline `EarlyMatcherType` logic with the new
`RequestMessageEarlyMatcher` class to support cases when several
matchers of the same type are present. For instance - Header, Cookie,
Param

Closes #1442

* test(request matchers): Early Mismatch

add unit tests for early mismatch scenarios with several matchers of
same type. Currently, headers and parameters

Closes #1442

* refactor(mapping): RequestModel.EarlyMatcherType

use fully qualified enum for EarlyMatcherType in serialization

Closes #1442

* style(review): fixes

- removed unused method
- added missing curly brackets

Closes #1442
2026-05-03 09:27:19 +02:00
Copilot
be9864461d Fix CVE-2026-40021: upgrade log4net to 3.3.0 in examples/WireMock.Net.Service/packages.config (#1453)
* Initial plan

* Fix CVE-2026-40021: update log4net to 3.3.0 in packages.config

Agent-Logs-Url: https://github.com/wiremock/WireMock.Net/sessions/a09a4576-1b17-41fe-9912-c1efdf922fed

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: StefH <249938+StefH@users.noreply.github.com>
2026-05-02 19:46:13 +02:00
dependabot[bot]
4bb7e8af45 Bump log4net from 2.0.15 to 3.3.0 (#1452)
---
updated-dependencies:
- dependency-name: log4net
  dependency-version: 3.3.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-02 18:30:43 +02:00
Stef Heyenrath
8bf42904ab 2.4.0 2026-04-24 16:48:28 +02:00
dependabot[bot]
0a48b40021 Bump OpenTelemetry.Exporter.OpenTelemetryProtocol from 1.14.0 to 1.15.x (#1450)
* Bump OpenTelemetry.Exporter.OpenTelemetryProtocol from 1.14.0 to 1.15.3

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

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

* Upgrade all OpenTelemetry related NuGet packages to latest versions

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

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

---------

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

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

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

* feat/1421 addressing PR comments - Updated logic to represent IsDisable insted of IsEnabled
2026-04-24 08:07:37 +02:00
53 changed files with 760 additions and 40 deletions

View File

@@ -1,3 +1,14 @@
# 2.5.0 (04 May 2026)
- [#1451](https://github.com/wiremock/WireMock.Net/pull/1451) - Feature/early mismatch [feature] contributed by [Stepami](https://github.com/Stepami)
- [#1452](https://github.com/wiremock/WireMock.Net/pull/1452) - Bump log4net from 2.0.15 to 3.3.0 in example console app [dependencies, .NET] contributed by [dependabot[bot]](https://github.com/apps/dependabot)
- [#1453](https://github.com/wiremock/WireMock.Net/pull/1453) - Fix CVE-2026-40021: upgrade log4net to 3.3.0 in examples/WireMock.Net.Service/packages.config [dependencies] contributed by [Copilot](https://github.com/apps/copilot-swe-agent)
- [#1442](https://github.com/wiremock/WireMock.Net/issues/1442) - Bug: [grpc] WithBodyAsProtoBuf exception on match [bug]
# 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)

View File

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

View File

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

View File

@@ -1,12 +1,7 @@
# 2.3.0 (20 April 2026)
- #1436 Moving Scenario state change before global response delay is set [feature]
- #1440 Bump log4net from 2.0.15 to 3.3.0 [dependencies]
- #1443 Fix ExactMatcher and JsonMatcher not working for ISO dates as string [bug]
- #1444 Update instructions.md [refactor]
- #1448 Use DefaultJsonSerializer for BodyAsJson-Response [bug]
- #1205 System.Private.Uri 4.3.0 Blackduck security High vulnerability [bug]
- #1260 Scenario state change before a response delay timeout ends [bug]
- #1441 ExactMatcher and JsonMatcher not working for ISO dates in v2.2.0 [bug]
- #1446 WithBodyAsJson does not return correctly formatted json when using SystemTextJsonConverter [bug]
# 2.5.0 (04 May 2026)
- #1451 Feature/early mismatch [feature]
- #1452 Bump log4net from 2.0.15 to 3.3.0 in example console app [dependencies, .NET]
- #1453 Fix CVE-2026-40021: upgrade log4net to 3.3.0 in examples/WireMock.Net.Service/packages.config [dependencies]
- #1442 Bug: [grpc] WithBodyAsProtoBuf exception on match [bug]
The full release notes can be found here: https://github.com/wiremock/WireMock.Net/blob/master/CHANGELOG.md

View File

@@ -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>

View File

@@ -16,7 +16,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>

View File

@@ -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>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="log4net" version="2.0.15" targetFramework="net48" />
<package id="log4net" version="3.3.0" targetFramework="net48" />
<package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net452" />
<package id="Microsoft.AspNet.WebApi.Core" version="5.2.3" targetFramework="net452" />
<package id="Microsoft.AspNet.WebApi.Owin" version="5.2.3" targetFramework="net452" />

View File

@@ -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; }

View File

@@ -1,5 +1,7 @@
// Copyright © WireMock.Net
using WireMock.Matchers.Request;
namespace WireMock.Admin.Mappings;
/// <summary>
@@ -61,9 +63,15 @@ public class RequestModel
/// Gets or sets the Params.
/// </summary>
public IList<ParamModel>? Params { get; set; }
/// <summary>
/// Gets or sets the body.
/// </summary>
public BodyModel? Body { get; set; }
/// <summary>
/// Type of the request matcher to return an immediate mismatch during mapping processing.
/// Optional.
/// </summary>
public RequestMatcherType? EarlyMatcherType { get; set; }
}

View File

@@ -7,6 +7,11 @@ namespace WireMock.Matchers.Request;
/// </summary>
public interface IRequestMatcher
{
/// <summary>
/// Gets the request matcher's type.
/// </summary>
public RequestMatcherType Type { get; }
/// <summary>
/// Determines whether the specified RequestMessage is match.
/// </summary>

View File

@@ -0,0 +1,84 @@
// Copyright © WireMock.Net
namespace WireMock.Matchers.Request;
/// <summary>
/// List of predefined request matcher types.
/// </summary>
public enum RequestMatcherType
{
/// <summary>
/// RequestMessageBodyMatcher
/// </summary>
Body = 0,
/// <summary>
/// RequestMessageBodyMatcher{T}
/// </summary>
BodyOfT = 1,
/// <summary>
/// RequestMessageClientIPMatcher
/// </summary>
ClientIP = 2,
/// <summary>
/// RequestMessageCookieMatcher
/// </summary>
Cookie = 3,
/// <summary>
/// RequestMessageGraphQLMatcher
/// </summary>
GraphQL = 4,
/// <summary>
/// RequestMessageHeaderMatcher
/// </summary>
Header = 5,
/// <summary>
/// RequestMessageHttpVersionMatcher
/// </summary>
HttpVersion = 6,
/// <summary>
/// RequestMessageMethodMatcher
/// </summary>
Method = 7,
/// <summary>
/// RequestMessageMultiPartMatcher
/// </summary>
MultiPart = 8,
/// <summary>
/// RequestMessageParamMatcher
/// </summary>
Param = 9,
/// <summary>
/// RequestMessagePathMatcher
/// </summary>
Path = 10,
/// <summary>
/// RequestMessageProtoBufMatcher
/// </summary>
ProtoBuf = 11,
/// <summary>
/// RequestMessageScenarioAndStateMatcher
/// </summary>
ScenarioAndState = 12,
/// <summary>
/// RequestMessageUrlMatcher
/// </summary>
Url = 13,
/// <summary>
/// RequestMessageCompositeMatcher
/// </summary>
Composite = 14
}

View File

@@ -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);

View File

@@ -142,6 +142,9 @@ public class RequestMessageBodyMatcher : IRequestMatcher
MatchOperator = matchOperator;
}
/// <inheritdoc />
public RequestMatcherType Type => RequestMatcherType.Body;
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{

View File

@@ -31,6 +31,9 @@ public class RequestMessageBodyMatcher<T> : IRequestMatcher
Func = Guard.NotNull(func);
}
/// <inheritdoc />
public RequestMatcherType Type => RequestMatcherType.BodyOfT;
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{

View File

@@ -74,6 +74,9 @@ public class RequestMessageClientIPMatcher : IRequestMatcher
Funcs = Guard.NotNull(funcs);
}
/// <inheritdoc />
public RequestMatcherType Type => RequestMatcherType.ClientIP;
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{

View File

@@ -20,6 +20,11 @@ public abstract class RequestMessageCompositeMatcher : IRequestMatcher
/// </value>
private IEnumerable<IRequestMatcher> RequestMatchers { get; }
/// <summary>
/// Selected type to choose the matcher from <see cref="RequestMatchers"/> which will immediately return a mismatch.
/// </summary>
internal RequestMatcherType? EarlyMatcherType { get; private protected set; }
/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageCompositeMatcher"/> class.
/// </summary>
@@ -31,6 +36,9 @@ public abstract class RequestMessageCompositeMatcher : IRequestMatcher
_type = type;
}
/// <inheritdoc />
public RequestMatcherType Type => RequestMatcherType.Composite;
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{
@@ -39,6 +47,13 @@ public abstract class RequestMessageCompositeMatcher : IRequestMatcher
return MatchScores.Mismatch;
}
var earlyMatcher = new RequestMessageEarlyMatcher(EarlyMatcherType, RequestMatchers);
var earlyMatchResult = earlyMatcher.GetMatchingScore(requestMessage, requestMatchResult);
if (!MatchScores.IsPerfect(earlyMatchResult))
{
return MatchScores.Mismatch;
}
if (_type == CompositeMatcherType.And)
{
return RequestMatchers.Average(requestMatcher => requestMatcher.GetMatchingScore(requestMessage, requestMatchResult));

View File

@@ -1,7 +1,6 @@
// Copyright © WireMock.Net
using Stef.Validation;
using System.Linq;
namespace WireMock.Matchers.Request;
@@ -93,6 +92,9 @@ public class RequestMessageCookieMatcher : IRequestMatcher
Name = string.Empty; // Not used when Func, but set to a non-null valid value.
}
/// <inheritdoc />
public RequestMatcherType Type => RequestMatcherType.Cookie;
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{

View File

@@ -0,0 +1,35 @@
// Copyright © WireMock.Net
namespace WireMock.Matchers.Request;
/// <summary>
/// Return the mismatch if the matching score of matchers is not perfect.
/// </summary>
internal sealed class RequestMessageEarlyMatcher(
RequestMatcherType? earlyMatcherType,
IEnumerable<IRequestMatcher> requestMatchers) : IRequestMatcher
{
/// <inheritdoc />
public RequestMatcherType Type => RequestMatcherType.Composite;
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{
if (earlyMatcherType is null)
{
return MatchScores.Perfect;
}
var earlyMatchers = requestMatchers
.Where(m => m.Type == earlyMatcherType)
.ToList();
if (earlyMatchers.Count is 0)
{
return MatchScores.Perfect;
}
var compositeMatcher = new RequestBuilders.Request(earlyMatchers);
return compositeMatcher.GetMatchingScore(requestMessage, requestMatchResult);
}
}

View File

@@ -1,6 +1,5 @@
// Copyright © WireMock.Net
using System.Linq;
using Stef.Validation;
using WireMock.Types;
@@ -12,7 +11,7 @@ namespace WireMock.Matchers.Request;
/// <inheritdoc cref="IRequestMatcher"/>
public class RequestMessageHeaderMatcher : IRequestMatcher
{
private const string _name = nameof(RequestMessageCookieMatcher);
private const string _name = nameof(RequestMessageHeaderMatcher);
/// <summary>
/// MatchBehaviour
@@ -106,6 +105,9 @@ public class RequestMessageHeaderMatcher : IRequestMatcher
Name = string.Empty; // Not used when Func, but set to a non-null valid value.
}
/// <inheritdoc />
public RequestMatcherType Type => RequestMatcherType.Header;
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{

View File

@@ -65,6 +65,9 @@ public class RequestMessageHttpVersionMatcher : IRequestMatcher
MatcherOnStringFunc = Guard.NotNull(func);
}
/// <inheritdoc />
public RequestMatcherType Type => RequestMatcherType.HttpVersion;
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{

View File

@@ -46,6 +46,9 @@ internal class RequestMessageMethodMatcher : IRequestMatcher
MatchOperator = matchOperator;
}
/// <inheritdoc />
public RequestMatcherType Type => RequestMatcherType.Method;
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{

View File

@@ -55,6 +55,9 @@ public class RequestMessageMultiPartMatcher : IRequestMatcher
MatchOperator = matchOperator;
}
/// <inheritdoc />
public RequestMatcherType Type => RequestMatcherType.MultiPart;
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{

View File

@@ -82,6 +82,9 @@ public class RequestMessageParamMatcher : IRequestMatcher
Funcs = Guard.NotNull(funcs);
}
/// <inheritdoc />
public RequestMatcherType Type => RequestMatcherType.Param;
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{

View File

@@ -72,6 +72,9 @@ public class RequestMessagePathMatcher : IRequestMatcher
Funcs = Guard.NotNull(funcs);
}
/// <inheritdoc />
public RequestMatcherType Type => RequestMatcherType.Path;
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{

View File

@@ -29,6 +29,9 @@ internal class RequestMessageScenarioAndStateMatcher : IRequestMatcher
_executionConditionState = executionConditionState;
}
/// <inheritdoc />
public RequestMatcherType Type => RequestMatcherType.ScenarioAndState;
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{

View File

@@ -72,6 +72,9 @@ public class RequestMessageUrlMatcher : IRequestMatcher
Funcs = Guard.NotNull(funcs);
}
/// <inheritdoc />
public RequestMatcherType Type => RequestMatcherType.Url;
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{

View File

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

View File

@@ -34,7 +34,7 @@ public partial class Request : RequestMessageCompositeMatcher, IRequestBuilder
/// Initializes a new instance of the <see cref="Request"/> class.
/// </summary>
/// <param name="requestMatchers">The request matchers.</param>
private Request(IList<IRequestMatcher> requestMatchers) : base(requestMatchers)
internal Request(IList<IRequestMatcher> requestMatchers) : base(requestMatchers)
{
_requestMatchers = Guard.NotNull(requestMatchers);
}
@@ -81,6 +81,13 @@ public partial class Request : RequestMessageCompositeMatcher, IRequestBuilder
return this;
}
/// <inheritdoc />
public IRequestBuilder WithEarlyMismatch(RequestMatcherType? earlyMatcherType)
{
EarlyMatcherType = earlyMatcherType;
return this;
}
internal bool TryGetProtoBufMatcher([NotNullWhen(true)] out IProtoBufMatcher? protoBufMatcher)
{
protoBufMatcher = GetRequestMessageMatcher<RequestMessageProtoBufMatcher>()?.Matcher;

View File

@@ -66,6 +66,12 @@ internal class MappingConverter(MatcherMapper mapper)
// Request
sb.AppendLine(" .Given(Request.Create()");
if (request.EarlyMatcherType != null)
{
sb.AppendLine($" .WithEarlyMismatch({request.EarlyMatcherType.Value.GetFullyQualifiedEnumValue()})");
}
sb.AppendLine($" .UsingMethod({To1Or2Or3Arguments(methodMatcher?.MatchBehaviour, methodMatcher?.MatchOperator, methodMatcher?.Methods, HttpRequestMethod.GET)})");
if (pathMatcher?.Matchers != null)
@@ -275,6 +281,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
@@ -299,7 +306,9 @@ internal class MappingConverter(MatcherMapper mapper)
IgnoreCase = pm.IgnoreCase ? true : null,
RejectOnMatch = pm.MatchBehaviour == MatchBehaviour.RejectOnMatch ? true : null,
Matchers = _mapper.Map(pm.Matchers)
}).ToList() : null
}).ToList() : null,
EarlyMatcherType = request.EarlyMatcherType
},
Response = new ResponseModel()
};

View File

@@ -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.

View File

@@ -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)
{

View File

@@ -57,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\\/.+$");
@@ -100,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));
@@ -426,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

View File

@@ -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)
{
@@ -263,6 +268,8 @@ public partial class WireMockServer
}
}
requestBuilder = requestBuilder.WithEarlyMismatch(requestModel.EarlyMatcherType);
return requestBuilder;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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; }

View File

@@ -69,6 +69,9 @@ public class RequestMessageGraphQLMatcher : IRequestMatcher
MatchOperator = matchOperator;
}
/// <inheritdoc />
public RequestMatcherType Type => RequestMatcherType.GraphQL;
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{

View File

@@ -30,6 +30,9 @@ public class RequestMessageProtoBufMatcher : IRequestMatcher
}
}
/// <inheritdoc />
public RequestMatcherType Type => RequestMatcherType.ProtoBuf;
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{

View File

@@ -10,7 +10,8 @@ namespace WireMock.RequestBuilders;
public interface IRequestBuilder : IClientIPRequestBuilder
{
/// <summary>
/// Adds a request matcher to the builder.
/// Adds a request matcher to the builder.<br/>
/// If the request matcher is already present, it will be replaced.
/// </summary>
/// <typeparam name="T">The type of the request matcher.</typeparam>
/// <param name="requestMatcher">The request matcher to add.</param>
@@ -21,4 +22,11 @@ public interface IRequestBuilder : IClientIPRequestBuilder
/// The link back to the Mapping.
/// </summary>
IMapping Mapping { get; set; }
/// <summary>
/// Chooses the <see cref="IRequestMatcher"/> which immediately returns a mismatch during mappings enumeration.
/// </summary>
/// <param name="earlyMatcherType">Selected type to choose the matcher from available list.</param>
/// <returns>The current <see cref="IRequestBuilder"/> instance.</returns>
IRequestBuilder WithEarlyMismatch(RequestMatcherType? earlyMatcherType);
}

View File

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

View File

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

View File

@@ -8,8 +8,11 @@ using ExampleIntegrationTest.Lookup;
using Google.Protobuf.WellKnownTypes;
using Greet;
using Grpc.Net.Client;
using Moq;
using WireMock.Constants;
using WireMock.Matchers;
using WireMock.Matchers.Request;
using WireMock.Net.Xunit;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
@@ -78,7 +81,7 @@ import ""google/protobuf/empty.proto"";
service Greeter {
rpc Nothing (google.protobuf.Empty) returns (google.protobuf.Empty);
rpc SayHello (HelloRequest) returns (HelloReply);
rpc SayOther (Other) returns (HelloReply);
@@ -731,6 +734,93 @@ message Other {
Then_ReplyMessage_Should_BeCorrect(reply);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task WireMockServer_WithBodyAsProtoBuf_WithEarlyMismatch_ErrorLogs_Issue1442(
bool withEarlyMismatch)
{
// Arrange
var greeterId = $"test-greeter-{Guid.NewGuid()}";
var policyId = $"test-policy-{Guid.NewGuid()}";
var ct = TestContext.Current.CancellationToken;
var greeterProtoDefinition = ProtoDefinition;
var policyProtoDefinition = File.ReadAllText("./Grpc/policy.proto");
var mockTestOutputHelper = new Mock<ITestOutputHelper>();
using var server = WireMockServer.Start(new WireMockServerSettings
{
UseHttp2 = true,
Logger = new TestOutputHelperWireMockLogger(mockTestOutputHelper.Object)
});
server
.AddProtoDefinition(greeterId, greeterProtoDefinition)
.Given(Request.Create()
.UsingPost()
.WithHttpVersion("2")
.WithPath("/greet.Greeter/SayHello")
.WithEarlyMismatch(withEarlyMismatch
? RequestMatcherType.Path
: null)
.WithBodyAsProtoBuf("greet.HelloRequest", new JsonMatcher(new { name = "stef" })))
.WithProtoDefinition(greeterId)
.RespondWith(Response.Create()
.WithHeader("Content-Type", "application/grpc")
.WithTrailingHeader("grpc-status", "0")
.WithBodyAsProtoBuf("greet.HelloReply",
new
{
message = "hello {{request.BodyAsJson.name}} {{request.method}}"
})
.WithTransformer());
server
.AddProtoDefinition(policyId, policyProtoDefinition)
.Given(Request.Create()
.UsingPost()
.WithHttpVersion("2")
.WithPath("/Policy.PolicyService/GetVersion")
.WithEarlyMismatch(withEarlyMismatch
? RequestMatcherType.Path
: null)
.WithBodyAsProtoBuf("ExampleIntegrationTest.Lookup.GetVersionRequest", new NotNullOrEmptyMatcher()))
.WithProtoDefinition(policyId)
.RespondWith(Response.Create()
.WithHeader("Content-Type", "application/grpc")
.WithTrailingHeader("grpc-status", "0")
.WithBodyAsProtoBuf("ExampleIntegrationTest.Lookup.GetVersionResponse",
new GetVersionResponse
{
Version = "test",
DateHired = new Timestamp
{
Seconds = 1722301323,
Nanos = 12300
},
Client = new ExampleIntegrationTest.Lookup.Client
{
ClientName = ExampleIntegrationTest.Lookup.Client.Types.Clients.Test,
CorrelationId = "correlation"
}
})
.WithTransformer());
// Act
var channel = GrpcChannel.ForAddress(server.Url!);
var policyServiceClient = new PolicyService.PolicyServiceClient(channel);
var greeterClient = new Greeter.GreeterClient(channel);
_ = await policyServiceClient.GetVersionAsync(new GetVersionRequest(), cancellationToken: ct);
_ = await greeterClient.SayHelloAsync(new HelloRequest { Name = "stef" }, cancellationToken: ct);
mockTestOutputHelper.Verify(
x => x.WriteLine(
It.Is<string>(log => log.Contains("[Error]") && log.Contains("Exception"))),
withEarlyMismatch ? Times.Never : Times.AtLeastOnce);
}
private static WireMockServer Given_When_ServerStarted_And_RunningOnHttpAndGrpc()
{
var settings = new WireMockServerSettings

View File

@@ -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)

View File

@@ -1,8 +1,11 @@
// Copyright © WireMock.Net
using Moq;
using WireMock.Constants;
using WireMock.Matchers;
using WireMock.Matchers.Request;
using WireMock.Models;
using WireMock.RequestBuilders;
namespace WireMock.Net.Tests.RequestMatchers;
@@ -10,8 +13,12 @@ public class RequestMessageCompositeMatcherTests
{
private class Helper : RequestMessageCompositeMatcher
{
public Helper(IEnumerable<IRequestMatcher> requestMatchers, CompositeMatcherType type = CompositeMatcherType.And) : base(requestMatchers, type)
public Helper(
IEnumerable<IRequestMatcher> requestMatchers,
CompositeMatcherType type = CompositeMatcherType.And,
RequestMatcherType? earlyMatcherType = null) : base(requestMatchers, type)
{
EarlyMatcherType = earlyMatcherType;
}
}
@@ -77,4 +84,74 @@ public class RequestMessageCompositeMatcherTests
requestMatcher1Mock.Verify(rm => rm.GetMatchingScore(It.IsAny<RequestMessage>(), It.IsAny<RequestMatchResult>()), Times.Once);
requestMatcher2Mock.Verify(rm => rm.GetMatchingScore(It.IsAny<RequestMessage>(), It.IsAny<RequestMatchResult>()), Times.Once);
}
[Fact]
public void RequestMessageCompositeMatcher_GetMatchingScore_EarlyMismatch()
{
// Assign
var requestMatcher1Mock = new Mock<IRequestMatcher>();
requestMatcher1Mock.Setup(rm => rm.GetMatchingScore(It.IsAny<RequestMessage>(), It.IsAny<RequestMatchResult>())).Returns(1.0d);
var requestMatcher2Mock = new Mock<IRequestMatcher>();
requestMatcher2Mock.Setup(rm => rm.GetMatchingScore(It.IsAny<RequestMessage>(), It.IsAny<RequestMatchResult>())).Returns(0.8d);
var postMatcher = new RequestMessageMethodMatcher(HttpRequestMethod.POST);
var requestMessage = new RequestMessage(new UrlDetails("http://localhost"), HttpRequestMethod.GET, "127.0.0.1");
var matcher = new Helper(
[requestMatcher1Mock.Object, requestMatcher2Mock.Object, postMatcher],
earlyMatcherType: RequestMatcherType.Method);
// Act
var result = new RequestMatchResult();
double score = matcher.GetMatchingScore(requestMessage, result);
// Assert
score.Should().Be(0.0d);
// Verify
requestMatcher1Mock.Verify(rm => rm.GetMatchingScore(It.IsAny<RequestMessage>(), It.IsAny<RequestMatchResult>()), Times.Never);
requestMatcher2Mock.Verify(rm => rm.GetMatchingScore(It.IsAny<RequestMessage>(), It.IsAny<RequestMatchResult>()), Times.Never);
}
[Fact]
public void RequestMessageCompositeMatcher_GetMatchingScore_SeveralHeadersEarlyMismatch()
{
// Assign
var headers = new Dictionary<string, string[]>
{
{ "teST", new[] { "x" } },
{ "teST2", new[] { "z" } }
};
var requestMessage = new RequestMessage(new UrlDetails("http://localhost"), "GET", "127.0.0.1", null, headers);
var request = Request.Create()
.WithEarlyMismatch(RequestMatcherType.Header)
.UsingAnyMethod()
.WithHeader("teST", "x")
.WithHeader("teST1", ["xx", "yy"])
.WithHeader("teST2", ["y", "z"], matchOperator: MatchOperator.And);
// Act
var score = request.GetMatchingScore(requestMessage, new RequestMatchResult());
// Assert
score.Should().Be(0.0d);
}
[Fact]
public void RequestMessageCompositeMatcher_GetMatchingScore_SeveralParamEarlyMismatchSuccess()
{
// Assign
var uriWithParams = new Uri("http://localhost?test1=1&test2=2");
var requestMessage = new RequestMessage(new UrlDetails(uriWithParams), "GET", "127.0.0.1");
var request = Request.Create()
.WithEarlyMismatch(RequestMatcherType.Param)
.UsingAnyMethod()
.WithParam("test1", "1")
.WithParam("test2", "2");
// Act
var score = request.GetMatchingScore(requestMessage, new RequestMatchResult());
// Assert
score.Should().Be(1.0d);
}
}

View File

@@ -1,5 +1,6 @@
// Copyright © WireMock.Net
using WireMock.Matchers.Request;
using WireMock.Net.Tests.VerifyExtensions;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
@@ -101,6 +102,7 @@ public partial class MappingConverterTests
var guid = new Guid("8e7b9ab7-e18e-4502-8bc9-11e6679811cc");
var request = Request.Create()
.UsingGet()
.WithEarlyMismatch(RequestMatcherType.Method)
.WithPath("/test_path")
.WithParam("q", "42")
.WithClientIP("112.123.100.99")

View File

@@ -1,5 +1,6 @@
builder
.Given(Request.Create()
.WithEarlyMismatch(WireMock.Matchers.Request.RequestMatcherType.Method)
.UsingMethod("GET")
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "/test_path", false, WireMock.Matchers.MatchOperator.Or))
.WithParam("q", new ExactMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, false, WireMock.Matchers.MatchOperator.And, "42"))

View File

@@ -1,6 +1,7 @@
var builder = new MappingBuilder();
builder
.Given(Request.Create()
.WithEarlyMismatch(WireMock.Matchers.Request.RequestMatcherType.Method)
.UsingMethod("GET")
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "/test_path", false, WireMock.Matchers.MatchOperator.Or))
.WithParam("q", new ExactMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, false, WireMock.Matchers.MatchOperator.And, "42"))

View File

@@ -1,5 +1,6 @@
server
.Given(Request.Create()
.WithEarlyMismatch(WireMock.Matchers.Request.RequestMatcherType.Method)
.UsingMethod("GET")
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "/test_path", false, WireMock.Matchers.MatchOperator.Or))
.WithParam("q", new ExactMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, false, WireMock.Matchers.MatchOperator.And, "42"))

View File

@@ -1,6 +1,7 @@
var server = WireMockServer.Start();
server
.Given(Request.Create()
.WithEarlyMismatch(WireMock.Matchers.Request.RequestMatcherType.Method)
.UsingMethod("GET")
.WithPath(new WildcardMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, "/test_path", false, WireMock.Matchers.MatchOperator.Or))
.WithParam("q", new ExactMatcher(WireMock.Matchers.MatchBehaviour.AcceptOnMatch, false, WireMock.Matchers.MatchOperator.And, "42"))

View File

@@ -19,7 +19,7 @@
id:ID!
firstName:String
lastName:String
fullName:String
fullName:String
}
}
}

View File

@@ -0,0 +1,34 @@
{
Guid: Guid_1,
UpdatedAt: DateTime_1,
Title: ,
Description: ,
Priority: 42,
Request: {
Path: {
Matchers: [
{
Name: WildcardMatcher,
Pattern: 1.2.3.4,
IgnoreCase: false
}
]
},
Headers: [
{
Name: x1,
Matchers: [
{
Name: WildcardMatcher,
Pattern: y,
IgnoreCase: true
}
],
IgnoreCase: true
}
],
EarlyMatcherType: ClientIP
},
Response: {},
UseWebhooksFireAndForget: false
}

View File

@@ -1,6 +1,7 @@
// Copyright © WireMock.Net
using WireMock.Matchers;
using WireMock.Matchers.Request;
using WireMock.Models;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
@@ -538,7 +539,7 @@ message HelloReply {
id:ID!
firstName:String
lastName:String
fullName:String
fullName:String
}";
var request = Request.Create().WithGraphQLSchema(schema);
var response = Response.Create();
@@ -640,4 +641,25 @@ message HelloReply {
// Verify
return Verify(model);
}
[Fact]
public Task ToMappingModel_Request_WithEarlyMismatch_ReturnsCorrectModel()
{
// Arrange
var request = Request.Create().WithEarlyMismatch(RequestMatcherType.ClientIP)
.WithHeader("x1", "y")
.WithClientIP("1.2.3.4");
var response = Response.Create();
var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, null);
// Act
var model = _sut.ToMappingModel(mapping);
// Assert
model.Should().NotBeNull();
model.Request.EarlyMatcherType.Should().Be(RequestMatcherType.ClientIP);
// Verify
return Verify(model);
}
}