diff --git a/Directory.Packages.props b/Directory.Packages.props
index 317b53e..cba59ad 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,7 +4,7 @@
-
+
diff --git a/README.md b/README.md
index 0564417..d45b655 100644
--- a/README.md
+++ b/README.md
@@ -80,3 +80,27 @@ mock.MyMethod(1);
// Fails, because the setup for MyMethod(2) was never used.
await That(mock.VerifyMock).AllSetupsAreUsed();
```
+
+### Web Extensions
+
+#### JSON Content
+
+With `It.IsJsonContent()`, you can precisely verify JSON content in HTTP requests during your tests. This feature is
+especially useful for testing HTTP clients and web APIs.
+
+```csharp
+// Verifies that a request was sent with a JSON body equivalent to { "foo": 1, "bar": "baz" }
+httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching(new { foo = 1, bar = \"baz\" }))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+// You can also provide a string representation of the JSON and it ignores formatting differences or property order
+httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBody("{\"bar\": \"baz\", \"foo\": 1}"))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+// In addition, you can specify the expected media type
+httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent("application/json"))
+ .ReturnsAsync(...);
+```
diff --git a/Source/aweXpect.Mockolate/Web/ItExtensions.HttpContent.IsJsonContent.cs b/Source/aweXpect.Mockolate/Web/ItExtensions.HttpContent.IsJsonContent.cs
new file mode 100644
index 0000000..8152725
--- /dev/null
+++ b/Source/aweXpect.Mockolate/Web/ItExtensions.HttpContent.IsJsonContent.cs
@@ -0,0 +1,284 @@
+#if NET8_0_OR_GREATER
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text.Json;
+using Mockolate.Parameters;
+
+// ReSharper disable once CheckNamespace
+namespace Mockolate.Web;
+
+#pragma warning disable S2325 // Methods and properties that don't access instance data should be static
+///
+/// Extensions for parameter matchers for HTTP-related types.
+///
+public static class AweXpectItExtensions
+{
+ ///
+ extension(It _)
+ {
+ ///
+ /// Expects the parameter to be a JSON content.
+ ///
+ public static IJsonContentParameter IsJsonContent()
+ => new JsonContentParameter();
+
+ ///
+ /// Expects the parameter to be a JSON content
+ /// with the given .
+ ///
+ public static IJsonContentParameter IsJsonContent(string mediaType)
+ => new JsonContentParameter().WithMediaType(mediaType);
+ }
+
+ ///
+ /// Further expectations on the JSON .
+ ///
+ public interface IJsonContentParameter : ItExtensions.IHttpContentParameter
+ {
+ ///
+ /// Expects the to have a body equal to the given .
+ ///
+ IJsonContentBodyParameter WithBody(string json, JsonDocumentOptions? options = null);
+
+ ///
+ /// Expects the to have a JSON body which matches the value.
+ ///
+ IJsonContentBodyParameter WithBodyMatching(object? expected, JsonDocumentOptions? options = null);
+
+ ///
+ /// Expects the to have a JSON body which matches the value.
+ ///
+ IJsonContentBodyParameter WithBodyMatching(IEnumerable expected, JsonDocumentOptions? options = null);
+ }
+
+ ///
+ /// Further expectations on the matching of a JSON body of the .
+ ///
+ public interface IJsonContentBodyParameter : IJsonContentParameter
+ {
+ ///
+ /// Ignores additional properties in JSON objects when comparing.
+ ///
+ IJsonContentBodyParameter IgnoringAdditionalProperties(bool ignoreAdditionalProperties = true);
+ }
+
+ private sealed class JsonContentParameter : HttpContentParameter, IJsonContentBodyParameter
+ {
+ private string? _body;
+ private bool _ignoringAdditionalProperties = true;
+ private JsonDocumentOptions? _jsonDocumentOptions;
+
+ ///
+ protected override IJsonContentParameter GetThis => this;
+
+ ///
+ public IJsonContentBodyParameter WithBody(string json,
+ JsonDocumentOptions? options = null)
+ {
+ _body = json;
+ _jsonDocumentOptions = options;
+ return this;
+ }
+
+ ///
+ public IJsonContentBodyParameter WithBodyMatching(object? expected,
+ JsonDocumentOptions? options = null)
+ => WithBody(JsonSerializer.Serialize(expected, JsonSerializerOptions.Default), options);
+
+ public IJsonContentBodyParameter WithBodyMatching(IEnumerable expected,
+ JsonDocumentOptions? options = null)
+ => WithBody(JsonSerializer.Serialize(expected, JsonSerializerOptions.Default), options);
+
+ ///
+ public IJsonContentBodyParameter IgnoringAdditionalProperties(bool ignoreAdditionalProperties = true)
+ {
+ _ignoringAdditionalProperties = ignoreAdditionalProperties;
+ return this;
+ }
+
+ protected override bool Matches(HttpContent value)
+ {
+ if (!base.Matches(value))
+ {
+ return false;
+ }
+
+ if (_body is not null)
+ {
+ try
+ {
+ JsonDocumentOptions options = _jsonDocumentOptions ?? GetDefaultOptions();
+ using JsonDocument actualDocument = JsonDocument.Parse(value.ReadAsStream(), options);
+ using JsonDocument expectedDocument = JsonDocument.Parse(_body, options);
+
+ if (!Compare(actualDocument.RootElement, expectedDocument.RootElement,
+ _ignoringAdditionalProperties))
+ {
+ return false;
+ }
+ }
+ catch (JsonException)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static JsonDocumentOptions GetDefaultOptions() => new()
+ {
+ AllowTrailingCommas = true,
+ };
+
+ private static bool Compare(
+ JsonElement actualElement,
+ JsonElement expectedElement,
+ bool ignoreAdditionalProperties)
+ {
+ if (actualElement.ValueKind != expectedElement.ValueKind)
+ {
+ return false;
+ }
+
+ return actualElement.ValueKind switch
+ {
+ JsonValueKind.Array => CompareJsonArray(actualElement, expectedElement, ignoreAdditionalProperties),
+ JsonValueKind.Number => CompareJsonNumber(actualElement, expectedElement),
+ JsonValueKind.String => CompareJsonString(actualElement, expectedElement),
+ JsonValueKind.Object => CompareJsonObject(actualElement, expectedElement, ignoreAdditionalProperties),
+ _ => true,
+ };
+ }
+
+ private static bool CompareJsonObject(JsonElement actualElement, JsonElement expectedElement,
+ bool ignoreAdditionalProperties)
+ {
+ foreach (JsonProperty item in expectedElement.EnumerateObject())
+ {
+ if (!actualElement.TryGetProperty(item.Name, out JsonElement property))
+ {
+ return false;
+ }
+
+ if (!Compare(property, item.Value, ignoreAdditionalProperties))
+ {
+ return false;
+ }
+ }
+
+ if (!ignoreAdditionalProperties)
+ {
+ foreach (JsonProperty property in actualElement.EnumerateObject())
+ {
+ if (!expectedElement.TryGetProperty(property.Name, out _))
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private static bool CompareJsonArray(JsonElement actualElement, JsonElement expectedElement,
+ bool ignoreAdditionalProperties)
+ {
+ for (int index = 0; index < expectedElement.GetArrayLength(); index++)
+ {
+ JsonElement expectedArrayElement = expectedElement[index];
+ if (actualElement.GetArrayLength() <= index)
+ {
+ return false;
+ }
+
+ JsonElement actualArrayElement = actualElement[index];
+ if (!Compare(actualArrayElement, expectedArrayElement, ignoreAdditionalProperties))
+ {
+ return false;
+ }
+ }
+
+ return ignoreAdditionalProperties || actualElement.GetArrayLength() <= expectedElement.GetArrayLength();
+ }
+
+ private static bool CompareJsonString(JsonElement actualElement, JsonElement expectedElement)
+ {
+ string? value1 = actualElement.GetString();
+ string? value2 = expectedElement.GetString();
+ return value1 == value2;
+ }
+
+ private static bool CompareJsonNumber(JsonElement actualElement, JsonElement expectedElement)
+ {
+ if (actualElement.TryGetInt32(out int v1) && expectedElement.TryGetInt32(out int v2))
+ {
+ return v1 == v2;
+ }
+
+ if (actualElement.TryGetDouble(out double n1) && expectedElement.TryGetDouble(out double n2))
+ {
+ return n1.Equals(n2);
+ }
+
+ return false;
+ }
+ }
+
+ private abstract class HttpContentParameter
+ : ItExtensions.IHttpContentParameter, IParameter
+ {
+ private List>? _callbacks;
+ private string? _mediaType;
+
+ ///
+ /// Returns this typed as for fluent API.
+ ///
+ protected abstract TParameter GetThis { get; }
+
+ ///
+ public TParameter WithMediaType(string? mediaType)
+ {
+ _mediaType = mediaType;
+ return GetThis;
+ }
+
+ ///
+ public IParameter Do(Action callback)
+ {
+ _callbacks ??= [];
+ _callbacks.Add(callback);
+ return this;
+ }
+
+ ///
+ public bool Matches(object? value)
+ => value is HttpContent typedValue && Matches(typedValue);
+
+ ///
+ public void InvokeCallbacks(object? value)
+ {
+ if (value is HttpContent httpContent)
+ {
+ _callbacks?.ForEach(a => a.Invoke(httpContent));
+ }
+ }
+
+ ///
+ /// Checks whether the given matches the expectations.
+ ///
+ protected virtual bool Matches(HttpContent value)
+ {
+ if (_mediaType is not null &&
+ value.Headers.ContentType?.MediaType?.Equals(_mediaType, StringComparison.OrdinalIgnoreCase) != true)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
+#pragma warning restore S2325 // Methods and properties that don't access instance data should be static
+#endif
diff --git a/Tests/aweXpect.Mockolate.Api.Tests/Expected/aweXpect.Mockolate_net10.0.txt b/Tests/aweXpect.Mockolate.Api.Tests/Expected/aweXpect.Mockolate_net10.0.txt
index 9b3c72a..d277a45 100644
--- a/Tests/aweXpect.Mockolate.Api.Tests/Expected/aweXpect.Mockolate_net10.0.txt
+++ b/Tests/aweXpect.Mockolate.Api.Tests/Expected/aweXpect.Mockolate_net10.0.txt
@@ -1,5 +1,26 @@
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/aweXpect/aweXpect.Mockolate.git")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v10.0", FrameworkDisplayName=".NET 10.0")]
+namespace Mockolate.Web
+{
+ public static class AweXpectItExtensions
+ {
+ public interface IJsonContentBodyParameter : Mockolate.Parameters.IParameter, Mockolate.Web.AweXpectItExtensions.IJsonContentParameter, Mockolate.Web.ItExtensions.IHttpContentParameter
+ {
+ Mockolate.Web.AweXpectItExtensions.IJsonContentBodyParameter IgnoringAdditionalProperties(bool ignoreAdditionalProperties = true);
+ }
+ public interface IJsonContentParameter : Mockolate.Parameters.IParameter, Mockolate.Web.ItExtensions.IHttpContentParameter
+ {
+ Mockolate.Web.AweXpectItExtensions.IJsonContentBodyParameter WithBody(string json, System.Text.Json.JsonDocumentOptions? options = default);
+ Mockolate.Web.AweXpectItExtensions.IJsonContentBodyParameter WithBodyMatching(object? expected, System.Text.Json.JsonDocumentOptions? options = default);
+ Mockolate.Web.AweXpectItExtensions.IJsonContentBodyParameter WithBodyMatching(System.Collections.Generic.IEnumerable expected, System.Text.Json.JsonDocumentOptions? options = default);
+ }
+ extension(Mockolate.It _)
+ {
+ Mockolate.Web.AweXpectItExtensions.IJsonContentParameter IsJsonContent();
+ Mockolate.Web.AweXpectItExtensions.IJsonContentParameter IsJsonContent(string mediaType);
+ }
+ }
+}
namespace aweXpect
{
public static class ThatMockVerify
diff --git a/Tests/aweXpect.Mockolate.Api.Tests/Expected/aweXpect.Mockolate_net8.0.txt b/Tests/aweXpect.Mockolate.Api.Tests/Expected/aweXpect.Mockolate_net8.0.txt
index fe91a4d..dc13a11 100644
--- a/Tests/aweXpect.Mockolate.Api.Tests/Expected/aweXpect.Mockolate_net8.0.txt
+++ b/Tests/aweXpect.Mockolate.Api.Tests/Expected/aweXpect.Mockolate_net8.0.txt
@@ -1,5 +1,26 @@
[assembly: System.Reflection.AssemblyMetadata("RepositoryUrl", "https://github.com/aweXpect/aweXpect.Mockolate.git")]
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v8.0", FrameworkDisplayName=".NET 8.0")]
+namespace Mockolate.Web
+{
+ public static class AweXpectItExtensions
+ {
+ public interface IJsonContentBodyParameter : Mockolate.Parameters.IParameter, Mockolate.Web.AweXpectItExtensions.IJsonContentParameter, Mockolate.Web.ItExtensions.IHttpContentParameter
+ {
+ Mockolate.Web.AweXpectItExtensions.IJsonContentBodyParameter IgnoringAdditionalProperties(bool ignoreAdditionalProperties = true);
+ }
+ public interface IJsonContentParameter : Mockolate.Parameters.IParameter, Mockolate.Web.ItExtensions.IHttpContentParameter
+ {
+ Mockolate.Web.AweXpectItExtensions.IJsonContentBodyParameter WithBody(string json, System.Text.Json.JsonDocumentOptions? options = default);
+ Mockolate.Web.AweXpectItExtensions.IJsonContentBodyParameter WithBodyMatching(object? expected, System.Text.Json.JsonDocumentOptions? options = default);
+ Mockolate.Web.AweXpectItExtensions.IJsonContentBodyParameter WithBodyMatching(System.Collections.Generic.IEnumerable expected, System.Text.Json.JsonDocumentOptions? options = default);
+ }
+ extension(Mockolate.It _)
+ {
+ Mockolate.Web.AweXpectItExtensions.IJsonContentParameter IsJsonContent();
+ Mockolate.Web.AweXpectItExtensions.IJsonContentParameter IsJsonContent(string mediaType);
+ }
+ }
+}
namespace aweXpect
{
public static class ThatMockVerify
diff --git a/Tests/aweXpect.Mockolate.Tests/Web/ItExtensionsTests.HttpContentTests.IsJsonContentTests.cs b/Tests/aweXpect.Mockolate.Tests/Web/ItExtensionsTests.HttpContentTests.IsJsonContentTests.cs
new file mode 100644
index 0000000..09549bd
--- /dev/null
+++ b/Tests/aweXpect.Mockolate.Tests/Web/ItExtensionsTests.HttpContentTests.IsJsonContentTests.cs
@@ -0,0 +1,548 @@
+#if NET8_0_OR_GREATER
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using Mockolate;
+using Mockolate.Web;
+
+namespace aweXpect.Mockolate.Tests.Web;
+
+public sealed partial class ItExtensionsTests
+{
+ public sealed class HttpContentTests
+ {
+ public sealed class IsJsonContentTests
+ {
+ [Theory]
+ [InlineData("true", true, true)]
+ [InlineData("true", false, false)]
+ [InlineData("false", true, false)]
+ [InlineData("false", false, true)]
+ public async Task BooleanValue_ShouldSucceedWhenMatching(string body, bool expected, bool expectSuccess)
+ {
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching(expected))
+ .ReturnsAsync(new HttpResponseMessage(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("42.1", 42.1, true)]
+ [InlineData("1.2", 2.1, false)]
+ public async Task DoubleValue_ShouldSucceedWhenMatching(string body, double expected, bool expectSuccess)
+ {
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching(expected))
+ .ReturnsAsync(new HttpResponseMessage(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("42", 42, true)]
+ [InlineData("1", 2, false)]
+ public async Task IntegerValue_ShouldSucceedWhenMatching(string body, int expected, bool expectSuccess)
+ {
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching(expected))
+ .ReturnsAsync(new HttpResponseMessage(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("null", true)]
+ [InlineData("{}", false)]
+ public async Task NullValue_ShouldSucceedWhenMatching(string body, bool expectSuccess)
+ {
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching(null))
+ .ReturnsAsync(new HttpResponseMessage(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);
+ }
+
+ [Fact]
+ public async Task ShouldSupportNestedObjects()
+ {
+ string body = "[{\"foo\": 2}, {\"foo\": 3}, {\"foo\": 4}]";
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching([
+ new
+ {
+ foo = 2,
+ },
+ new
+ {
+ foo = 3,
+ },
+ new
+ {
+ foo = 4,
+ },
+ ]))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsEqualTo(HttpStatusCode.OK);
+ }
+
+ [Theory]
+ [InlineData("application/json", true)]
+ [InlineData("text/plain", false)]
+ [InlineData("application/txt", false)]
+ public async Task ShouldVerifyMediaType(string mediaType, bool expectSuccess)
+ {
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent("application/json"))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent("", Encoding.UTF8, mediaType),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsEqualTo(expectSuccess ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
+ }
+
+ [Theory]
+ [InlineData("\"foo\"", "foo", true)]
+ [InlineData("\"foo\"", "bar", false)]
+ public async Task StringValue_ShouldSucceedWhenMatching(string body, string expected, bool expectSuccess)
+ {
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching(expected))
+ .ReturnsAsync(new HttpResponseMessage(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("{\n \"foo\": 1,\n \"bar\": 2,\n}", "{\"bar\":2,\"foo\": 1}", true)]
+ [InlineData("\"foo\"", "\"foo\"", true)]
+ [InlineData("foo", "bar", false)]
+ public async Task WithBody_ShouldCompareAsJson(string body,
+ string expected, bool expectSuccess)
+ {
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBody(expected))
+ .ReturnsAsync(new HttpResponseMessage(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);
+ }
+
+ [Fact]
+ public async Task WithBodyMatching_ShouldCompareAsJson()
+ {
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching(new
+ {
+ foo = 1,
+ bar = 2,
+ }))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent("""
+ {
+ "bar": 2,
+ "foo": 1
+ }
+ """),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsEqualTo(HttpStatusCode.OK);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task WithBodyMatching_WithAdditionalProperties_ShouldMatchWhenIgnoringAdditionalProperties(
+ bool ignoreAdditionalProperties)
+ {
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching(new
+ {
+ foo = 1,
+ bar = 2,
+ }).IgnoringAdditionalProperties(ignoreAdditionalProperties))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent("""
+ {
+ "bar": 2,
+ "foo": 1,
+ "baz": null,
+ }
+ """),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsEqualTo(ignoreAdditionalProperties ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
+ }
+
+ [Theory]
+ [InlineData("[1, 2,]", "[1, 2,]", true)]
+ [InlineData("[1, 2,]", "[1, 2,]", false)]
+ [InlineData("[1, 2]", "[1, 2,]", false)]
+ [InlineData("[1, 2,]", "[1, 2]", false)]
+ public async Task WithOptions_ShouldApplyOptions(string body, string expected, bool allowTrailingCommas)
+ {
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBody(expected, new JsonDocumentOptions
+ {
+ AllowTrailingCommas = allowTrailingCommas,
+ }))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsEqualTo(allowTrailingCommas ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
+ }
+
+ public sealed class ArrayTests
+ {
+ [Theory]
+ [MemberData(nameof(MatchingArrayValues))]
+ public async Task MatchingValues_ShouldSucceed(string[] expected, string body)
+ {
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching(expected))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsEqualTo(HttpStatusCode.OK);
+ }
+
+ [Theory]
+ [MemberData(nameof(NotMatchingArrayValues))]
+ public async Task NotMatchingValues_ShouldFail(string[] expected, string body)
+ {
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching(expected))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsNotEqualTo(HttpStatusCode.OK);
+ }
+
+ [Fact]
+ public async Task WhenElementsAreInDifferentOrder_ShouldFail()
+ {
+ string body = "[1, 2]";
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching([2, 1,]))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsNotEqualTo(HttpStatusCode.OK);
+ }
+
+ [Fact]
+ public async Task WhenExpectedContainsAdditionalElements_ShouldFail()
+ {
+ string body = "[1, 2]";
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching([1, 2, 3,]))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsNotEqualTo(HttpStatusCode.OK);
+ }
+
+ [Fact]
+ public async Task WhenSubjectContainsAdditionalElements_ShouldSucceed()
+ {
+ string body = "[1, 2, 3]";
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching([1, 2,]))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsEqualTo(HttpStatusCode.OK);
+ }
+
+ [Fact]
+ public async Task WhenSubjectContainsAdditionalElements_WhenNotIgnoringAdditionalProperties_ShouldFail()
+ {
+ string body = "[1, 2, 3]";
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(),
+ It.IsJsonContent().WithBodyMatching([1, 2,]).IgnoringAdditionalProperties(false))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsNotEqualTo(HttpStatusCode.OK);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task WithOptions_ShouldApplyOptions(bool allowTrailingCommas)
+ {
+ string body = "[1, 2,]";
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching([1, 2,], new JsonDocumentOptions
+ {
+ AllowTrailingCommas = allowTrailingCommas,
+ }))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsEqualTo(allowTrailingCommas ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
+ }
+
+ public static TheoryData MatchingArrayValues
+ => new()
+ {
+ {
+ [], "[]"
+ },
+ {
+ [], "[\"foo\"]"
+ },
+ {
+ [
+ "foo", "bar",
+ ],
+ "[\"foo\", \"bar\"]"
+ },
+ };
+
+ public static TheoryData NotMatchingArrayValues
+ => new()
+ {
+ {
+ [
+ "foo",
+ ],
+ "[]"
+ },
+ {
+ [
+ "bar", "foo",
+ ],
+ "[\"foo\", \"bar\"]"
+ },
+ };
+ }
+
+ public sealed class ObjectTests
+ {
+ [Theory]
+ [InlineData("{}", false)]
+ [InlineData("{\"foo\": 2}", true)]
+ public async Task ShouldFailIfPropertyIsMissing(string body, bool expectSuccess)
+ {
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching(new
+ {
+ foo = 2,
+ }))
+ .ReturnsAsync(new HttpResponseMessage(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("{}")]
+ [InlineData("{\"foo\": 1}")]
+ public async Task WhenExpectedIsEmpty_ShouldSucceed(string body)
+ {
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching(new object()))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsEqualTo(HttpStatusCode.OK);
+ }
+
+ [Fact]
+ public async Task WhenPropertyHasDifferentValue_ShouldFail()
+ {
+ string body = "{\"bar\": 2}";
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching(new
+ {
+ bar = 3,
+ }))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsNotEqualTo(HttpStatusCode.OK);
+ }
+
+ [Fact]
+ public async Task WhenSubjectHasAdditionalProperties_ShouldSucceed()
+ {
+ string body = "{\"foo\": null, \"bar\": 2}";
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching(new
+ {
+ bar = 2,
+ }))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsEqualTo(HttpStatusCode.OK);
+ }
+
+ [Fact]
+ public async Task WhenSubjectHasAdditionalProperties_WhenNotIgnoringAdditionalProperties_ShouldFail()
+ {
+ string body = "{\"foo\": null, \"bar\": 2}";
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching(new
+ {
+ bar = 2,
+ }).IgnoringAdditionalProperties(false))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsNotEqualTo(HttpStatusCode.OK);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task WithOptions_ShouldApplyOptions(bool allowTrailingCommas)
+ {
+ string body = "{\"foo\": 1,}";
+ HttpClient httpClient = Mock.Create();
+ httpClient.SetupMock.Method
+ .PostAsync(It.IsAny(), It.IsJsonContent().WithBodyMatching(new
+ {
+ foo = 1,
+ },
+ new JsonDocumentOptions
+ {
+ AllowTrailingCommas = allowTrailingCommas,
+ }))
+ .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+ HttpResponseMessage result = await httpClient.PostAsync("https://www.aweXpect.com",
+ new StringContent(body),
+ CancellationToken.None);
+
+ await That(result.StatusCode)
+ .IsEqualTo(allowTrailingCommas ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
+ }
+ }
+ }
+ }
+}
+#endif
diff --git a/Tests/aweXpect.Mockolate.Tests/Web/ItExtensionsTests.cs b/Tests/aweXpect.Mockolate.Tests/Web/ItExtensionsTests.cs
new file mode 100644
index 0000000..7a95d96
--- /dev/null
+++ b/Tests/aweXpect.Mockolate.Tests/Web/ItExtensionsTests.cs
@@ -0,0 +1,5 @@
+#if NET8_0_OR_GREATER
+namespace aweXpect.Mockolate.Tests.Web;
+
+public sealed partial class ItExtensionsTests;
+#endif