diff --git a/src/WireMock.Net.Abstractions/Models/IBodyData.cs b/src/WireMock.Net.Abstractions/Models/IBodyData.cs index 6eb4c5d8..e293689c 100644 --- a/src/WireMock.Net.Abstractions/Models/IBodyData.cs +++ b/src/WireMock.Net.Abstractions/Models/IBodyData.cs @@ -46,7 +46,7 @@ public interface IBodyData /// /// The body as Form UrlEncoded dictionary. /// - IDictionary? BodyAsFormUrlEncoded { get; set; } + IDictionary>? BodyAsFormUrlEncoded { get; set; } /// /// The detected body type (detection based on body content). diff --git a/src/WireMock.Net.Abstractions/Types/WireMockList.cs b/src/WireMock.Net.Abstractions/Types/WireMockList.cs index de7d0f29..cdb4dedd 100644 --- a/src/WireMock.Net.Abstractions/Types/WireMockList.cs +++ b/src/WireMock.Net.Abstractions/Types/WireMockList.cs @@ -46,6 +46,98 @@ public class WireMockList : List /// The values to set. public static implicit operator WireMockList(T[] values) => new(values); + /// + /// Operator for equality comparison from WireMockList to T + /// + public static bool operator ==(WireMockList? left, T? right) + { + if (ReferenceEquals(left, right)) + { + return true; + } + + if (left?.Count == 1 && Equals(left[0], right)) + { + return true; + } + + return false; + } + + /// + /// Operator for equality comparison from T to WireMockList + /// + public static bool operator ==(T? left, WireMockList? right) + { + if (ReferenceEquals(left, right)) + { + return true; + } + + if (right?.Count == 1 && Equals(left, right[0])) + { + return true; + } + + return false; + } + + /// + /// Operator for inequality comparison from WireMockList to T + /// + public static bool operator !=(WireMockList? left, T? right) => !(left == right); + + /// + /// Operator for inequality comparison from T to WireMockList + /// + public static bool operator !=(T? left, WireMockList? right) => !(left == right); + + /// + /// Determines whether the specified object is equal to the current instance. + /// Two instances are equal if they contain the same elements in the same order. + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is WireMockList other) + { + if (Count != other.Count) + { + return false; + } + + for (var i = 0; i < Count; i++) + { + if (!Equals(this[i], other[i])) + { + return false; + } + } + + return true; + } + + return false; + } + + /// + /// Returns a hash code for this instance based on its elements. + /// + public override int GetHashCode() + { + var hashCode = 17; + foreach (var item in this) + { + hashCode = hashCode * 31 + (item?.GetHashCode() ?? 0); + } + + return hashCode; + } + /// /// Returns a that represents this instance. /// @@ -61,7 +153,7 @@ public class WireMockList : List { return strValue; } - + return this[0]?.ToString() ?? string.Empty; default: diff --git a/src/WireMock.Net.Minimal/Matchers/FormUrlEncodedMatcher.cs b/src/WireMock.Net.Minimal/Matchers/FormUrlEncodedMatcher.cs index e83bafc4..02ef334e 100644 --- a/src/WireMock.Net.Minimal/Matchers/FormUrlEncodedMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/FormUrlEncodedMatcher.cs @@ -1,10 +1,10 @@ // Copyright © WireMock.Net -using System.Linq; using AnyOfTypes; using Stef.Validation; using WireMock.Extensions; using WireMock.Models; +using WireMock.Types; using WireMock.Util; namespace WireMock.Matchers; @@ -21,7 +21,7 @@ public class FormUrlEncodedMatcher : IStringMatcher, IIgnoreCaseMatcher /// public MatchBehaviour MatchBehaviour { get; } - private readonly List<(WildcardMatcher Key, WildcardMatcher? Value)> _pairs = []; + private readonly List<(WildcardMatcher Key, WildcardMatcher[]? Values)> KeyValueMatchers = []; /// /// Initializes a new instance of the class. @@ -91,9 +91,10 @@ public class FormUrlEncodedMatcher : IStringMatcher, IIgnoreCaseMatcher { 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)); + var keyMatcher = new WildcardMatcher(MatchBehaviour.AcceptOnMatch, nameValue.Key, ignoreCase); + var valueMatchers = nameValue.Value.Select(value => new WildcardMatcher(MatchBehaviour.AcceptOnMatch, value, ignoreCase)).ToArray(); + + KeyValueMatchers.Add((keyMatcher, valueMatchers)); } } } @@ -116,37 +117,51 @@ public class FormUrlEncodedMatcher : IStringMatcher, IIgnoreCaseMatcher var matches = GetMatches(inputNameValueCollection); var score = MatchScores.ToScore(matches, MatchOperator); - return MatchResult.From(Name, MatchBehaviourHelper.Convert(MatchBehaviour, score)); + return MatchResult.From(Name, score); } - private bool[] GetMatches(IDictionary inputNameValueCollection) + private List GetMatches(IDictionary> inputNameValueCollection) { - var matches = new List(); - if (_pairs.Count > inputNameValueCollection.Count) + var inputPairs = inputNameValueCollection.ToArray(); + var rowCount = inputPairs.Length; + var columnCount = KeyValueMatchers.Count; + + if (rowCount == 0 && columnCount == 0) { - matches.AddRange(Enumerable.Repeat(false, _pairs.Count - inputNameValueCollection.Count)); + return []; } - foreach (var inputKeyValuePair in inputNameValueCollection) + var matrix = new double[rowCount][]; + + for (var row = 0; row < rowCount; row++) { - var match = false; - foreach (var pair in _pairs) + matrix[row] = new double[columnCount]; + + var inputKeyValuePair = inputPairs[row]; + var inputKey = inputKeyValuePair.Key; + var inputValues = inputKeyValuePair.Value; + + for (var column = 0; column < columnCount; column++) { - var keyMatchResult = pair.Key.IsMatch(inputKeyValuePair.Key).IsPerfect(); - if (keyMatchResult) - { - match = pair.Value?.IsMatch(inputKeyValuePair.Value).IsPerfect() ?? false; - if (match) - { - break; - } - } - } + var (keyMatcher, valuesMatchers) = KeyValueMatchers[column]; - matches.Add(match); + var keyScore = keyMatcher.IsMatch(inputKey).Score; + var valueScore = valuesMatchers != null ? MatchScores.ToScore(inputValues, valuesMatchers) : MatchScores.Perfect; + + matrix[row][column] = Math.Min(keyScore, valueScore); + } } - return matches.ToArray(); + var rowScores = rowCount == 0 ? [] : matrix.Select(row => row.Length == 0 ? MatchScores.Mismatch : row.Max()).ToList(); + + var columnScores = new List(); + for (var column = 0; column < columnCount; column++) + { + columnScores.Add(rowCount == 0 ? MatchScores.Mismatch : matrix.Max(row => row[column])); + } + + rowScores.AddRange(columnScores); + return rowScores; } /// diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageBodyMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageBodyMatcher.cs index 71c01f8e..818cd494 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageBodyMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageBodyMatcher.cs @@ -2,6 +2,7 @@ using Stef.Validation; using WireMock.Matchers.Helpers; +using WireMock.Types; using WireMock.Util; namespace WireMock.Matchers.Request; @@ -34,7 +35,7 @@ public class RequestMessageBodyMatcher : IRequestMatcher /// /// The body data function for FormUrlEncoded /// - public Func?, bool>? MatchOnBodyAsFormUrlEncodedFunc { get; } + public Func>?, bool>? MatchOnBodyAsFormUrlEncodedFunc { get; } /// /// The matchers. @@ -116,7 +117,7 @@ public class RequestMessageBodyMatcher : IRequestMatcher /// Initializes a new instance of the class. /// /// The function. - public RequestMessageBodyMatcher(Func?, bool> func) + public RequestMessageBodyMatcher(Func>?, bool> func) { MatchOnBodyAsFormUrlEncodedFunc = Guard.NotNull(func); } diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageParamMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageParamMatcher.cs index 5ca2f6d8..70d9563a 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageParamMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageParamMatcher.cs @@ -115,36 +115,6 @@ public class RequestMessageParamMatcher : IRequestMatcher } // Return the score based on Matchers and valuesPresentInRequestMessage - return CalculateScore(Matchers, valuesPresentInRequestMessage); - } - - private static double CalculateScore(IReadOnlyList matchers, WireMockList valuesPresentInRequestMessage) - { - var total = new List(); - - // If the total patterns in all matchers > values in message, use the matcher as base - if (matchers.Sum(m => m.GetPatterns().Length) > valuesPresentInRequestMessage.Count) - { - foreach (var matcher in matchers) - { - double score = 0d; - foreach (string valuePresentInRequestMessage in valuesPresentInRequestMessage) - { - score += matcher.IsMatch(valuePresentInRequestMessage).Score / matcher.GetPatterns().Length; - } - - total.Add(score); - } - } - else - { - foreach (string valuePresentInRequestMessage in valuesPresentInRequestMessage) - { - var score = matchers.Max(m => m.IsMatch(valuePresentInRequestMessage).Score); - total.Add(score); - } - } - - return total.Any() ? MatchScores.ToScore(total, MatchOperator.Average) : 0; + return MatchScores.ToScore(valuesPresentInRequestMessage, Matchers.ToArray()); } } \ No newline at end of file diff --git a/src/WireMock.Net.Minimal/RequestBuilders/Request.WithBody.cs b/src/WireMock.Net.Minimal/RequestBuilders/Request.WithBody.cs index 20658d98..fbde536c 100644 --- a/src/WireMock.Net.Minimal/RequestBuilders/Request.WithBody.cs +++ b/src/WireMock.Net.Minimal/RequestBuilders/Request.WithBody.cs @@ -5,6 +5,7 @@ using Stef.Validation; using WireMock.Matchers; using WireMock.Matchers.Request; +using WireMock.Types; using WireMock.Util; namespace WireMock.RequestBuilders; @@ -84,7 +85,7 @@ public partial class Request } /// - public IRequestBuilder WithBody(Func?, bool> func) + public IRequestBuilder WithBody(Func>?, bool> func) { _requestMatchers.Add(new RequestMessageBodyMatcher(Guard.NotNull(func))); return this; diff --git a/src/WireMock.Net.Shared/Extensions/AnyOfExtensions.cs b/src/WireMock.Net.Shared/Extensions/AnyOfExtensions.cs index aefe5e1e..470b17ab 100644 --- a/src/WireMock.Net.Shared/Extensions/AnyOfExtensions.cs +++ b/src/WireMock.Net.Shared/Extensions/AnyOfExtensions.cs @@ -1,7 +1,5 @@ // Copyright © WireMock.Net -using System.Collections.Generic; -using System.Linq; using AnyOfTypes; using WireMock.Models; diff --git a/src/WireMock.Net.Shared/Matchers/MatchScores.cs b/src/WireMock.Net.Shared/Matchers/MatchScores.cs index deeca2d8..83ee708a 100644 --- a/src/WireMock.Net.Shared/Matchers/MatchScores.cs +++ b/src/WireMock.Net.Shared/Matchers/MatchScores.cs @@ -1,8 +1,6 @@ // Copyright © WireMock.Net -using System; -using System.Collections.Generic; -using System.Linq; +using WireMock.Types; namespace WireMock.Matchers; @@ -57,7 +55,7 @@ public static class MatchScores /// The values. /// The . /// average score - public static double ToScore(IReadOnlyCollection values, MatchOperator matchOperator) + public static double ToScore(IEnumerable values, MatchOperator matchOperator) { return ToScore(values.Select(ToScore).ToArray(), matchOperator); } @@ -68,7 +66,7 @@ public static class MatchScores /// The values. /// /// average score - public static double ToScore(IReadOnlyCollection values, MatchOperator matchOperator) + public static double ToScore(IEnumerable values, MatchOperator matchOperator) { if (!values.Any()) { @@ -82,4 +80,29 @@ public static class MatchScores _ => values.Average() }; } + + internal static double ToScore(WireMockList values, IStringMatcher[] matchers, MatchOperator matchOperator = MatchOperator.And) + { + // Create a matrix of scores where each row corresponds to a value and each column corresponds to a matcher. + var matrix = values + .Select(value => matchers + .Select(matcher => matcher.IsMatch(value).Score).ToArray() + ) + .ToArray(); + + if (matrix.Length == 0 || matchers.Length == 0) + { + return Mismatch; + } + + // For each value, how well was it matched by its best matcher? + var rowRange = Enumerable.Range(0, matchers.Length); + var rowScore = matchOperator == MatchOperator.And ? matrix.Average(row => row.Max()) : matrix.Max(row => row.Max()); + + // For each matcher, how well was it satisfied by its best value? + var columnRange = Enumerable.Range(0, matchers.Length); + var columnScore = matchOperator == MatchOperator.And ? columnRange.Average(column => matrix.Max(row => row[column])) : columnRange.Max(column => matrix.Max(row => row[column])); + + return matchOperator == MatchOperator.And ? Math.Min(rowScore, columnScore) : Math.Max(rowScore, columnScore); + } } \ No newline at end of file diff --git a/src/WireMock.Net.Shared/Models/BodyData.cs b/src/WireMock.Net.Shared/Models/BodyData.cs index d49fcbb4..65b5bc3d 100644 --- a/src/WireMock.Net.Shared/Models/BodyData.cs +++ b/src/WireMock.Net.Shared/Models/BodyData.cs @@ -22,7 +22,7 @@ public class BodyData : IBodyData public string? BodyAsString { get; set; } /// - public IDictionary? BodyAsFormUrlEncoded { get; set; } + public IDictionary>? BodyAsFormUrlEncoded { get; set; } /// public object? BodyAsJson { get; set; } diff --git a/src/WireMock.Net.Shared/RequestBuilders/IBodyRequestBuilder.cs b/src/WireMock.Net.Shared/RequestBuilders/IBodyRequestBuilder.cs index 1e049b91..1c7d697b 100644 --- a/src/WireMock.Net.Shared/RequestBuilders/IBodyRequestBuilder.cs +++ b/src/WireMock.Net.Shared/RequestBuilders/IBodyRequestBuilder.cs @@ -1,8 +1,7 @@ // Copyright © WireMock.Net -using System; -using System.Collections.Generic; using WireMock.Matchers; +using WireMock.Types; using WireMock.Util; namespace WireMock.RequestBuilders; @@ -100,5 +99,5 @@ public interface IBodyRequestBuilder : IMultiPartRequestBuilder /// /// The form-urlencoded values. /// The . - IRequestBuilder WithBody(Func?, bool> func); + IRequestBuilder WithBody(Func>?, bool> func); } \ No newline at end of file diff --git a/src/WireMock.Net.Shared/Util/QueryStringParser.cs b/src/WireMock.Net.Shared/Util/QueryStringParser.cs index 0a2c4630..17693a5f 100644 --- a/src/WireMock.Net.Shared/Util/QueryStringParser.cs +++ b/src/WireMock.Net.Shared/Util/QueryStringParser.cs @@ -1,9 +1,6 @@ // Copyright © WireMock.Net -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Net; using WireMock.Types; @@ -16,7 +13,7 @@ internal static class QueryStringParser { private static readonly Dictionary> Empty = new(); - public static bool TryParse(string? queryString, bool caseIgnore, [NotNullWhen(true)] out IDictionary? nameValueCollection) + public static bool TryParse(string? queryString, bool caseIgnore, [NotNullWhen(true)] out IDictionary>? nameValueCollection) { if (queryString == null) { @@ -29,12 +26,22 @@ internal static class QueryStringParser .Select(parameter => parameter.Split('=')) .Distinct(); - nameValueCollection = caseIgnore ? new Dictionary(StringComparer.OrdinalIgnoreCase) : new Dictionary(); + nameValueCollection = caseIgnore ? new Dictionary>(StringComparer.OrdinalIgnoreCase) : new Dictionary>(); foreach (var part in parts) { if (part.Length == 2) { - nameValueCollection.Add(part[0], WebUtility.UrlDecode(part[1])); + var key = part[0]; + var value = WebUtility.UrlDecode(part[1]); + + if (!nameValueCollection.TryGetValue(key, out var stringList)) + { + nameValueCollection.Add(key, value); + } + else + { + stringList.Add(value); + } } } diff --git a/test/WireMock.Net.Tests/Matchers/FormUrlEncodedMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/FormUrlEncodedMatcherTests.cs index 139e7534..9512b2f1 100644 --- a/test/WireMock.Net.Tests/Matchers/FormUrlEncodedMatcherTests.cs +++ b/test/WireMock.Net.Tests/Matchers/FormUrlEncodedMatcherTests.cs @@ -1,9 +1,8 @@ // Copyright © WireMock.Net using System.Net.Http; -using AnyOfTypes; +using WireMock.Extensions; using WireMock.Matchers; -using WireMock.Models; namespace WireMock.Net.Tests.Matchers; @@ -12,18 +11,26 @@ public class FormUrlEncodedMatcherTest private readonly CancellationToken _ct = TestContext.Current.CancellationToken; [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) + [InlineData(true, "*=*")] + [InlineData(true, "name=John Doe")] + [InlineData(false, "name=Stef")] + [InlineData(false, "name=John Doe&name=Stef")] + [InlineData(true, "name=*")] + [InlineData(true, "*=John Doe")] + [InlineData(false, "*=Stef")] + [InlineData(false, "*=John Doe&*=Stef")] + [InlineData(true, "email=johndoe@example.com")] + [InlineData(true, "email=*")] + [InlineData(true, "*=johndoe@example.com")] + [InlineData(true, "name=John Doe", "email=johndoe@example.com")] + [InlineData(true, "name=John Doe", "email=*")] + [InlineData(true, "name=John Doe&name=Stef", "email=*")] + [InlineData(true, "name=Stef", "email=*")] + [InlineData(true, "name=*", "email=*")] + [InlineData(true, "*=John Doe", "*=johndoe@example.com")] + [InlineData(true, "*=Stef", "*=johndoe@example.com")] + [InlineData(true, "name=John Doe&name=Stef", "*=johndoe@example.com")] + public async Task FormUrlEncodedMatcher_IsMatch_Single_Or(bool expected, params string[] patterns) { // Arrange var content = new FormUrlEncodedContent( @@ -33,28 +40,76 @@ public class FormUrlEncodedMatcherTest ]); var contentAsString = await content.ReadAsStringAsync(_ct); - var matcher = new FormUrlEncodedMatcher(patterns.Select(p => new AnyOf(p)).ToArray()); + var matcher = new FormUrlEncodedMatcher(patterns.ToAnyOfPatterns()); // Act var score = matcher.IsMatch(contentAsString).IsPerfect(); // Assert - score.Should().BeTrue(); + score.Should().Be(expected); } [Theory] + [InlineData(true, "*=*")] [InlineData(false, "name=John Doe")] + [InlineData(false, "name=Stef")] + [InlineData(true, "name=John Doe&name=Stef")] + [InlineData(true, "name=*")] + [InlineData(false, "*=John Doe")] + [InlineData(false, "*=Stef")] + [InlineData(true, "*=John Doe&*=Stef")] + [InlineData(true, "email=johndoe@example.com")] + [InlineData(true, "email=*")] + [InlineData(true, "*=johndoe@example.com")] + [InlineData(true, "name=John Doe", "email=johndoe@example.com")] + [InlineData(true, "name=John Doe", "email=*")] + [InlineData(true, "name=John Doe&name=Stef", "email=*")] + [InlineData(true, "name=Stef", "email=*")] + [InlineData(true, "name=*", "email=*")] + [InlineData(true, "*=John Doe", "*=johndoe@example.com")] + [InlineData(true, "*=Stef", "*=johndoe@example.com")] + [InlineData(true, "name=John Doe&name=Stef", "*=johndoe@example.com")] + public async Task FormUrlEncodedMatcher_IsMatch_Multiple_Or(bool expected, params string[] patterns) + { + // Arrange + var content = new FormUrlEncodedContent( + [ + new KeyValuePair("name", "John Doe"), + new KeyValuePair("name", "Stef"), + new KeyValuePair("email", "johndoe@example.com") + ]); + var contentAsString = await content.ReadAsStringAsync(_ct); + + var matcher = new FormUrlEncodedMatcher(patterns.ToAnyOfPatterns()); + + // Act + var score = matcher.IsMatch(contentAsString).IsPerfect(); + + // Assert + score.Should().Be(expected); + } + + [Theory] + [InlineData(true, "*=*")] + [InlineData(false, "name=John Doe")] + [InlineData(false, "name=Stef")] + [InlineData(false, "name=John Doe&name=Stef")] [InlineData(false, "name=*")] [InlineData(false, "*=John Doe")] + [InlineData(false, "*=Stef")] + [InlineData(false, "*=John Doe&*=Stef")] [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(false, "name=John Doe&name=Stef", "email=*")] + [InlineData(false, "name=Stef", "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) + [InlineData(false, "*=Stef", "*=johndoe@example.com")] + [InlineData(false, "name=John Doe&name=Stef", "*=johndoe@example.com")] + public async Task FormUrlEncodedMatcher_IsMatch_Single_And(bool expected, params string[] patterns) { // Arrange var content = new FormUrlEncodedContent( @@ -64,7 +119,47 @@ public class FormUrlEncodedMatcherTest ]); var contentAsString = await content.ReadAsStringAsync(_ct); - var matcher = new FormUrlEncodedMatcher(patterns.Select(p => new AnyOf(p)).ToArray(), true, MatchOperator.And); + var matcher = new FormUrlEncodedMatcher(patterns.ToAnyOfPatterns(), true, MatchOperator.And); + + // Act + var score = matcher.IsMatch(contentAsString).IsPerfect(); + + // Assert + score.Should().Be(expected); + } + + [Theory] + [InlineData(true, "*=*")] + [InlineData(false, "name=John Doe")] + [InlineData(false, "name=Stef")] + [InlineData(false, "name=John Doe&name=Stef")] + [InlineData(false, "name=*")] + [InlineData(false, "*=John Doe")] + [InlineData(false, "*=Stef")] + [InlineData(false, "*=John Doe&*=Stef")] + [InlineData(false, "email=johndoe@example.com")] + [InlineData(false, "email=*")] + [InlineData(false, "*=johndoe@example.com")] + [InlineData(false, "name=John Doe", "email=johndoe@example.com")] + [InlineData(false, "name=John Doe", "email=*")] + [InlineData(true, "name=John Doe&name=Stef", "email=*")] + [InlineData(false, "name=Stef", "email=*")] + [InlineData(true, "name=*", "email=*")] + [InlineData(false, "*=John Doe", "*=johndoe@example.com")] + [InlineData(false, "*=Stef", "*=johndoe@example.com")] + [InlineData(true, "name=John Doe&name=Stef", "*=johndoe@example.com")] + public async Task FormUrlEncodedMatcher_IsMatch_Multiple_And(bool expected, params string[] patterns) + { + // Arrange + var content = new FormUrlEncodedContent( + [ + new KeyValuePair("name", "John Doe"), + new KeyValuePair("name", "Stef"), + new KeyValuePair("email", "johndoe@example.com") + ]); + var contentAsString = await content.ReadAsStringAsync(_ct); + + var matcher = new FormUrlEncodedMatcher(patterns.ToAnyOfPatterns(), true, MatchOperator.And); // Act var score = matcher.IsMatch(contentAsString).IsPerfect(); @@ -74,12 +169,13 @@ public class FormUrlEncodedMatcherTest } [Fact] - public async Task FormUrlEncodedMatcher_IsMatch_And_MatchAllProperties() + public async Task FormUrlEncodedMatcher_IsMatch_And_MatchAllProperties_Test_1() { // Arrange var content = new FormUrlEncodedContent( [ new KeyValuePair("name", "John Doe"), + new KeyValuePair("name", "Stef"), new KeyValuePair("email", "johndoe@example.com") ]); var contentAsString = await content.ReadAsStringAsync(_ct); @@ -93,4 +189,25 @@ public class FormUrlEncodedMatcherTest // Assert score.Should().BeFalse(); } + + [Fact] + public async Task FormUrlEncodedMatcher_IsMatch_And_MatchAllProperties_Test_2() + { + // Arrange + var content = new FormUrlEncodedContent( + [ + new KeyValuePair("name", "John Doe"), + new KeyValuePair("name", "Stef"), + new KeyValuePair("email", "johndoe@example.com") + ]); + var contentAsString = await content.ReadAsStringAsync(_ct); + + var matcher = new FormUrlEncodedMatcher(["name=*", "email=*", "email=x"], matchOperator: MatchOperator.And); + + // Act + var score = matcher.IsMatch(contentAsString).IsPerfect(); + + // Assert + score.Should().BeFalse(); + } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithBodyTests.cs b/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithBodyTests.cs index 31767152..8c40eb69 100644 --- a/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithBodyTests.cs +++ b/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithBodyTests.cs @@ -142,12 +142,12 @@ public class RequestBuilderWithBodyTests public void Request_WithBody_FuncFormUrlEncoded() { // Assign - var requestBuilder = Request.Create().UsingAnyMethod().WithBody((IDictionary? values) => values != null); + var requestBuilder = Request.Create().UsingAnyMethod().WithBody((IDictionary>? values) => values != null); // Act var body = new BodyData { - BodyAsFormUrlEncoded = new Dictionary(), + BodyAsFormUrlEncoded = new Dictionary>(), DetectedBodyTypeFromContentType = BodyType.FormUrlEncoded, DetectedBodyType = BodyType.FormUrlEncoded }; diff --git a/test/WireMock.Net.Tests/RequestMatchers/RequestMessageParamMatcherTests.cs b/test/WireMock.Net.Tests/RequestMatchers/RequestMessageParamMatcherTests.cs index c5db79a1..4f57bb84 100644 --- a/test/WireMock.Net.Tests/RequestMatchers/RequestMessageParamMatcherTests.cs +++ b/test/WireMock.Net.Tests/RequestMatchers/RequestMessageParamMatcherTests.cs @@ -15,7 +15,7 @@ public class RequestMessageParamMatcherTests { // Assign var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=test1"), "GET", "127.0.0.1"); - var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "KeY", true, new[] { "test1" }); + var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "KeY", true, ["test1"]); // Act var result = new RequestMatchResult(); @@ -30,7 +30,7 @@ public class RequestMessageParamMatcherTests { // Assign var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=test1"), "GET", "127.0.0.1"); - var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, new[] { "test1", "test2" }); + var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, ["test1", "test2"]); // Act var result = new RequestMatchResult(); @@ -60,7 +60,7 @@ public class RequestMessageParamMatcherTests { // Assign var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=test1,test2,test3"), "GET", "127.0.0.1"); - var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, new IStringMatcher[] { new ExactMatcher("test1", "test2") }); + var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, [new ExactMatcher("test1", "test2")]); // Act var result = new RequestMatchResult(); @@ -71,18 +71,18 @@ public class RequestMessageParamMatcherTests } [Fact] - public void RequestMessageParamMatcher_GetMatchingScore_KeyWith2ValuesPresentInUrl_And_With1ExactStringWith3Patterns_Returns0_66() + public void RequestMessageParamMatcher_GetMatchingScore_KeyWith2ValuesPresentInUrl_And_With1ExactStringWith3Patterns_Returns1_0() { // Assign var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=test1,test2"), "GET", "127.0.0.1"); - var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, new IStringMatcher[] { new ExactMatcher("test1", "test2", "test3") }); + var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, [new ExactMatcher("test1", "test2", "test3")]); // Act var result = new RequestMatchResult(); double score = matcher.GetMatchingScore(requestMessage, result); // Assert - score.Should().BeApproximately(0.66d, 0.1d); + score.Should().Be(1.0d); } [Fact] @@ -90,7 +90,7 @@ public class RequestMessageParamMatcherTests { // Assign var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=test1,test2"), "GET", "127.0.0.1"); - var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, new[] { "test1", "test2" }); + var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, ["test1", "test2"]); // Act var result = new RequestMatchResult(); @@ -105,7 +105,7 @@ public class RequestMessageParamMatcherTests { // Assign var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=test1,test2"), "GET", "127.0.0.1"); - var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, new IStringMatcher[] { new ExactMatcher("test1"), new ExactMatcher("test2") }); + var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, [new ExactMatcher("test1"), new ExactMatcher("test2")]); // Act var result = new RequestMatchResult(); @@ -120,7 +120,7 @@ public class RequestMessageParamMatcherTests { // Assign var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=test0,test2"), "GET", "127.0.0.1"); - var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, new[] { "test1", "test2" }); + var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, ["test1", "test2"]); // Act var result = new RequestMatchResult(); @@ -135,7 +135,7 @@ public class RequestMessageParamMatcherTests { // Assign var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key"), "GET", "127.0.0.1"); - var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, new[] { "test1", "test2" }); + var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, ["test1", "test2"]); // Act var result = new RequestMatchResult(); @@ -215,7 +215,7 @@ public class RequestMessageParamMatcherTests { // Assign: the param value in the URL matches the reject pattern -> the mapping must be rejected (score 0.0). var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=abc"), "GET", "127.0.0.1"); - var matcher = new RequestMessageParamMatcher(MatchBehaviour.RejectOnMatch, "key", false, new[] { "abc" }); + var matcher = new RequestMessageParamMatcher(MatchBehaviour.RejectOnMatch, "key", false, ["abc"]); // Act var result = new RequestMatchResult(); @@ -230,7 +230,7 @@ public class RequestMessageParamMatcherTests { // Assign: the param value in the URL does NOT match the reject pattern -> the mapping is accepted (score 1.0). var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=xyz"), "GET", "127.0.0.1"); - var matcher = new RequestMessageParamMatcher(MatchBehaviour.RejectOnMatch, "key", false, new[] { "abc" }); + var matcher = new RequestMessageParamMatcher(MatchBehaviour.RejectOnMatch, "key", false, ["abc"]); // Act var result = new RequestMatchResult(); @@ -245,7 +245,7 @@ public class RequestMessageParamMatcherTests { // Assign: ignoreCase must still be honored on the inner matcher after the fix. var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=ABC"), "GET", "127.0.0.1"); - var matcher = new RequestMessageParamMatcher(MatchBehaviour.RejectOnMatch, "key", true, new[] { "abc" }); + var matcher = new RequestMessageParamMatcher(MatchBehaviour.RejectOnMatch, "key", true, ["abc"]); // Act var result = new RequestMatchResult(); @@ -260,7 +260,7 @@ public class RequestMessageParamMatcherTests { // Assign var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=abc"), "GET", "127.0.0.1"); - var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, new[] { "abc" }); + var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, ["abc"]); // Act var result = new RequestMatchResult(); @@ -275,7 +275,7 @@ public class RequestMessageParamMatcherTests { // Assign var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=xyz"), "GET", "127.0.0.1"); - var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, new[] { "abc" }); + var matcher = new RequestMessageParamMatcher(MatchBehaviour.AcceptOnMatch, "key", false, ["abc"]); // Act var result = new RequestMatchResult(); diff --git a/test/WireMock.Net.Tests/Util/QueryStringParserTests.cs b/test/WireMock.Net.Tests/Util/QueryStringParserTests.cs index 9e23ec0e..75cba9eb 100644 --- a/test/WireMock.Net.Tests/Util/QueryStringParserTests.cs +++ b/test/WireMock.Net.Tests/Util/QueryStringParserTests.cs @@ -7,27 +7,27 @@ namespace WireMock.Net.Tests.Util; public class QueryStringParserTests { - public static IEnumerable QueryStringTestData => new List + public static List QueryStringTestData => new() { new object?[] { null, false, false, null }, - new object?[] { string.Empty, false, true, new Dictionary() }, - new object?[] { "test", false, true, new Dictionary() }, - new object?[] { "&", false, true, new Dictionary() }, - new object?[] { "&&", false, true, new Dictionary() }, - new object?[] { "a=", false, true, new Dictionary { { "a", "" } } }, - new object?[] { "&a", false, true, new Dictionary() }, - new object?[] { "&a=", false, true, new Dictionary { { "a", "" } } }, - new object?[] { "&key1=value1", false, true, new Dictionary { { "key1", "value1" } } }, - new object?[] { "key1=value1", false, true, new Dictionary { { "key1", "value1" } } }, - new object?[] { "key1=value1&key2=value2", false, true, new Dictionary { { "key1", "value1" }, { "key2", "value2" } } }, - new object?[] { "key1=value1&key2=value2&", false, true, new Dictionary { { "key1", "value1" }, { "key2", "value2" } } }, - new object?[] { "key1=value1&&key2=value2", false, true, new Dictionary { { "key1", "value1" }, { "key2", "value2" } } }, - new object?[] { "&key1=value1&key2=value2&&", false, true, new Dictionary { { "key1", "value1" }, { "key2", "value2" } } }, + new object?[] { string.Empty, false, true, new Dictionary>() }, + new object?[] { "test", false, true, new Dictionary>() }, + new object?[] { "&", false, true, new Dictionary>() }, + new object?[] { "&&", false, true, new Dictionary>() }, + new object?[] { "a=", false, true, new Dictionary> { { "a", new WireMockList("") } } }, + new object?[] { "&a", false, true, new Dictionary>() }, + new object?[] { "&a=", false, true, new Dictionary> { { "a", new WireMockList("") } } }, + new object?[] { "&key1=value1", false, true, new Dictionary> { { "key1", new WireMockList("value1") } } }, + new object?[] { "key1=value1", false, true, new Dictionary> { { "key1", new WireMockList("value1") } } }, + new object?[] { "key1=value1&key2=value2", false, true, new Dictionary> { { "key1", new WireMockList("value1") }, { "key2", new WireMockList("value2") } } }, + new object?[] { "key1=value1&key2=value2&", false, true, new Dictionary> { { "key1", new WireMockList("value1") }, { "key2", new WireMockList("value2") } } }, + new object?[] { "key1=value1&&key2=value2", false, true, new Dictionary> { { "key1", new WireMockList("value1") }, { "key2", new WireMockList("value2") } } }, + new object?[] { "&key1=value1&key2=value2&&", false, true, new Dictionary> { { "key1", new WireMockList("value1") }, { "key2", new WireMockList("value2") } } }, }; [Theory] [MemberData(nameof(QueryStringTestData))] - public void TryParse_Should_Parse_QueryString(string queryString, bool caseIgnore, bool expectedResult, IDictionary expectedOutput) + public void TryParse_Should_Parse_QueryString(string queryString, bool caseIgnore, bool expectedResult, IDictionary> expectedOutput) { // Act var result = QueryStringParser.TryParse(queryString, caseIgnore, out var actual); @@ -49,7 +49,7 @@ public class QueryStringParserTests // Assert result.Should().BeTrue(); - actual.Should().BeEquivalentTo(new Dictionary { { "x", "rNaCP7hv8UOmS/JcujdvLw==" } }); + actual.Should().BeEquivalentTo(new Dictionary> { { "x", new WireMockList("rNaCP7hv8UOmS/JcujdvLw==") } }); } [Fact] diff --git a/test/WireMock.Net.Tests/WireMockServerTests.WithBody.cs b/test/WireMock.Net.Tests/WireMockServerTests.WithBody.cs index 2a01d7ab..ec35df6c 100644 --- a/test/WireMock.Net.Tests/WireMockServerTests.WithBody.cs +++ b/test/WireMock.Net.Tests/WireMockServerTests.WithBody.cs @@ -12,6 +12,7 @@ using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Server; using WireMock.Settings; +using WireMock.Types; namespace WireMock.Net.Tests; @@ -339,7 +340,7 @@ public partial class WireMockServerTests #endif [Fact] - public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithFunc() + public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithFunc1() { // Arrange using var server = WireMockServer.Start(); @@ -364,6 +365,32 @@ public partial class WireMockServerTests server.Stop(); } + [Fact] + public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithFunc2() + { + // Arrange + using var server = WireMockServer.Start(); + server.Given( + Request.Create() + .UsingPost() + .WithPath("/foo") + .WithBody((IDictionary>? values) => values != null && values["key1"] == "value1") + ) + .RespondWith( + Response.Create() + ); + + // Act + var content = new FormUrlEncodedContent([new KeyValuePair("key1", "value1")]); + var response = await new HttpClient() + .PostAsync($"{server.Url}/foo", content, _ct); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + + server.Stop(); + } + [Fact] public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithExactMatcher() {