Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ private static string SerializeConstantValue(TypedConstant value)
{
SpecialType.System_Char => $"'{value.Value}'",
SpecialType.System_String => $"\"{value.Value.ToString().Replace("\"", "\\\"")}\"",
SpecialType.System_Boolean => value.Value.ToString().ToLower(),
SpecialType.System_Boolean => value.Value.ToString().ToLowerInvariant(),
SpecialType.System_Double => ((double)value.Value).ToString(CultureInfo.InvariantCulture),
SpecialType.System_Single => $"{((float)value.Value).ToString(CultureInfo.InvariantCulture)}F",
SpecialType.System_Byte => $"(byte){value.Value}",
Expand Down
124 changes: 50 additions & 74 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 @@ -269,6 +276,19 @@ private bool Matches(HttpContent value)
return true;
}

private static string GetStringFromHttpContent(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
string stringContent = reader.ReadToEnd();
return stringContent;
}

private interface IContentMatcher
{
bool Matches(HttpContent content);
Expand All @@ -290,14 +310,7 @@ public StringMatcher(string value, bool isExact)

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
string stringContent = reader.ReadToEnd();
string stringContent = GetStringFromHttpContent(content);
switch (_bodyMatchType)
{
case BodyMatchType.Exact when
Expand Down Expand Up @@ -345,15 +358,29 @@ private enum BodyMatchType
}
}

private sealed class PredicateStringMatcher : IContentMatcher
{
private readonly Func<string, bool> _predicate;

public PredicateStringMatcher(Func<string, bool> predicate)
{
_predicate = predicate;
}

public bool Matches(HttpContent content)
{
string stringContent = GetStringFromHttpContent(content);
return _predicate.Invoke(stringContent);
}
Comment thread
vbreuss marked this conversation as resolved.
}

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

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

public bool Matches(HttpContent content)
Expand All @@ -366,58 +393,7 @@ public bool Matches(HttpContent content)
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;
}

return true;
}

private static bool Contains(byte[] data, byte[] otherData)
{
#if NET8_0_OR_GREATER
return data.AsSpan().IndexOf(otherData) >= 0;
#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;
#endif
}

private enum BodyMatchType
{
Exact,
Contains,
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.Equals(c.ToLowerInvariant(), StringComparison.Ordinal)))
.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