Fix FormUrlEncodedMatcher

This commit is contained in:
Stef Heyenrath
2026-08-18 22:25:15 +02:00
parent 42353f035e
commit a31f4768a8
16 changed files with 382 additions and 132 deletions
@@ -46,7 +46,7 @@ public interface IBodyData
/// <summary> /// <summary>
/// The body as Form UrlEncoded dictionary. /// The body as Form UrlEncoded dictionary.
/// </summary> /// </summary>
IDictionary<string, string>? BodyAsFormUrlEncoded { get; set; } IDictionary<string, WireMockList<string>>? BodyAsFormUrlEncoded { get; set; }
/// <summary> /// <summary>
/// The detected body type (detection based on body content). /// The detected body type (detection based on body content).
@@ -46,6 +46,98 @@ public class WireMockList<T> : List<T>
/// <param name="values">The values to set.</param> /// <param name="values">The values to set.</param>
public static implicit operator WireMockList<T>(T[] values) => new(values); public static implicit operator WireMockList<T>(T[] values) => new(values);
/// <summary>
/// Operator for equality comparison from WireMockList to T
/// </summary>
public static bool operator ==(WireMockList<T>? left, T? right)
{
if (ReferenceEquals(left, right))
{
return true;
}
if (left?.Count == 1 && Equals(left[0], right))
{
return true;
}
return false;
}
/// <summary>
/// Operator for equality comparison from T to WireMockList
/// </summary>
public static bool operator ==(T? left, WireMockList<T>? right)
{
if (ReferenceEquals(left, right))
{
return true;
}
if (right?.Count == 1 && Equals(left, right[0]))
{
return true;
}
return false;
}
/// <summary>
/// Operator for inequality comparison from WireMockList to T
/// </summary>
public static bool operator !=(WireMockList<T>? left, T? right) => !(left == right);
/// <summary>
/// Operator for inequality comparison from T to WireMockList
/// </summary>
public static bool operator !=(T? left, WireMockList<T>? right) => !(left == right);
/// <summary>
/// Determines whether the specified object is equal to the current instance.
/// Two <see cref="WireMockList{T}"/> instances are equal if they contain the same elements in the same order.
/// </summary>
public override bool Equals(object? obj)
{
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj is WireMockList<T> 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;
}
/// <summary>
/// Returns a hash code for this instance based on its elements.
/// </summary>
public override int GetHashCode()
{
var hashCode = 17;
foreach (var item in this)
{
hashCode = hashCode * 31 + (item?.GetHashCode() ?? 0);
}
return hashCode;
}
/// <summary> /// <summary>
/// Returns a <see cref="string" /> that represents this instance. /// Returns a <see cref="string" /> that represents this instance.
/// </summary> /// </summary>
@@ -1,10 +1,10 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Linq;
using AnyOfTypes; using AnyOfTypes;
using Stef.Validation; using Stef.Validation;
using WireMock.Extensions; using WireMock.Extensions;
using WireMock.Models; using WireMock.Models;
using WireMock.Types;
using WireMock.Util; using WireMock.Util;
namespace WireMock.Matchers; namespace WireMock.Matchers;
@@ -21,7 +21,7 @@ public class FormUrlEncodedMatcher : IStringMatcher, IIgnoreCaseMatcher
/// <inheritdoc /> /// <inheritdoc />
public MatchBehaviour MatchBehaviour { get; } public MatchBehaviour MatchBehaviour { get; }
private readonly List<(WildcardMatcher Key, WildcardMatcher? Value)> _pairs = []; private readonly List<(WildcardMatcher Key, WildcardMatcher[]? Values)> KeyValueMatchers = [];
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FormUrlEncodedMatcher"/> class. /// Initializes a new instance of the <see cref="FormUrlEncodedMatcher"/> class.
@@ -91,9 +91,10 @@ public class FormUrlEncodedMatcher : IStringMatcher, IIgnoreCaseMatcher
{ {
foreach (var nameValue in nameValueCollection) foreach (var nameValue in nameValueCollection)
{ {
var keyMatcher = new WildcardMatcher(MatchBehaviour.AcceptOnMatch, [nameValue.Key], ignoreCase, MatchOperator); var keyMatcher = new WildcardMatcher(MatchBehaviour.AcceptOnMatch, nameValue.Key, ignoreCase);
var valueMatcher = new WildcardMatcher(MatchBehaviour.AcceptOnMatch, [nameValue.Value], ignoreCase, MatchOperator); var valueMatchers = nameValue.Value.Select(value => new WildcardMatcher(MatchBehaviour.AcceptOnMatch, value, ignoreCase)).ToArray();
_pairs.Add((keyMatcher, valueMatcher));
KeyValueMatchers.Add((keyMatcher, valueMatchers));
} }
} }
} }
@@ -116,37 +117,51 @@ public class FormUrlEncodedMatcher : IStringMatcher, IIgnoreCaseMatcher
var matches = GetMatches(inputNameValueCollection); var matches = GetMatches(inputNameValueCollection);
var score = MatchScores.ToScore(matches, MatchOperator); var score = MatchScores.ToScore(matches, MatchOperator);
return MatchResult.From(Name, MatchBehaviourHelper.Convert(MatchBehaviour, score)); return MatchResult.From(Name, score);
} }
private bool[] GetMatches(IDictionary<string, string> inputNameValueCollection) private List<double> GetMatches(IDictionary<string, WireMockList<string>> inputNameValueCollection)
{ {
var matches = new List<bool>(); var inputPairs = inputNameValueCollection.ToArray();
if (_pairs.Count > inputNameValueCollection.Count) 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; matrix[row] = new double[columnCount];
foreach (var pair in _pairs)
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(); var (keyMatcher, valuesMatchers) = KeyValueMatchers[column];
if (keyMatchResult)
{
match = pair.Value?.IsMatch(inputKeyValuePair.Value).IsPerfect() ?? false;
if (match)
{
break;
}
}
}
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<double>();
for (var column = 0; column < columnCount; column++)
{
columnScores.Add(rowCount == 0 ? MatchScores.Mismatch : matrix.Max(row => row[column]));
}
rowScores.AddRange(columnScores);
return rowScores;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -2,6 +2,7 @@
using Stef.Validation; using Stef.Validation;
using WireMock.Matchers.Helpers; using WireMock.Matchers.Helpers;
using WireMock.Types;
using WireMock.Util; using WireMock.Util;
namespace WireMock.Matchers.Request; namespace WireMock.Matchers.Request;
@@ -34,7 +35,7 @@ public class RequestMessageBodyMatcher : IRequestMatcher
/// <summary> /// <summary>
/// The body data function for FormUrlEncoded /// The body data function for FormUrlEncoded
/// </summary> /// </summary>
public Func<IDictionary<string, string>?, bool>? MatchOnBodyAsFormUrlEncodedFunc { get; } public Func<IDictionary<string, WireMockList<string>>?, bool>? MatchOnBodyAsFormUrlEncodedFunc { get; }
/// <summary> /// <summary>
/// The matchers. /// The matchers.
@@ -116,7 +117,7 @@ public class RequestMessageBodyMatcher : IRequestMatcher
/// Initializes a new instance of the <see cref="RequestMessageBodyMatcher"/> class. /// Initializes a new instance of the <see cref="RequestMessageBodyMatcher"/> class.
/// </summary> /// </summary>
/// <param name="func">The function.</param> /// <param name="func">The function.</param>
public RequestMessageBodyMatcher(Func<IDictionary<string, string>?, bool> func) public RequestMessageBodyMatcher(Func<IDictionary<string, WireMockList<string>>?, bool> func)
{ {
MatchOnBodyAsFormUrlEncodedFunc = Guard.NotNull(func); MatchOnBodyAsFormUrlEncodedFunc = Guard.NotNull(func);
} }
@@ -115,36 +115,6 @@ public class RequestMessageParamMatcher : IRequestMatcher
} }
// Return the score based on Matchers and valuesPresentInRequestMessage // Return the score based on Matchers and valuesPresentInRequestMessage
return CalculateScore(Matchers, valuesPresentInRequestMessage); return MatchScores.ToScore(valuesPresentInRequestMessage, Matchers.ToArray());
}
private static double CalculateScore(IReadOnlyList<IStringMatcher> matchers, WireMockList<string> valuesPresentInRequestMessage)
{
var total = new List<double>();
// 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;
} }
} }
@@ -5,6 +5,7 @@
using Stef.Validation; using Stef.Validation;
using WireMock.Matchers; using WireMock.Matchers;
using WireMock.Matchers.Request; using WireMock.Matchers.Request;
using WireMock.Types;
using WireMock.Util; using WireMock.Util;
namespace WireMock.RequestBuilders; namespace WireMock.RequestBuilders;
@@ -84,7 +85,7 @@ public partial class Request
} }
/// <inheritdoc /> /// <inheritdoc />
public IRequestBuilder WithBody(Func<IDictionary<string, string>?, bool> func) public IRequestBuilder WithBody(Func<IDictionary<string, WireMockList<string>>?, bool> func)
{ {
_requestMatchers.Add(new RequestMessageBodyMatcher(Guard.NotNull(func))); _requestMatchers.Add(new RequestMessageBodyMatcher(Guard.NotNull(func)));
return this; return this;
@@ -1,7 +1,5 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Collections.Generic;
using System.Linq;
using AnyOfTypes; using AnyOfTypes;
using WireMock.Models; using WireMock.Models;
@@ -1,8 +1,6 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System; using WireMock.Types;
using System.Collections.Generic;
using System.Linq;
namespace WireMock.Matchers; namespace WireMock.Matchers;
@@ -57,7 +55,7 @@ public static class MatchScores
/// <param name="values">The values.</param> /// <param name="values">The values.</param>
/// <param name="matchOperator">The <see cref="MatchOperator"/>.</param> /// <param name="matchOperator">The <see cref="MatchOperator"/>.</param>
/// <returns>average score</returns> /// <returns>average score</returns>
public static double ToScore(IReadOnlyCollection<bool> values, MatchOperator matchOperator) public static double ToScore(IEnumerable<bool> values, MatchOperator matchOperator)
{ {
return ToScore(values.Select(ToScore).ToArray(), matchOperator); return ToScore(values.Select(ToScore).ToArray(), matchOperator);
} }
@@ -68,7 +66,7 @@ public static class MatchScores
/// <param name="values">The values.</param> /// <param name="values">The values.</param>
/// <param name="matchOperator"></param> /// <param name="matchOperator"></param>
/// <returns>average score</returns> /// <returns>average score</returns>
public static double ToScore(IReadOnlyCollection<double> values, MatchOperator matchOperator) public static double ToScore(IEnumerable<double> values, MatchOperator matchOperator)
{ {
if (!values.Any()) if (!values.Any())
{ {
@@ -82,4 +80,29 @@ public static class MatchScores
_ => values.Average() _ => values.Average()
}; };
} }
internal static double ToScore(WireMockList<string> 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);
}
} }
+1 -1
View File
@@ -22,7 +22,7 @@ public class BodyData : IBodyData
public string? BodyAsString { get; set; } public string? BodyAsString { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public IDictionary<string, string>? BodyAsFormUrlEncoded { get; set; } public IDictionary<string, WireMockList<string>>? BodyAsFormUrlEncoded { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public object? BodyAsJson { get; set; } public object? BodyAsJson { get; set; }
@@ -1,8 +1,7 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using WireMock.Matchers; using WireMock.Matchers;
using WireMock.Types;
using WireMock.Util; using WireMock.Util;
namespace WireMock.RequestBuilders; namespace WireMock.RequestBuilders;
@@ -100,5 +99,5 @@ public interface IBodyRequestBuilder : IMultiPartRequestBuilder
/// </summary> /// </summary>
/// <param name="func">The form-urlencoded values.</param> /// <param name="func">The form-urlencoded values.</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns> /// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBody(Func<IDictionary<string, string>?, bool> func); IRequestBuilder WithBody(Func<IDictionary<string, WireMockList<string>>?, bool> func);
} }
@@ -1,9 +1,6 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net; using System.Net;
using WireMock.Types; using WireMock.Types;
@@ -16,7 +13,7 @@ internal static class QueryStringParser
{ {
private static readonly Dictionary<string, WireMockList<string>> Empty = new(); private static readonly Dictionary<string, WireMockList<string>> Empty = new();
public static bool TryParse(string? queryString, bool caseIgnore, [NotNullWhen(true)] out IDictionary<string, string>? nameValueCollection) public static bool TryParse(string? queryString, bool caseIgnore, [NotNullWhen(true)] out IDictionary<string, WireMockList<string>>? nameValueCollection)
{ {
if (queryString == null) if (queryString == null)
{ {
@@ -29,12 +26,22 @@ internal static class QueryStringParser
.Select(parameter => parameter.Split('=')) .Select(parameter => parameter.Split('='))
.Distinct(); .Distinct();
nameValueCollection = caseIgnore ? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) : new Dictionary<string, string>(); nameValueCollection = caseIgnore ? new Dictionary<string, WireMockList<string>>(StringComparer.OrdinalIgnoreCase) : new Dictionary<string, WireMockList<string>>();
foreach (var part in parts) foreach (var part in parts)
{ {
if (part.Length == 2) 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);
}
} }
} }
@@ -1,9 +1,8 @@
// Copyright © WireMock.Net // Copyright © WireMock.Net
using System.Net.Http; using System.Net.Http;
using AnyOfTypes; using WireMock.Extensions;
using WireMock.Matchers; using WireMock.Matchers;
using WireMock.Models;
namespace WireMock.Net.Tests.Matchers; namespace WireMock.Net.Tests.Matchers;
@@ -12,18 +11,26 @@ public class FormUrlEncodedMatcherTest
private readonly CancellationToken _ct = TestContext.Current.CancellationToken; private readonly CancellationToken _ct = TestContext.Current.CancellationToken;
[Theory] [Theory]
[InlineData("*=*")] [InlineData(true, "*=*")]
[InlineData("name=John Doe")] [InlineData(true, "name=John Doe")]
[InlineData("name=*")] [InlineData(false, "name=Stef")]
[InlineData("*=John Doe")] [InlineData(false, "name=John Doe&name=Stef")]
[InlineData("email=johndoe@example.com")] [InlineData(true, "name=*")]
[InlineData("email=*")] [InlineData(true, "*=John Doe")]
[InlineData("*=johndoe@example.com")] [InlineData(false, "*=Stef")]
[InlineData("name=John Doe", "email=johndoe@example.com")] [InlineData(false, "*=John Doe&*=Stef")]
[InlineData("name=John Doe", "email=*")] [InlineData(true, "email=johndoe@example.com")]
[InlineData("name=*", "email=*")] [InlineData(true, "email=*")]
[InlineData("*=John Doe", "*=johndoe@example.com")] [InlineData(true, "*=johndoe@example.com")]
public async Task FormUrlEncodedMatcher_IsMatch(params string[] patterns) [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 // Arrange
var content = new FormUrlEncodedContent( var content = new FormUrlEncodedContent(
@@ -33,28 +40,76 @@ public class FormUrlEncodedMatcherTest
]); ]);
var contentAsString = await content.ReadAsStringAsync(_ct); var contentAsString = await content.ReadAsStringAsync(_ct);
var matcher = new FormUrlEncodedMatcher(patterns.Select(p => new AnyOf<string, StringPattern>(p)).ToArray()); var matcher = new FormUrlEncodedMatcher(patterns.ToAnyOfPatterns());
// Act // Act
var score = matcher.IsMatch(contentAsString).IsPerfect(); var score = matcher.IsMatch(contentAsString).IsPerfect();
// Assert // Assert
score.Should().BeTrue(); score.Should().Be(expected);
} }
[Theory] [Theory]
[InlineData(true, "*=*")]
[InlineData(false, "name=John Doe")] [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<string, string>("name", "John Doe"),
new KeyValuePair<string, string>("name", "Stef"),
new KeyValuePair<string, string>("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, "name=*")]
[InlineData(false, "*=John Doe")] [InlineData(false, "*=John Doe")]
[InlineData(false, "*=Stef")]
[InlineData(false, "*=John Doe&*=Stef")]
[InlineData(false, "email=johndoe@example.com")] [InlineData(false, "email=johndoe@example.com")]
[InlineData(false, "email=*")] [InlineData(false, "email=*")]
[InlineData(false, "*=johndoe@example.com")] [InlineData(false, "*=johndoe@example.com")]
[InlineData(true, "name=John Doe", "email=johndoe@example.com")] [InlineData(true, "name=John Doe", "email=johndoe@example.com")]
[InlineData(true, "name=John Doe", "email=*")] [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, "name=*", "email=*")]
[InlineData(true, "*=John Doe", "*=johndoe@example.com")] [InlineData(true, "*=John Doe", "*=johndoe@example.com")]
[InlineData(true, "*=*")] [InlineData(false, "*=Stef", "*=johndoe@example.com")]
public async Task FormUrlEncodedMatcher_IsMatch_And(bool expected, params string[] patterns) [InlineData(false, "name=John Doe&name=Stef", "*=johndoe@example.com")]
public async Task FormUrlEncodedMatcher_IsMatch_Single_And(bool expected, params string[] patterns)
{ {
// Arrange // Arrange
var content = new FormUrlEncodedContent( var content = new FormUrlEncodedContent(
@@ -64,7 +119,47 @@ public class FormUrlEncodedMatcherTest
]); ]);
var contentAsString = await content.ReadAsStringAsync(_ct); var contentAsString = await content.ReadAsStringAsync(_ct);
var matcher = new FormUrlEncodedMatcher(patterns.Select(p => new AnyOf<string, StringPattern>(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<string, string>("name", "John Doe"),
new KeyValuePair<string, string>("name", "Stef"),
new KeyValuePair<string, string>("email", "johndoe@example.com")
]);
var contentAsString = await content.ReadAsStringAsync(_ct);
var matcher = new FormUrlEncodedMatcher(patterns.ToAnyOfPatterns(), true, MatchOperator.And);
// Act // Act
var score = matcher.IsMatch(contentAsString).IsPerfect(); var score = matcher.IsMatch(contentAsString).IsPerfect();
@@ -74,12 +169,13 @@ public class FormUrlEncodedMatcherTest
} }
[Fact] [Fact]
public async Task FormUrlEncodedMatcher_IsMatch_And_MatchAllProperties() public async Task FormUrlEncodedMatcher_IsMatch_And_MatchAllProperties_Test_1()
{ {
// Arrange // Arrange
var content = new FormUrlEncodedContent( var content = new FormUrlEncodedContent(
[ [
new KeyValuePair<string, string>("name", "John Doe"), new KeyValuePair<string, string>("name", "John Doe"),
new KeyValuePair<string, string>("name", "Stef"),
new KeyValuePair<string, string>("email", "johndoe@example.com") new KeyValuePair<string, string>("email", "johndoe@example.com")
]); ]);
var contentAsString = await content.ReadAsStringAsync(_ct); var contentAsString = await content.ReadAsStringAsync(_ct);
@@ -93,4 +189,25 @@ public class FormUrlEncodedMatcherTest
// Assert // Assert
score.Should().BeFalse(); score.Should().BeFalse();
} }
[Fact]
public async Task FormUrlEncodedMatcher_IsMatch_And_MatchAllProperties_Test_2()
{
// Arrange
var content = new FormUrlEncodedContent(
[
new KeyValuePair<string, string>("name", "John Doe"),
new KeyValuePair<string, string>("name", "Stef"),
new KeyValuePair<string, string>("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();
}
} }
@@ -142,12 +142,12 @@ public class RequestBuilderWithBodyTests
public void Request_WithBody_FuncFormUrlEncoded() public void Request_WithBody_FuncFormUrlEncoded()
{ {
// Assign // Assign
var requestBuilder = Request.Create().UsingAnyMethod().WithBody((IDictionary<string, string>? values) => values != null); var requestBuilder = Request.Create().UsingAnyMethod().WithBody((IDictionary<string, WireMockList<string>>? values) => values != null);
// Act // Act
var body = new BodyData var body = new BodyData
{ {
BodyAsFormUrlEncoded = new Dictionary<string, string>(), BodyAsFormUrlEncoded = new Dictionary<string, WireMockList<string>>(),
DetectedBodyTypeFromContentType = BodyType.FormUrlEncoded, DetectedBodyTypeFromContentType = BodyType.FormUrlEncoded,
DetectedBodyType = BodyType.FormUrlEncoded DetectedBodyType = BodyType.FormUrlEncoded
}; };
@@ -15,7 +15,7 @@ public class RequestMessageParamMatcherTests
{ {
// Assign // Assign
var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=test1"), "GET", "127.0.0.1"); 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 // Act
var result = new RequestMatchResult(); var result = new RequestMatchResult();
@@ -30,7 +30,7 @@ public class RequestMessageParamMatcherTests
{ {
// Assign // Assign
var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=test1"), "GET", "127.0.0.1"); 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 // Act
var result = new RequestMatchResult(); var result = new RequestMatchResult();
@@ -60,7 +60,7 @@ public class RequestMessageParamMatcherTests
{ {
// Assign // Assign
var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=test1,test2,test3"), "GET", "127.0.0.1"); 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 // Act
var result = new RequestMatchResult(); var result = new RequestMatchResult();
@@ -71,18 +71,18 @@ public class RequestMessageParamMatcherTests
} }
[Fact] [Fact]
public void RequestMessageParamMatcher_GetMatchingScore_KeyWith2ValuesPresentInUrl_And_With1ExactStringWith3Patterns_Returns0_66() public void RequestMessageParamMatcher_GetMatchingScore_KeyWith2ValuesPresentInUrl_And_With1ExactStringWith3Patterns_Returns1_0()
{ {
// Assign // Assign
var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=test1,test2"), "GET", "127.0.0.1"); 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 // Act
var result = new RequestMatchResult(); var result = new RequestMatchResult();
double score = matcher.GetMatchingScore(requestMessage, result); double score = matcher.GetMatchingScore(requestMessage, result);
// Assert // Assert
score.Should().BeApproximately(0.66d, 0.1d); score.Should().Be(1.0d);
} }
[Fact] [Fact]
@@ -90,7 +90,7 @@ public class RequestMessageParamMatcherTests
{ {
// Assign // Assign
var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=test1,test2"), "GET", "127.0.0.1"); 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 // Act
var result = new RequestMatchResult(); var result = new RequestMatchResult();
@@ -105,7 +105,7 @@ public class RequestMessageParamMatcherTests
{ {
// Assign // Assign
var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=test1,test2"), "GET", "127.0.0.1"); 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 // Act
var result = new RequestMatchResult(); var result = new RequestMatchResult();
@@ -120,7 +120,7 @@ public class RequestMessageParamMatcherTests
{ {
// Assign // Assign
var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=test0,test2"), "GET", "127.0.0.1"); 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 // Act
var result = new RequestMatchResult(); var result = new RequestMatchResult();
@@ -135,7 +135,7 @@ public class RequestMessageParamMatcherTests
{ {
// Assign // Assign
var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key"), "GET", "127.0.0.1"); 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 // Act
var result = new RequestMatchResult(); 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). // 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 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 // Act
var result = new RequestMatchResult(); 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). // 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 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 // Act
var result = new RequestMatchResult(); var result = new RequestMatchResult();
@@ -245,7 +245,7 @@ public class RequestMessageParamMatcherTests
{ {
// Assign: ignoreCase must still be honored on the inner matcher after the fix. // 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 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 // Act
var result = new RequestMatchResult(); var result = new RequestMatchResult();
@@ -260,7 +260,7 @@ public class RequestMessageParamMatcherTests
{ {
// Assign // Assign
var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=abc"), "GET", "127.0.0.1"); 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 // Act
var result = new RequestMatchResult(); var result = new RequestMatchResult();
@@ -275,7 +275,7 @@ public class RequestMessageParamMatcherTests
{ {
// Assign // Assign
var requestMessage = new RequestMessage(new UrlDetails("http://localhost?key=xyz"), "GET", "127.0.0.1"); 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 // Act
var result = new RequestMatchResult(); var result = new RequestMatchResult();
@@ -7,27 +7,27 @@ namespace WireMock.Net.Tests.Util;
public class QueryStringParserTests public class QueryStringParserTests
{ {
public static IEnumerable<object?[]> QueryStringTestData => new List<object?[]> public static List<object?[]> QueryStringTestData => new()
{ {
new object?[] { null, false, false, null }, new object?[] { null, false, false, null },
new object?[] { string.Empty, false, true, new Dictionary<string, string>() }, new object?[] { string.Empty, false, true, new Dictionary<string, WireMockList<string>>() },
new object?[] { "test", false, true, new Dictionary<string, string>() }, new object?[] { "test", false, true, new Dictionary<string, WireMockList<string>>() },
new object?[] { "&", false, true, new Dictionary<string, string>() }, new object?[] { "&", false, true, new Dictionary<string, WireMockList<string>>() },
new object?[] { "&&", false, true, new Dictionary<string, string>() }, new object?[] { "&&", false, true, new Dictionary<string, WireMockList<string>>() },
new object?[] { "a=", false, true, new Dictionary<string, string> { { "a", "" } } }, new object?[] { "a=", false, true, new Dictionary<string, WireMockList<string>> { { "a", new WireMockList<string>("") } } },
new object?[] { "&a", false, true, new Dictionary<string, string>() }, new object?[] { "&a", false, true, new Dictionary<string, WireMockList<string>>() },
new object?[] { "&a=", false, true, new Dictionary<string, string> { { "a", "" } } }, new object?[] { "&a=", false, true, new Dictionary<string, WireMockList<string>> { { "a", new WireMockList<string>("") } } },
new object?[] { "&key1=value1", false, true, new Dictionary<string, string> { { "key1", "value1" } } }, new object?[] { "&key1=value1", false, true, new Dictionary<string, WireMockList<string>> { { "key1", new WireMockList<string>("value1") } } },
new object?[] { "key1=value1", false, true, new Dictionary<string, string> { { "key1", "value1" } } }, new object?[] { "key1=value1", false, true, new Dictionary<string, WireMockList<string>> { { "key1", new WireMockList<string>("value1") } } },
new object?[] { "key1=value1&key2=value2", false, true, new Dictionary<string, string> { { "key1", "value1" }, { "key2", "value2" } } }, new object?[] { "key1=value1&key2=value2", false, true, new Dictionary<string, WireMockList<string>> { { "key1", new WireMockList<string>("value1") }, { "key2", new WireMockList<string>("value2") } } },
new object?[] { "key1=value1&key2=value2&", false, true, new Dictionary<string, string> { { "key1", "value1" }, { "key2", "value2" } } }, new object?[] { "key1=value1&key2=value2&", false, true, new Dictionary<string, WireMockList<string>> { { "key1", new WireMockList<string>("value1") }, { "key2", new WireMockList<string>("value2") } } },
new object?[] { "key1=value1&&key2=value2", false, true, new Dictionary<string, string> { { "key1", "value1" }, { "key2", "value2" } } }, new object?[] { "key1=value1&&key2=value2", false, true, new Dictionary<string, WireMockList<string>> { { "key1", new WireMockList<string>("value1") }, { "key2", new WireMockList<string>("value2") } } },
new object?[] { "&key1=value1&key2=value2&&", false, true, new Dictionary<string, string> { { "key1", "value1" }, { "key2", "value2" } } }, new object?[] { "&key1=value1&key2=value2&&", false, true, new Dictionary<string, WireMockList<string>> { { "key1", new WireMockList<string>("value1") }, { "key2", new WireMockList<string>("value2") } } },
}; };
[Theory] [Theory]
[MemberData(nameof(QueryStringTestData))] [MemberData(nameof(QueryStringTestData))]
public void TryParse_Should_Parse_QueryString(string queryString, bool caseIgnore, bool expectedResult, IDictionary<string, string> expectedOutput) public void TryParse_Should_Parse_QueryString(string queryString, bool caseIgnore, bool expectedResult, IDictionary<string, WireMockList<string>> expectedOutput)
{ {
// Act // Act
var result = QueryStringParser.TryParse(queryString, caseIgnore, out var actual); var result = QueryStringParser.TryParse(queryString, caseIgnore, out var actual);
@@ -49,7 +49,7 @@ public class QueryStringParserTests
// Assert // Assert
result.Should().BeTrue(); result.Should().BeTrue();
actual.Should().BeEquivalentTo(new Dictionary<string, string> { { "x", "rNaCP7hv8UOmS/JcujdvLw==" } }); actual.Should().BeEquivalentTo(new Dictionary<string, WireMockList<string>> { { "x", new WireMockList<string>("rNaCP7hv8UOmS/JcujdvLw==") } });
} }
[Fact] [Fact]
@@ -12,6 +12,7 @@ using WireMock.RequestBuilders;
using WireMock.ResponseBuilders; using WireMock.ResponseBuilders;
using WireMock.Server; using WireMock.Server;
using WireMock.Settings; using WireMock.Settings;
using WireMock.Types;
namespace WireMock.Net.Tests; namespace WireMock.Net.Tests;
@@ -339,7 +340,7 @@ public partial class WireMockServerTests
#endif #endif
[Fact] [Fact]
public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithFunc() public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithFunc1()
{ {
// Arrange // Arrange
using var server = WireMockServer.Start(); using var server = WireMockServer.Start();
@@ -364,6 +365,32 @@ public partial class WireMockServerTests
server.Stop(); 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<string, WireMockList<string>>? values) => values != null && values["key1"] == "value1")
)
.RespondWith(
Response.Create()
);
// Act
var content = new FormUrlEncodedContent([new KeyValuePair<string, string>("key1", "value1")]);
var response = await new HttpClient()
.PostAsync($"{server.Url}/foo", content, _ct);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
server.Stop();
}
[Fact] [Fact]
public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithExactMatcher() public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithExactMatcher()
{ {