From 4bb7e8af45503358e574fb111324866fb4c27a75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 18:30:43 +0200 Subject: [PATCH 1/3] 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] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../WireMock.Net.Console.MimePart.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/WireMock.Net.Console.MimePart/WireMock.Net.Console.MimePart.csproj b/examples/WireMock.Net.Console.MimePart/WireMock.Net.Console.MimePart.csproj index 537b650a..0ce124d9 100644 --- a/examples/WireMock.Net.Console.MimePart/WireMock.Net.Console.MimePart.csproj +++ b/examples/WireMock.Net.Console.MimePart/WireMock.Net.Console.MimePart.csproj @@ -16,7 +16,7 @@ - + From be9864461dbf535f0e7eb9fb72c926342b653039 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 19:46:13 +0200 Subject: [PATCH 2/3] 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> --- examples/WireMock.Net.Service/packages.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/WireMock.Net.Service/packages.config b/examples/WireMock.Net.Service/packages.config index 489a0859..d01e7098 100644 --- a/examples/WireMock.Net.Service/packages.config +++ b/examples/WireMock.Net.Service/packages.config @@ -1,6 +1,6 @@ - + From f8d3b51fbc661499b722ad1e90a8902b91786e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD?= Date: Sun, 3 May 2026 10:27:19 +0300 Subject: [PATCH 3/3] 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 --- .../Admin/Mappings/RequestModel.cs | 10 +- .../Matchers/Request/IRequestMatcher.cs | 5 + .../Matchers/Request/RequestMatcherType.cs | 84 +++++++++++++++++ .../Request/RequestMessageBodyMatcher.cs | 3 + .../Request/RequestMessageBodyMatcher`1.cs | 3 + .../Request/RequestMessageClientIPMatcher.cs | 3 + .../Request/RequestMessageCompositeMatcher.cs | 15 +++ .../Request/RequestMessageCookieMatcher.cs | 3 + .../Request/RequestMessageEarlyMatcher.cs | 35 +++++++ .../Request/RequestMessageHeaderMatcher.cs | 3 + .../RequestMessageHttpVersionMatcher.cs | 3 + .../Request/RequestMessageMethodMatcher.cs | 3 + .../Request/RequestMessageMultiPartMatcher.cs | 3 + .../Request/RequestMessageParamMatcher.cs | 3 + .../Request/RequestMessagePathMatcher.cs | 3 + .../RequestMessageScenarioAndStateMatcher.cs | 3 + .../Request/RequestMessageUrlMatcher.cs | 3 + .../RequestBuilders/Request.cs | 9 +- .../Serialization/MappingConverter.cs | 10 +- .../Server/WireMockServer.ConvertMapping.cs | 2 + .../Request/RequestMessageGraphQLMatcher.cs | 3 + .../Request/RequestMessageProtoBufMatcher.cs | 3 + .../RequestBuilders/IRequestBuilder.cs | 10 +- .../Grpc/WireMockServerTests.Grpc.cs | 92 ++++++++++++++++++- .../RequestMessageCompositeMatcherTests.cs | 79 +++++++++++++++- .../MappingConverterTests.ToCSharpCode.cs | 2 + ...h_Builder_And_AddStartIsFalse.verified.txt | 1 + ...th_Builder_And_AddStartIsTrue.verified.txt | 1 + ...th_Server_And_AddStartIsFalse.verified.txt | 1 + ...ith_Server_And_AddStartIsTrue.verified.txt | 1 + ...hQLSchema_ReturnsCorrectModel.verified.txt | 2 +- ...yMismatch_ReturnsCorrectModel.verified.txt | 34 +++++++ .../Serialization/MappingConverterTests.cs | 24 ++++- 33 files changed, 451 insertions(+), 8 deletions(-) create mode 100644 src/WireMock.Net.Abstractions/Matchers/Request/RequestMatcherType.cs create mode 100644 src/WireMock.Net.Minimal/Matchers/Request/RequestMessageEarlyMatcher.cs create mode 100644 test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithEarlyMismatch_ReturnsCorrectModel.verified.txt diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/RequestModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/RequestModel.cs index a1c5faba..c3370453 100644 --- a/src/WireMock.Net.Abstractions/Admin/Mappings/RequestModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/RequestModel.cs @@ -1,5 +1,7 @@ // Copyright © WireMock.Net +using WireMock.Matchers.Request; + namespace WireMock.Admin.Mappings; /// @@ -61,9 +63,15 @@ public class RequestModel /// Gets or sets the Params. /// public IList? Params { get; set; } - + /// /// Gets or sets the body. /// public BodyModel? Body { get; set; } + + /// + /// Type of the request matcher to return an immediate mismatch during mapping processing. + /// Optional. + /// + public RequestMatcherType? EarlyMatcherType { get; set; } } \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Matchers/Request/IRequestMatcher.cs b/src/WireMock.Net.Abstractions/Matchers/Request/IRequestMatcher.cs index 8410846b..81b3531f 100644 --- a/src/WireMock.Net.Abstractions/Matchers/Request/IRequestMatcher.cs +++ b/src/WireMock.Net.Abstractions/Matchers/Request/IRequestMatcher.cs @@ -7,6 +7,11 @@ namespace WireMock.Matchers.Request; /// public interface IRequestMatcher { + /// + /// Gets the request matcher's type. + /// + public RequestMatcherType Type { get; } + /// /// Determines whether the specified RequestMessage is match. /// diff --git a/src/WireMock.Net.Abstractions/Matchers/Request/RequestMatcherType.cs b/src/WireMock.Net.Abstractions/Matchers/Request/RequestMatcherType.cs new file mode 100644 index 00000000..b469fe60 --- /dev/null +++ b/src/WireMock.Net.Abstractions/Matchers/Request/RequestMatcherType.cs @@ -0,0 +1,84 @@ +// Copyright © WireMock.Net + +namespace WireMock.Matchers.Request; + +/// +/// List of predefined request matcher types. +/// +public enum RequestMatcherType +{ + /// + /// RequestMessageBodyMatcher + /// + Body = 0, + + /// + /// RequestMessageBodyMatcher{T} + /// + BodyOfT = 1, + + /// + /// RequestMessageClientIPMatcher + /// + ClientIP = 2, + + /// + /// RequestMessageCookieMatcher + /// + Cookie = 3, + + /// + /// RequestMessageGraphQLMatcher + /// + GraphQL = 4, + + /// + /// RequestMessageHeaderMatcher + /// + Header = 5, + + /// + /// RequestMessageHttpVersionMatcher + /// + HttpVersion = 6, + + /// + /// RequestMessageMethodMatcher + /// + Method = 7, + + /// + /// RequestMessageMultiPartMatcher + /// + MultiPart = 8, + + /// + /// RequestMessageParamMatcher + /// + Param = 9, + + /// + /// RequestMessagePathMatcher + /// + Path = 10, + + /// + /// RequestMessageProtoBufMatcher + /// + ProtoBuf = 11, + + /// + /// RequestMessageScenarioAndStateMatcher + /// + ScenarioAndState = 12, + + /// + /// RequestMessageUrlMatcher + /// + Url = 13, + + /// + /// RequestMessageCompositeMatcher + /// + Composite = 14 +} \ No newline at end of file diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageBodyMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageBodyMatcher.cs index 090f07fb..55728137 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageBodyMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageBodyMatcher.cs @@ -142,6 +142,9 @@ public class RequestMessageBodyMatcher : IRequestMatcher MatchOperator = matchOperator; } + /// + public RequestMatcherType Type => RequestMatcherType.Body; + /// public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) { diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageBodyMatcher`1.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageBodyMatcher`1.cs index 9699f81f..94209b55 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageBodyMatcher`1.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageBodyMatcher`1.cs @@ -31,6 +31,9 @@ public class RequestMessageBodyMatcher : IRequestMatcher Func = Guard.NotNull(func); } + /// + public RequestMatcherType Type => RequestMatcherType.BodyOfT; + /// public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) { diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageClientIPMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageClientIPMatcher.cs index f6b64e25..bcbfddeb 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageClientIPMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageClientIPMatcher.cs @@ -74,6 +74,9 @@ public class RequestMessageClientIPMatcher : IRequestMatcher Funcs = Guard.NotNull(funcs); } + /// + public RequestMatcherType Type => RequestMatcherType.ClientIP; + /// public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) { diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageCompositeMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageCompositeMatcher.cs index fd2587ab..e403df51 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageCompositeMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageCompositeMatcher.cs @@ -20,6 +20,11 @@ public abstract class RequestMessageCompositeMatcher : IRequestMatcher /// private IEnumerable RequestMatchers { get; } + /// + /// Selected type to choose the matcher from which will immediately return a mismatch. + /// + internal RequestMatcherType? EarlyMatcherType { get; private protected set; } + /// /// Initializes a new instance of the class. /// @@ -31,6 +36,9 @@ public abstract class RequestMessageCompositeMatcher : IRequestMatcher _type = type; } + /// + public RequestMatcherType Type => RequestMatcherType.Composite; + /// 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)); diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageCookieMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageCookieMatcher.cs index 76deae3d..ab8d7d26 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageCookieMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageCookieMatcher.cs @@ -93,6 +93,9 @@ public class RequestMessageCookieMatcher : IRequestMatcher Name = string.Empty; // Not used when Func, but set to a non-null valid value. } + /// + public RequestMatcherType Type => RequestMatcherType.Cookie; + /// public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) { diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageEarlyMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageEarlyMatcher.cs new file mode 100644 index 00000000..91cbcbd2 --- /dev/null +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageEarlyMatcher.cs @@ -0,0 +1,35 @@ +// Copyright © WireMock.Net + +namespace WireMock.Matchers.Request; + +/// +/// Return the mismatch if the matching score of matchers is not perfect. +/// +internal sealed class RequestMessageEarlyMatcher( + RequestMatcherType? earlyMatcherType, + IEnumerable requestMatchers) : IRequestMatcher +{ + /// + public RequestMatcherType Type => RequestMatcherType.Composite; + + /// + 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); + } +} \ No newline at end of file diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageHeaderMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageHeaderMatcher.cs index 614836c8..a81b50a4 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageHeaderMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageHeaderMatcher.cs @@ -106,6 +106,9 @@ public class RequestMessageHeaderMatcher : IRequestMatcher Name = string.Empty; // Not used when Func, but set to a non-null valid value. } + /// + public RequestMatcherType Type => RequestMatcherType.Header; + /// public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) { diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageHttpVersionMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageHttpVersionMatcher.cs index 474f93c3..4bb482af 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageHttpVersionMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageHttpVersionMatcher.cs @@ -65,6 +65,9 @@ public class RequestMessageHttpVersionMatcher : IRequestMatcher MatcherOnStringFunc = Guard.NotNull(func); } + /// + public RequestMatcherType Type => RequestMatcherType.HttpVersion; + /// public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) { diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageMethodMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageMethodMatcher.cs index 49e6397b..e470d716 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageMethodMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageMethodMatcher.cs @@ -46,6 +46,9 @@ internal class RequestMessageMethodMatcher : IRequestMatcher MatchOperator = matchOperator; } + /// + public RequestMatcherType Type => RequestMatcherType.Method; + /// public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) { diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageMultiPartMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageMultiPartMatcher.cs index ea5e53e7..e63acfa0 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageMultiPartMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageMultiPartMatcher.cs @@ -55,6 +55,9 @@ public class RequestMessageMultiPartMatcher : IRequestMatcher MatchOperator = matchOperator; } + /// + public RequestMatcherType Type => RequestMatcherType.MultiPart; + /// public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) { diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageParamMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageParamMatcher.cs index c9ccdce4..f7be7fb1 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageParamMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageParamMatcher.cs @@ -82,6 +82,9 @@ public class RequestMessageParamMatcher : IRequestMatcher Funcs = Guard.NotNull(funcs); } + /// + public RequestMatcherType Type => RequestMatcherType.Param; + /// public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) { diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessagePathMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessagePathMatcher.cs index 29cdeec9..49109a28 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessagePathMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessagePathMatcher.cs @@ -72,6 +72,9 @@ public class RequestMessagePathMatcher : IRequestMatcher Funcs = Guard.NotNull(funcs); } + /// + public RequestMatcherType Type => RequestMatcherType.Path; + /// public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) { diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageScenarioAndStateMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageScenarioAndStateMatcher.cs index f342a767..47a8e6d4 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageScenarioAndStateMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageScenarioAndStateMatcher.cs @@ -29,6 +29,9 @@ internal class RequestMessageScenarioAndStateMatcher : IRequestMatcher _executionConditionState = executionConditionState; } + /// + public RequestMatcherType Type => RequestMatcherType.ScenarioAndState; + /// public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) { diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageUrlMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageUrlMatcher.cs index 0bac8014..466aae11 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageUrlMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageUrlMatcher.cs @@ -72,6 +72,9 @@ public class RequestMessageUrlMatcher : IRequestMatcher Funcs = Guard.NotNull(funcs); } + /// + public RequestMatcherType Type => RequestMatcherType.Url; + /// public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) { diff --git a/src/WireMock.Net.Minimal/RequestBuilders/Request.cs b/src/WireMock.Net.Minimal/RequestBuilders/Request.cs index aac67bab..df224d5d 100644 --- a/src/WireMock.Net.Minimal/RequestBuilders/Request.cs +++ b/src/WireMock.Net.Minimal/RequestBuilders/Request.cs @@ -34,7 +34,7 @@ public partial class Request : RequestMessageCompositeMatcher, IRequestBuilder /// Initializes a new instance of the class. /// /// The request matchers. - private Request(IList requestMatchers) : base(requestMatchers) + internal Request(IList requestMatchers) : base(requestMatchers) { _requestMatchers = Guard.NotNull(requestMatchers); } @@ -81,6 +81,13 @@ public partial class Request : RequestMessageCompositeMatcher, IRequestBuilder return this; } + /// + public IRequestBuilder WithEarlyMismatch(RequestMatcherType? earlyMatcherType) + { + EarlyMatcherType = earlyMatcherType; + return this; + } + internal bool TryGetProtoBufMatcher([NotNullWhen(true)] out IProtoBufMatcher? protoBufMatcher) { protoBufMatcher = GetRequestMessageMatcher()?.Matcher; diff --git a/src/WireMock.Net.Minimal/Serialization/MappingConverter.cs b/src/WireMock.Net.Minimal/Serialization/MappingConverter.cs index f26c4ddf..2981aff7 100644 --- a/src/WireMock.Net.Minimal/Serialization/MappingConverter.cs +++ b/src/WireMock.Net.Minimal/Serialization/MappingConverter.cs @@ -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) @@ -300,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() }; diff --git a/src/WireMock.Net.Minimal/Server/WireMockServer.ConvertMapping.cs b/src/WireMock.Net.Minimal/Server/WireMockServer.ConvertMapping.cs index fa27c1a2..5df902de 100644 --- a/src/WireMock.Net.Minimal/Server/WireMockServer.ConvertMapping.cs +++ b/src/WireMock.Net.Minimal/Server/WireMockServer.ConvertMapping.cs @@ -268,6 +268,8 @@ public partial class WireMockServer } } + requestBuilder = requestBuilder.WithEarlyMismatch(requestModel.EarlyMatcherType); + return requestBuilder; } diff --git a/src/WireMock.Net.Shared/Matchers/Request/RequestMessageGraphQLMatcher.cs b/src/WireMock.Net.Shared/Matchers/Request/RequestMessageGraphQLMatcher.cs index 960ad662..9df12bee 100644 --- a/src/WireMock.Net.Shared/Matchers/Request/RequestMessageGraphQLMatcher.cs +++ b/src/WireMock.Net.Shared/Matchers/Request/RequestMessageGraphQLMatcher.cs @@ -69,6 +69,9 @@ public class RequestMessageGraphQLMatcher : IRequestMatcher MatchOperator = matchOperator; } + /// + public RequestMatcherType Type => RequestMatcherType.GraphQL; + /// public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) { diff --git a/src/WireMock.Net.Shared/Matchers/Request/RequestMessageProtoBufMatcher.cs b/src/WireMock.Net.Shared/Matchers/Request/RequestMessageProtoBufMatcher.cs index 0f378fd4..9f1d5378 100644 --- a/src/WireMock.Net.Shared/Matchers/Request/RequestMessageProtoBufMatcher.cs +++ b/src/WireMock.Net.Shared/Matchers/Request/RequestMessageProtoBufMatcher.cs @@ -30,6 +30,9 @@ public class RequestMessageProtoBufMatcher : IRequestMatcher } } + /// + public RequestMatcherType Type => RequestMatcherType.ProtoBuf; + /// public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) { diff --git a/src/WireMock.Net.Shared/RequestBuilders/IRequestBuilder.cs b/src/WireMock.Net.Shared/RequestBuilders/IRequestBuilder.cs index 9f217d86..fb85f9a3 100644 --- a/src/WireMock.Net.Shared/RequestBuilders/IRequestBuilder.cs +++ b/src/WireMock.Net.Shared/RequestBuilders/IRequestBuilder.cs @@ -10,7 +10,8 @@ namespace WireMock.RequestBuilders; public interface IRequestBuilder : IClientIPRequestBuilder { /// - /// Adds a request matcher to the builder. + /// Adds a request matcher to the builder.
+ /// If the request matcher is already present, it will be replaced. ///
/// The type of the request matcher. /// The request matcher to add. @@ -21,4 +22,11 @@ public interface IRequestBuilder : IClientIPRequestBuilder /// The link back to the Mapping. /// IMapping Mapping { get; set; } + + /// + /// Chooses the which immediately returns a mismatch during mappings enumeration. + /// + /// Selected type to choose the matcher from available list. + /// The current instance. + IRequestBuilder WithEarlyMismatch(RequestMatcherType? earlyMatcherType); } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs b/test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs index 6009e8aa..5c6cf701 100644 --- a/test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs +++ b/test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs @@ -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(); + 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(log => log.Contains("[Error]") && log.Contains("Exception"))), + withEarlyMismatch ? Times.Never : Times.AtLeastOnce); + } + private static WireMockServer Given_When_ServerStarted_And_RunningOnHttpAndGrpc() { var settings = new WireMockServerSettings diff --git a/test/WireMock.Net.Tests/RequestMatchers/RequestMessageCompositeMatcherTests.cs b/test/WireMock.Net.Tests/RequestMatchers/RequestMessageCompositeMatcherTests.cs index 3fcaae75..04d46fdb 100644 --- a/test/WireMock.Net.Tests/RequestMatchers/RequestMessageCompositeMatcherTests.cs +++ b/test/WireMock.Net.Tests/RequestMatchers/RequestMessageCompositeMatcherTests.cs @@ -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 requestMatchers, CompositeMatcherType type = CompositeMatcherType.And) : base(requestMatchers, type) + public Helper( + IEnumerable 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(), It.IsAny()), Times.Once); requestMatcher2Mock.Verify(rm => rm.GetMatchingScore(It.IsAny(), It.IsAny()), Times.Once); } + + [Fact] + public void RequestMessageCompositeMatcher_GetMatchingScore_EarlyMismatch() + { + // Assign + var requestMatcher1Mock = new Mock(); + requestMatcher1Mock.Setup(rm => rm.GetMatchingScore(It.IsAny(), It.IsAny())).Returns(1.0d); + var requestMatcher2Mock = new Mock(); + requestMatcher2Mock.Setup(rm => rm.GetMatchingScore(It.IsAny(), It.IsAny())).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(), It.IsAny()), Times.Never); + requestMatcher2Mock.Verify(rm => rm.GetMatchingScore(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void RequestMessageCompositeMatcher_GetMatchingScore_SeveralHeadersEarlyMismatch() + { + // Assign + var headers = new Dictionary + { + { "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); + } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode.cs b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode.cs index 3d00b532..811b9113 100644 --- a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode.cs +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode.cs @@ -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") diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode_With_Builder_And_AddStartIsFalse.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode_With_Builder_And_AddStartIsFalse.verified.txt index 5a4b9e1b..b74e4e6b 100644 --- a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode_With_Builder_And_AddStartIsFalse.verified.txt +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode_With_Builder_And_AddStartIsFalse.verified.txt @@ -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")) diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode_With_Builder_And_AddStartIsTrue.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode_With_Builder_And_AddStartIsTrue.verified.txt index b85622a1..6cbf645c 100644 --- a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode_With_Builder_And_AddStartIsTrue.verified.txt +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode_With_Builder_And_AddStartIsTrue.verified.txt @@ -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")) diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode_With_Server_And_AddStartIsFalse.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode_With_Server_And_AddStartIsFalse.verified.txt index 80754949..f68a1a55 100644 --- a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode_With_Server_And_AddStartIsFalse.verified.txt +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode_With_Server_And_AddStartIsFalse.verified.txt @@ -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")) diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode_With_Server_And_AddStartIsTrue.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode_With_Server_And_AddStartIsTrue.verified.txt index a2589a27..887b143c 100644 --- a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode_With_Server_And_AddStartIsTrue.verified.txt +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode_With_Server_And_AddStartIsTrue.verified.txt @@ -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")) diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithBodyAsGraphQLSchema_ReturnsCorrectModel.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithBodyAsGraphQLSchema_ReturnsCorrectModel.verified.txt index a0c07a93..c4f97f09 100644 --- a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithBodyAsGraphQLSchema_ReturnsCorrectModel.verified.txt +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithBodyAsGraphQLSchema_ReturnsCorrectModel.verified.txt @@ -19,7 +19,7 @@ id:ID! firstName:String lastName:String - fullName:String + fullName:String } } } diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithEarlyMismatch_ReturnsCorrectModel.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithEarlyMismatch_ReturnsCorrectModel.verified.txt new file mode 100644 index 00000000..b15a7c89 --- /dev/null +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithEarlyMismatch_ReturnsCorrectModel.verified.txt @@ -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 +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs index e1dd5e42..0986a637 100644 --- a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs @@ -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); + } } \ No newline at end of file