From 04d53f3a9ea3873f56e65fce67887776efc56ca0 Mon Sep 17 00:00:00 2001 From: Francesco Venturoli Date: Tue, 22 Apr 2025 22:51:40 +0200 Subject: [PATCH] feat(awesome-assertions): Added new project WireMock.Net.AwesomeAssertions (#1273) * feat(awesome-assertions): Added new project WireMock.Net.AwesomeAssertions * feat(awesome-assertions): Applied dotnet naming convention for private readonly fields --------- Co-authored-by: Francesco Venturoli --- WireMock.Net Solution.sln | 7 + .../WireMockANumberOfCallsAssertions.cs | 40 +++++ .../Assertions/WireMockAssertions.AtUrl.cs | 83 +++++++++ .../WireMockAssertions.FromClientIP.cs | 35 ++++ .../WireMockAssertions.UsingMethod.cs | 81 +++++++++ .../Assertions/WireMockAssertions.WithBody.cs | 147 ++++++++++++++++ .../WireMockAssertions.WithHeader.cs | 157 ++++++++++++++++++ .../WireMockAssertions.WithProxy.cs | 36 ++++ .../Assertions/WireMockAssertions.cs | 48 ++++++ .../Assertions/WireMockReceivedAssertions.cs | 56 +++++++ .../Extensions/WireMockExtensions.cs | 23 +++ src/WireMock.Net.AwesomeAssertions/Usings.cs | 5 + .../WireMock.Net.AwesomeAssertions.csproj | 39 +++++ 13 files changed, 757 insertions(+) create mode 100644 src/WireMock.Net.AwesomeAssertions/Assertions/WireMockANumberOfCallsAssertions.cs create mode 100644 src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.AtUrl.cs create mode 100644 src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.FromClientIP.cs create mode 100644 src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.UsingMethod.cs create mode 100644 src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithBody.cs create mode 100644 src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithHeader.cs create mode 100644 src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithProxy.cs create mode 100644 src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.cs create mode 100644 src/WireMock.Net.AwesomeAssertions/Assertions/WireMockReceivedAssertions.cs create mode 100644 src/WireMock.Net.AwesomeAssertions/Extensions/WireMockExtensions.cs create mode 100644 src/WireMock.Net.AwesomeAssertions/Usings.cs create mode 100644 src/WireMock.Net.AwesomeAssertions/WireMock.Net.AwesomeAssertions.csproj diff --git a/WireMock.Net Solution.sln b/WireMock.Net Solution.sln index a1000602..0438f830 100644 --- a/WireMock.Net Solution.sln +++ b/WireMock.Net Solution.sln @@ -128,6 +128,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.TestWebApplica EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Middleware.Tests", "test\WireMock.Net.Middleware.Tests\WireMock.Net.Middleware.Tests.csproj", "{A5FEF4F7-7DA2-4962-89A8-16BA942886E5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.AwesomeAssertions", "src\WireMock.Net.AwesomeAssertions\WireMock.Net.AwesomeAssertions.csproj", "{7753670F-7C7F-44BF-8BC7-08325588E60C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -302,6 +304,10 @@ Global {A5FEF4F7-7DA2-4962-89A8-16BA942886E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {A5FEF4F7-7DA2-4962-89A8-16BA942886E5}.Release|Any CPU.ActiveCfg = Release|Any CPU {A5FEF4F7-7DA2-4962-89A8-16BA942886E5}.Release|Any CPU.Build.0 = Release|Any CPU + {7753670F-7C7F-44BF-8BC7-08325588E60C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7753670F-7C7F-44BF-8BC7-08325588E60C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7753670F-7C7F-44BF-8BC7-08325588E60C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7753670F-7C7F-44BF-8BC7-08325588E60C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -351,6 +357,7 @@ Global {B6269AAC-170A-4346-8B9A-579DED3D9A13} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} {6B30AA9F-DA04-4EB5-B03C-45A8EF272ECE} = {0BB8B634-407A-4610-A91F-11586990767A} {A5FEF4F7-7DA2-4962-89A8-16BA942886E5} = {0BB8B634-407A-4610-A91F-11586990767A} + {7753670F-7C7F-44BF-8BC7-08325588E60C} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458} diff --git a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockANumberOfCallsAssertions.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockANumberOfCallsAssertions.cs new file mode 100644 index 00000000..65dc2713 --- /dev/null +++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockANumberOfCallsAssertions.cs @@ -0,0 +1,40 @@ +// Copyright © WireMock.Net + +using Stef.Validation; +using WireMock.Server; + +// ReSharper disable once CheckNamespace +namespace WireMock.FluentAssertions; + +/// +/// Provides assertion methods to verify the number of calls made to a WireMock server. +/// This class is used in the context of FluentAssertions. +/// +public class WireMockANumberOfCallsAssertions +{ + private readonly IWireMockServer _server; + private readonly int _callsCount; + private readonly AssertionChain _chain; + + /// + /// Initializes a new instance of the class. + /// + /// The WireMock server to assert against. + /// The expected number of calls to assert. + /// The assertion chain + public WireMockANumberOfCallsAssertions(IWireMockServer server, int callsCount, AssertionChain chain) + { + _server = Guard.NotNull(server); + _callsCount = callsCount; + _chain = chain; + } + + /// + /// Returns an instance of which can be used to assert the expected number of calls. + /// + /// A instance for asserting the number of calls to the server. + public WireMockAssertions Calls() + { + return new WireMockAssertions(_server, _callsCount, _chain); + } +} \ No newline at end of file diff --git a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.AtUrl.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.AtUrl.cs new file mode 100644 index 00000000..87d8fb0f --- /dev/null +++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.AtUrl.cs @@ -0,0 +1,83 @@ +// Copyright © WireMock.Net + +using WireMock.Extensions; +using WireMock.Matchers; + +// ReSharper disable once CheckNamespace +namespace WireMock.FluentAssertions; + +#pragma warning disable CS1591 +public partial class WireMockAssertions +{ + [CustomAssertion] + public AndWhichConstraint AtAbsoluteUrl(string absoluteUrl, string because = "", params object[] becauseArgs) + { + _ = AtAbsoluteUrl(new ExactMatcher(true, absoluteUrl), because, becauseArgs); + + return new AndWhichConstraint(this, absoluteUrl); + } + + [CustomAssertion] + public AndWhichConstraint AtAbsoluteUrl(IStringMatcher absoluteUrlMatcher, string because = "", params object[] becauseArgs) + { + var (filter, condition) = BuildFilterAndCondition(request => absoluteUrlMatcher.IsPerfectMatch(request.AbsoluteUrl)); + + var absoluteUrl = absoluteUrlMatcher.GetPatterns().FirstOrDefault().GetPattern(); + + _chain + .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, absoluteUrlMatcher); + } + + [CustomAssertion] + public AndWhichConstraint AtUrl(string url, string because = "", params object[] becauseArgs) + { + _ = AtUrl(new ExactMatcher(true, url), because, becauseArgs); + + return new AndWhichConstraint(this, url); + } + + [CustomAssertion] + public AndWhichConstraint AtUrl(IStringMatcher urlMatcher, string because = "", params object[] becauseArgs) + { + var (filter, condition) = BuildFilterAndCondition(request => urlMatcher.IsPerfectMatch(request.Url)); + + var url = urlMatcher.GetPatterns().FirstOrDefault().GetPattern(); + + _chain + .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, urlMatcher); + } +} \ No newline at end of file diff --git a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.FromClientIP.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.FromClientIP.cs new file mode 100644 index 00000000..9c0977ed --- /dev/null +++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.FromClientIP.cs @@ -0,0 +1,35 @@ +// Copyright © WireMock.Net + +#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)); + + _chain + .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.AwesomeAssertions/Assertions/WireMockAssertions.UsingMethod.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.UsingMethod.cs new file mode 100644 index 00000000..62301d31 --- /dev/null +++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.UsingMethod.cs @@ -0,0 +1,81 @@ +// Copyright © WireMock.Net + +#pragma warning disable CS1591 +using System; +using WireMock.Constants; + +// ReSharper disable once CheckNamespace +namespace WireMock.FluentAssertions; + +public partial class WireMockAssertions +{ + [CustomAssertion] + public AndConstraint UsingConnect(string because = "", params object[] becauseArgs) + => UsingMethod(HttpRequestMethod.CONNECT, because, becauseArgs); + + [CustomAssertion] + public AndConstraint UsingDelete(string because = "", params object[] becauseArgs) + => UsingMethod(HttpRequestMethod.DELETE, because, becauseArgs); + + [CustomAssertion] + public AndConstraint UsingGet(string because = "", params object[] becauseArgs) + => UsingMethod(HttpRequestMethod.GET, because, becauseArgs); + + [CustomAssertion] + public AndConstraint UsingHead(string because = "", params object[] becauseArgs) + => UsingMethod(HttpRequestMethod.HEAD, because, becauseArgs); + + [CustomAssertion] + public AndConstraint UsingOptions(string because = "", params object[] becauseArgs) + => UsingMethod(HttpRequestMethod.OPTIONS, because, becauseArgs); + + [CustomAssertion] + public AndConstraint UsingPost(string because = "", params object[] becauseArgs) + => UsingMethod(HttpRequestMethod.POST, because, becauseArgs); + + [CustomAssertion] + public AndConstraint UsingPatch(string because = "", params object[] becauseArgs) + => UsingMethod(HttpRequestMethod.PATCH, because, becauseArgs); + + [CustomAssertion] + public AndConstraint UsingPut(string because = "", params object[] becauseArgs) + => UsingMethod(HttpRequestMethod.PUT, because, becauseArgs); + + [CustomAssertion] + public AndConstraint UsingTrace(string because = "", params object[] becauseArgs) + => UsingMethod(HttpRequestMethod.TRACE, because, becauseArgs); + + [CustomAssertion] + public AndConstraint UsingAnyMethod(string because = "", params object[] becauseArgs) + => UsingMethod(Any, because, becauseArgs); + + [CustomAssertion] + public AndConstraint UsingMethod(string method, string because = "", params object[] becauseArgs) + { + var any = method == Any; + Func predicate = request => (any && !string.IsNullOrEmpty(request.Method)) || + string.Equals(request.Method, method, StringComparison.OrdinalIgnoreCase); + + var (filter, condition) = BuildFilterAndCondition(predicate); + + _chain + .BecauseOf(because, becauseArgs) + .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 + ) + .Then + .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) + ); + + FilterRequestMessages(filter); + + return new AndConstraint(this); + } +} \ No newline at end of file diff --git a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithBody.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithBody.cs new file mode 100644 index 00000000..bdd04a42 --- /dev/null +++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithBody.cs @@ -0,0 +1,147 @@ +// Copyright © WireMock.Net + +#pragma warning disable CS1591 +using System; +using System.Collections.Generic; +using AnyOfTypes; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WireMock.Extensions; +using WireMock.Matchers; +using WireMock.Models; + +// ReSharper disable once CheckNamespace +namespace WireMock.FluentAssertions; + +public partial class WireMockAssertions +{ + private const string MessageFormatNoCalls = "Expected {context:wiremockserver} to have been called using body {0}{reason}, but no calls were made."; + private const string MessageFormat = "Expected {context:wiremockserver} to have been called using body {0}{reason}, but didn't find it among the body/bodies {1}."; + + [CustomAssertion] + public AndConstraint WithBody(string body, string because = "", params object[] becauseArgs) + { + return WithBody(new WildcardMatcher(body), because, becauseArgs); + } + + [CustomAssertion] + public AndConstraint WithBody(IStringMatcher matcher, string because = "", params object[] becauseArgs) + { + var (filter, condition) = BuildFilterAndCondition(r => r.Body, matcher); + + return ExecuteAssertionWithBodyStringMatcher(matcher, because, becauseArgs, condition, filter, r => r.Body); + } + + [CustomAssertion] + public AndConstraint WithBodyAsJson(object body, string because = "", params object[] becauseArgs) + { + return WithBodyAsJson(new JsonMatcher(body), because, becauseArgs); + } + + [CustomAssertion] + public AndConstraint WithBodyAsJson(string body, string because = "", params object[] becauseArgs) + { + return WithBodyAsJson(new JsonMatcher(body), because, becauseArgs); + } + + [CustomAssertion] + public AndConstraint WithBodyAsJson(IObjectMatcher matcher, string because = "", params object[] becauseArgs) + { + var (filter, condition) = BuildFilterAndCondition(r => r.BodyAsJson, matcher); + + return ExecuteAssertionWithBodyAsIObjectMatcher(matcher, because, becauseArgs, condition, filter, r => r.BodyAsJson); + } + + [CustomAssertion] + public AndConstraint WithBodyAsBytes(byte[] body, string because = "", params object[] becauseArgs) + { + return WithBodyAsBytes(new ExactObjectMatcher(body), because, becauseArgs); + } + + [CustomAssertion] + public AndConstraint WithBodyAsBytes(ExactObjectMatcher matcher, string because = "", params object[] becauseArgs) + { + var (filter, condition) = BuildFilterAndCondition(r => r.BodyAsBytes, matcher); + + return ExecuteAssertionWithBodyAsIObjectMatcher(matcher, because, becauseArgs, condition, filter, r => r.BodyAsBytes); + } + + private AndConstraint ExecuteAssertionWithBodyStringMatcher( + IStringMatcher matcher, + string because, + object[] becauseArgs, + Func, bool> condition, + Func, IReadOnlyList> filter, + Func expression + ) + { + _chain + .BecauseOf(because, becauseArgs) + .Given(() => RequestMessages) + .ForCondition(requests => CallsCount == 0 || requests.Any()) + .FailWith( + MessageFormatNoCalls, + FormatBody(matcher.GetPatterns()) + ) + .Then + .ForCondition(condition) + .FailWith( + MessageFormat, + _ => FormatBody(matcher.GetPatterns()), + requests => FormatBodies(requests.Select(expression)) + ); + + FilterRequestMessages(filter); + + return new AndConstraint(this); + } + + private AndConstraint ExecuteAssertionWithBodyAsIObjectMatcher( + IObjectMatcher matcher, + string because, + object[] becauseArgs, + Func, bool> condition, + Func, IReadOnlyList> filter, + Func expression + ) + { + _chain + .BecauseOf(because, becauseArgs) + .Given(() => RequestMessages) + .ForCondition(requests => CallsCount == 0 || requests.Any()) + .FailWith( + MessageFormatNoCalls, + FormatBody(matcher.Value) + ) + .Then + .ForCondition(condition) + .FailWith( + MessageFormat, + _ => FormatBody(matcher.Value), + requests => FormatBodies(requests.Select(expression)) + ); + + FilterRequestMessages(filter); + + return new AndConstraint(this); + } + + private static string? FormatBody(object? body) + { + return body switch + { + null => null, + string str => str, + AnyOf[] stringPatterns => FormatBodies(stringPatterns.Select(p => p.GetPattern())), + byte[] bytes => $"byte[{bytes.Length}] {{...}}", + JToken jToken => jToken.ToString(Formatting.None), + _ => JToken.FromObject(body).ToString(Formatting.None) + }; + } + + private static string? FormatBodies(IEnumerable bodies) + { + var valueAsArray = bodies as object[] ?? bodies.ToArray(); + return valueAsArray.Length == 1 ? FormatBody(valueAsArray[0]) : $"[ {string.Join(", ", valueAsArray.Select(FormatBody))} ]"; + } +} \ No newline at end of file diff --git a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithHeader.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithHeader.cs new file mode 100644 index 00000000..d5642759 --- /dev/null +++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithHeader.cs @@ -0,0 +1,157 @@ +// Copyright © WireMock.Net + +#pragma warning disable CS1591 + +// ReSharper disable once CheckNamespace +namespace WireMock.FluentAssertions; + +public partial class WireMockAssertions +{ + [CustomAssertion] + public AndWhichConstraint WitHeaderKey(string expectedKey, string because = "", params object[] becauseArgs) + { + var (filter, condition) = BuildFilterAndCondition(request => + { + return request.Headers?.Any(h => h.Key == expectedKey) == true; + }); + + _chain + .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) + ); + + FilterRequestMessages(filter); + + return new AndWhichConstraint(this, expectedKey); + } + + [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() ?? []; + + 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); + }); + + _chain + .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) + ); + + FilterRequestMessages(filter); + + 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; + }); + + _chain + .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) + ); + + FilterRequestMessages(filter); + + 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() ?? []; + + 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); + }); + + _chain + .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) + ); + + FilterRequestMessages(filter); + + return new AndConstraint(this); + } +} \ No newline at end of file diff --git a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithProxy.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithProxy.cs new file mode 100644 index 00000000..6d51b3fd --- /dev/null +++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithProxy.cs @@ -0,0 +1,36 @@ +// Copyright © WireMock.Net + +#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)); + + _chain + .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.AwesomeAssertions/Assertions/WireMockAssertions.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.cs new file mode 100644 index 00000000..ca7a4194 --- /dev/null +++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.cs @@ -0,0 +1,48 @@ +// Copyright © WireMock.Net + +#pragma warning disable CS1591 +using System; +using System.Collections.Generic; +using WireMock.Matchers; +using WireMock.Server; + +// ReSharper disable once CheckNamespace +namespace WireMock.FluentAssertions; + +public partial class WireMockAssertions +{ + public const string Any = "*"; + + public int? CallsCount { get; } + public IReadOnlyList RequestMessages { get; private set; } + private readonly AssertionChain _chain; + + public WireMockAssertions(IWireMockServer subject, int? callsCount, AssertionChain chain) + { + CallsCount = callsCount; + RequestMessages = subject.LogEntries.Select(logEntry => logEntry.RequestMessage).ToList(); + _chain = chain; + } + + 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); + } + + public (Func, IReadOnlyList> Filter, Func, bool> Condition) BuildFilterAndCondition(Func expression, IStringMatcher matcher) + { + return BuildFilterAndCondition(r => matcher.IsMatch(expression(r)).IsPerfect()); + } + + 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/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockReceivedAssertions.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockReceivedAssertions.cs new file mode 100644 index 00000000..b2043fe4 --- /dev/null +++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockReceivedAssertions.cs @@ -0,0 +1,56 @@ +// Copyright © WireMock.Net + +using FluentAssertions.Primitives; +using WireMock.Server; + +// ReSharper disable once CheckNamespace +namespace WireMock.FluentAssertions; + +/// +/// Contains a number of methods to assert that the is in the expected state. +/// +public class WireMockReceivedAssertions : ReferenceTypeAssertions +{ + private readonly AssertionChain _chain; + + /// + /// Create a WireMockReceivedAssertions. + /// + /// The . + /// The assertion chain + public WireMockReceivedAssertions(IWireMockServer server, AssertionChain chain) : base(server, chain) + { + _chain = chain; + } + + /// + /// Asserts if has received no calls. + /// + /// + public WireMockAssertions HaveReceivedNoCalls() + { + return new WireMockAssertions(Subject, 0, _chain); + } + + /// + /// Asserts if has received a call. + /// + /// + public WireMockAssertions HaveReceivedACall() + { + return new WireMockAssertions(Subject, null, _chain); + } + + /// + /// Asserts if has received n-calls. + /// + /// + /// + public WireMockANumberOfCallsAssertions HaveReceived(int callsCount) + { + return new WireMockANumberOfCallsAssertions(Subject, callsCount, _chain); + } + + /// + protected override string Identifier => "wiremockserver"; +} \ No newline at end of file diff --git a/src/WireMock.Net.AwesomeAssertions/Extensions/WireMockExtensions.cs b/src/WireMock.Net.AwesomeAssertions/Extensions/WireMockExtensions.cs new file mode 100644 index 00000000..73e2eac7 --- /dev/null +++ b/src/WireMock.Net.AwesomeAssertions/Extensions/WireMockExtensions.cs @@ -0,0 +1,23 @@ +// Copyright © WireMock.Net + +using WireMock.Server; + +// ReSharper disable once CheckNamespace +namespace WireMock.FluentAssertions +{ + /// + /// Contains extension methods for custom assertions in unit tests. + /// + public static class WireMockExtensions + { + /// + /// Returns a object that can be used to assert the current . + /// + /// The WireMockServer + /// + public static WireMockReceivedAssertions Should(this IWireMockServer instance) + { + return new WireMockReceivedAssertions(instance, AssertionChain.GetOrCreate()); + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net.AwesomeAssertions/Usings.cs b/src/WireMock.Net.AwesomeAssertions/Usings.cs new file mode 100644 index 00000000..82139e4f --- /dev/null +++ b/src/WireMock.Net.AwesomeAssertions/Usings.cs @@ -0,0 +1,5 @@ +// Copyright © WireMock.Net + +global using System.Linq; +global using FluentAssertions; +global using FluentAssertions.Execution; \ No newline at end of file diff --git a/src/WireMock.Net.AwesomeAssertions/WireMock.Net.AwesomeAssertions.csproj b/src/WireMock.Net.AwesomeAssertions/WireMock.Net.AwesomeAssertions.csproj new file mode 100644 index 00000000..5bebc1a1 --- /dev/null +++ b/src/WireMock.Net.AwesomeAssertions/WireMock.Net.AwesomeAssertions.csproj @@ -0,0 +1,39 @@ + + + + AwesomeAssertions extensions for WireMock.Net + WireMock.Net.AwesomeAssertions + Francesco Venturoli;Mahmoud Ali;Stef Heyenrath + net47;netstandard2.0;netstandard2.1 + true + WireMock.Net.AwesomeAssertions + WireMock.Net.AwesomeAssertions + wiremock;AwesomeAssertions;UnitTest;Assert;Assertions + WireMock.AwesomeAssertions + {9565C395-FC5D-4CB1-8381-EC3D9DA74779} + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + true + true + true + ../WireMock.Net/WireMock.Net.ruleset + true + ../WireMock.Net/WireMock.Net.snk + + true + MIT + + + + true + + + + + + + + + + + \ No newline at end of file