From 5b609915e117b5ee9b348c682a8a47738f477a3a Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 9 Mar 2024 08:39:52 +0100 Subject: [PATCH] Fix FluentAssertions on Header(s) (#1080) * Fix FluentAssertions on Header(s) * ... --- .../WireMockAssertions.WithHeader.cs | 157 +++++++++++++++++ .../Assertions/WireMockAssertions.cs | 87 +--------- .../WireMockAssertionsTests.cs | 162 ++++++++++-------- 3 files changed, 256 insertions(+), 150 deletions(-) create mode 100644 src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithHeader.cs diff --git a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithHeader.cs b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithHeader.cs new file mode 100644 index 00000000..d6ca8940 --- /dev/null +++ b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithHeader.cs @@ -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 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(this); + } + + [CustomAssertion] + public AndConstraint WithHeader(string expectedKey, string value, string because = "", params object[] becauseArgs) + => WithHeader(expectedKey, new[] { value }, because, becauseArgs); + + [CustomAssertion] + public AndConstraint WithHeader(string expectedKey, string[] expectedValues, string because = "", params object[] becauseArgs) + { + var (filter, condition) = BuildFilterAndCondition(request => + { + var headers = request.Headers?.ToArray() ?? new KeyValuePair>[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(this); + } + + [CustomAssertion] + public AndConstraint 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(this); + } + + [CustomAssertion] + public AndConstraint WithoutHeader(string unexpectedKey, string value, string because = "", params object[] becauseArgs) + => WithoutHeader(unexpectedKey, new[] { value }, because, becauseArgs); + + [CustomAssertion] + public AndConstraint WithoutHeader(string unexpectedKey, string[] expectedValues, string because = "", params object[] becauseArgs) + { + var (filter, condition) = BuildFilterAndCondition(request => + { + var headers = request.Headers?.ToArray() ?? new KeyValuePair>[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(this); + } +} \ No newline at end of file diff --git a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs index 5a08f32f..692ca198 100644 --- a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs +++ b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using WireMock.Matchers; using WireMock.Server; -using WireMock.Types; // ReSharper disable once CheckNamespace namespace WireMock.FluentAssertions; @@ -13,13 +12,13 @@ public partial class WireMockAssertions private const string Any = "*"; private readonly int? _callsCount; private IReadOnlyList _requestMessages; - private readonly IReadOnlyList>> _headers; + //private readonly IReadOnlyList>> _headers; public WireMockAssertions(IWireMockServer subject, int? callsCount) { _callsCount = callsCount; _requestMessages = subject.LogEntries.Select(logEntry => logEntry.RequestMessage).ToList(); - _headers = _requestMessages.SelectMany(req => req.Headers).ToList(); + // _headers = _requestMessages.SelectMany(req => req.Headers).ToList(); } [CustomAssertion] @@ -39,7 +38,8 @@ public partial class WireMockAssertions .ForCondition(condition) .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}.", - _ => absoluteUrl, requests => requests.Select(request => request.AbsoluteUrl) + _ => absoluteUrl, + requests => requests.Select(request => request.AbsoluteUrl) ); _requestMessages = filter(_requestMessages).ToList(); @@ -124,85 +124,6 @@ public partial class WireMockAssertions return new AndWhichConstraint(this, clientIP); } - [CustomAssertion] - public AndConstraint 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(this); - } - - [CustomAssertion] - public AndConstraint WithHeader(string expectedKey, string value, string because = "", params object[] becauseArgs) - => WithHeader(expectedKey, new[] { value }, because, becauseArgs); - - [CustomAssertion] - public AndConstraint 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(this); - } - - [CustomAssertion] - public AndConstraint 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(this); - } - - [CustomAssertion] - public AndConstraint WithoutHeader(string unexpectedKey, string value, string because = "", params object[] becauseArgs) - => WithoutHeader(unexpectedKey, new[] { value }, because, becauseArgs); - - [CustomAssertion] - public AndConstraint 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(this); - } - private (Func, IReadOnlyList> Filter, Func, bool> Condition) BuildFilterAndCondition(Func predicate) { Func, IReadOnlyList> filter = requests => requests.Where(predicate).ToList(); diff --git a/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsTests.cs b/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsTests.cs index c16fa4b9..7961a459 100644 --- a/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsTests.cs +++ b/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsTests.cs @@ -12,7 +12,6 @@ using WireMock.ResponseBuilders; using WireMock.Server; using WireMock.Settings; using Xunit; -using static System.Environment; namespace WireMock.Net.Tests.FluentAssertions; @@ -103,10 +102,9 @@ public class WireMockAssertionsTests : IDisposable .HaveReceivedACall() .AtAbsoluteUrl("anyurl"); - act.Should().Throw() - .And.Message.Should() - .Be( - "Expected _server to have been called at address matching the absolute url \"anyurl\", but no calls were made."); + act.Should() + .Throw() + .WithMessage("Expected _server to have been called at address matching the absolute url \"anyurl\", but no calls were made."); } [Fact] @@ -118,10 +116,9 @@ public class WireMockAssertionsTests : IDisposable .HaveReceivedACall() .AtAbsoluteUrl("anyurl"); - act.Should().Throw() - .And.Message.Should() - .Be( - $"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}/\"}}."); + act.Should() + .Throw() + .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}/\"}}."); } [Fact] @@ -172,9 +169,9 @@ public class WireMockAssertionsTests : IDisposable .HaveReceivedACall() .WithHeader("Authorization", "value"); - act.Should().Throw() - .And.Message.Should() - .Contain("\"Authorization\""); + act.Should() + .Throw() + .WithMessage("*\"Authorization\"*"); } [Fact] @@ -188,38 +185,27 @@ public class WireMockAssertionsTests : IDisposable .HaveReceivedACall() .WithHeader("Accept", "missing-value"); - var sentHeaders = _server.LogEntries.SelectMany(x => x.RequestMessage.Headers) - .ToDictionary(x => x.Key, x => x.Value)["Accept"] - .Select(x => $"\"{x}\"") - .ToList(); - - var sentHeaderString = "{" + string.Join(", ", sentHeaders) + "}"; - - act.Should().Throw() - .And.Message.Should() - .Be( - $"Expected header \"Accept\" from requests sent with value(s) {sentHeaderString} to contain \"missing-value\".{NewLine}"); + act.Should() + .Throw() + .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:*\"}}}."); } [Fact] public async Task HaveReceivedACall_WithHeader_Should_ThrowWhenNoCallsMatchingTheHeaderWithMultipleValuesWereMade() { - _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); - _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - await _httpClient.GetAsync("").ConfigureAwait(false); + using var httpClient = new HttpClient { BaseAddress = new Uri(_server.Url!) }; + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + await httpClient.GetAsync("").ConfigureAwait(false); Action act = () => _server.Should() .HaveReceivedACall() .WithHeader("Accept", new[] { "missing-value1", "missing-value2" }); - const string missingValue1Message = - "Expected header \"Accept\" from requests sent with value(s) {\"application/xml\", \"application/json\"} to contain \"missing-value1\"."; - const string missingValue2Message = - "Expected header \"Accept\" from requests sent with value(s) {\"application/xml\", \"application/json\"} to contain \"missing-value2\"."; - - act.Should().Throw() - .And.Message.Should() - .Be($"{string.Join(NewLine, missingValue1Message, missingValue2Message)}{NewLine}"); + act.Should() + .Throw() + .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:*\"}}}."); } [Fact] @@ -277,10 +263,9 @@ public class WireMockAssertionsTests : IDisposable .HaveReceivedACall() .AtUrl("anyurl"); - act.Should().Throw() - .And.Message.Should() - .Be( - "Expected _server to have been called at address matching the url \"anyurl\", but no calls were made."); + act.Should() + .Throw() + .WithMessage("Expected _server to have been called at address matching the url \"anyurl\", but no calls were made."); } [Fact] @@ -292,10 +277,9 @@ public class WireMockAssertionsTests : IDisposable .HaveReceivedACall() .AtUrl("anyurl"); - act.Should().Throw() - .And.Message.Should() - .Be( - $"Expected _server to have been called at address matching the url \"anyurl\", but didn't find it among the calls to {{\"http://localhost:{_portUsed}/\"}}."); + act.Should() + .Throw() + .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}/\"}}."); } [Fact] @@ -309,7 +293,7 @@ public class WireMockAssertionsTests : IDisposable _server.Should() .HaveReceivedACall() - .WithProxyUrl($"http://localhost:9999"); + .WithProxyUrl("http://localhost:9999"); } [Fact] @@ -323,10 +307,9 @@ public class WireMockAssertionsTests : IDisposable .HaveReceivedACall() .WithProxyUrl("anyurl"); - act.Should().Throw() - .And.Message.Should() - .Be( - "Expected _server to have been called with proxy url \"anyurl\", but no calls were made."); + act.Should() + .Throw() + .WithMessage("Expected _server to have been called with proxy url \"anyurl\", but no calls were made."); } [Fact] @@ -342,10 +325,9 @@ public class WireMockAssertionsTests : IDisposable .HaveReceivedACall() .WithProxyUrl("anyurl"); - act.Should().Throw() - .And.Message.Should() - .Be( - $"Expected _server to have been called with proxy url \"anyurl\", but didn't find it among the calls with {{\"http://localhost:9999\"}}."); + act.Should() + .Throw() + .WithMessage("Expected _server to have been called with proxy url \"anyurl\", but didn't find it among the calls with {\"http://localhost:9999\"}."); } [Fact] @@ -366,10 +348,9 @@ public class WireMockAssertionsTests : IDisposable .HaveReceivedACall() .FromClientIP("different-ip"); - act.Should().Throw() - .And.Message.Should() - .Be( - "Expected _server to have been called from client IP \"different-ip\", but no calls were made."); + act.Should() + .Throw() + .WithMessage("Expected _server to have been called from client IP \"different-ip\", but no calls were made."); } [Fact] @@ -382,10 +363,9 @@ public class WireMockAssertionsTests : IDisposable .HaveReceivedACall() .FromClientIP("different-ip"); - act.Should().Throw() - .And.Message.Should() - .Be( - $"Expected _server to have been called from client IP \"different-ip\", but didn't find it among the calls from IP(s) {{\"{clientIP}\"}}."); + act.Should() + .Throw() + .WithMessage($"Expected _server to have been called from client IP \"different-ip\", but didn't find it among the calls from IP(s) {{\"{clientIP}\"}}."); } [Fact] @@ -419,10 +399,9 @@ public class WireMockAssertionsTests : IDisposable .HaveReceivedACall() .UsingPatch(); - act.Should().Throw() - .And.Message.Should() - .Be( - "Expected _server to have been called using method \"PATCH\", but no calls were made."); + act.Should() + .Throw() + .WithMessage("Expected _server to have been called using method \"PATCH\", but no calls were made."); } [Fact] @@ -434,10 +413,9 @@ public class WireMockAssertionsTests : IDisposable .HaveReceivedACall() .UsingOptions(); - act.Should().Throw() - .And.Message.Should() - .Be( - "Expected _server to have been called using method \"OPTIONS\", but didn't find it among the methods {\"POST\"}."); + act.Should() + .Throw() + .WithMessage("Expected _server to have been called using method \"OPTIONS\", but didn't find it among the methods {\"POST\"}."); } #if !NET452 @@ -838,6 +816,56 @@ public class WireMockAssertionsTests : IDisposable 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() + .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() + .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() { _server?.Stop();