Fix FluentAssertions on Header(s) (#1080)

* Fix FluentAssertions on Header(s)

* ...
This commit is contained in:
Stef Heyenrath
2024-03-09 08:39:52 +01:00
committed by GitHub
parent a7b0d502c6
commit 5b609915e1
3 changed files with 256 additions and 150 deletions

View File

@@ -0,0 +1,157 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using WireMock.Types;
// ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions;
public partial class WireMockAssertions
{
[CustomAssertion]
public AndConstraint<WireMockAssertions> WitHeaderKey(string expectedKey, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(request =>
{
return request.Headers?.Any(h => h.Key == expectedKey) == true;
});
Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => _requestMessages)
.ForCondition(requests => _callsCount == 0 || requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called with Header {0}{reason}.",
expectedKey
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} to have been called with Header {0}{reason}, but didn't find it among the calls with Header(s) {1}.",
_ => expectedKey,
requests => requests.Select(request => request.Headers)
);
_requestMessages = filter(_requestMessages).ToList();
return new AndConstraint<WireMockAssertions>(this);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithHeader(string expectedKey, string value, string because = "", params object[] becauseArgs)
=> WithHeader(expectedKey, new[] { value }, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithHeader(string expectedKey, string[] expectedValues, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(request =>
{
var headers = request.Headers?.ToArray() ?? new KeyValuePair<string, WireMockList<string>>[0];
var matchingHeaderValues = headers.Where(h => h.Key == expectedKey).SelectMany(h => h.Value.ToArray()).ToArray();
if (expectedValues.Length == 1 && matchingHeaderValues.Length == 1)
{
return matchingHeaderValues[0] == expectedValues[0];
}
var trimmedHeaderValues = string.Join(",", matchingHeaderValues.Select(x => x)).Split(',').Select(x => x.Trim()).ToArray();
return expectedValues.Any(trimmedHeaderValues.Contains);
});
Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => _requestMessages)
.ForCondition(requests => _callsCount == 0 || requests.Any())
.FailWith(
"Expected {context:wiremockserver} to have been called with Header {0} and Values {1}{reason}.",
expectedKey,
expectedValues
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} to have been called with Header {0} and Values {1}{reason}, but didn't find it among the calls with Header(s) {2}.",
_ => expectedKey,
_ => expectedValues,
requests => requests.Select(request => request.Headers)
);
_requestMessages = filter(_requestMessages).ToList();
return new AndConstraint<WireMockAssertions>(this);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithoutHeaderKey(string unexpectedKey, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(request =>
{
return request.Headers?.Any(h => h.Key == unexpectedKey) != true;
});
Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => _requestMessages)
.ForCondition(requests => _callsCount == 0 || requests.Any())
.FailWith(
"Expected {context:wiremockserver} not to have been called with Header {0}{reason}.",
unexpectedKey
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} not to have been called with Header {0}{reason}, but found it among the calls with Header(s) {1}.",
_ => unexpectedKey,
requests => requests.Select(request => request.Headers)
);
_requestMessages = filter(_requestMessages).ToList();
return new AndConstraint<WireMockAssertions>(this);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithoutHeader(string unexpectedKey, string value, string because = "", params object[] becauseArgs)
=> WithoutHeader(unexpectedKey, new[] { value }, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithoutHeader(string unexpectedKey, string[] expectedValues, string because = "", params object[] becauseArgs)
{
var (filter, condition) = BuildFilterAndCondition(request =>
{
var headers = request.Headers?.ToArray() ?? new KeyValuePair<string, WireMockList<string>>[0];
var matchingHeaderValues = headers.Where(h => h.Key == unexpectedKey).SelectMany(h => h.Value.ToArray()).ToArray();
if (expectedValues.Length == 1 && matchingHeaderValues.Length == 1)
{
return matchingHeaderValues[0] != expectedValues[0];
}
var trimmedHeaderValues = string.Join(",", matchingHeaderValues.Select(x => x)).Split(',').Select(x => x.Trim()).ToArray();
return !expectedValues.Any(trimmedHeaderValues.Contains);
});
Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => _requestMessages)
.ForCondition(requests => _callsCount == 0 || requests.Any())
.FailWith(
"Expected {context:wiremockserver} not to have been called with Header {0} and Values {1}{reason}.",
unexpectedKey,
expectedValues
)
.Then
.ForCondition(condition)
.FailWith(
"Expected {context:wiremockserver} not to have been called with Header {0} and Values {1}{reason}, but found it among the calls with Header(s) {2}.",
_ => unexpectedKey,
_ => expectedValues,
requests => requests.Select(request => request.Headers)
);
_requestMessages = filter(_requestMessages).ToList();
return new AndConstraint<WireMockAssertions>(this);
}
}

View File

@@ -3,7 +3,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using WireMock.Matchers; using WireMock.Matchers;
using WireMock.Server; using WireMock.Server;
using WireMock.Types;
// ReSharper disable once CheckNamespace // ReSharper disable once CheckNamespace
namespace WireMock.FluentAssertions; namespace WireMock.FluentAssertions;
@@ -13,13 +12,13 @@ public partial class WireMockAssertions
private const string Any = "*"; private const string Any = "*";
private readonly int? _callsCount; private readonly int? _callsCount;
private IReadOnlyList<IRequestMessage> _requestMessages; private IReadOnlyList<IRequestMessage> _requestMessages;
private readonly IReadOnlyList<KeyValuePair<string, WireMockList<string>>> _headers; //private readonly IReadOnlyList<KeyValuePair<string, WireMockList<string>>> _headers;
public WireMockAssertions(IWireMockServer subject, int? callsCount) public WireMockAssertions(IWireMockServer subject, int? callsCount)
{ {
_callsCount = callsCount; _callsCount = callsCount;
_requestMessages = subject.LogEntries.Select(logEntry => logEntry.RequestMessage).ToList(); _requestMessages = subject.LogEntries.Select(logEntry => logEntry.RequestMessage).ToList();
_headers = _requestMessages.SelectMany(req => req.Headers).ToList(); // _headers = _requestMessages.SelectMany(req => req.Headers).ToList();
} }
[CustomAssertion] [CustomAssertion]
@@ -39,7 +38,8 @@ public partial class WireMockAssertions
.ForCondition(condition) .ForCondition(condition)
.FailWith( .FailWith(
"Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but didn't find it among the calls to {1}.", "Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but didn't find it among the calls to {1}.",
_ => absoluteUrl, requests => requests.Select(request => request.AbsoluteUrl) _ => absoluteUrl,
requests => requests.Select(request => request.AbsoluteUrl)
); );
_requestMessages = filter(_requestMessages).ToList(); _requestMessages = filter(_requestMessages).ToList();
@@ -124,85 +124,6 @@ public partial class WireMockAssertions
return new AndWhichConstraint<WireMockAssertions, string>(this, clientIP); return new AndWhichConstraint<WireMockAssertions, string>(this, clientIP);
} }
[CustomAssertion]
public AndConstraint<WireMockAssertions> WitHeaderKey(string expectedKey, string because = "", params object[] becauseArgs)
{
using (new AssertionScope("headers from requests sent"))
{
_headers.Select(h => h.Key).Should().Contain(expectedKey, because, becauseArgs);
}
return new AndConstraint<WireMockAssertions>(this);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithHeader(string expectedKey, string value, string because = "", params object[] becauseArgs)
=> WithHeader(expectedKey, new[] { value }, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithHeader(string expectedKey, string[] expectedValues, string because = "", params object[] becauseArgs)
{
using (new AssertionScope($"header \"{expectedKey}\" from requests sent with value(s)"))
{
var matchingHeaderValues = _headers.Where(h => h.Key == expectedKey).SelectMany(h => h.Value.ToArray())
.ToArray();
if (expectedValues.Length == 1)
{
matchingHeaderValues.Should().Contain(expectedValues.First(), because, becauseArgs);
}
else
{
var trimmedHeaderValues = string.Join(",", matchingHeaderValues.Select(x => x)).Split(',').Select(x => x.Trim()).ToList();
foreach (var expectedValue in expectedValues)
{
trimmedHeaderValues.Should().Contain(expectedValue, because, becauseArgs);
}
}
}
return new AndConstraint<WireMockAssertions>(this);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithoutHeaderKey(string unexpectedKey, string because = "", params object[] becauseArgs)
{
using (new AssertionScope("headers from requests sent"))
{
_headers.Select(h => h.Key).Should().NotContain(unexpectedKey, because, becauseArgs);
}
return new AndConstraint<WireMockAssertions>(this);
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithoutHeader(string unexpectedKey, string value, string because = "", params object[] becauseArgs)
=> WithoutHeader(unexpectedKey, new[] { value }, because, becauseArgs);
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithoutHeader(string unexpectedKey, string[] expectedValues, string because = "", params object[] becauseArgs)
{
using (new AssertionScope($"header \"{unexpectedKey}\" from requests sent with value(s)"))
{
var matchingHeaderValues = _headers.Where(h => h.Key == unexpectedKey).SelectMany(h => h.Value.ToArray()).ToArray();
if (expectedValues.Length == 1)
{
matchingHeaderValues.Should().NotContain(expectedValues.First(), because, becauseArgs);
}
else
{
var trimmedHeaderValues = string.Join(",", matchingHeaderValues.Select(x => x)).Split(',').Select(x => x.Trim()).ToList();
foreach (var expectedValue in expectedValues)
{
trimmedHeaderValues.Should().NotContain(expectedValue, because, becauseArgs);
}
}
}
return new AndConstraint<WireMockAssertions>(this);
}
private (Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> Filter, Func<IReadOnlyList<IRequestMessage>, bool> Condition) BuildFilterAndCondition(Func<IRequestMessage, bool> predicate) private (Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> Filter, Func<IReadOnlyList<IRequestMessage>, bool> Condition) BuildFilterAndCondition(Func<IRequestMessage, bool> predicate)
{ {
Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> filter = requests => requests.Where(predicate).ToList(); Func<IReadOnlyList<IRequestMessage>, IReadOnlyList<IRequestMessage>> filter = requests => requests.Where(predicate).ToList();

View File

@@ -12,7 +12,6 @@ using WireMock.ResponseBuilders;
using WireMock.Server; using WireMock.Server;
using WireMock.Settings; using WireMock.Settings;
using Xunit; using Xunit;
using static System.Environment;
namespace WireMock.Net.Tests.FluentAssertions; namespace WireMock.Net.Tests.FluentAssertions;
@@ -103,10 +102,9 @@ public class WireMockAssertionsTests : IDisposable
.HaveReceivedACall() .HaveReceivedACall()
.AtAbsoluteUrl("anyurl"); .AtAbsoluteUrl("anyurl");
act.Should().Throw<Exception>() act.Should()
.And.Message.Should() .Throw<Exception>()
.Be( .WithMessage("Expected _server to have been called at address matching the absolute url \"anyurl\", but no calls were made.");
"Expected _server to have been called at address matching the absolute url \"anyurl\", but no calls were made.");
} }
[Fact] [Fact]
@@ -118,10 +116,9 @@ public class WireMockAssertionsTests : IDisposable
.HaveReceivedACall() .HaveReceivedACall()
.AtAbsoluteUrl("anyurl"); .AtAbsoluteUrl("anyurl");
act.Should().Throw<Exception>() act.Should()
.And.Message.Should() .Throw<Exception>()
.Be( .WithMessage($"Expected _server to have been called at address matching the absolute url \"anyurl\", but didn't find it among the calls to {{\"http://localhost:{_portUsed}/\"}}.");
$"Expected _server to have been called at address matching the absolute url \"anyurl\", but didn't find it among the calls to {{\"http://localhost:{_portUsed}/\"}}.");
} }
[Fact] [Fact]
@@ -172,9 +169,9 @@ public class WireMockAssertionsTests : IDisposable
.HaveReceivedACall() .HaveReceivedACall()
.WithHeader("Authorization", "value"); .WithHeader("Authorization", "value");
act.Should().Throw<Exception>() act.Should()
.And.Message.Should() .Throw<Exception>()
.Contain("\"Authorization\""); .WithMessage("*\"Authorization\"*");
} }
[Fact] [Fact]
@@ -188,38 +185,27 @@ public class WireMockAssertionsTests : IDisposable
.HaveReceivedACall() .HaveReceivedACall()
.WithHeader("Accept", "missing-value"); .WithHeader("Accept", "missing-value");
var sentHeaders = _server.LogEntries.SelectMany(x => x.RequestMessage.Headers) act.Should()
.ToDictionary(x => x.Key, x => x.Value)["Accept"] .Throw<Exception>()
.Select(x => $"\"{x}\"") .WithMessage("Expected _server to have been called with Header \"Accept\" and Values {\"missing-value\"}, but didn't find it among the calls with Header(s) {{[\"Accept\"] = {\"application/xml, application/json\"}, [\"Host\"] = {\"localhost:*\"}}}.");
.ToList();
var sentHeaderString = "{" + string.Join(", ", sentHeaders) + "}";
act.Should().Throw<Exception>()
.And.Message.Should()
.Be(
$"Expected header \"Accept\" from requests sent with value(s) {sentHeaderString} to contain \"missing-value\".{NewLine}");
} }
[Fact] [Fact]
public async Task HaveReceivedACall_WithHeader_Should_ThrowWhenNoCallsMatchingTheHeaderWithMultipleValuesWereMade() public async Task HaveReceivedACall_WithHeader_Should_ThrowWhenNoCallsMatchingTheHeaderWithMultipleValuesWereMade()
{ {
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); using var httpClient = new HttpClient { BaseAddress = new Uri(_server.Url!) };
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
await _httpClient.GetAsync("").ConfigureAwait(false); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
await httpClient.GetAsync("").ConfigureAwait(false);
Action act = () => _server.Should() Action act = () => _server.Should()
.HaveReceivedACall() .HaveReceivedACall()
.WithHeader("Accept", new[] { "missing-value1", "missing-value2" }); .WithHeader("Accept", new[] { "missing-value1", "missing-value2" });
const string missingValue1Message = act.Should()
"Expected header \"Accept\" from requests sent with value(s) {\"application/xml\", \"application/json\"} to contain \"missing-value1\"."; .Throw<Exception>()
const string missingValue2Message = .WithMessage("Expected _server to have been called with Header \"Accept\" and Values {\"missing-value1\", \"missing-value2\"}, but didn't find it among the calls with Header(s) {{[\"Accept\"] = {\"application/xml, application/json\"}, [\"Host\"] = {\"localhost:*\"}}}.");
"Expected header \"Accept\" from requests sent with value(s) {\"application/xml\", \"application/json\"} to contain \"missing-value2\".";
act.Should().Throw<Exception>()
.And.Message.Should()
.Be($"{string.Join(NewLine, missingValue1Message, missingValue2Message)}{NewLine}");
} }
[Fact] [Fact]
@@ -277,10 +263,9 @@ public class WireMockAssertionsTests : IDisposable
.HaveReceivedACall() .HaveReceivedACall()
.AtUrl("anyurl"); .AtUrl("anyurl");
act.Should().Throw<Exception>() act.Should()
.And.Message.Should() .Throw<Exception>()
.Be( .WithMessage("Expected _server to have been called at address matching the url \"anyurl\", but no calls were made.");
"Expected _server to have been called at address matching the url \"anyurl\", but no calls were made.");
} }
[Fact] [Fact]
@@ -292,10 +277,9 @@ public class WireMockAssertionsTests : IDisposable
.HaveReceivedACall() .HaveReceivedACall()
.AtUrl("anyurl"); .AtUrl("anyurl");
act.Should().Throw<Exception>() act.Should()
.And.Message.Should() .Throw<Exception>()
.Be( .WithMessage($"Expected _server to have been called at address matching the url \"anyurl\", but didn't find it among the calls to {{\"http://localhost:{_portUsed}/\"}}.");
$"Expected _server to have been called at address matching the url \"anyurl\", but didn't find it among the calls to {{\"http://localhost:{_portUsed}/\"}}.");
} }
[Fact] [Fact]
@@ -309,7 +293,7 @@ public class WireMockAssertionsTests : IDisposable
_server.Should() _server.Should()
.HaveReceivedACall() .HaveReceivedACall()
.WithProxyUrl($"http://localhost:9999"); .WithProxyUrl("http://localhost:9999");
} }
[Fact] [Fact]
@@ -323,10 +307,9 @@ public class WireMockAssertionsTests : IDisposable
.HaveReceivedACall() .HaveReceivedACall()
.WithProxyUrl("anyurl"); .WithProxyUrl("anyurl");
act.Should().Throw<Exception>() act.Should()
.And.Message.Should() .Throw<Exception>()
.Be( .WithMessage("Expected _server to have been called with proxy url \"anyurl\", but no calls were made.");
"Expected _server to have been called with proxy url \"anyurl\", but no calls were made.");
} }
[Fact] [Fact]
@@ -342,10 +325,9 @@ public class WireMockAssertionsTests : IDisposable
.HaveReceivedACall() .HaveReceivedACall()
.WithProxyUrl("anyurl"); .WithProxyUrl("anyurl");
act.Should().Throw<Exception>() act.Should()
.And.Message.Should() .Throw<Exception>()
.Be( .WithMessage("Expected _server to have been called with proxy url \"anyurl\", but didn't find it among the calls with {\"http://localhost:9999\"}.");
$"Expected _server to have been called with proxy url \"anyurl\", but didn't find it among the calls with {{\"http://localhost:9999\"}}.");
} }
[Fact] [Fact]
@@ -366,10 +348,9 @@ public class WireMockAssertionsTests : IDisposable
.HaveReceivedACall() .HaveReceivedACall()
.FromClientIP("different-ip"); .FromClientIP("different-ip");
act.Should().Throw<Exception>() act.Should()
.And.Message.Should() .Throw<Exception>()
.Be( .WithMessage("Expected _server to have been called from client IP \"different-ip\", but no calls were made.");
"Expected _server to have been called from client IP \"different-ip\", but no calls were made.");
} }
[Fact] [Fact]
@@ -382,10 +363,9 @@ public class WireMockAssertionsTests : IDisposable
.HaveReceivedACall() .HaveReceivedACall()
.FromClientIP("different-ip"); .FromClientIP("different-ip");
act.Should().Throw<Exception>() act.Should()
.And.Message.Should() .Throw<Exception>()
.Be( .WithMessage($"Expected _server to have been called from client IP \"different-ip\", but didn't find it among the calls from IP(s) {{\"{clientIP}\"}}.");
$"Expected _server to have been called from client IP \"different-ip\", but didn't find it among the calls from IP(s) {{\"{clientIP}\"}}.");
} }
[Fact] [Fact]
@@ -419,10 +399,9 @@ public class WireMockAssertionsTests : IDisposable
.HaveReceivedACall() .HaveReceivedACall()
.UsingPatch(); .UsingPatch();
act.Should().Throw<Exception>() act.Should()
.And.Message.Should() .Throw<Exception>()
.Be( .WithMessage("Expected _server to have been called using method \"PATCH\", but no calls were made.");
"Expected _server to have been called using method \"PATCH\", but no calls were made.");
} }
[Fact] [Fact]
@@ -434,10 +413,9 @@ public class WireMockAssertionsTests : IDisposable
.HaveReceivedACall() .HaveReceivedACall()
.UsingOptions(); .UsingOptions();
act.Should().Throw<Exception>() act.Should()
.And.Message.Should() .Throw<Exception>()
.Be( .WithMessage("Expected _server to have been called using method \"OPTIONS\", but didn't find it among the methods {\"POST\"}.");
"Expected _server to have been called using method \"OPTIONS\", but didn't find it among the methods {\"POST\"}.");
} }
#if !NET452 #if !NET452
@@ -838,6 +816,56 @@ public class WireMockAssertionsTests : IDisposable
server.Stop(); server.Stop();
} }
[Fact]
public async Task HaveReceivedACall_WithHeader_Should_ThrowWhenHttpMethodDoesNotMatch()
{
// Arrange
using var server = WireMockServer.Start();
// Act : HTTP GET
using var httpClient = new HttpClient();
await httpClient.GetAsync(server.Url!);
// Act : HTTP POST
var request = new HttpRequestMessage(HttpMethod.Post, server.Url!);
request.Headers.Add("TestHeader", new[] { "Value", "Value2" });
await httpClient.SendAsync(request);
// Assert
server.Should().HaveReceivedACall().UsingPost().And.WithHeader("TestHeader", new[] { "Value", "Value2" });
Action act = () => server.Should().HaveReceivedACall().UsingGet().And.WithHeader("TestHeader", "Value");
act.Should()
.Throw<Exception>()
.WithMessage("Expected server to have been called with Header \"TestHeader\" and Values {\"Value\"}, but didn't find it among the calls with Header(s) {{[\"Host\"] = {\"localhost:*\"}}}.");
}
[Fact]
public async Task HaveReceivedACall_WithHeaderKey_Should_ThrowWhenHttpMethodDoesNotMatch()
{
// Arrange
using var server = WireMockServer.Start();
// Act : HTTP GET
using var httpClient = new HttpClient();
await httpClient.GetAsync(server.Url!);
// Act : HTTP POST
var request = new HttpRequestMessage(HttpMethod.Post, server.Url!);
request.Headers.Add("TestHeader", new[] { "Value", "Value2" });
await httpClient.SendAsync(request);
// Assert
server.Should().HaveReceivedACall().UsingPost().And.WitHeaderKey("TestHeader");
Action act = () => server.Should().HaveReceivedACall().UsingGet().And.WitHeaderKey("TestHeader");
act.Should()
.Throw<Exception>()
.WithMessage("Expected server to have been called with Header \"TestHeader\", but didn't find it among the calls with Header(s) {{[\"Host\"] = {\"localhost:*\"}}}.");
}
public void Dispose() public void Dispose()
{ {
_server?.Stop(); _server?.Stop();