From 3353be65b5b7bc20a323c4abc510097389ff0bba Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 27 Jul 2024 14:40:23 +0200 Subject: [PATCH] Add FormUrlEncodedMatcher (#1147) * FormUrlEncodedMatcher * . * Fix * new * support wildcard --- .../Matchers/FormUrlEncodedMatcher.cs | 153 ++++++++++++++++++ .../Serialization/MappingConverter.cs | 25 +-- .../Serialization/MatcherMapper.cs | 7 +- ...ppingBuilderTests.GetMappings.verified.txt | 76 ++++++++- ...derTests.ToCSharpCode_Builder.verified.txt | 30 +++- ...lderTests.ToCSharpCode_Server.verified.txt | 30 +++- .../MappingBuilderTests.ToJson.verified.txt | 74 ++++++++- .../WireMock.Net.Tests/MappingBuilderTests.cs | 29 ++-- .../Matchers/FormUrlEncodedMatcherTests.cs | 78 +++++++++ .../WireMockServerTests.WithBody.cs | 58 +++++++ 10 files changed, 530 insertions(+), 30 deletions(-) create mode 100644 src/WireMock.Net/Matchers/FormUrlEncodedMatcher.cs create mode 100644 test/WireMock.Net.Tests/Matchers/FormUrlEncodedMatcherTests.cs diff --git a/src/WireMock.Net/Matchers/FormUrlEncodedMatcher.cs b/src/WireMock.Net/Matchers/FormUrlEncodedMatcher.cs new file mode 100644 index 000000000..decbb1d82 --- /dev/null +++ b/src/WireMock.Net/Matchers/FormUrlEncodedMatcher.cs @@ -0,0 +1,153 @@ +// Copyright © WireMock.Net + +using System.Collections.Generic; +using AnyOfTypes; +using Stef.Validation; +using WireMock.Models; +using WireMock.Util; + +namespace WireMock.Matchers; + +/// +/// FormUrl Encoded fields Matcher +/// +/// +/// +public class FormUrlEncodedMatcher : IStringMatcher, IIgnoreCaseMatcher +{ + private readonly AnyOf[] _patterns; + + /// + public MatchBehaviour MatchBehaviour { get; } + + private readonly List<(WildcardMatcher Key, WildcardMatcher? Value)> _pairs = []; + + /// + /// Initializes a new instance of the class. + /// + /// The pattern. + /// Ignore the case from the pattern. + /// The to use. (default = "Or") + public FormUrlEncodedMatcher( + AnyOf pattern, + bool ignoreCase = false, + MatchOperator matchOperator = MatchOperator.Or) : + this(MatchBehaviour.AcceptOnMatch, [pattern], ignoreCase, matchOperator) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The match behaviour. + /// The pattern. + /// Ignore the case from the pattern. + /// The to use. (default = "Or") + public FormUrlEncodedMatcher( + MatchBehaviour matchBehaviour, + AnyOf pattern, + bool ignoreCase = false, + MatchOperator matchOperator = MatchOperator.Or) : + this(matchBehaviour, [pattern], ignoreCase, matchOperator) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The patterns. + /// Ignore the case from the pattern. + /// The to use. (default = "Or") + public FormUrlEncodedMatcher( + AnyOf[] patterns, + bool ignoreCase = false, + MatchOperator matchOperator = MatchOperator.Or) : + this(MatchBehaviour.AcceptOnMatch, patterns, ignoreCase, matchOperator) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The match behaviour. + /// The patterns. + /// Ignore the case from the pattern. + /// The to use. (default = "Or") + public FormUrlEncodedMatcher( + MatchBehaviour matchBehaviour, + AnyOf[] patterns, + bool ignoreCase = false, + MatchOperator matchOperator = MatchOperator.Or) + { + _patterns = Guard.NotNull(patterns); + IgnoreCase = ignoreCase; + MatchBehaviour = matchBehaviour; + MatchOperator = matchOperator; + + foreach (var pattern in _patterns) + { + if (QueryStringParser.TryParse(pattern, IgnoreCase, out var nameValueCollection)) + { + foreach (var nameValue in nameValueCollection) + { + var keyMatcher = new WildcardMatcher(MatchBehaviour.AcceptOnMatch, [nameValue.Key], ignoreCase, MatchOperator); + var valueMatcher = new WildcardMatcher(MatchBehaviour.AcceptOnMatch, [nameValue.Value], ignoreCase, MatchOperator); + _pairs.Add((keyMatcher, valueMatcher)); + } + } + } + } + + /// + public MatchResult IsMatch(string? input) + { + // Input is null or empty and if no patterns defined, return Perfect match. + if (string.IsNullOrEmpty(input) && _patterns.Length == 0) + { + return new MatchResult(MatchScores.Perfect); + } + + if (!QueryStringParser.TryParse(input, IgnoreCase, out var inputNameValueCollection)) + { + return new MatchResult(MatchScores.Mismatch); + } + + var matches = new List(); + foreach (var inputKeyValuePair in inputNameValueCollection) + { + var match = false; + foreach (var pair in _pairs) + { + var keyMatchResult = pair.Key.IsMatch(inputKeyValuePair.Key).IsPerfect(); + if (keyMatchResult) + { + match = pair.Value?.IsMatch(inputKeyValuePair.Value).IsPerfect() ?? false; + if (match) + { + break; + } + } + } + + matches.Add(match); + } + + var score = MatchScores.ToScore(matches.ToArray(), MatchOperator); + return new MatchResult(MatchBehaviourHelper.Convert(MatchBehaviour, score)); + } + + /// + public virtual AnyOf[] GetPatterns() + { + return _patterns; + } + + /// + public virtual string Name => nameof(FormUrlEncodedMatcher); + + /// + public bool IgnoreCase { get; } + + /// + public MatchOperator MatchOperator { get; } +} \ No newline at end of file diff --git a/src/WireMock.Net/Serialization/MappingConverter.cs b/src/WireMock.Net/Serialization/MappingConverter.cs index 98e6011aa..b67175a94 100644 --- a/src/WireMock.Net/Serialization/MappingConverter.cs +++ b/src/WireMock.Net/Serialization/MappingConverter.cs @@ -147,19 +147,22 @@ public string ToCSharpCode(IMapping mapping, MappingConverterSettings? settings { var firstMatcher = requestMessageBodyMatcher.Matchers.FirstOrDefault(); - if (firstMatcher is WildcardMatcher wildcardMatcher && wildcardMatcher.GetPatterns().Any()) + switch (firstMatcher) { - sb.AppendLine($" .WithBody({GetString(wildcardMatcher)})"); - } + case IStringMatcher stringMatcher when stringMatcher.GetPatterns().Length > 0: + sb.AppendLine($" .WithBody({GetString(stringMatcher)})"); + break; - if (firstMatcher is JsonMatcher jsonMatcher) - { - var matcherType = jsonMatcher.GetType().Name; - sb.AppendLine($" .WithBody(new {matcherType}("); - sb.AppendLine($" value: {ConvertToAnonymousObjectDefinition(jsonMatcher.Value, 3)},"); - sb.AppendLine($" ignoreCase: {ToCSharpBooleanLiteral(jsonMatcher.IgnoreCase)},"); - sb.AppendLine($" regex: {ToCSharpBooleanLiteral(jsonMatcher.Regex)}"); - sb.AppendLine(@" ))"); + case JsonMatcher jsonMatcher: + { + var matcherType = jsonMatcher.GetType().Name; + sb.AppendLine($" .WithBody(new {matcherType}("); + sb.AppendLine($" value: {ConvertToAnonymousObjectDefinition(jsonMatcher.Value, 3)},"); + sb.AppendLine($" ignoreCase: {ToCSharpBooleanLiteral(jsonMatcher.IgnoreCase)},"); + sb.AppendLine($" regex: {ToCSharpBooleanLiteral(jsonMatcher.Regex)}"); + sb.AppendLine(@" ))"); + break; + } } } diff --git a/src/WireMock.Net/Serialization/MatcherMapper.cs b/src/WireMock.Net/Serialization/MatcherMapper.cs index 370c2a572..b4e661946 100644 --- a/src/WireMock.Net/Serialization/MatcherMapper.cs +++ b/src/WireMock.Net/Serialization/MatcherMapper.cs @@ -111,6 +111,9 @@ public MatcherMapper(WireMockServerSettings settings) case nameof(ContentTypeMatcher): return new ContentTypeMatcher(matchBehaviour, stringPatterns, ignoreCase); + case nameof(FormUrlEncodedMatcher): + return new FormUrlEncodedMatcher(matchBehaviour, stringPatterns, ignoreCase); + case nameof(SimMetricsMatcher): SimMetricType type = SimMetricType.Levenstein; if (!string.IsNullOrEmpty(matcherType) && !Enum.TryParse(matcherType, out type)) @@ -224,7 +227,7 @@ private AnyOf[] ParseStringPatterns(MatcherModel matcher) { if (matcher.Pattern is string patternAsString) { - return new[] { new AnyOf(patternAsString) }; + return [new AnyOf(patternAsString)]; } if (matcher.Pattern is IEnumerable patternAsStringArray) @@ -241,7 +244,7 @@ private AnyOf[] ParseStringPatterns(MatcherModel matcher) { var patternAsFile = matcher.PatternAsFile!; var pattern = _settings.FileSystemHandler.ReadFileAsString(patternAsFile); - return new[] { new AnyOf(new StringPattern { Pattern = pattern, PatternAsFile = patternAsFile }) }; + return [new AnyOf(new StringPattern { Pattern = pattern, PatternAsFile = patternAsFile })]; } return EmptyArray>.Value; diff --git a/test/WireMock.Net.Tests/MappingBuilderTests.GetMappings.verified.txt b/test/WireMock.Net.Tests/MappingBuilderTests.GetMappings.verified.txt index 8812a2fff..5c5c98c8c 100644 --- a/test/WireMock.Net.Tests/MappingBuilderTests.GetMappings.verified.txt +++ b/test/WireMock.Net.Tests/MappingBuilderTests.GetMappings.verified.txt @@ -1,6 +1,6 @@ [ { - Guid: Guid_1, + Guid: 41372914-1838-4c67-916b-b9aacdd096ce, UpdatedAt: 2023-01-14 15:16:17, Request: { Path: { @@ -33,7 +33,7 @@ } }, { - Guid: Guid_2, + Guid: 98fae52e-76df-47d9-876f-2ee32e931002, UpdatedAt: 2023-01-14 15:16:17, Request: { Path: { @@ -61,5 +61,77 @@ } }, Response: {} + }, + { + Guid: 98fae52e-76df-47d9-876f-2ee32e931003, + UpdatedAt: 2023-01-14 15:16:17, + Request: { + Path: { + Matchers: [ + { + Name: WildcardMatcher, + Pattern: /form-urlencoded, + IgnoreCase: false + } + ] + }, + Methods: [ + POST + ], + Headers: [ + { + Name: Content-Type, + Matchers: [ + { + Name: WildcardMatcher, + Pattern: application/x-www-form-urlencoded, + IgnoreCase: true + } + ], + IgnoreCase: true + } + ], + Body: { + Matcher: { + Name: FormUrlEncodedMatcher, + Patterns: [ + name=John Doe, + email=johndoe@example.com + ], + IgnoreCase: false, + MatchOperator: Or + } + } + }, + Response: {} + }, + { + Guid: 98fae52e-76df-47d9-876f-2ee32e931001, + UpdatedAt: 2023-01-14 15:16:17, + Request: { + Path: { + Matchers: [ + { + Name: WildcardMatcher, + Pattern: /users/post1, + IgnoreCase: false + } + ] + }, + Methods: [ + POST + ], + Body: { + Matcher: { + Name: JsonMatcher, + Pattern: { + Request: Hello? + }, + IgnoreCase: false, + Regex: false + } + } + }, + Response: {} } ] \ No newline at end of file diff --git a/test/WireMock.Net.Tests/MappingBuilderTests.ToCSharpCode_Builder.verified.txt b/test/WireMock.Net.Tests/MappingBuilderTests.ToCSharpCode_Builder.verified.txt index be439d3bb..17ef2023b 100644 --- a/test/WireMock.Net.Tests/MappingBuilderTests.ToCSharpCode_Builder.verified.txt +++ b/test/WireMock.Net.Tests/MappingBuilderTests.ToCSharpCode_Builder.verified.txt @@ -24,7 +24,35 @@ builder regex: false )) ) - .WithGuid("98fae52e-76df-47d9-876f-2ee32e931d9b") + .WithGuid("98fae52e-76df-47d9-876f-2ee32e931002") + .RespondWith(Response.Create() + ); + +builder + .Given(Request.Create() + .UsingMethod("POST") + .WithPath("/form-urlencoded") + .WithHeader("Content-Type", "application/x-www-form-urlencoded", true) + .WithBody("name=John Doe") + ) + .WithGuid("98fae52e-76df-47d9-876f-2ee32e931003") + .RespondWith(Response.Create() + ); + +builder + .Given(Request.Create() + .UsingMethod("POST") + .WithPath("/users/post1") + .WithBody(new JsonMatcher( + value: new + { + Request = "Hello?" + }, + ignoreCase: false, + regex: false + )) + ) + .WithGuid("98fae52e-76df-47d9-876f-2ee32e931001") .RespondWith(Response.Create() ); diff --git a/test/WireMock.Net.Tests/MappingBuilderTests.ToCSharpCode_Server.verified.txt b/test/WireMock.Net.Tests/MappingBuilderTests.ToCSharpCode_Server.verified.txt index 6091acbd2..c9e10fa18 100644 --- a/test/WireMock.Net.Tests/MappingBuilderTests.ToCSharpCode_Server.verified.txt +++ b/test/WireMock.Net.Tests/MappingBuilderTests.ToCSharpCode_Server.verified.txt @@ -24,7 +24,35 @@ server regex: false )) ) - .WithGuid("98fae52e-76df-47d9-876f-2ee32e931d9b") + .WithGuid("98fae52e-76df-47d9-876f-2ee32e931002") + .RespondWith(Response.Create() + ); + +server + .Given(Request.Create() + .UsingMethod("POST") + .WithPath("/form-urlencoded") + .WithHeader("Content-Type", "application/x-www-form-urlencoded", true) + .WithBody("name=John Doe") + ) + .WithGuid("98fae52e-76df-47d9-876f-2ee32e931003") + .RespondWith(Response.Create() + ); + +server + .Given(Request.Create() + .UsingMethod("POST") + .WithPath("/users/post1") + .WithBody(new JsonMatcher( + value: new + { + Request = "Hello?" + }, + ignoreCase: false, + regex: false + )) + ) + .WithGuid("98fae52e-76df-47d9-876f-2ee32e931001") .RespondWith(Response.Create() ); diff --git a/test/WireMock.Net.Tests/MappingBuilderTests.ToJson.verified.txt b/test/WireMock.Net.Tests/MappingBuilderTests.ToJson.verified.txt index bf66f9df4..20c083465 100644 --- a/test/WireMock.Net.Tests/MappingBuilderTests.ToJson.verified.txt +++ b/test/WireMock.Net.Tests/MappingBuilderTests.ToJson.verified.txt @@ -1,6 +1,6 @@ [ { - Guid: Guid_1, + Guid: 41372914-1838-4c67-916b-b9aacdd096ce, UpdatedAt: 2023-01-14T15:16:17, Request: { Path: { @@ -33,7 +33,7 @@ } }, { - Guid: Guid_2, + Guid: 98fae52e-76df-47d9-876f-2ee32e931002, UpdatedAt: 2023-01-14T15:16:17, Request: { Path: { @@ -60,5 +60,75 @@ } } } + }, + { + Guid: 98fae52e-76df-47d9-876f-2ee32e931003, + UpdatedAt: 2023-01-14T15:16:17, + Request: { + Path: { + Matchers: [ + { + Name: WildcardMatcher, + Pattern: /form-urlencoded, + IgnoreCase: false + } + ] + }, + Methods: [ + POST + ], + Headers: [ + { + Name: Content-Type, + Matchers: [ + { + Name: WildcardMatcher, + Pattern: application/x-www-form-urlencoded, + IgnoreCase: true + } + ], + IgnoreCase: true + } + ], + Body: { + Matcher: { + Name: FormUrlEncodedMatcher, + Patterns: [ + name=John Doe, + email=johndoe@example.com + ], + IgnoreCase: false, + MatchOperator: Or + } + } + } + }, + { + Guid: 98fae52e-76df-47d9-876f-2ee32e931001, + UpdatedAt: 2023-01-14T15:16:17, + Request: { + Path: { + Matchers: [ + { + Name: WildcardMatcher, + Pattern: /users/post1, + IgnoreCase: false + } + ] + }, + Methods: [ + POST + ], + Body: { + Matcher: { + Name: JsonMatcher, + Pattern: { + Request: Hello? + }, + IgnoreCase: false, + Regex: false + } + } + } } ] \ No newline at end of file diff --git a/test/WireMock.Net.Tests/MappingBuilderTests.cs b/test/WireMock.Net.Tests/MappingBuilderTests.cs index 0b798c8ec..6a62af2f0 100644 --- a/test/WireMock.Net.Tests/MappingBuilderTests.cs +++ b/test/WireMock.Net.Tests/MappingBuilderTests.cs @@ -30,7 +30,6 @@ static MappingBuilderTests() VerifySettings.Init(); } - private static readonly Guid NewGuid = new("98fae52e-76df-47d9-876f-2ee32e931d9b"); private const string MappingGuid = "41372914-1838-4c67-916b-b9aacdd096ce"; private static readonly DateTime UtcNow = new(2023, 1, 14, 15, 16, 17); @@ -43,7 +42,8 @@ public MappingBuilderTests() _fileSystemHandlerMock = new Mock(); var guidUtilsMock = new Mock(); - guidUtilsMock.Setup(g => g.NewGuid()).Returns(NewGuid); + var startGuid = 1000; + guidUtilsMock.Setup(g => g.NewGuid()).Returns(() => new Guid($"98fae52e-76df-47d9-876f-2ee32e93{startGuid++}")); var dateTimeUtilsMock = new Mock(); dateTimeUtilsMock.SetupGet(d => d.UtcNow).Returns(UtcNow); @@ -95,6 +95,13 @@ public MappingBuilderTests() country = "The Netherlands" })) ).RespondWith(Response.Create()); + + _sut.Given(Request.Create() + .UsingPost() + .WithPath("/form-urlencoded") + .WithHeader("Content-Type", "application/x-www-form-urlencoded") + .WithBody(new FormUrlEncodedMatcher(["name=John Doe", "email=johndoe@example.com"])) + ).RespondWith(Response.Create()); } [Fact] @@ -104,7 +111,7 @@ public Task GetMappings() var mappings = _sut.GetMappings(); // Verify - return Verifier.Verify(mappings, VerifySettings); + return Verifier.Verify(mappings, VerifySettings).DontScrubGuids(); } [Fact] @@ -114,7 +121,7 @@ public Task ToJson() var json = _sut.ToJson(); // Verify - return Verifier.VerifyJson(json, VerifySettings); + return Verifier.VerifyJson(json, VerifySettings).DontScrubGuids(); } [Fact] @@ -124,7 +131,7 @@ public Task ToCSharpCode_Server() var code = _sut.ToCSharpCode(MappingConverterType.Server); // Verify - return Verifier.Verify(code, VerifySettings); + return Verifier.Verify(code, VerifySettings).DontScrubGuids(); } [Fact] @@ -134,7 +141,7 @@ public Task ToCSharpCode_Builder() var code = _sut.ToCSharpCode(MappingConverterType.Builder); // Verify - return Verifier.Verify(code, VerifySettings); + return Verifier.Verify(code, VerifySettings).DontScrubGuids(); } [Fact] @@ -183,9 +190,9 @@ public void SaveMappingsToFolder_FolderIsNull() _sut.SaveMappingsToFolder(null); // Verify - _fileSystemHandlerMock.Verify(fs => fs.GetMappingFolder(), Times.Exactly(2)); - _fileSystemHandlerMock.Verify(fs => fs.FolderExists(mappingFolder), Times.Exactly(2)); - _fileSystemHandlerMock.Verify(fs => fs.WriteMappingFile(It.IsAny(), It.IsAny()), Times.Exactly(2)); + _fileSystemHandlerMock.Verify(fs => fs.GetMappingFolder(), Times.Exactly(4)); + _fileSystemHandlerMock.Verify(fs => fs.FolderExists(mappingFolder), Times.Exactly(4)); + _fileSystemHandlerMock.Verify(fs => fs.WriteMappingFile(It.IsAny(), It.IsAny()), Times.Exactly(4)); _fileSystemHandlerMock.VerifyNoOtherCalls(); } @@ -201,8 +208,8 @@ public void SaveMappingsToFolder_FolderExists_IsTrue() // Verify _fileSystemHandlerMock.Verify(fs => fs.GetMappingFolder(), Times.Never); - _fileSystemHandlerMock.Verify(fs => fs.FolderExists(path), Times.Exactly(2)); - _fileSystemHandlerMock.Verify(fs => fs.WriteMappingFile(It.IsAny(), It.IsAny()), Times.Exactly(2)); + _fileSystemHandlerMock.Verify(fs => fs.FolderExists(path), Times.Exactly(4)); + _fileSystemHandlerMock.Verify(fs => fs.WriteMappingFile(It.IsAny(), It.IsAny()), Times.Exactly(4)); _fileSystemHandlerMock.VerifyNoOtherCalls(); } } diff --git a/test/WireMock.Net.Tests/Matchers/FormUrlEncodedMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/FormUrlEncodedMatcherTests.cs new file mode 100644 index 000000000..712b4ece5 --- /dev/null +++ b/test/WireMock.Net.Tests/Matchers/FormUrlEncodedMatcherTests.cs @@ -0,0 +1,78 @@ +// Copyright © WireMock.Net + +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using AnyOfTypes; +using FluentAssertions; +using WireMock.Matchers; +using WireMock.Models; +using Xunit; + +namespace WireMock.Net.Tests.Matchers; + +public class FormUrlEncodedMatcherTest +{ + [Theory] + [InlineData("*=*")] + [InlineData("name=John Doe")] + [InlineData("name=*")] + [InlineData("*=John Doe")] + [InlineData("email=johndoe@example.com")] + [InlineData("email=*")] + [InlineData("*=johndoe@example.com")] + [InlineData("name=John Doe", "email=johndoe@example.com")] + [InlineData("name=John Doe", "email=*")] + [InlineData("name=*", "email=*")] + [InlineData("*=John Doe", "*=johndoe@example.com")] + public async Task FormUrlEncodedMatcher_IsMatch(params string[] patterns) + { + // Arrange + var content = new FormUrlEncodedContent(new[] + { + new KeyValuePair("name", "John Doe"), + new KeyValuePair("email", "johndoe@example.com") + }); + var contentAsString = await content.ReadAsStringAsync(); + + var matcher = new FormUrlEncodedMatcher(patterns.Select(p => new AnyOf(p)).ToArray()); + + // Act + var score = matcher.IsMatch(contentAsString).IsPerfect(); + + // Assert + score.Should().BeTrue(); + } + + [Theory] + [InlineData(false, "name=John Doe")] + [InlineData(false, "name=*")] + [InlineData(false, "*=John Doe")] + [InlineData(false, "email=johndoe@example.com")] + [InlineData(false, "email=*")] + [InlineData(false, "*=johndoe@example.com")] + [InlineData(true, "name=John Doe", "email=johndoe@example.com")] + [InlineData(true, "name=John Doe", "email=*")] + [InlineData(true, "name=*", "email=*")] + [InlineData(true, "*=John Doe", "*=johndoe@example.com")] + [InlineData(true, "*=*")] + public async Task FormUrlEncodedMatcher_IsMatch_And(bool expected, params string[] patterns) + { + // Arrange + var content = new FormUrlEncodedContent(new[] + { + new KeyValuePair("name", "John Doe"), + new KeyValuePair("email", "johndoe@example.com") + }); + var contentAsString = await content.ReadAsStringAsync(); + + var matcher = new FormUrlEncodedMatcher(patterns.Select(p => new AnyOf(p)).ToArray(), true, MatchOperator.And); + + // Act + var score = matcher.IsMatch(contentAsString).IsPerfect(); + + // Assert + score.Should().Be(expected); + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/WireMockServerTests.WithBody.cs b/test/WireMock.Net.Tests/WireMockServerTests.WithBody.cs index 525e76880..3bcfbaf65 100644 --- a/test/WireMock.Net.Tests/WireMockServerTests.WithBody.cs +++ b/test/WireMock.Net.Tests/WireMockServerTests.WithBody.cs @@ -225,5 +225,63 @@ public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_Wi server.Stop(); } + + [Fact] + public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithFormUrlEncodedMatcher() + { + // Arrange + var matcher = new FormUrlEncodedMatcher(["email=johndoe@example.com", "name=John Doe"]); + var server = WireMockServer.Start(); + server.Given( + Request.Create() + .UsingPost() + .WithPath("/foo") + .WithHeader("Content-Type", "application/x-www-form-urlencoded") + .WithBody(matcher) + ) + .RespondWith( + Response.Create() + ); + + server.Given( + Request.Create() + .UsingPost() + .WithPath("/bar") + .WithHeader("Content-Type", "application/x-www-form-urlencoded") + .WithBody(matcher) + ) + .RespondWith( + Response.Create() + ); + + // Act 1 + var contentOrdered = new FormUrlEncodedContent(new[] + { + new KeyValuePair("name", "John Doe"), + new KeyValuePair("email", "johndoe@example.com") + }); + var responseOrdered = await new HttpClient() + .PostAsync($"{server.Url}/foo", contentOrdered) + .ConfigureAwait(false); + + // Assert 1 + responseOrdered.StatusCode.Should().Be(HttpStatusCode.OK); + + + // Act 2 + var contentUnordered = new FormUrlEncodedContent(new[] + { + new KeyValuePair("email", "johndoe@example.com"), + new KeyValuePair("name", "John Doe"), + }); + var responseUnordered = await new HttpClient() + .PostAsync($"{server.Url}/bar", contentUnordered) + .ConfigureAwait(false); + + // Assert 2 + responseUnordered.StatusCode.Should().Be(HttpStatusCode.OK); + + server.Stop(); + } } #endif \ No newline at end of file