Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Docs/pages/special-types/01-httpclient.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ httpClient.SetupMock.Method

To verify against the string content, use the following methods:

- `.WithString(Func<string, bool>)`: to match the string content against the given predicate
- `.WithString(string)`: to match the content exactly as provided
- `.WithStringMatching(string)`: to match the content using wildcard patterns
- `.WithStringMatching(string).AsRegex()`: to match the content using regular expressions
Expand All @@ -181,8 +182,8 @@ httpClient.SetupMock.Method

To verify against the binary content, use the following methods:

- `.WithBytes(Func<byte[], bool>)`: to match the binary content against the given predicate
- `.WithBytes(byte[])`: to match the content exactly as provided
- `.WithBytesContaining(byte[])`: to match the content to contain the provided byte sequence in order

```csharp
httpClient.SetupMock.Method
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,7 @@ httpClient.SetupMock.Method

To verify against the string content, use the following methods:

- `.WithString(Func<string, bool>)`: to match the string content against the given predicate
- `.WithString(string)`: to match the content exactly as provided
- `.WithStringMatching(string)`: to match the content using wildcard patterns
- `.WithStringMatching(string).AsRegex()`: to match the content using regular expressions
Expand All @@ -1162,8 +1163,8 @@ httpClient.SetupMock.Method

To verify against the binary content, use the following methods:

- `.WithBytes(Func<byte[], bool>)`: to match the binary content against the given predicate
- `.WithBytes(byte[])`: to match the content exactly as provided
- `.WithBytesContaining(byte[])`: to match the content to contain the provided byte sequence in order

```csharp
httpClient.SetupMock.Method
Expand Down
103 changes: 40 additions & 63 deletions Source/Mockolate/Web/ItExtensions.HttpContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ public interface IFormDataContentParameter : IHttpContentParameter
/// </summary>
public interface IHttpContentParameter : IParameter<HttpContent?>, IHttpHeaderParameter<IHttpContentParameter>
{
/// <summary>
/// Expects the content to have a string body that satisfies the <paramref name="predicate" />.
/// </summary>
IHttpContentParameter WithString(Func<string, bool> predicate);

/// <summary>
/// Expects the content to have a string body equal to the <paramref name="expected" /> value.
/// </summary>
Expand All @@ -88,9 +93,9 @@ public interface IHttpContentParameter : IParameter<HttpContent?>, IHttpHeaderPa
IHttpContentParameter WithBytes(byte[] bytes);

/// <summary>
Comment thread
vbreuss marked this conversation as resolved.
/// Expects the binary content to contain the given <paramref name="bytes" />.
/// Expects the binary content to satisfy the <paramref name="predicate" />.
/// </summary>
IHttpContentParameter WithBytesContaining(byte[] bytes);
IHttpContentParameter WithBytes(Func<byte[], bool> predicate);

/// <summary>
/// Expects the form data content to contain the given <paramref name="key" />-<paramref name="value" /> pair.
Expand Down Expand Up @@ -144,6 +149,11 @@ public void InvokeCallbacks(object? value)
}
}

public IHttpContentParameter WithString(Func<string, bool> predicate)
{
_contentMatcher = new PredicateStringMatcher(predicate);
return this;
}

public IStringContentBodyParameter WithString(string expected)
{
Expand All @@ -157,17 +167,14 @@ public IStringContentBodyMatchingParameter WithStringMatching(string pattern)
return this;
}

public IHttpContentParameter WithBytes(byte[] bytes)
public IHttpContentParameter WithBytes(Func<byte[], bool> predicate)
{
_contentMatcher = new BinaryMatcher(bytes, true);
_contentMatcher = new BinaryMatcher(predicate);
return this;
}

public IHttpContentParameter WithBytesContaining(byte[] bytes)
{
_contentMatcher = new BinaryMatcher(bytes, false);
return this;
}
public IHttpContentParameter WithBytes(byte[] bytes)
=> WithBytes(b => bytes.SequenceEqual(b));

public IFormDataContentParameter WithFormData(string key, HttpFormDataValue value)
{
Expand Down Expand Up @@ -345,79 +352,49 @@ private enum BodyMatchType
}
}

private sealed class BinaryMatcher : IContentMatcher
private sealed class PredicateStringMatcher : IContentMatcher
{
private readonly BodyMatchType _bodyMatchType;
private readonly byte[] _expectedBytes;
private readonly Func<string, bool> _predicate;

public BinaryMatcher(byte[] bytes, bool isExact)
public PredicateStringMatcher(Func<string, bool> predicate)
{
_expectedBytes = bytes;
_bodyMatchType = isExact ? BodyMatchType.Exact : BodyMatchType.Contains;
_predicate = predicate;
}

public bool Matches(HttpContent content)
{
#if NET8_0_OR_GREATER
Stream stream = content.ReadAsStream();
using StreamReader reader = new(stream, leaveOpen: true);
#else
Stream stream = content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult();
using StreamReader reader = new(stream);
#endif
using MemoryStream ms = new();
stream.CopyTo(ms);
byte[] bytes = ms.ToArray();
switch (_bodyMatchType)
{
case BodyMatchType.Exact when
!bytes.SequenceEqual(_expectedBytes):
return false;
case BodyMatchType.Contains when
!Contains(bytes, _expectedBytes):
return false;
}
string stringContent = reader.ReadToEnd();
return _predicate.Invoke(stringContent);
}
Comment thread
vbreuss marked this conversation as resolved.
}

return true;
private sealed class BinaryMatcher : IContentMatcher
{
private readonly Func<byte[], bool> _predicate;

public BinaryMatcher(Func<byte[], bool> predicate)
{
_predicate = predicate;
}

private static bool Contains(byte[] data, byte[] otherData)
public bool Matches(HttpContent content)
{
#if NET8_0_OR_GREATER
return data.AsSpan().IndexOf(otherData) >= 0;
Stream stream = content.ReadAsStream();
#else
int dataLength = data.Length;
int otherDataLength = otherData.Length;

if (dataLength < otherDataLength)
{
return false;
}

for (int i = 0; i <= dataLength - otherDataLength; i++)
{
bool isMatch = true;
for (int j = 0; j < otherDataLength; j++)
{
if (data[i + j] != otherData[j])
{
isMatch = false;
break;
}
}

if (isMatch)
{
return true;
}
}

return false;
Stream stream = content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult();
#endif
}

private enum BodyMatchType
{
Exact,
Contains,
using MemoryStream ms = new();
stream.CopyTo(ms);
byte[] bytes = ms.ToArray();
return _predicate.Invoke(bytes);
}
}

Expand Down
3 changes: 2 additions & 1 deletion Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1866,14 +1866,15 @@ namespace Mockolate.Web
}
public interface IHttpContentParameter : Mockolate.Parameters.IParameter<System.Net.Http.HttpContent?>, Mockolate.Web.ItExtensions.IHttpHeaderParameter<Mockolate.Web.ItExtensions.IHttpContentParameter>
{
Mockolate.Web.ItExtensions.IHttpContentParameter WithBytes(System.Func<byte[], bool> predicate);
Mockolate.Web.ItExtensions.IHttpContentParameter WithBytes(byte[] bytes);
Mockolate.Web.ItExtensions.IHttpContentParameter WithBytesContaining(byte[] bytes);
Mockolate.Web.ItExtensions.IFormDataContentParameter WithFormData([System.Runtime.CompilerServices.TupleElementNames(new string[] {
"Key",
"Value"})] System.Collections.Generic.IEnumerable<System.ValueTuple<string, Mockolate.Web.HttpFormDataValue>> values);
Mockolate.Web.ItExtensions.IFormDataContentParameter WithFormData(string values);
Mockolate.Web.ItExtensions.IFormDataContentParameter WithFormData(string key, Mockolate.Web.HttpFormDataValue value);
Mockolate.Web.ItExtensions.IHttpContentParameter WithMediaType(string? mediaType);
Mockolate.Web.ItExtensions.IHttpContentParameter WithString(System.Func<string, bool> predicate);
Mockolate.Web.ItExtensions.IStringContentBodyParameter WithString(string expected);
Mockolate.Web.ItExtensions.IStringContentBodyMatchingParameter WithStringMatching(string pattern);
}
Expand Down
3 changes: 2 additions & 1 deletion Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1865,14 +1865,15 @@ namespace Mockolate.Web
}
public interface IHttpContentParameter : Mockolate.Parameters.IParameter<System.Net.Http.HttpContent?>, Mockolate.Web.ItExtensions.IHttpHeaderParameter<Mockolate.Web.ItExtensions.IHttpContentParameter>
{
Mockolate.Web.ItExtensions.IHttpContentParameter WithBytes(System.Func<byte[], bool> predicate);
Mockolate.Web.ItExtensions.IHttpContentParameter WithBytes(byte[] bytes);
Mockolate.Web.ItExtensions.IHttpContentParameter WithBytesContaining(byte[] bytes);
Mockolate.Web.ItExtensions.IFormDataContentParameter WithFormData([System.Runtime.CompilerServices.TupleElementNames(new string[] {
"Key",
"Value"})] System.Collections.Generic.IEnumerable<System.ValueTuple<string, Mockolate.Web.HttpFormDataValue>> values);
Mockolate.Web.ItExtensions.IFormDataContentParameter WithFormData(string values);
Mockolate.Web.ItExtensions.IFormDataContentParameter WithFormData(string key, Mockolate.Web.HttpFormDataValue value);
Mockolate.Web.ItExtensions.IHttpContentParameter WithMediaType(string? mediaType);
Mockolate.Web.ItExtensions.IHttpContentParameter WithString(System.Func<string, bool> predicate);
Mockolate.Web.ItExtensions.IStringContentBodyParameter WithString(string expected);
Mockolate.Web.ItExtensions.IStringContentBodyMatchingParameter WithStringMatching(string pattern);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1806,14 +1806,15 @@ namespace Mockolate.Web
}
public interface IHttpContentParameter : Mockolate.Parameters.IParameter<System.Net.Http.HttpContent?>, Mockolate.Web.ItExtensions.IHttpHeaderParameter<Mockolate.Web.ItExtensions.IHttpContentParameter>
{
Mockolate.Web.ItExtensions.IHttpContentParameter WithBytes(System.Func<byte[], bool> predicate);
Mockolate.Web.ItExtensions.IHttpContentParameter WithBytes(byte[] bytes);
Mockolate.Web.ItExtensions.IHttpContentParameter WithBytesContaining(byte[] bytes);
Mockolate.Web.ItExtensions.IFormDataContentParameter WithFormData([System.Runtime.CompilerServices.TupleElementNames(new string[] {
"Key",
"Value"})] System.Collections.Generic.IEnumerable<System.ValueTuple<string, Mockolate.Web.HttpFormDataValue>> values);
Mockolate.Web.ItExtensions.IFormDataContentParameter WithFormData(string values);
Mockolate.Web.ItExtensions.IFormDataContentParameter WithFormData(string key, Mockolate.Web.HttpFormDataValue value);
Mockolate.Web.ItExtensions.IHttpContentParameter WithMediaType(string? mediaType);
Mockolate.Web.ItExtensions.IHttpContentParameter WithString(System.Func<string, bool> predicate);
Mockolate.Web.ItExtensions.IStringContentBodyParameter WithString(string expected);
Mockolate.Web.ItExtensions.IStringContentBodyMatchingParameter WithStringMatching(string pattern);
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,28 @@ public sealed partial class IsHttpContentTests
{
public sealed class WithBytesTests
{
[Theory]
[InlineData(new byte[0], 0x1, false)]
[InlineData(new byte[] { 0x1, }, 0x1, true)]
[InlineData(new byte[] { 0x1, }, 0x2, false)]
[InlineData(new byte[] { 0x1, 0x2, 0x3, }, 0x1, true)]
[InlineData(new byte[] { 0x1, 0x2, 0x3, }, 0x2, false)]
[InlineData(new byte[] { 0x1, 0x2, 0x3, }, 0x3, false)]
public async Task Predicate_ShouldValidatePredicate(byte[] body, byte expectedFirstByte, bool expectSuccess)
{
HttpClient httpClient = Mock.Create<HttpClient>();
httpClient.SetupMock.Method
.PostAsync(It.IsAny<Uri>(), It.IsHttpContent().WithBytes(b => b.Length > 0 && b[0] == expectedFirstByte))
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));

HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
new ByteArrayContent(body),
CancellationToken.None);

await That(result.StatusCode)
.IsEqualTo(expectSuccess ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
}

[Theory]
[InlineData(new byte[0], new byte[0], true)]
[InlineData(new byte[] { 0x66, }, new byte[] { 0x66, }, true)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Mockolate.Web;

Expand All @@ -13,6 +12,28 @@ public sealed partial class IsHttpContentTests
{
public sealed class WithStringTests
{
[Theory]
[InlineData("", true)]
[InlineData("foo", true)]
[InlineData("FOO", false)]
[InlineData("bar", true)]
[InlineData("BAR", false)]
public async Task Predicate_ShouldValidatePredicate(string content, bool expectSuccess)
{
HttpClient httpClient = Mock.Create<HttpClient>();
httpClient.SetupMock.Method
.PostAsync(It.IsAny<Uri>(), It.IsHttpContent()
.WithString(c => c.ToLower() == c))
Comment thread
vbreuss marked this conversation as resolved.
Outdated
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));

HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
new StringContent(content),
CancellationToken.None);

await That(result.StatusCode)
.IsEqualTo(expectSuccess ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
}

[Fact]
public async Task ShouldNotCheckHttpContentType()
{
Expand Down
Loading
Loading