From b523ab9125aa5d1d5f9927d1e050d755d1da2417 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 15 Oct 2022 08:21:48 +0200 Subject: [PATCH] Some fixes to WireMock.Net.Assertions (#816) * Add extra unit test for UsingPost * .X * ok * ok2 * header --- .../Assertions/WireMockAssertions.cs | 126 +++++++++++++----- .../WireMock.Net.FluentAssertions.csproj | 1 + .../WireMockAssertionsTests.cs | 111 +++++++++++++-- .../Owin/GlobalExceptionMiddlewareTests.cs | 9 +- 4 files changed, 196 insertions(+), 51 deletions(-) diff --git a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs index e811e6dc..022917c0 100644 --- a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs +++ b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs @@ -1,97 +1,133 @@ #pragma warning disable CS1591 +using System; +using System.Collections.Generic; using System.Linq; using FluentAssertions; using FluentAssertions.Execution; using WireMock.Server; +using WireMock.Types; // ReSharper disable once CheckNamespace namespace WireMock.FluentAssertions; public class WireMockAssertions { - private readonly IWireMockServer _subject; private readonly int? _callsCount; + private IReadOnlyList _requestMessages; + private IReadOnlyList>> _headers; public WireMockAssertions(IWireMockServer subject, int? callsCount) { - _subject = subject; _callsCount = callsCount; + _requestMessages = subject.LogEntries.Select(logEntry => logEntry.RequestMessage).ToList(); + _headers = _requestMessages.SelectMany(req => req.Headers).ToList(); } [CustomAssertion] - public AndConstraint AtAbsoluteUrl(string absoluteUrl, string because = "", params object[] becauseArgs) + public AndWhichConstraint AtAbsoluteUrl(string absoluteUrl, string because = "", params object[] becauseArgs) { + Func predicate = request => string.Equals(request.AbsoluteUrl, absoluteUrl, StringComparison.OrdinalIgnoreCase); + var (filter, condition) = BuildFilterAndCondition(predicate); + Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => _subject.LogEntries.Select(x => x.RequestMessage).ToList()) + .Given(() => _requestMessages) .ForCondition(requests => requests.Any()) .FailWith( "Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but no calls were made.", - absoluteUrl) + absoluteUrl + ) .Then - .ForCondition(x => (_callsCount == null && x.Any(y => y.AbsoluteUrl == absoluteUrl)) || (_callsCount == x.Count(y => y.AbsoluteUrl == absoluteUrl))) + .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) + ); - return new AndConstraint(this); + _requestMessages = filter(_requestMessages).ToList(); + + return new AndWhichConstraint(this, absoluteUrl); } [CustomAssertion] - public AndConstraint AtUrl(string url, string because = "", params object[] becauseArgs) + public AndWhichConstraint AtUrl(string url, string because = "", params object[] becauseArgs) { + Func predicate = request => string.Equals(request.Url, url, StringComparison.OrdinalIgnoreCase); + var (filter, condition) = BuildFilterAndCondition(predicate); + Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => _subject.LogEntries.Select(x => x.RequestMessage).ToList()) + .Given(() => _requestMessages) .ForCondition(requests => requests.Any()) .FailWith( "Expected {context:wiremockserver} to have been called at address matching the url {0}{reason}, but no calls were made.", url) .Then - .ForCondition(x => (_callsCount == null && x.Any(y => y.Url == url)) || (_callsCount == x.Count(y => y.Url == url))) + .ForCondition(condition) .FailWith( "Expected {context:wiremockserver} to have been called at address matching the url {0}{reason}, but didn't find it among the calls to {1}.", - _ => url, requests => requests.Select(request => request.Url)); + _ => url, + requests => requests.Select(request => request.Url) + ); - return new AndConstraint(this); + _requestMessages = filter(_requestMessages).ToList(); + + return new AndWhichConstraint(this, url); } [CustomAssertion] - public AndConstraint WithProxyUrl(string proxyUrl, string because = "", params object[] becauseArgs) + public AndWhichConstraint WithProxyUrl(string proxyUrl, string because = "", params object[] becauseArgs) { + Func predicate = request => string.Equals(request.ProxyUrl, proxyUrl, StringComparison.OrdinalIgnoreCase); + var (filter, condition) = BuildFilterAndCondition(predicate); + Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => _subject.LogEntries.Select(x => x.RequestMessage).ToList()) + .Given(() => _requestMessages) .ForCondition(requests => requests.Any()) .FailWith( "Expected {context:wiremockserver} to have been called with proxy url {0}{reason}, but no calls were made.", - proxyUrl) + proxyUrl + ) .Then - .ForCondition(x => (_callsCount == null && x.Any(y => y.ProxyUrl == proxyUrl)) || (_callsCount == x.Count(y => y.ProxyUrl == proxyUrl))) + .ForCondition(condition) .FailWith( "Expected {context:wiremockserver} to have been called with proxy url {0}{reason}, but didn't find it among the calls with {1}.", - _ => proxyUrl, requests => requests.Select(request => request.ProxyUrl)); + _ => proxyUrl, + requests => requests + .Select(request => request.ProxyUrl) + ); - return new AndConstraint(this); + _requestMessages = filter(_requestMessages).ToList(); + + return new AndWhichConstraint(this, proxyUrl); } [CustomAssertion] - public AndConstraint FromClientIP(string clientIP, string because = "", params object[] becauseArgs) + public AndWhichConstraint FromClientIP(string clientIP, string because = "", params object[] becauseArgs) { + Func predicate = request => string.Equals(request.ClientIP, clientIP, StringComparison.OrdinalIgnoreCase); + var (filter, condition) = BuildFilterAndCondition(predicate); + Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => _subject.LogEntries.Select(x => x.RequestMessage).ToList()) + .Given(() => _requestMessages) .ForCondition(requests => requests.Any()) .FailWith( "Expected {context:wiremockserver} to have been called from client IP {0}{reason}, but no calls were made.", clientIP) .Then - .ForCondition(x => (_callsCount == null && x.Any(y => y.ClientIP == clientIP)) || (_callsCount == x.Count(y => y.ClientIP == clientIP))) + .ForCondition(requests => + (_callsCount == null && requests.Any(req => req.ClientIP == clientIP)) || + (_callsCount == requests.Count(req => req.ClientIP == clientIP)) + ) .FailWith( "Expected {context:wiremockserver} to have been called from client IP {0}{reason}, but didn't find it among the calls from IP(s) {1}.", _ => clientIP, requests => requests.Select(request => request.ClientIP)); - return new AndConstraint(this); + _requestMessages = filter(_requestMessages).ToList(); + + return new AndWhichConstraint(this, clientIP); } [CustomAssertion] @@ -101,28 +137,25 @@ public class WireMockAssertions [CustomAssertion] public AndConstraint WithHeader(string expectedKey, string[] expectedValues, string because = "", params object[] becauseArgs) { - var headersDictionary = _subject.LogEntries.SelectMany(x => x.RequestMessage.Headers) - .ToDictionary(x => x.Key, x => x.Value); - using (new AssertionScope("headers from requests sent")) { - headersDictionary.Should().ContainKey(expectedKey, because, becauseArgs); + _headers.Select(h => h.Key).Should().Contain(expectedKey, because, becauseArgs); } using (new AssertionScope($"header \"{expectedKey}\" from requests sent with value(s)")) { + var headerValues = _headers.First(h => h.Key == expectedKey).Value; + if (expectedValues.Length == 1) { - headersDictionary[expectedKey].Should().Contain(expectedValues.First()); + headerValues.Should().Contain(expectedValues.First(), because, becauseArgs); } else { - var trimmedHeaderValues = string.Join(",", headersDictionary[expectedKey].Select(x => x)).Split(',') - .Select(x => x.Trim()) - .ToList(); + var trimmedHeaderValues = string.Join(",", headerValues.Select(x => x)).Split(',').Select(x => x.Trim()).ToList(); foreach (var expectedValue in expectedValues) { - trimmedHeaderValues.Should().Contain(expectedValue); + trimmedHeaderValues.Should().Contain(expectedValue, because, becauseArgs); } } } @@ -169,19 +202,40 @@ public class WireMockAssertions [CustomAssertion] public AndConstraint UsingMethod(string method, string because = "", params object[] becauseArgs) { + Func predicate = request => string.Equals(request.Method, method, StringComparison.OrdinalIgnoreCase); + var (filter, condition) = BuildFilterAndCondition(predicate); + Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => _subject.LogEntries.Select(x => x.RequestMessage).ToList()) + .Given(() => _requestMessages) .ForCondition(requests => requests.Any()) .FailWith( "Expected {context:wiremockserver} to have been called using method {0}{reason}, but no calls were made.", - method) + method + ) .Then - .ForCondition(x => (_callsCount == null && x.Any(y => y.Method == method)) || (_callsCount == x.Count(y => y.Method == method))) + .ForCondition(condition) .FailWith( "Expected {context:wiremockserver} to have been called using method {0}{reason}, but didn't find it among the methods {1}.", - _ => method, requests => requests.Select(request => request.Method)); + _ => method, + requests => requests.Select(request => request.Method) + ); + + _requestMessages = filter(_requestMessages).ToList(); return new AndConstraint(this); } + + private (Func, IReadOnlyList> Filter, Func, bool> Condition) BuildFilterAndCondition(Func predicate) + { + Func, IReadOnlyList> filter = requests => requests.Where(predicate).ToList(); + + return + ( + filter, + requests => + (_callsCount == null && filter(_requestMessages).Any()) || + (_callsCount == filter(_requestMessages).Count()) + ); + } } \ No newline at end of file diff --git a/src/WireMock.Net.FluentAssertions/WireMock.Net.FluentAssertions.csproj b/src/WireMock.Net.FluentAssertions/WireMock.Net.FluentAssertions.csproj index 15e68822..cad5bcdd 100644 --- a/src/WireMock.Net.FluentAssertions/WireMock.Net.FluentAssertions.csproj +++ b/src/WireMock.Net.FluentAssertions/WireMock.Net.FluentAssertions.csproj @@ -22,6 +22,7 @@ true MIT + 10 diff --git a/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsTests.cs b/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsTests.cs index 0767911e..c7b86aa6 100644 --- a/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsTests.cs +++ b/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsTests.cs @@ -24,11 +24,11 @@ public class WireMockAssertionsTests : IDisposable public WireMockAssertionsTests() { _server = WireMockServer.Start(); - _server.Given(Request.Create().UsingAnyMethod()) - .RespondWith(Response.Create().WithSuccess()); + _server.Given(Request.Create().UsingAnyMethod()).RespondWith(Response.Create().WithSuccess()); + _portUsed = _server.Ports.First(); - _httpClient = new HttpClient { BaseAddress = new Uri(_server.Urls[0]) }; + _httpClient = new HttpClient { BaseAddress = new Uri(_server.Url!) }; } [Fact] @@ -61,6 +61,18 @@ public class WireMockAssertionsTests : IDisposable .AtAbsoluteUrl($"http://localhost:{_portUsed}/anyurl"); } + [Fact] + public async Task HaveReceived1Calls_AtAbsoluteUrlUsingPost_WhenAPostCallWasMadeToAbsoluteUrl_Should_BeOK() + { + await _httpClient.PostAsync("anyurl", new StringContent("")).ConfigureAwait(false); + + _server.Should() + .HaveReceived(1).Calls() + .AtAbsoluteUrl($"http://localhost:{_portUsed}/anyurl") + .And + .UsingPost(); + } + [Fact] public async Task HaveReceived2Calls_AtAbsoluteUrl_WhenACallWasMadeToAbsoluteUrl_Should_BeOK() { @@ -123,15 +135,20 @@ public class WireMockAssertionsTests : IDisposable } [Fact] - public async Task HaveReceivedACall_WithHeader_WhenACallWasMadeWithExpectedHeaderAmongMultipleHeaderValues_Should_BeOK() + public async Task HaveReceivedACall_WithHeader_WhenMultipleCallsWereMadeWithExpectedHeaderAmongMultipleHeaderValues_Should_BeOK() { _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - await _httpClient.GetAsync("").ConfigureAwait(false); + await _httpClient.GetAsync("1").ConfigureAwait(false); + + _httpClient.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("EN")); + await _httpClient.GetAsync("2").ConfigureAwait(false); _server.Should() .HaveReceivedACall() - .WithHeader("Accept", new[] { "application/xml", "application/json" }); + .WithHeader("Accept", new[] { "application/xml", "application/json" }) + .And + .WithHeader("Accept-Language", new[] { "EN" }); } [Fact] @@ -145,7 +162,7 @@ public class WireMockAssertionsTests : IDisposable act.Should().Throw() .And.Message.Should() - .Contain("to contain key \"Authorization\"."); + .Contain("to contain \"Authorization\"."); } [Fact] @@ -431,16 +448,90 @@ public class WireMockAssertionsTests : IDisposable .UsingOptions(); } - [Fact] - public async Task HaveReceivedACall_UsingPost_WhenACallWasMadeUsingPost_Should_BeOK() + [Theory] + [InlineData("POST")] + [InlineData("Post")] + public async Task HaveReceivedACall_UsingPost_WhenACallWasMadeUsingPost_Should_BeOK(string method) { - await _httpClient.SendAsync(new HttpRequestMessage(new HttpMethod("POST"), "anyurl")).ConfigureAwait(false); + await _httpClient.SendAsync(new HttpRequestMessage(new HttpMethod(method), "anyurl")).ConfigureAwait(false); _server.Should() .HaveReceivedACall() .UsingPost(); } + [Fact] + public async Task HaveReceived1Calls_AtAbsoluteUrlUsingPost_ShouldChain() + { + // Arrange + var server = WireMockServer.Start(); + + server + .Given(Request.Create().WithPath("/a").UsingGet()) + .RespondWith(Response.Create().WithBody("A response").WithStatusCode(HttpStatusCode.OK)); + + server + .Given(Request.Create().WithPath("/b").UsingPost()) + .RespondWith(Response.Create().WithBody("B response").WithStatusCode(HttpStatusCode.OK)); + + server + .Given(Request.Create().WithPath("/c").UsingPost()) + .RespondWith(Response.Create().WithBody("C response").WithStatusCode(HttpStatusCode.OK)); + + // Act + var httpClient = new HttpClient(); + + await httpClient.GetAsync($"{server.Url}/a"); + + await httpClient.PostAsync($"{server.Url}/b", new StringContent("B")); + + await httpClient.PostAsync($"{server.Url}/c", new StringContent("C")); + + // Assert + server + .Should() + .HaveReceived(1) + .Calls() + .AtUrl($"{server.Url}/a") + .And + .UsingGet(); + + server + .Should() + .HaveReceived(1) + .Calls() + .AtUrl($"{server.Url}/b") + .And + .UsingPost(); + + server + .Should() + .HaveReceived(1) + .Calls() + .AtUrl($"{server.Url}/c") + .And + .UsingPost(); + + server + .Should() + .HaveReceived(3) + .Calls(); + + server + .Should() + .HaveReceived(1) + .Calls() + .UsingGet(); + + server + .Should() + .HaveReceived(2) + .Calls() + .UsingPost(); + + server.Stop(); + } + [Fact] public async Task HaveReceivedACall_UsingPatch_WhenACallWasMadeUsingPatch_Should_BeOK() { diff --git a/test/WireMock.Net.Tests/Owin/GlobalExceptionMiddlewareTests.cs b/test/WireMock.Net.Tests/Owin/GlobalExceptionMiddlewareTests.cs index ce6f8789..5b1926c9 100644 --- a/test/WireMock.Net.Tests/Owin/GlobalExceptionMiddlewareTests.cs +++ b/test/WireMock.Net.Tests/Owin/GlobalExceptionMiddlewareTests.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Moq; using NFluent; using WireMock.Owin; @@ -18,7 +18,6 @@ namespace WireMock.Net.Tests.Owin { private readonly Mock _optionsMock; private readonly Mock _responseMapperMock; - private readonly Mock _contextMock; private readonly GlobalExceptionMiddleware _sut; @@ -35,10 +34,10 @@ namespace WireMock.Net.Tests.Owin } [Fact] - public void GlobalExceptionMiddleware_Invoke_NullAsNext_Throws() + public void GlobalExceptionMiddleware_Invoke_NullAsNext_DoesNotInvokeNextAndDoesNotThrow() { // Act - Check.ThatAsyncCode(() => _sut.Invoke(_contextMock.Object)).ThrowsAny(); + Check.ThatAsyncCode(() => _sut.Invoke(null)).DoesNotThrow(); } } -} +} \ No newline at end of file