From 511540a7f1947e1a05783d7e4270bd7dc9b5fcdd Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Tue, 12 Mar 2024 20:27:12 +0100 Subject: [PATCH] Make WireMockAssertions extendable (#1082) --- .../Assertions/WireMockAssertions.AtUrl.cs | 60 ++++++++ .../WireMockAssertions.FromClientIP.cs | 33 +++++ .../WireMockAssertions.UsingMethod.cs | 6 +- .../Assertions/WireMockAssertions.WithBody.cs | 18 +-- .../WireMockAssertions.WithHeader.cs | 28 ++-- .../WireMockAssertions.WithProxy.cs | 34 +++++ .../Assertions/WireMockAssertions.cs | 129 ++---------------- .../WireMockAssertionsExtensions.cs | 37 +++++ .../WireMockAssertionsTests.cs | 12 +- 9 files changed, 216 insertions(+), 141 deletions(-) create mode 100644 src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.AtUrl.cs create mode 100644 src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.FromClientIP.cs create mode 100644 src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithProxy.cs create mode 100644 test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsExtensions.cs diff --git a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.AtUrl.cs b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.AtUrl.cs new file mode 100644 index 00000000..39451e21 --- /dev/null +++ b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.AtUrl.cs @@ -0,0 +1,60 @@ +#pragma warning disable CS1591 +using System; + +// ReSharper disable once CheckNamespace +namespace WireMock.FluentAssertions; + +public partial class WireMockAssertions +{ + [CustomAssertion] + public AndWhichConstraint AtAbsoluteUrl(string absoluteUrl, string because = "", params object[] becauseArgs) + { + var (filter, condition) = BuildFilterAndCondition(request => string.Equals(request.AbsoluteUrl, absoluteUrl, StringComparison.OrdinalIgnoreCase)); + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => RequestMessages) + .ForCondition(requests => CallsCount == 0 || requests.Any()) + .FailWith( + "Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but no calls were made.", + absoluteUrl + ) + .Then + .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) + ); + + FilterRequestMessages(filter); + + return new AndWhichConstraint(this, absoluteUrl); + } + + [CustomAssertion] + public AndWhichConstraint AtUrl(string url, string because = "", params object[] becauseArgs) + { + var (filter, condition) = BuildFilterAndCondition(request => string.Equals(request.Url, url, StringComparison.OrdinalIgnoreCase)); + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => RequestMessages) + .ForCondition(requests => CallsCount == 0 || 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(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) + ); + + FilterRequestMessages(filter); + + return new AndWhichConstraint(this, url); + } +} \ No newline at end of file diff --git a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.FromClientIP.cs b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.FromClientIP.cs new file mode 100644 index 00000000..e7651bc1 --- /dev/null +++ b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.FromClientIP.cs @@ -0,0 +1,33 @@ +#pragma warning disable CS1591 +using System; + +// ReSharper disable once CheckNamespace +namespace WireMock.FluentAssertions; + +public partial class WireMockAssertions +{ + [CustomAssertion] + public AndWhichConstraint FromClientIP(string clientIP, string because = "", params object[] becauseArgs) + { + var (filter, condition) = BuildFilterAndCondition(request => string.Equals(request.ClientIP, clientIP, StringComparison.OrdinalIgnoreCase)); + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => RequestMessages) + .ForCondition(requests => CallsCount == 0 || requests.Any()) + .FailWith( + "Expected {context:wiremockserver} to have been called from client IP {0}{reason}, but no calls were made.", + clientIP + ) + .Then + .ForCondition(condition) + .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) + ); + + FilterRequestMessages(filter); + + return new AndWhichConstraint(this, clientIP); + } +} \ No newline at end of file diff --git a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.UsingMethod.cs b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.UsingMethod.cs index 410ad46f..3b29d992 100644 --- a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.UsingMethod.cs +++ b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.UsingMethod.cs @@ -58,8 +58,8 @@ public partial class WireMockAssertions Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => _requestMessages) - .ForCondition(requests => _callsCount == 0 || requests.Any()) + .Given(() => RequestMessages) + .ForCondition(requests => CallsCount == 0 || requests.Any()) .FailWith( "Expected {context:wiremockserver} to have been called using method {0}{reason}, but no calls were made.", method @@ -72,7 +72,7 @@ public partial class WireMockAssertions requests => requests.Select(request => request.Method) ); - _requestMessages = filter(_requestMessages).ToList(); + FilterRequestMessages(filter); return new AndConstraint(this); } diff --git a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithBody.cs b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithBody.cs index e1078f74..129c1bab 100644 --- a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithBody.cs +++ b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithBody.cs @@ -70,8 +70,8 @@ public partial class WireMockAssertions { Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => _requestMessages) - .ForCondition(requests => _callsCount == 0 || requests.Any()) + .Given(() => RequestMessages) + .ForCondition(requests => CallsCount == 0 || requests.Any()) .FailWith( MessageFormatNoCalls, matcher.GetPatterns() @@ -84,7 +84,7 @@ public partial class WireMockAssertions requests => requests.Select(expression) ); - _requestMessages = filter(_requestMessages).ToList(); + FilterRequestMessages(filter); return new AndConstraint(this); } @@ -100,8 +100,8 @@ public partial class WireMockAssertions { Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => _requestMessages) - .ForCondition(requests => _callsCount == 0 || requests.Any()) + .Given(() => RequestMessages) + .ForCondition(requests => CallsCount == 0 || requests.Any()) .FailWith( MessageFormatNoCalls, matcher.Value @@ -114,7 +114,7 @@ public partial class WireMockAssertions requests => requests.Select(expression) ); - _requestMessages = filter(_requestMessages).ToList(); + FilterRequestMessages(filter); return new AndConstraint(this); } @@ -130,8 +130,8 @@ public partial class WireMockAssertions { Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => _requestMessages) - .ForCondition(requests => _callsCount == 0 || requests.Any()) + .Given(() => RequestMessages) + .ForCondition(requests => CallsCount == 0 || requests.Any()) .FailWith( MessageFormatNoCalls, matcher.Value @@ -144,7 +144,7 @@ public partial class WireMockAssertions requests => requests.Select(expression) ); - _requestMessages = filter(_requestMessages).ToList(); + FilterRequestMessages(filter); return new AndConstraint(this); } diff --git a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithHeader.cs b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithHeader.cs index d6ca8940..9e90c5de 100644 --- a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithHeader.cs +++ b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithHeader.cs @@ -8,7 +8,7 @@ namespace WireMock.FluentAssertions; public partial class WireMockAssertions { [CustomAssertion] - public AndConstraint WitHeaderKey(string expectedKey, string because = "", params object[] becauseArgs) + public AndWhichConstraint WitHeaderKey(string expectedKey, string because = "", params object[] becauseArgs) { var (filter, condition) = BuildFilterAndCondition(request => { @@ -17,8 +17,8 @@ public partial class WireMockAssertions Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => _requestMessages) - .ForCondition(requests => _callsCount == 0 || requests.Any()) + .Given(() => RequestMessages) + .ForCondition(requests => CallsCount == 0 || requests.Any()) .FailWith( "Expected {context:wiremockserver} to have been called with Header {0}{reason}.", expectedKey @@ -31,9 +31,9 @@ public partial class WireMockAssertions requests => requests.Select(request => request.Headers) ); - _requestMessages = filter(_requestMessages).ToList(); + FilterRequestMessages(filter); - return new AndConstraint(this); + return new AndWhichConstraint(this, expectedKey); } [CustomAssertion] @@ -60,8 +60,8 @@ public partial class WireMockAssertions Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => _requestMessages) - .ForCondition(requests => _callsCount == 0 || requests.Any()) + .Given(() => RequestMessages) + .ForCondition(requests => CallsCount == 0 || requests.Any()) .FailWith( "Expected {context:wiremockserver} to have been called with Header {0} and Values {1}{reason}.", expectedKey, @@ -76,7 +76,7 @@ public partial class WireMockAssertions requests => requests.Select(request => request.Headers) ); - _requestMessages = filter(_requestMessages).ToList(); + FilterRequestMessages(filter); return new AndConstraint(this); } @@ -91,8 +91,8 @@ public partial class WireMockAssertions Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => _requestMessages) - .ForCondition(requests => _callsCount == 0 || requests.Any()) + .Given(() => RequestMessages) + .ForCondition(requests => CallsCount == 0 || requests.Any()) .FailWith( "Expected {context:wiremockserver} not to have been called with Header {0}{reason}.", unexpectedKey @@ -105,7 +105,7 @@ public partial class WireMockAssertions requests => requests.Select(request => request.Headers) ); - _requestMessages = filter(_requestMessages).ToList(); + FilterRequestMessages(filter); return new AndConstraint(this); } @@ -134,8 +134,8 @@ public partial class WireMockAssertions Execute.Assertion .BecauseOf(because, becauseArgs) - .Given(() => _requestMessages) - .ForCondition(requests => _callsCount == 0 || requests.Any()) + .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, @@ -150,7 +150,7 @@ public partial class WireMockAssertions requests => requests.Select(request => request.Headers) ); - _requestMessages = filter(_requestMessages).ToList(); + FilterRequestMessages(filter); return new AndConstraint(this); } diff --git a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithProxy.cs b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithProxy.cs new file mode 100644 index 00000000..b820b0a6 --- /dev/null +++ b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithProxy.cs @@ -0,0 +1,34 @@ +#pragma warning disable CS1591 +using System; + +// ReSharper disable once CheckNamespace +namespace WireMock.FluentAssertions; + +public partial class WireMockAssertions +{ + [CustomAssertion] + public AndWhichConstraint WithProxyUrl(string proxyUrl, string because = "", params object[] becauseArgs) + { + var (filter, condition) = BuildFilterAndCondition(request => string.Equals(request.ProxyUrl, proxyUrl, StringComparison.OrdinalIgnoreCase)); + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => RequestMessages) + .ForCondition(requests => CallsCount == 0 || requests.Any()) + .FailWith( + "Expected {context:wiremockserver} to have been called with proxy url {0}{reason}, but no calls were made.", + proxyUrl + ) + .Then + .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) + ); + + FilterRequestMessages(filter); + + return new AndWhichConstraint(this, proxyUrl); + } +} \ 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 692ca198..b59181b0 100644 --- a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs +++ b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs @@ -9,135 +9,36 @@ namespace WireMock.FluentAssertions; public partial class WireMockAssertions { - private const string Any = "*"; - private readonly int? _callsCount; - private IReadOnlyList _requestMessages; - //private readonly IReadOnlyList>> _headers; + public const string Any = "*"; + + public int? CallsCount { get; } + public IReadOnlyList RequestMessages { get; private set; } public WireMockAssertions(IWireMockServer subject, int? callsCount) { - _callsCount = callsCount; - _requestMessages = subject.LogEntries.Select(logEntry => logEntry.RequestMessage).ToList(); - // _headers = _requestMessages.SelectMany(req => req.Headers).ToList(); + CallsCount = callsCount; + RequestMessages = subject.LogEntries.Select(logEntry => logEntry.RequestMessage).ToList(); } - [CustomAssertion] - public AndWhichConstraint AtAbsoluteUrl(string absoluteUrl, string because = "", params object[] becauseArgs) - { - var (filter, condition) = BuildFilterAndCondition(request => string.Equals(request.AbsoluteUrl, absoluteUrl, StringComparison.OrdinalIgnoreCase)); - - Execute.Assertion - .BecauseOf(because, becauseArgs) - .Given(() => _requestMessages) - .ForCondition(requests => _callsCount == 0 || requests.Any()) - .FailWith( - "Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but no calls were made.", - absoluteUrl - ) - .Then - .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) - ); - - _requestMessages = filter(_requestMessages).ToList(); - - return new AndWhichConstraint(this, absoluteUrl); - } - - [CustomAssertion] - public AndWhichConstraint AtUrl(string url, string because = "", params object[] becauseArgs) - { - var (filter, condition) = BuildFilterAndCondition(request => string.Equals(request.Url, url, StringComparison.OrdinalIgnoreCase)); - - Execute.Assertion - .BecauseOf(because, becauseArgs) - .Given(() => _requestMessages) - .ForCondition(requests => _callsCount == 0 || 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(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) - ); - - _requestMessages = filter(_requestMessages).ToList(); - - return new AndWhichConstraint(this, url); - } - - [CustomAssertion] - public AndWhichConstraint WithProxyUrl(string proxyUrl, string because = "", params object[] becauseArgs) - { - var (filter, condition) = BuildFilterAndCondition(request => string.Equals(request.ProxyUrl, proxyUrl, StringComparison.OrdinalIgnoreCase)); - - Execute.Assertion - .BecauseOf(because, becauseArgs) - .Given(() => _requestMessages) - .ForCondition(requests => _callsCount == 0 || requests.Any()) - .FailWith( - "Expected {context:wiremockserver} to have been called with proxy url {0}{reason}, but no calls were made.", - proxyUrl - ) - .Then - .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) - ); - - _requestMessages = filter(_requestMessages).ToList(); - - return new AndWhichConstraint(this, proxyUrl); - } - - [CustomAssertion] - public AndWhichConstraint FromClientIP(string clientIP, string because = "", params object[] becauseArgs) - { - var (filter, condition) = BuildFilterAndCondition(request => string.Equals(request.ClientIP, clientIP, StringComparison.OrdinalIgnoreCase)); - - Execute.Assertion - .BecauseOf(because, becauseArgs) - .Given(() => _requestMessages) - .ForCondition(requests => _callsCount == 0 || requests.Any()) - .FailWith( - "Expected {context:wiremockserver} to have been called from client IP {0}{reason}, but no calls were made.", - clientIP - ) - .Then - .ForCondition(condition) - .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) - ); - - _requestMessages = filter(_requestMessages).ToList(); - - return new AndWhichConstraint(this, clientIP); - } - - private (Func, IReadOnlyList> Filter, Func, bool> Condition) BuildFilterAndCondition(Func predicate) + public (Func, IReadOnlyList> Filter, Func, bool> Condition) BuildFilterAndCondition(Func predicate) { Func, IReadOnlyList> filter = requests => requests.Where(predicate).ToList(); - return (filter, requests => (_callsCount is null && filter(requests).Any()) || _callsCount == filter(requests).Count); + return (filter, requests => (CallsCount is null && filter(requests).Any()) || CallsCount == filter(requests).Count); } - private (Func, IReadOnlyList> Filter, Func, bool> Condition) BuildFilterAndCondition(Func expression, IStringMatcher matcher) + public (Func, IReadOnlyList> Filter, Func, bool> Condition) BuildFilterAndCondition(Func expression, IStringMatcher matcher) { return BuildFilterAndCondition(r => matcher.IsMatch(expression(r)).IsPerfect()); } - private (Func, IReadOnlyList> Filter, Func, bool> Condition) BuildFilterAndCondition(Func expression, IObjectMatcher matcher) + public (Func, IReadOnlyList> Filter, Func, bool> Condition) BuildFilterAndCondition(Func expression, IObjectMatcher matcher) { return BuildFilterAndCondition(r => matcher.IsMatch(expression(r)).IsPerfect()); } + + public void FilterRequestMessages(Func, IReadOnlyList> filter) + { + RequestMessages = filter(RequestMessages).ToList(); + } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsExtensions.cs b/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsExtensions.cs new file mode 100644 index 00000000..4d890d5b --- /dev/null +++ b/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsExtensions.cs @@ -0,0 +1,37 @@ +using System; +using System.Linq; +using FluentAssertions; +using FluentAssertions.Execution; + +// ReSharper disable once CheckNamespace +namespace WireMock.FluentAssertions; + +public static class WireMockAssertionsExtensions +{ + [CustomAssertion] + public static AndWhichConstraint AtAbsoluteUrl2(this WireMockAssertions assertions, + string absoluteUrl, string because = "", params object[] becauseArgs) + { + var (filter, condition) = assertions.BuildFilterAndCondition(request => string.Equals(request.AbsoluteUrl, absoluteUrl, StringComparison.OrdinalIgnoreCase)); + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .Given(() => assertions.RequestMessages) + .ForCondition(requests => assertions.CallsCount == 0 || requests.Any()) + .FailWith( + "Expected {context:wiremockserver} to have been called at address matching the absolute url {0}{reason}, but no calls were made.", + absoluteUrl + ) + .Then + .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) + ); + + assertions.FilterRequestMessages(filter); + + return new AndWhichConstraint(assertions, absoluteUrl); + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsTests.cs b/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsTests.cs index 7961a459..266ef688 100644 --- a/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsTests.cs +++ b/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsTests.cs @@ -61,6 +61,16 @@ public class WireMockAssertionsTests : IDisposable .AtAbsoluteUrl($"http://localhost:{_portUsed}/anyurl"); } + [Fact] + public async Task HaveReceived1Calls_AtAbsoluteUrl2_WhenACallWasMadeToAbsoluteUrl_Should_BeOK() + { + await _httpClient.GetAsync("anyurl").ConfigureAwait(false); + + _server.Should() + .HaveReceived(1).Calls() + .AtAbsoluteUrl2($"http://localhost:{_portUsed}/anyurl"); + } + [Fact] public async Task HaveReceived1Calls_AtAbsoluteUrlUsingPost_WhenAPostCallWasMadeToAbsoluteUrl_Should_BeOK() { @@ -129,7 +139,7 @@ public class WireMockAssertionsTests : IDisposable _server.Should() .HaveReceivedACall() - .WitHeaderKey("Authorization"); + .WitHeaderKey("Authorization").Which.Should().StartWith("A"); } [Fact]