diff --git a/Source/Mockolate/Internals/Polyfills/StringExtensions.cs b/Source/Mockolate/Internals/Polyfills/StringExtensions.cs
index 95253878..5b8601be 100644
--- a/Source/Mockolate/Internals/Polyfills/StringExtensions.cs
+++ b/Source/Mockolate/Internals/Polyfills/StringExtensions.cs
@@ -1,4 +1,5 @@
#if NETSTANDARD2_0
+using System;
using System.Diagnostics.CodeAnalysis;
namespace Mockolate.Internals.Polyfills;
@@ -9,6 +10,27 @@ namespace Mockolate.Internals.Polyfills;
[ExcludeFromCodeCoverage]
internal static class StringExtensionMethods
{
+
+ ///
+ /// Returns a value indicating whether a specified character occurs within this string, using the specified comparison
+ /// rules.
+ ///
+ ///
+ /// if the parameter occurs within this string; otherwise,
+ /// .
+ ///
+ internal static bool Contains(
+ this string @this,
+ string value,
+ StringComparison comparisonType)
+ => comparisonType switch
+ {
+ StringComparison.OrdinalIgnoreCase => @this.ToLowerInvariant().Contains(value.ToLowerInvariant()),
+ StringComparison.InvariantCultureIgnoreCase => @this.ToLowerInvariant().Contains(value.ToLowerInvariant()),
+ StringComparison.CurrentCultureIgnoreCase => @this.ToLower().Contains(value.ToLower()),
+ _ => @this.Contains(value),
+ };
+
///
/// Determines whether the end of this string instance matches the specified character.
///
diff --git a/Source/Mockolate/Web/ItExtensions.HttpContent.WithString.cs b/Source/Mockolate/Web/ItExtensions.HttpContent.WithString.cs
index 13a8e782..ad34d4a6 100644
--- a/Source/Mockolate/Web/ItExtensions.HttpContent.WithString.cs
+++ b/Source/Mockolate/Web/ItExtensions.HttpContent.WithString.cs
@@ -2,6 +2,9 @@
using System.Net.Http;
using System.Text.RegularExpressions;
using Mockolate.Internals;
+#if NETSTANDARD2_0
+using Mockolate.Internals.Polyfills;
+#endif
namespace Mockolate.Web;
@@ -12,7 +15,7 @@ public static partial class ItExtensions
extension(IHttpContentParameter parameter)
{
///
- /// Expects the content to have a string body equal to the value.
+ /// Expects the content to have a string body that contains the value.
///
public IStringContentBodyParameter WithString(string expected)
{
@@ -54,28 +57,39 @@ public interface IStringContentBodyParameter : IHttpContentParameter
/// Ignores case when matching the body.
///
IStringContentBodyParameter IgnoringCase();
+
+ ///
+ /// Requires the body to completely match the given string.
+ ///
+ IStringContentBodyParameter Exactly();
}
private sealed class StringMatcher(string value, bool isExact)
{
private BodyMatchType _bodyMatchType = isExact ? BodyMatchType.Exact : BodyMatchType.Wildcard;
+ private bool _exactly;
private bool _ignoringCase;
private RegexOptions _regexOptions;
private TimeSpan? _timeout;
public bool Matches(string stringContent)
{
- switch (_bodyMatchType)
+ switch (_bodyMatchType, _exactly)
{
- case BodyMatchType.Exact when
+ case (BodyMatchType.Exact, false) when
+ !stringContent.Contains(value, _ignoringCase
+ ? StringComparison.OrdinalIgnoreCase
+ : StringComparison.Ordinal):
+ return false;
+ case (BodyMatchType.Exact, true) when
!stringContent.Equals(value, _ignoringCase
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal):
return false;
- case BodyMatchType.Wildcard when
- !Wildcard.Pattern(value, _ignoringCase, false).Matches(stringContent):
+ case (BodyMatchType.Wildcard, _) when
+ !Wildcard.Pattern(value, _ignoringCase, _exactly).Matches(stringContent):
return false;
- case BodyMatchType.Regex:
+ case (BodyMatchType.Regex, _):
{
Regex regex = new(value,
_ignoringCase ? _regexOptions | RegexOptions.IgnoreCase : _regexOptions,
@@ -101,6 +115,9 @@ public void AsRegex(
_bodyMatchType = BodyMatchType.Regex;
}
+ public void Exactly()
+ => _exactly = true;
+
public void IgnoringCase()
=> _ignoringCase = true;
@@ -128,6 +145,12 @@ public IStringContentBodyParameter IgnoringCase()
return this;
}
+ public IStringContentBodyParameter Exactly()
+ {
+ _data.Exactly();
+ return this;
+ }
+
public IStringContentBodyParameter AsRegex(RegexOptions options = RegexOptions.None, TimeSpan? timeout = null)
{
_data.AsRegex(options, timeout);
diff --git a/Source/Mockolate/Web/ItExtensions.HttpContent.cs b/Source/Mockolate/Web/ItExtensions.HttpContent.cs
index 526c003b..325ce1b5 100644
--- a/Source/Mockolate/Web/ItExtensions.HttpContent.cs
+++ b/Source/Mockolate/Web/ItExtensions.HttpContent.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Net.Http;
using System.Text;
using Mockolate.Parameters;
@@ -61,10 +62,11 @@ public interface IHttpContentParameter : IParameter, IHttpHeaderPa
private sealed class HttpContentParameter
: IHttpContentHeaderParameter, IHttpRequestMessagePropertyParameter, IParameter
{
+ private BinaryMatcher? _binaryContentMatcher;
private List>? _callbacks;
- private IContentMatcher? _contentMatcher;
private HttpHeadersMatcher? _headers;
private string? _mediaType;
+ private PredicateStringMatcher? _stringContentMatcher;
public IHttpContentParameter IncludingRequestHeaders()
{
@@ -74,13 +76,14 @@ public IHttpContentParameter IncludingRequestHeaders()
public IHttpContentParameter WithString(Func predicate)
{
- _contentMatcher = new PredicateStringMatcher(predicate);
+ _stringContentMatcher ??= new PredicateStringMatcher();
+ _stringContentMatcher.AddPredicate(predicate);
return this;
}
public IHttpContentParameter WithBytes(Func predicate)
{
- _contentMatcher = new BinaryMatcher(predicate);
+ _binaryContentMatcher ??= new BinaryMatcher(predicate);
return this;
}
@@ -126,8 +129,14 @@ public bool Matches(HttpContent? value, HttpRequestMessage? requestMessage)
return false;
}
- if (_contentMatcher is not null &&
- !_contentMatcher.Matches(value, requestMessage))
+ if (_stringContentMatcher is not null &&
+ !_stringContentMatcher.Matches(value, requestMessage))
+ {
+ return false;
+ }
+
+ if (_binaryContentMatcher is not null &&
+ !_binaryContentMatcher.Matches(value, requestMessage))
{
return false;
}
@@ -153,8 +162,10 @@ private interface IContentMatcher
bool Matches(HttpContent content, HttpRequestMessage? message);
}
- private sealed class PredicateStringMatcher(Func predicate) : IContentMatcher
+ private sealed class PredicateStringMatcher : IContentMatcher
{
+ private readonly List> _predicates = [];
+
public bool Matches(HttpContent content, HttpRequestMessage? message)
{
static Encoding GetEncodingFromCharset(string? charset)
@@ -184,8 +195,7 @@ static Encoding GetEncodingFromCharset(string? charset)
stream.Position = position;
#else
string stringContent;
- if (message?.Properties.TryGetValue("Mockolate:HttpContent", out object value) == true
- && value is byte[] bytes)
+ if (message?.Properties.TryGetValue("Mockolate:HttpContent", out object value) == true && value is byte[] bytes)
{
stringContent = encoding.GetString(bytes);
}
@@ -198,8 +208,11 @@ static Encoding GetEncodingFromCharset(string? charset)
stream.Position = position;
}
#endif
- return predicate.Invoke(stringContent);
+ return _predicates.All(predicate => predicate.Invoke(stringContent));
}
+
+ public void AddPredicate(Func predicate)
+ => _predicates.Add(predicate);
}
private sealed class BinaryMatcher(Func predicate) : IContentMatcher
diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt
index 7eacc9c5..354d5d8f 100644
--- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt
+++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt
@@ -1943,6 +1943,7 @@ namespace Mockolate.Web
}
public interface IStringContentBodyParameter : Mockolate.Parameters.IParameter, Mockolate.Web.ItExtensions.IHttpContentParameter, Mockolate.Web.ItExtensions.IHttpHeaderParameter
{
+ Mockolate.Web.ItExtensions.IStringContentBodyParameter Exactly();
Mockolate.Web.ItExtensions.IStringContentBodyParameter IgnoringCase();
}
public interface IUriParameter : Mockolate.Parameters.IParameter
diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt
index a12e74e4..15265de0 100644
--- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt
+++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt
@@ -1942,6 +1942,7 @@ namespace Mockolate.Web
}
public interface IStringContentBodyParameter : Mockolate.Parameters.IParameter, Mockolate.Web.ItExtensions.IHttpContentParameter, Mockolate.Web.ItExtensions.IHttpHeaderParameter
{
+ Mockolate.Web.ItExtensions.IStringContentBodyParameter Exactly();
Mockolate.Web.ItExtensions.IStringContentBodyParameter IgnoringCase();
}
public interface IUriParameter : Mockolate.Parameters.IParameter
diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt
index c39e9cfe..124a62a3 100644
--- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt
+++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt
@@ -1883,6 +1883,7 @@ namespace Mockolate.Web
}
public interface IStringContentBodyParameter : Mockolate.Parameters.IParameter, Mockolate.Web.ItExtensions.IHttpContentParameter, Mockolate.Web.ItExtensions.IHttpHeaderParameter
{
+ Mockolate.Web.ItExtensions.IStringContentBodyParameter Exactly();
Mockolate.Web.ItExtensions.IStringContentBodyParameter IgnoringCase();
}
public interface IUriParameter : Mockolate.Parameters.IParameter
diff --git a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringMatchingTests.cs b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringMatchingTests.cs
index 88ce8700..7740efa8 100644
--- a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringMatchingTests.cs
+++ b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringMatchingTests.cs
@@ -135,6 +135,39 @@ public async Task ShouldCheckForMatchingWildcard(string body, string pattern, bo
await That(result.StatusCode)
.IsEqualTo(expectSuccess ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
}
+
+ [Theory]
+ [InlineData(true, "f?o", "foo")]
+ [InlineData(true, "f?o", "bar")]
+ [InlineData(true, "bar", "f?o")]
+ [InlineData(true, "bar", "bar")]
+ [InlineData(true, "f?o", "?oo", "b?r", "?ar")]
+ [InlineData(false, "f?o", "b?r", "baz")]
+ [InlineData(false, "f?o", "baz", "b?r")]
+ [InlineData(false, "?az", "f?o", "b?r")]
+ [InlineData(false, "?az")]
+ public async Task WithMultipleExpectations_ShouldVerifyAll(bool expectSuccess,
+ params string[] expectedValues)
+ {
+ ItExtensions.IHttpContentParameter isHttpContent = It.IsHttpContent();
+ foreach (string expectedValue in expectedValues)
+ {
+ isHttpContent = isHttpContent.WithStringMatching(expectedValue);
+ }
+
+ string body = "foo;bar";
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), isHttpContent)
+ .ReturnsAsync(HttpStatusCode.OK);
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsEqualTo(expectSuccess ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
+ }
}
}
}
diff --git a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringTests.cs b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringTests.cs
index e0f421cf..77ffc968 100644
--- a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringTests.cs
+++ b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringTests.cs
@@ -16,9 +16,50 @@ public sealed partial class IsHttpContentTests
{
public sealed class WithStringTests
{
+ [Theory]
+ [InlineData("foo", "FOO", true)]
+ public async Task Exactly_IgnoringCase_ShouldCheckCaseInsensitiveForFullContentBody(
+ string body, string expected, bool expectSuccess)
+ {
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsHttpContent().WithString(expected).Exactly().IgnoringCase())
+ .ReturnsAsync(HttpStatusCode.OK);
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsEqualTo(expectSuccess ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
+ }
+
+ [Theory]
+ [InlineData("foo", "foo", true)]
+ [InlineData("foo", "FOO", false)]
+ [InlineData("foo", "baz", false)]
+ [InlineData(" bar", "bar", false)]
+ [InlineData("baz ", "baz", false)]
+ public async Task Exactly_ShouldCheckForFullContentBody(string body, string expected, bool expectSuccess)
+ {
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsHttpContent().WithString(expected).Exactly())
+ .ReturnsAsync(HttpStatusCode.OK);
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsEqualTo(expectSuccess ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
+ }
+
[Theory]
[InlineData("foo", "foo", true)]
[InlineData("foo", "FOO", true)]
+ [InlineData(" foo", "FOO", true)]
+ [InlineData("FOO ", "foo", true)]
[InlineData("foo", "bar", false)]
public async Task IgnoringCase_ShouldCheckForCaseInsensitiveEquality(string body,
string expected, bool expectSuccess)
@@ -233,6 +274,39 @@ public async Task WithInvalidCharsetHeader_ShouldFallbackToUtf8(string charsetHe
await That(result.StatusCode)
.IsEqualTo(HttpStatusCode.OK);
}
+
+ [Theory]
+ [InlineData(true, "foo", "foo")]
+ [InlineData(true, "foo", "bar")]
+ [InlineData(true, "bar", "foo")]
+ [InlineData(true, "bar", "bar")]
+ [InlineData(true, "foo", "foo", "bar", "bar")]
+ [InlineData(false, "foo", "bar", "baz")]
+ [InlineData(false, "foo", "baz", "bar")]
+ [InlineData(false, "baz", "foo", "bar")]
+ [InlineData(false, "baz")]
+ public async Task WithMultipleExpectations_ShouldVerifyAll(bool expectSuccess,
+ params string[] expectedValues)
+ {
+ ItExtensions.IHttpContentParameter isHttpContent = It.IsHttpContent();
+ foreach (string expectedValue in expectedValues)
+ {
+ isHttpContent = isHttpContent.WithString(expectedValue);
+ }
+
+ string body = "foo;bar";
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), isHttpContent)
+ .ReturnsAsync(HttpStatusCode.OK);
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsEqualTo(expectSuccess ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
+ }
}
}
}