diff --git a/WireMock.Net Solution.sln b/WireMock.Net Solution.sln index 7ec3b687..de80e0a7 100644 --- a/WireMock.Net Solution.sln +++ b/WireMock.Net Solution.sln @@ -156,6 +156,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.TestWebApplica EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.TestWebApplication", "test\WireMock.Net.TestWebApplication\WireMock.Net.TestWebApplication.csproj", "{3B05CC76-C3CB-8667-6B65-3129DFB25681}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.RestClient.AwesomeAssertions", "src\WireMock.Net.RestClient.AwesomeAssertions\WireMock.Net.RestClient.AwesomeAssertions.csproj", "{F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -850,6 +852,18 @@ Global {3B05CC76-C3CB-8667-6B65-3129DFB25681}.Release|x64.Build.0 = Release|Any CPU {3B05CC76-C3CB-8667-6B65-3129DFB25681}.Release|x86.ActiveCfg = Release|Any CPU {3B05CC76-C3CB-8667-6B65-3129DFB25681}.Release|x86.Build.0 = Release|Any CPU + {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Debug|x64.ActiveCfg = Debug|Any CPU + {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Debug|x64.Build.0 = Debug|Any CPU + {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Debug|x86.ActiveCfg = Debug|Any CPU + {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Debug|x86.Build.0 = Debug|Any CPU + {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Release|Any CPU.Build.0 = Release|Any CPU + {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Release|x64.ActiveCfg = Release|Any CPU + {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Release|x64.Build.0 = Release|Any CPU + {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Release|x86.ActiveCfg = Release|Any CPU + {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -914,6 +928,7 @@ Global {2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B} = {985E0ADB-D4B4-473A-AA40-567E279B7946} {2EA75541-E63A-37B7-DA0A-BEA82ECD7652} = {985E0ADB-D4B4-473A-AA40-567E279B7946} {3B05CC76-C3CB-8667-6B65-3129DFB25681} = {0BB8B634-407A-4610-A91F-11586990767A} + {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458} diff --git a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.cs index cdbc8640..abcbaa7b 100644 --- a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.cs +++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.cs @@ -25,7 +25,7 @@ public partial class WireMockAssertions public (Func, IReadOnlyList> Filter, Func, bool> Condition) BuildFilterAndCondition(Func predicate) { - Func, IReadOnlyList> filter = requests => requests.Where(predicate).ToList(); + IReadOnlyList filter(IReadOnlyList requests) => requests.Where(predicate).ToList(); return (filter, requests => (CallsCount is null && filter(requests).Any()) || CallsCount == filter(requests).Count); } diff --git a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockReceivedAssertions.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockReceivedAssertions.cs index 184be0e9..d7b55949 100644 --- a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockReceivedAssertions.cs +++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockReceivedAssertions.cs @@ -9,19 +9,13 @@ namespace WireMock.AwesomeAssertions; /// /// Contains a number of methods to assert that the is in the expected state. /// -public class WireMockReceivedAssertions : ReferenceTypeAssertions +/// +/// Create a WireMockReceivedAssertions. +/// +/// The . +/// The assertion chain +public class WireMockReceivedAssertions(IWireMockServer server, AssertionChain chain) : ReferenceTypeAssertions(server, chain) { - 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. @@ -29,7 +23,7 @@ public class WireMockReceivedAssertions : ReferenceTypeAssertions public WireMockAssertions HaveReceivedNoCalls() { - return new WireMockAssertions(Subject, 0, _chain); + return new WireMockAssertions(Subject, 0, CurrentAssertionChain); } /// @@ -38,7 +32,7 @@ public class WireMockReceivedAssertions : ReferenceTypeAssertions public WireMockAssertions HaveReceivedACall() { - return new WireMockAssertions(Subject, null, _chain); + return new WireMockAssertions(Subject, null, CurrentAssertionChain); } /// @@ -48,7 +42,7 @@ public class WireMockReceivedAssertions : ReferenceTypeAssertions public WireMockANumberOfCallsAssertions HaveReceived(int callsCount) { - return new WireMockANumberOfCallsAssertions(Subject, callsCount, _chain); + return new WireMockANumberOfCallsAssertions(Subject, callsCount, CurrentAssertionChain); } /// diff --git a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs index fc8c8252..4aec5f2f 100644 --- a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs +++ b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.cs @@ -7,22 +7,16 @@ using WireMock.Server; // ReSharper disable once CheckNamespace namespace WireMock.FluentAssertions; -public partial class WireMockAssertions +public partial class WireMockAssertions(IWireMockServer subject, int? callsCount) { 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(); - } + public int? CallsCount { get; } = callsCount; + public IReadOnlyList RequestMessages { get; private set; } = subject.LogEntries.Select(logEntry => logEntry.RequestMessage).OfType().ToList(); public (Func, IReadOnlyList> Filter, Func, bool> Condition) BuildFilterAndCondition(Func predicate) { - Func, IReadOnlyList> filter = requests => requests.Where(predicate).ToList(); + IReadOnlyList filter(IReadOnlyList requests) => requests.Where(predicate).ToList(); return (filter, requests => (CallsCount is null && filter(requests).Any()) || CallsCount == filter(requests).Count); } diff --git a/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiANumberOfCallsAssertions.cs b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiANumberOfCallsAssertions.cs new file mode 100644 index 00000000..41ab134f --- /dev/null +++ b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiANumberOfCallsAssertions.cs @@ -0,0 +1,26 @@ +// Copyright © WireMock.Net + +// ReSharper disable once CheckNamespace +namespace WireMock.Client.AwesomeAssertions; + +/// +/// Provides assertion methods to verify the number of calls made to a WireMock server. +/// This class is used in the context of AwesomeAssertions. +/// +/// +/// Initializes a new instance of the class. +/// +/// The WireMock Admin API to assert against. +/// The expected number of calls to assert. +/// The assertion chain +public class WireMockAdminApiANumberOfCallsAssertions(IWireMockAdminApi adminApi, int callsCount, AssertionChain 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 WireMockAdminApiAssertions Calls() + { + return new WireMockAdminApiAssertions(adminApi, callsCount, chain); + } +} diff --git a/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.AtPath.cs b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.AtPath.cs new file mode 100644 index 00000000..314ac23b --- /dev/null +++ b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.AtPath.cs @@ -0,0 +1,83 @@ +// Copyright © WireMock.Net + +using WireMock.Extensions; +using WireMock.Matchers; + +// ReSharper disable once CheckNamespace +namespace WireMock.Client.AwesomeAssertions; + +#pragma warning disable CS1591 +public partial class WireMockAdminApiAssertions +{ + [CustomAssertion] + public AndWhichConstraint AtAbsolutePath(string absolutePath, string because = "", params object[] becauseArgs) + { + _ = AtAbsolutePath(new ExactMatcher(true, absolutePath), because, becauseArgs); + + return new AndWhichConstraint(this, absolutePath); + } + + [CustomAssertion] + public AndWhichConstraint AtAbsolutePath(IStringMatcher absolutePathMatcher, string because = "", params object[] becauseArgs) + { + var (filter, condition) = BuildFilterAndCondition(request => absolutePathMatcher.IsPerfectMatch(request.AbsolutePath)); + + var absolutePath = absolutePathMatcher.GetPatterns().FirstOrDefault().GetPattern(); + + chain + .BecauseOf(because, becauseArgs) + .Given(() => RequestMessages) + .ForCondition(requests => CallsCount == 0 || requests.Any()) + .FailWith( + "Expected {context:wiremockadminapi} to have been called at address matching the absolute path {0}{reason}, but no calls were made.", + absolutePath + ) + .Then + .ForCondition(condition) + .FailWith( + "Expected {context:wiremockadminapi} to have been called at address matching the absolute path {0}{reason}, but didn't find it among the calls to {1}.", + _ => absolutePath, + requests => requests.Select(request => request.AbsolutePath) + ); + + FilterRequestMessages(filter); + + return new AndWhichConstraint(this, absolutePathMatcher); + } + + [CustomAssertion] + public AndWhichConstraint AtPath(string path, string because = "", params object[] becauseArgs) + { + _ = AtPath(new ExactMatcher(true, path), because, becauseArgs); + + return new AndWhichConstraint(this, path); + } + + [CustomAssertion] + public AndWhichConstraint AtPath(IStringMatcher pathMatcher, string because = "", params object[] becauseArgs) + { + var (filter, condition) = BuildFilterAndCondition(request => pathMatcher.IsPerfectMatch(request.Path)); + + var path = pathMatcher.GetPatterns().FirstOrDefault().GetPattern(); + + chain + .BecauseOf(because, becauseArgs) + .Given(() => RequestMessages) + .ForCondition(requests => CallsCount == 0 || requests.Any()) + .FailWith( + "Expected {context:wiremockadminapi} to have been called at address matching the path {0}{reason}, but no calls were made.", + path + ) + .Then + .ForCondition(condition) + .FailWith( + "Expected {context:wiremockadminapi} to have been called at address matching the path {0}{reason}, but didn't find it among the calls to {1}.", + _ => path, + requests => requests.Select(request => request.Path) + ); + + FilterRequestMessages(filter); + + return new AndWhichConstraint(this, pathMatcher); + } +} diff --git a/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.AtUrl.cs b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.AtUrl.cs new file mode 100644 index 00000000..3add7c25 --- /dev/null +++ b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.AtUrl.cs @@ -0,0 +1,83 @@ +// Copyright © WireMock.Net + +using WireMock.Extensions; +using WireMock.Matchers; + +// ReSharper disable once CheckNamespace +namespace WireMock.Client.AwesomeAssertions; + +#pragma warning disable CS1591 +public partial class WireMockAdminApiAssertions +{ + [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:wiremockadminapi} to have been called at address matching the absolute url {0}{reason}, but no calls were made.", + absoluteUrl + ) + .Then + .ForCondition(condition) + .FailWith( + "Expected {context:wiremockadminapi} 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:wiremockadminapi} to have been called at address matching the url {0}{reason}, but no calls were made.", + url + ) + .Then + .ForCondition(condition) + .FailWith( + "Expected {context:wiremockadminapi} 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); + } +} diff --git a/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.FromClientIP.cs b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.FromClientIP.cs new file mode 100644 index 00000000..9935ef27 --- /dev/null +++ b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.FromClientIP.cs @@ -0,0 +1,33 @@ +// Copyright © WireMock.Net + +#pragma warning disable CS1591 +// ReSharper disable once CheckNamespace +namespace WireMock.Client.AwesomeAssertions; + +public partial class WireMockAdminApiAssertions +{ + [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:wiremockadminapi} to have been called from client IP {0}{reason}, but no calls were made.", + clientIP + ) + .Then + .ForCondition(condition) + .FailWith( + "Expected {context:wiremockadminapi} 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); + } +} diff --git a/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.UsingMethod.cs b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.UsingMethod.cs new file mode 100644 index 00000000..dd2e5691 --- /dev/null +++ b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.UsingMethod.cs @@ -0,0 +1,81 @@ +// Copyright © WireMock.Net + +#pragma warning disable CS1591 +using WireMock.Admin.Requests; +using WireMock.Constants; + +// ReSharper disable once CheckNamespace +namespace WireMock.Client.AwesomeAssertions; + +public partial class WireMockAdminApiAssertions +{ + [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:wiremockadminapi} to have been called using method {0}{reason}, but no calls were made.", + method + ) + .Then + .ForCondition(condition) + .FailWith( + "Expected {context:wiremockadminapi} 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); + } +} diff --git a/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.WithBody.cs b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.WithBody.cs new file mode 100644 index 00000000..b01a305f --- /dev/null +++ b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.WithBody.cs @@ -0,0 +1,146 @@ +// Copyright © WireMock.Net + +#pragma warning disable CS1591 +using AnyOfTypes; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WireMock.Admin.Requests; +using WireMock.Extensions; +using WireMock.Matchers; +using WireMock.Models; + +// ReSharper disable once CheckNamespace +namespace WireMock.Client.AwesomeAssertions; + +public partial class WireMockAdminApiAssertions +{ + private const string MessageFormatNoCalls = "Expected {context:wiremockadminapi} to have been called using body {0}{reason}, but no calls were made."; + private const string MessageFormat = "Expected {context:wiremockadminapi} 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))} ]"; + } +} diff --git a/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.WithHeader.cs b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.WithHeader.cs new file mode 100644 index 00000000..e4bf7b92 --- /dev/null +++ b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.WithHeader.cs @@ -0,0 +1,157 @@ +// Copyright © WireMock.Net + +#pragma warning disable CS1591 + +// ReSharper disable once CheckNamespace +namespace WireMock.Client.AwesomeAssertions; + +public partial class WireMockAdminApiAssertions +{ + [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:wiremockadminapi} to have been called with Header {0}{reason}.", + expectedKey + ) + .Then + .ForCondition(condition) + .FailWith( + "Expected {context:wiremockadminapi} 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:wiremockadminapi} to have been called with Header {0} and Values {1}{reason}.", + expectedKey, + expectedValues + ) + .Then + .ForCondition(condition) + .FailWith( + "Expected {context:wiremockadminapi} 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:wiremockadminapi} not to have been called with Header {0}{reason}.", + unexpectedKey + ) + .Then + .ForCondition(condition) + .FailWith( + "Expected {context:wiremockadminapi} 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:wiremockadminapi} not to have been called with Header {0} and Values {1}{reason}.", + unexpectedKey, + expectedValues + ) + .Then + .ForCondition(condition) + .FailWith( + "Expected {context:wiremockadminapi} 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); + } +} diff --git a/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.WithProxy.cs b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.WithProxy.cs new file mode 100644 index 00000000..4fd54145 --- /dev/null +++ b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.WithProxy.cs @@ -0,0 +1,34 @@ +// Copyright © WireMock.Net + +#pragma warning disable CS1591 +// ReSharper disable once CheckNamespace +namespace WireMock.Client.AwesomeAssertions; + +public partial class WireMockAdminApiAssertions +{ + [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:wiremockadminapi} to have been called with proxy url {0}{reason}, but no calls were made.", + proxyUrl + ) + .Then + .ForCondition(condition) + .FailWith( + "Expected {context:wiremockadminapi} 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); + } +} diff --git a/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.cs b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.cs new file mode 100644 index 00000000..8c619656 --- /dev/null +++ b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.cs @@ -0,0 +1,42 @@ +// Copyright © WireMock.Net + +#pragma warning disable CS1591 +using WireMock.Admin.Requests; +using WireMock.Matchers; + +// ReSharper disable once CheckNamespace +namespace WireMock.Client.AwesomeAssertions; + +public partial class WireMockAdminApiAssertions(IWireMockAdminApi subject, int? callsCount, AssertionChain chain) +{ + public const string Any = "*"; + + public int? CallsCount { get; } = callsCount; + + public IReadOnlyList RequestMessages { get; private set; } = subject.GetRequestsAsync().GetAwaiter().GetResult() + .Select(logEntry => logEntry.Request) + .OfType() + .ToList(); + + public (Func, IReadOnlyList> Filter, Func, bool> Condition) BuildFilterAndCondition(Func predicate) + { + IReadOnlyList filter(IReadOnlyList 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(); + } +} diff --git a/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiReceivedAssertions.cs b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiReceivedAssertions.cs new file mode 100644 index 00000000..f6124bf4 --- /dev/null +++ b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiReceivedAssertions.cs @@ -0,0 +1,50 @@ +// Copyright © WireMock.Net + +using AwesomeAssertions.Primitives; + +// ReSharper disable once CheckNamespace +namespace WireMock.Client.AwesomeAssertions; + +/// +/// Contains a number of methods to assert that the is in the expected state. +/// +/// +/// Create a WireMockReceivedAssertions. +/// +/// The . +/// The assertion chain +public class WireMockAdminApiReceivedAssertions(IWireMockAdminApi adminApi, AssertionChain chain) : + ReferenceTypeAssertions(adminApi, chain) +{ + + /// + /// Asserts if has received no calls. + /// + /// + public WireMockAdminApiAssertions HaveReceivedNoCalls() + { + return new WireMockAdminApiAssertions(Subject, 0, CurrentAssertionChain); + } + + /// + /// Asserts if has received a call. + /// + /// + public WireMockAdminApiAssertions HaveReceivedACall() + { + return new WireMockAdminApiAssertions(Subject, null, CurrentAssertionChain); + } + + /// + /// Asserts if has received n-calls. + /// + /// + /// + public WireMockAdminApiANumberOfCallsAssertions HaveReceived(int callsCount) + { + return new WireMockAdminApiANumberOfCallsAssertions(Subject, callsCount, CurrentAssertionChain); + } + + /// + protected override string Identifier => "wiremockadminapi"; +} diff --git a/src/WireMock.Net.RestClient.AwesomeAssertions/Extensions/WireMockAdminApiExtensions.cs b/src/WireMock.Net.RestClient.AwesomeAssertions/Extensions/WireMockAdminApiExtensions.cs new file mode 100644 index 00000000..9d76a08d --- /dev/null +++ b/src/WireMock.Net.RestClient.AwesomeAssertions/Extensions/WireMockAdminApiExtensions.cs @@ -0,0 +1,20 @@ +// Copyright © WireMock.Net + +// ReSharper disable once CheckNamespace +namespace WireMock.Client.AwesomeAssertions; + +/// +/// Contains extension methods for custom assertions in unit tests. +/// +public static class WireMockAdminApiExtensions +{ + /// + /// Returns a object that can be used to assert the current . + /// + /// The WireMock Admin API client. + /// + public static WireMockAdminApiReceivedAssertions Should(this IWireMockAdminApi instance) + { + return new WireMockAdminApiReceivedAssertions(instance, AssertionChain.GetOrCreate()); + } +} diff --git a/src/WireMock.Net.RestClient.AwesomeAssertions/Usings.cs b/src/WireMock.Net.RestClient.AwesomeAssertions/Usings.cs new file mode 100644 index 00000000..7bd7a73c --- /dev/null +++ b/src/WireMock.Net.RestClient.AwesomeAssertions/Usings.cs @@ -0,0 +1,4 @@ +// Copyright © WireMock.Net + +global using AwesomeAssertions; +global using AwesomeAssertions.Execution; \ No newline at end of file diff --git a/src/WireMock.Net.RestClient.AwesomeAssertions/WireMock.Net.RestClient.AwesomeAssertions.csproj b/src/WireMock.Net.RestClient.AwesomeAssertions/WireMock.Net.RestClient.AwesomeAssertions.csproj new file mode 100644 index 00000000..8ce2e1b7 --- /dev/null +++ b/src/WireMock.Net.RestClient.AwesomeAssertions/WireMock.Net.RestClient.AwesomeAssertions.csproj @@ -0,0 +1,44 @@ + + + + 0.0.1-preview-01 + AwesomeAssertions extensions for WireMock.Net RestClient + WireMock.Net.RestClient.AwesomeAssertions + Stef Heyenrath + netstandard2.0 + true + WireMock.Net.RestClient.AwesomeAssertions + WireMock.Net.RestClient.AwesomeAssertions + wiremock;AwesomeAssertions;UnitTest;Assert;Assertions;restclient + WireMock.Client.AwesomeAssertions + {F4B2B967-7878-4D93-9A5C-5EF7B84B941A} + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + true + true + true + true + ../WireMock.Net/WireMock.Net.snk + + true + MIT + + + + ../WireMock.Net/WireMock.Net.ruleset + + + + true + + + + + + + + + + + + diff --git a/test/WireMock.Net.Tests/FluentAssertions/WireMockAdminApiAssertionsTests.cs b/test/WireMock.Net.Tests/FluentAssertions/WireMockAdminApiAssertionsTests.cs new file mode 100644 index 00000000..bf2578e8 --- /dev/null +++ b/test/WireMock.Net.Tests/FluentAssertions/WireMockAdminApiAssertionsTests.cs @@ -0,0 +1,1168 @@ +// Copyright © WireMock.Net + +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using AwesomeAssertions; +using RestEase; +using WireMock.Client; +using WireMock.Client.AwesomeAssertions; +using WireMock.Matchers; +using WireMock.Net.Xunit; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; +using WireMock.Settings; + +namespace WireMock.Net.Tests.FluentAssertions; + +public class WireMockAdminApiAssertionsTests : IDisposable +{ + private readonly CancellationToken _ct = TestContext.Current.CancellationToken; + + private readonly WireMockServer _server; + private readonly HttpClient _httpClient; + private readonly IWireMockAdminApi _adminApi; + private readonly int _portUsed; + private readonly ITestOutputHelper _testOutputHelper; + + public WireMockAdminApiAssertionsTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + + _server = WireMockServer.Start(settings => + { + settings.StartAdminInterface = true; + settings.Logger = new TestOutputHelperWireMockLogger(testOutputHelper); + }); + _server.Given(Request.Create().UsingAnyMethod()).RespondWith(Response.Create().WithSuccess()); + + _portUsed = _server.Ports.First(); + _httpClient = _server.CreateClient(); + _adminApi = RestClient.For(_server.Url); + } + + [Fact] + public async Task HaveReceivedNoCalls_AtAbsoluteUrl_WhenACallWasNotMadeToAbsoluteUrl_Should_BeOK() + { + await _httpClient.GetAsync("xxx", _ct); + + _adminApi.Should() + .HaveReceivedNoCalls() + .AtAbsoluteUrl($"http://localhost:{_portUsed}/anyurl"); + } + + [Fact] + public async Task HaveReceived0Calls_AtAbsoluteUrl_WhenACallWasNotMadeToAbsoluteUrl_Should_BeOK() + { + await _httpClient.GetAsync("xxx", _ct); + + _adminApi.Should() + .HaveReceived(0).Calls() + .AtAbsoluteUrl($"http://localhost:{_portUsed}/anyurl"); + } + + [Fact] + public async Task HaveReceived1Call_AtAbsoluteUrl_WhenACallWasMadeToAbsoluteUrl_Should_BeOK() + { + await _httpClient.GetAsync("anyurl", _ct); + + _adminApi.Should() + .HaveReceived(1).Calls() + .AtAbsoluteUrl($"http://localhost:{_portUsed}/anyurl"); + } + + [Fact] + public async Task HaveReceived1Call_AtAbsoluteUrl2_WhenACallWasMadeToAbsoluteUrl_Should_BeOK() + { + await _httpClient.GetAsync("anyurl", _ct); + + _adminApi.Should() + .HaveReceived(1).Calls() + .AtAbsoluteUrl($"http://localhost:{_portUsed}/anyurl"); + } + + [Fact] + public async Task HaveReceived1Call_AtAbsoluteUrlUsingPost_WhenAPostCallWasMadeToAbsoluteUrl_Should_BeOK() + { + await _httpClient.PostAsync("anyurl", new StringContent(""), _ct); + + _adminApi.Should() + .HaveReceived(1).Calls() + .AtAbsoluteUrl($"http://localhost:{_portUsed}/anyurl") + .And + .UsingPost(); + } + + [Fact] + public async Task HaveReceived2Calls_AtAbsoluteUrl_WhenACallWasMadeToAbsoluteUrl_Should_BeOK() + { + await _httpClient.GetAsync("anyurl", _ct); + + await _httpClient.GetAsync("anyurl", _ct); + + _adminApi.Should() + .HaveReceived(2).Calls() + .AtAbsoluteUrl($"http://localhost:{_portUsed}/anyurl"); + } + + [Fact] + public async Task HaveReceivedACall_AtAbsoluteUrl_WhenACallWasMadeToAbsoluteUrl_Should_BeOK() + { + await _httpClient.GetAsync("anyurl", _ct); + + _adminApi.Should() + .HaveReceivedACall() + .AtAbsoluteUrl(new WildcardMatcher($"http://localhost:{_portUsed}/any*")); + } + + [Fact] + public async Task HaveReceivedACall_AtAbsoluteUrlWildcardMatcher_WhenACallWasMadeToAbsoluteUrl_Should_BeOK() + { + await _httpClient.GetAsync("anyurl", _ct); + + _adminApi.Should() + .HaveReceivedACall() + .AtAbsoluteUrl($"http://localhost:{_portUsed}/anyurl"); + } + + [Fact] + public void HaveReceivedACall_AtAbsoluteUrl_Should_ThrowWhenNoCallsWereMade() + { + Action act = () => _adminApi.Should() + .HaveReceivedACall() + .AtAbsoluteUrl("anyurl"); + + act.Should() + .Throw() + .WithMessage("Expected _adminApi to have been called at address matching the absolute url \"anyurl\", but no calls were made."); + } + + [Fact] + public async Task HaveReceivedACall_AtAbsoluteUrl_Should_ThrowWhenNoCallsMatchingTheAbsoluteUrlWereMade() + { + await _httpClient.GetAsync("", _ct); + + Action act = () => _adminApi.Should() + .HaveReceivedACall() + .AtAbsoluteUrl("anyurl"); + + act.Should() + .Throw() + .WithMessage($"Expected _adminApi to have been called at address matching the absolute url \"anyurl\", but didn't find it among the calls to {{\"http://localhost:{_portUsed}/\"}}."); + } + + [Fact] + public async Task HaveReceivedNoCalls_AtAbsolutePath_WhenACallWasNotMadeToAbsolutePath_Should_BeOK() + { + await _httpClient.GetAsync("xxx", _ct); + + _adminApi.Should() + .HaveReceivedNoCalls() + .AtAbsolutePath("anypath"); + } + + [Fact] + public async Task HaveReceived0Calls_AtAbsolutePath_WhenACallWasNotMadeToAbsolutePath_Should_BeOK() + { + await _httpClient.GetAsync("xxx", _ct); + + _adminApi.Should() + .HaveReceived(0).Calls() + .AtAbsolutePath("anypath"); + } + + [Fact] + public async Task HaveReceived1Call_AtAbsolutePath_WhenACallWasMadeToAbsolutePath_Should_BeOK() + { + await _httpClient.GetAsync("anypath", _ct); + + _adminApi.Should() + .HaveReceived(1).Calls() + .AtAbsolutePath("/anypath"); + } + + [Fact] + public async Task HaveReceived1Call_AtAbsolutePathUsingPost_WhenAPostCallWasMadeToAbsolutePath_Should_BeOK() + { + await _httpClient.PostAsync("anypath", new StringContent(""), _ct); + + _adminApi.Should() + .HaveReceived(1).Calls() + .AtAbsolutePath("/anypath") + .And + .UsingPost(); + } + + [Fact] + public async Task HaveReceived2Calls_AtAbsolutePath_WhenACallWasMadeToAbsolutePath_Should_BeOK() + { + await _httpClient.GetAsync("anypath", _ct); + + await _httpClient.GetAsync("anypath", _ct); + + _adminApi.Should() + .HaveReceived(2).Calls() + .AtAbsolutePath("/anypath"); + } + + [Fact] + public async Task HaveReceivedACall_AtAbsolutePath_WhenACallWasMadeToAbsolutePath_Should_BeOK() + { + await _httpClient.GetAsync("anypath", _ct); + + _adminApi.Should() + .HaveReceivedACall() + .AtAbsolutePath(new WildcardMatcher("/any*")); + } + + [Fact] + public async Task HaveReceivedACall_AtAbsolutePathWildcardMatcher_WhenACallWasMadeToAbsolutePath_Should_BeOK() + { + await _httpClient.GetAsync("anypath", _ct); + + _adminApi.Should() + .HaveReceivedACall() + .AtAbsolutePath("/anypath"); + } + + [Fact] + public void HaveReceivedACall_AtAbsolutePath_Should_ThrowWhenNoCallsWereMade() + { + Action act = () => _adminApi.Should() + .HaveReceivedACall() + .AtAbsolutePath("anypath"); + + act.Should() + .Throw() + .WithMessage("Expected _adminApi to have been called at address matching the absolute path \"anypath\", but no calls were made."); + } + + [Fact] + public async Task HaveReceivedACall_AtAbsolutePath_Should_ThrowWhenNoCallsMatchingTheAbsolutePathWereMade() + { + await _httpClient.GetAsync("", _ct); + + Action act = () => _adminApi.Should() + .HaveReceivedACall() + .AtAbsolutePath("/anypath"); + + act.Should() + .Throw() + .WithMessage($"Expected _adminApi to have been called at address matching the absolute path \"/anypath\", but didn't find it among the calls to {{\"/\"}}."); + } + + [Fact] + public async Task HaveReceivedACall_WithHeader_WhenACallWasMadeWithExpectedHeader_Should_BeOK() + { + _httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer a"); + await _httpClient.GetAsync("", _ct); + + _adminApi.Should() + .HaveReceivedACall() + .WitHeaderKey("Authorization").Which.Should().StartWith("A"); + } + + [Fact] + public async Task HaveReceivedACall_WithHeader_WhenACallWasMadeWithExpectedHeaderWithValue_Should_BeOK() + { + _httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer a"); + await _httpClient.GetAsync("", _ct); + + _adminApi.Should() + .HaveReceivedACall() + .WithHeader("Authorization", "Bearer a"); + } + + [Fact] + 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("1", _ct); + + _httpClient.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("EN")); + await _httpClient.GetAsync("2", _ct); + + _adminApi.Should() + .HaveReceivedACall() + .WithHeader("Accept", ["application/xml", "application/json"]) + .And + .WithHeader("Accept-Language", ["EN"]); + } + + [Fact] + public async Task HaveReceivedACall_WithHeader_Should_ThrowWhenNoCallsMatchingTheHeaderNameWereMade() + { + await _httpClient.GetAsync("", _ct); + + Action act = () => _adminApi.Should() + .HaveReceivedACall() + .WithHeader("Authorization", "value"); + + act.Should() + .Throw() + .WithMessage("*\"Authorization\"*"); + } + + [Fact] + public async Task HaveReceivedACall_WithHeader_Should_ThrowWhenNoCallsMatchingTheHeaderValuesWereMade() + { + _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); + _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + await _httpClient.GetAsync("", _ct); + + Action act = () => _adminApi.Should() + .HaveReceivedACall() + .WithHeader("Accept", "missing-value"); + + act.Should() + .Throw() + .WithMessage("Expected _adminApi to have been called with Header \"Accept\" and Values {\"missing-value\"}, but didn't find it among the calls with Header(s)*"); + } + + [Fact] + public async Task HaveReceivedACall_WithHeader_Should_ThrowWhenNoCallsMatchingTheHeaderWithMultipleValuesWereMade() + { + 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("", _ct); + + Action act = () => _adminApi.Should() + .HaveReceivedACall() + .WithHeader("Accept", ["missing-value1", "missing-value2"]); + + act.Should() + .Throw() + .WithMessage("Expected _adminApi to have been called with Header \"Accept\" and Values {\"missing-value1\", \"missing-value2\"}, but didn't find it among the calls with Header(s)*"); + } + + [Fact] + public async Task HaveReceivedACall_WithHeader_ShouldCheckAllRequests() + { + // Arrange + var cancellationToken = _ct; + using var server = WireMockServer.Start(settings => + { + settings.StartAdminInterface = true; + settings.Logger = new TestOutputHelperWireMockLogger(_testOutputHelper); + }); + using var client1 = server.CreateClient(); + var adminAdmin = RestClient.For(server.Url); + + var handler = new HttpClientHandler(); + using var client2 = server.CreateClient(handler); + + // Act 1 + var task1 = client1.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/") + { + Headers = + { + Authorization = new AuthenticationHeaderValue("Bearer", "invalidToken") + } + }, cancellationToken); + + // Act 2 + var task2 = client2.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/") + { + Headers = + { + Authorization = new AuthenticationHeaderValue("Bearer", "validToken") + } + }, cancellationToken); + + await Task.WhenAll(task1, task2); + + // Assert + adminAdmin.Should() + .HaveReceivedACall() + .WithHeader("Authorization", "Bearer invalidToken").And.WithoutHeader("x", "y").And.WithoutHeaderKey("a"); + + adminAdmin.Should(). + HaveReceivedACall() + .WithHeader("Authorization", "Bearer validToken").And.WithoutHeader("Authorization", "y"); + } + + [Fact] + public async Task HaveReceivedACall_AtUrl_WhenACallWasMadeToUrl_Should_BeOK() + { + await _httpClient.GetAsync("anyurl", _ct); + + _adminApi.Should() + .HaveReceivedACall() + .AtUrl($"http://localhost:{_portUsed}/anyurl"); + } + + [Fact] + public async Task HaveReceivedACall_AtUrlWildcardMatcher_WhenACallWasMadeToUrl_Should_BeOK() + { + await _httpClient.GetAsync("anyurl", _ct); + + _adminApi.Should() + .HaveReceivedACall() + .AtUrl(new WildcardMatcher($"http://localhost:{_portUsed}/AN*", true)); + } + + [Fact] + public void HaveReceivedACall_AtUrl_Should_ThrowWhenNoCallsWereMade() + { + Action act = () => _adminApi.Should() + .HaveReceivedACall() + .AtUrl("anyurl"); + + act.Should() + .Throw() + .WithMessage("Expected _adminApi to have been called at address matching the url \"anyurl\", but no calls were made."); + } + + [Fact] + public async Task HaveReceivedACall_AtUrl_Should_ThrowWhenNoCallsMatchingTheUrlWereMade() + { + await _httpClient.GetAsync("", _ct); + + Action act = () => _adminApi.Should() + .HaveReceivedACall() + .AtUrl("anyurl"); + + act.Should() + .Throw() + .WithMessage($"Expected _adminApi to have been called at address matching the url \"anyurl\", but didn't find it among the calls to {{\"http://localhost:{_portUsed}/\"}}."); + } + + [Fact] + public async Task HaveReceivedACall_WithProxyUrl_WhenACallWasMadeWithProxyUrl_Should_BeOK() + { + _server.ResetMappings(); + _server.Given(Request.Create().UsingAnyMethod()) + .RespondWith(Response.Create().WithProxy(new ProxyAndRecordSettings { Url = "http://localhost:9999" })); + + await _httpClient.GetAsync("", _ct); + + _adminApi.Should() + .HaveReceivedACall() + .WithProxyUrl("http://localhost:9999"); + } + + [Fact] + public void HaveReceivedACall_WithProxyUrl_Should_ThrowWhenNoCallsWereMade() + { + _server.ResetMappings(); + _server.Given(Request.Create().UsingAnyMethod()) + .RespondWith(Response.Create().WithProxy(new ProxyAndRecordSettings { Url = "http://localhost:9999" })); + + Action act = () => _adminApi.Should() + .HaveReceivedACall() + .WithProxyUrl("anyurl"); + + act.Should() + .Throw() + .WithMessage("Expected _adminApi to have been called with proxy url \"anyurl\", but no calls were made."); + } + + [Fact] + public async Task HaveReceivedACall_WithProxyUrl_Should_ThrowWhenNoCallsWithTheProxyUrlWereMade() + { + _server.ResetMappings(); + _server.Given(Request.Create().UsingAnyMethod()) + .RespondWith(Response.Create().WithProxy(new ProxyAndRecordSettings { Url = "http://localhost:9999" })); + + await _httpClient.GetAsync("", _ct); + + Action act = () => _adminApi.Should() + .HaveReceivedACall() + .WithProxyUrl("anyurl"); + + act.Should() + .Throw() + .WithMessage("Expected _adminApi to have been called with proxy url \"anyurl\", but didn't find it among the calls with {\"http://localhost:9999\"}."); + } + + [Fact] + public async Task HaveReceivedACall_FromClientIP_whenACallWasMadeFromClientIP_Should_BeOK() + { + await _httpClient.GetAsync("", _ct); + var clientIP = _server.LogEntries.Last().RequestMessage.ClientIP; + + _adminApi.Should() + .HaveReceivedACall() + .FromClientIP(clientIP); + } + + [Fact] + public void HaveReceivedACall_FromClientIP_Should_ThrowWhenNoCallsWereMade() + { + Action act = () => _adminApi.Should() + .HaveReceivedACall() + .FromClientIP("different-ip"); + + act.Should() + .Throw() + .WithMessage("Expected _adminApi to have been called from client IP \"different-ip\", but no calls were made."); + } + + [Fact] + public async Task HaveReceivedACall_FromClientIP_Should_ThrowWhenNoCallsFromClientIPWereMade() + { + await _httpClient.GetAsync("", _ct); + var clientIP = _server.LogEntries.Last().RequestMessage.ClientIP; + + Action act = () => _adminApi.Should() + .HaveReceivedACall() + .FromClientIP("different-ip"); + + act.Should() + .Throw() + .WithMessage($"Expected _adminApi to have been called from client IP \"different-ip\", but didn't find it among the calls from IP(s) {{\"{clientIP}\"}}."); + } + + [Fact] + public async Task HaveReceivedNoCalls_UsingPost_WhenACallWasNotMadeUsingPost_Should_BeOK() + { + await _httpClient.GetAsync("anyurl", _ct); + + _adminApi.Should() + .HaveReceivedNoCalls() + .UsingPost(); + } + + [Fact] + public async Task HaveReceived2Calls_UsingDelete_WhenACallWasMadeUsingDelete_Should_BeOK() + { + var tasks = new[] + { + _httpClient.DeleteAsync("anyurl", _ct), + _httpClient.DeleteAsync("anyurl", _ct), + _httpClient.GetAsync("anyurl", _ct) + }; + + await Task.WhenAll(tasks); + + _adminApi.Should() + .HaveReceived(2).Calls() + .UsingDelete(); + } + + [Fact] + public void HaveReceivedACall_UsingPatch_Should_ThrowWhenNoCallsWereMade() + { + Action act = () => _adminApi.Should() + .HaveReceivedACall() + .UsingPatch(); + + act.Should() + .Throw() + .WithMessage("Expected _adminApi to have been called using method \"PATCH\", but no calls were made."); + } + + [Fact] + public async Task HaveReceivedACall_UsingOptions_Should_ThrowWhenCallsWereNotMadeUsingOptions() + { + await _httpClient.PostAsync("anyurl", new StringContent("anycontent"), _ct); + + Action act = () => _adminApi.Should() + .HaveReceivedACall() + .UsingOptions(); + + act.Should() + .Throw() + .WithMessage("Expected _adminApi to have been called using method \"OPTIONS\", but didn't find it among the methods {\"POST\"}."); + } + +#if !NET452 + [Fact] + public async Task HaveReceivedACall_UsingConnect_WhenACallWasMadeUsingConnect_Should_BeOK() + { + _server.ResetMappings(); + _server.Given(Request.Create().UsingAnyMethod()) + .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.Found)); + + _httpClient.DefaultRequestHeaders.Add("Host", new Uri(_server.Urls[0]).Authority); + + await _httpClient.SendAsync(new HttpRequestMessage(new HttpMethod("CONNECT"), "anyurl"), _ct); + + _adminApi.Should() + .HaveReceivedACall() + .UsingConnect(); + } +#endif + + [Fact] + public async Task HaveReceivedACall_UsingDelete_WhenACallWasMadeUsingDelete_Should_BeOK() + { + await _httpClient.SendAsync(new HttpRequestMessage(new HttpMethod("DELETE"), "anyurl"), _ct); + + _adminApi.Should() + .HaveReceivedACall() + .UsingDelete(); + } + + [Fact] + public async Task HaveReceivedACall_UsingGet_WhenACallWasMadeUsingGet_Should_BeOK() + { + await _httpClient.SendAsync(new HttpRequestMessage(new HttpMethod("GET"), "anyurl"), _ct); + + _adminApi.Should() + .HaveReceivedACall() + .UsingGet(); + } + + [Fact] + public async Task HaveReceivedACall_UsingHead_WhenACallWasMadeUsingHead_Should_BeOK() + { + await _httpClient.SendAsync(new HttpRequestMessage(new HttpMethod("HEAD"), "anyurl"), _ct); + + _adminApi.Should() + .HaveReceivedACall() + .UsingHead(); + } + + [Fact] + public async Task HaveReceivedACall_UsingOptions_WhenACallWasMadeUsingOptions_Should_BeOK() + { + await _httpClient.SendAsync(new HttpRequestMessage(new HttpMethod("OPTIONS"), "anyurl"), _ct); + + _adminApi.Should() + .HaveReceivedACall() + .UsingOptions(); + } + + [Theory] + [InlineData("POST")] + [InlineData("Post")] + public async Task HaveReceivedACall_UsingPost_WhenACallWasMadeUsingPost_Should_BeOK(string method) + { + await _httpClient.SendAsync(new HttpRequestMessage(new HttpMethod(method), "anyurl"), _ct); + + _adminApi.Should() + .HaveReceivedACall() + .UsingPost(); + } + + [Fact] + public async Task HaveReceived1Call_AtAbsoluteUrlUsingPost_ShouldChain() + { + // Arrange + using var server = WireMockServer.Start(settings => + { + settings.StartAdminInterface = true; + settings.Logger = new TestOutputHelperWireMockLogger(_testOutputHelper); + }); + var adminApi = RestClient.For(server.Url); + + 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(); + + var tasks = new[] + { + httpClient.GetAsync($"{server.Url}/a", _ct), + httpClient.PostAsync($"{server.Url}/b", new StringContent("B"), _ct), + httpClient.PostAsync($"{server.Url}/c", new StringContent("C"), _ct) + }; + + await Task.WhenAll(tasks); + + // Assert + adminApi + .Should() + .HaveReceived(1) + .Calls() + .AtUrl($"{server.Url}/a") + .And + .UsingGet(); + + adminApi + .Should() + .HaveReceived(1) + .Calls() + .AtUrl($"{server.Url}/b") + .And + .UsingPost(); + + adminApi + .Should() + .HaveReceived(1) + .Calls() + .AtUrl($"{server.Url}/c") + .And + .UsingPost(); + + adminApi + .Should() + .HaveReceived(3) + .Calls(); + + adminApi + .Should() + .HaveReceived(1) + .Calls() + .UsingGet(); + + adminApi + .Should() + .HaveReceived(2) + .Calls() + .UsingPost(); + } + + [Fact] + public async Task HaveReceivedACall_UsingPatch_WhenACallWasMadeUsingPatch_Should_BeOK() + { + await _httpClient.SendAsync(new HttpRequestMessage(new HttpMethod("PATCH"), "anyurl"), _ct); + + _adminApi.Should() + .HaveReceivedACall() + .UsingPatch(); + } + + [Fact] + public async Task HaveReceivedACall_UsingPut_WhenACallWasMadeUsingPut_Should_BeOK() + { + await _httpClient.SendAsync(new HttpRequestMessage(new HttpMethod("PUT"), "anyurl"), _ct); + + _adminApi.Should() + .HaveReceivedACall() + .UsingPut(); + } + + [Fact] + public async Task HaveReceivedACall_UsingTrace_WhenACallWasMadeUsingTrace_Should_BeOK() + { + await _httpClient.SendAsync(new HttpRequestMessage(new HttpMethod("TRACE"), "anyurl"), _ct); + + _adminApi.Should() + .HaveReceivedACall() + .UsingTrace(); + } + + [Fact] + public async Task HaveReceivedACall_UsingAnyMethod_WhenACallWasMadeUsingGet_Should_BeOK() + { + await _httpClient.SendAsync(new HttpRequestMessage(new HttpMethod("GET"), "anyurl"), _ct); + + _adminApi.Should() + .HaveReceivedACall() + .UsingAnyMethod(); + } + + [Fact] + public void HaveReceivedNoCalls_UsingAnyMethod_WhenNoCallsWereMade_Should_BeOK() + { + _adminApi + .Should() + .HaveReceived(0) + .Calls() + .UsingAnyMethod(); + + _adminApi + .Should() + .HaveReceivedNoCalls() + .UsingAnyMethod(); + } + + [Fact] + public void HaveReceivedNoCalls_AtUrl_WhenNoCallsWereMade_Should_BeOK() + { + _adminApi.Should() + .HaveReceived(0) + .Calls() + .AtUrl(_server.Url ?? string.Empty); + + _adminApi.Should() + .HaveReceivedNoCalls() + .AtUrl(_server.Url ?? string.Empty); + } + + [Fact] + public async Task HaveReceived1Call_WithBodyAsString() + { + // Arrange + using var server = WireMockServer.Start(settings => + { + settings.StartAdminInterface = true; + settings.Logger = new TestOutputHelperWireMockLogger(_testOutputHelper); + }); + var adminApi = RestClient.For(server.Url); + + server + .Given(Request.Create().WithPath("/a").UsingPost().WithBody("x")) + .RespondWith(Response.Create().WithBody("A response")); + + // Act + var httpClient = new HttpClient(); + + await httpClient.PostAsync($"{server.Url}/a", new StringContent("x"), _ct); + + // Assert + adminApi + .Should() + .HaveReceived(1) + .Calls() + .WithBody("*") + .And + .UsingPost(); + + adminApi + .Should() + .HaveReceived(1) + .Calls() + .WithBody("x") + .And + .UsingPost(); + + adminApi + .Should() + .HaveReceived(0) + .Calls() + .WithBody("") + .And + .UsingPost(); + + adminApi + .Should() + .HaveReceived(0) + .Calls() + .WithBody("y") + .And + .UsingPost(); + } + + [Fact] + public async Task HaveReceived1Call_WithBodyAsJson() + { + // Arrange + using var server = WireMockServer.Start(settings => + { + settings.StartAdminInterface = true; + settings.Logger = new TestOutputHelperWireMockLogger(_testOutputHelper); + }); + var adminApi = RestClient.For(server.Url); + + server + .Given(Request.Create().WithPath("/a").UsingPost().WithBodyAsJson(new { x = "y" })) + .RespondWith(Response.Create().WithBody("A response")); + + // Act + using var httpClient = new HttpClient(); + + var requestBody = new + { + x = "y" + }; + await httpClient.PostAsJsonAsync($"{server.Url}/a", requestBody, _ct); + + // Assert + adminApi + .Should() + .HaveReceived(1) + .Calls() + .WithBodyAsJson(new { x = "y" }) + .And + .UsingPost(); + + adminApi + .Should() + .HaveReceived(1) + .Calls() + .WithBodyAsJson(@"{ ""x"": ""y"" }") + .And + .UsingPost(); + + adminApi + .Should() + .HaveReceived(0) + .Calls() + .WithBodyAsJson(new { x = "?" }) + .And + .UsingPost(); + + adminApi + .Should() + .HaveReceived(0) + .Calls() + .WithBodyAsJson(@"{ ""x"": 1234 }") + .And + .UsingPost(); + } + + [Fact] + public async Task WithBodyAsJson_When_NoMatch_ShouldHaveCorrectErrorMessage() + { + // Arrange + using var server = WireMockServer.Start(settings => + { + settings.StartAdminInterface = true; + settings.Logger = new TestOutputHelperWireMockLogger(_testOutputHelper); + }); + var adminApi = RestClient.For(server.Url); + + server + .Given(Request.Create().WithPath("/a").UsingPost()) + .RespondWith(Response.Create().WithBody("A response")); + + // Act + using var httpClient = new HttpClient(); + + var requestBody = new + { + x = "123" + }; + await httpClient.PostAsJsonAsync($"{server.Url}/a", requestBody, _ct); + + // Assert + Action act = () => adminApi + .Should() + .HaveReceived(1) + .Calls() + .WithBodyAsJson(new { x = "y" }) + .And + .UsingPost(); + + act.Should() + .Throw() + .WithMessage("""Expected wiremockadminapi to have been called using body "{"x":"y"}", but didn't find it among the body/bodies "{"x":"123"}"."""); + } + + [Fact] + public async Task WithBodyAsString_When_NoMatch_ShouldHaveCorrectErrorMessage() + { + // Arrange + using var server = WireMockServer.Start(settings => + { + settings.StartAdminInterface = true; + settings.Logger = new TestOutputHelperWireMockLogger(_testOutputHelper); + }); + var adminApi = RestClient.For(server.Url); + + server + .Given(Request.Create().WithPath("/a").UsingPost()) + .RespondWith(Response.Create().WithBody("A response")); + + // Act + using var httpClient = new HttpClient(); + + await httpClient.PostAsync($"{server.Url}/a", new StringContent("123"), _ct); + + // Assert + Action act = () => adminApi + .Should() + .HaveReceived(1) + .Calls() + .WithBody("abc") + .And + .UsingPost(); + + act.Should() + .Throw() + .WithMessage("""Expected wiremockadminapi to have been called using body "abc", but didn't find it among the body/bodies "123"."""); + } + + [Fact] + public async Task WithBodyAsBytes_When_NoMatch_ShouldHaveCorrectErrorMessage() + { + // Arrange + using var server = WireMockServer.Start(settings => + { + settings.StartAdminInterface = true; + settings.Logger = new TestOutputHelperWireMockLogger(_testOutputHelper); + }); + var adminApi = RestClient.For(server.Url); + + server + .Given(Request.Create().WithPath("/a").UsingPost()) + .RespondWith(Response.Create().WithBody("A response")); + + // Act + using var httpClient = new HttpClient(); + + await httpClient.PostAsync($"{server.Url}/a", new ByteArrayContent([5]), _ct); + + // Assert + Action act = () => adminApi + .Should() + .HaveReceived(1) + .Calls() + .WithBodyAsBytes([1]) + .And + .UsingPost(); + + act.Should() + .Throw() + .WithMessage("Expected wiremockadminapi to have been called using body \"byte[1] {...}\", but didn't find it among the body/bodies ."); + } + + [Fact] + public async Task HaveReceived1Call_WithBodyAsBytes() + { + // Arrange + using var server = WireMockServer.Start(settings => + { + settings.StartAdminInterface = true; + settings.Logger = new TestOutputHelperWireMockLogger(_testOutputHelper); + }); + var adminApi = RestClient.For(server.Url); + + server + .Given(Request.Create().WithPath("/a").UsingPut().WithBody([100])) + .RespondWith(Response.Create().WithBody("A response")); + + // Act + using var httpClient = new HttpClient(); + + await httpClient.PutAsync($"{server.Url}/a", new ByteArrayContent([100]), _ct); + + // Assert + adminApi + .Should() + .HaveReceived(1) + .Calls() + .WithBodyAsBytes([100]) + .And + .UsingPut(); + + adminApi + .Should() + .HaveReceived(0) + .Calls() + .WithBodyAsBytes([]) + .And + .UsingPut(); + + adminApi + .Should() + .HaveReceived(0) + .Calls() + .WithBodyAsBytes([42]) + .And + .UsingPut(); + } + + [Fact] + public async Task HaveReceived1Call_WithBodyAsString_UsingStringMatcher() + { + // Arrange + using var server = WireMockServer.Start(settings => + { + settings.StartAdminInterface = true; + settings.Logger = new TestOutputHelperWireMockLogger(_testOutputHelper); + }); + var adminApi = RestClient.For(server.Url); + + server + .Given(Request.Create().WithPath("/a").UsingPost().WithBody("x")) + .RespondWith(Response.Create().WithBody("A response")); + + // Act + using var httpClient = new HttpClient(); + + await httpClient.PostAsync($"{server.Url}/a", new StringContent("x"), _ct); + + // Assert + adminApi + .Should() + .HaveReceived(1) + .Calls() + .WithBody(new ExactMatcher("x")) + .And + .UsingPost(); + + adminApi + .Should() + .HaveReceived(0) + .Calls() + .WithBody(new ExactMatcher("")) + .And + .UsingPost(); + + adminApi + .Should() + .HaveReceived(0) + .Calls() + .WithBody(new ExactMatcher("y")) + .And + .UsingPost(); + } + + [Fact] + public async Task HaveReceivedACall_WithHeader_Should_ThrowWhenHttpMethodDoesNotMatch() + { + // Arrange + using var server = WireMockServer.Start(settings => + { + settings.StartAdminInterface = true; + settings.Logger = new TestOutputHelperWireMockLogger(_testOutputHelper); + }); + var adminApi = RestClient.For(server.Url); + + // Act : HTTP GET + using var httpClient = new HttpClient(); + await httpClient.GetAsync(server.Url, _ct); + + // Act : HTTP POST + var request = new HttpRequestMessage(HttpMethod.Post, server.Url); + request.Headers.Add("TestHeader", ["Value", "Value2"]); + + await httpClient.SendAsync(request, _ct); + + // Assert + adminApi.Should().HaveReceivedACall().UsingPost().And.WithHeader("TestHeader", ["Value", "Value2"]); + + Action act = () => adminApi.Should().HaveReceivedACall().UsingGet().And.WithHeader("TestHeader", "Value"); + act.Should() + .Throw() + .WithMessage("Expected adminapi to have been called with Header \"TestHeader\" and Values {\"Value\"}, but didn't find it among the calls with Header(s)*"); + } + + [Fact] + public async Task HaveReceivedACall_WithHeaderKey_Should_ThrowWhenHttpMethodDoesNotMatch() + { + // Arrange + using var server = WireMockServer.Start(settings => + { + settings.StartAdminInterface = true; + settings.Logger = new TestOutputHelperWireMockLogger(_testOutputHelper); + }); + var adminApi = RestClient.For(server.Url); + + // Act : HTTP GET + using var httpClient = new HttpClient(); + await httpClient.GetAsync(server.Url, _ct); + + // Act : HTTP POST + var request = new HttpRequestMessage(HttpMethod.Post, server.Url); + request.Headers.Add("TestHeader", ["Value", "Value2"]); + + await httpClient.SendAsync(request, _ct); + + // Assert + adminApi.Should().HaveReceivedACall().UsingPost().And.WitHeaderKey("TestHeader"); + + Action act = () => adminApi.Should().HaveReceivedACall().UsingGet().And.WitHeaderKey("TestHeader"); + act.Should() + .Throw() + .WithMessage("Expected adminapi to have been called with Header \"TestHeader\", but didn't find it among the calls with Header(s)*"); + } + + public void Dispose() + { + _server?.Stop(); + _server?.Dispose(); + _httpClient?.Dispose(); + + GC.SuppressFinalize(this); + } +} \ 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 e1d02c53..eaffd1aa 100644 --- a/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsTests.cs +++ b/test/WireMock.Net.Tests/FluentAssertions/WireMockAssertionsTests.cs @@ -52,7 +52,7 @@ public class WireMockAssertionsTests : IDisposable } [Fact] - public async Task HaveReceived1Calls_AtAbsoluteUrl_WhenACallWasMadeToAbsoluteUrl_Should_BeOK() + public async Task HaveReceived1Call_AtAbsoluteUrl_WhenACallWasMadeToAbsoluteUrl_Should_BeOK() { await _httpClient.GetAsync("anyurl", _ct); @@ -62,7 +62,7 @@ public class WireMockAssertionsTests : IDisposable } [Fact] - public async Task HaveReceived1Calls_AtAbsoluteUrl2_WhenACallWasMadeToAbsoluteUrl_Should_BeOK() + public async Task HaveReceived1Call_AtAbsoluteUrl2_WhenACallWasMadeToAbsoluteUrl_Should_BeOK() { await _httpClient.GetAsync("anyurl", _ct); @@ -72,7 +72,7 @@ public class WireMockAssertionsTests : IDisposable } [Fact] - public async Task HaveReceived1Calls_AtAbsoluteUrlUsingPost_WhenAPostCallWasMadeToAbsoluteUrl_Should_BeOK() + public async Task HaveReceived1Call_AtAbsoluteUrlUsingPost_WhenAPostCallWasMadeToAbsoluteUrl_Should_BeOK() { await _httpClient.PostAsync("anyurl", new StringContent(""), _ct); @@ -162,7 +162,7 @@ public class WireMockAssertionsTests : IDisposable } [Fact] - public async Task HaveReceived1Calls_AtAbsolutePath_WhenACallWasMadeToAbsolutePath_Should_BeOK() + public async Task HaveReceived1Call_AtAbsolutePath_WhenACallWasMadeToAbsolutePath_Should_BeOK() { await _httpClient.GetAsync("anypath", _ct); @@ -172,7 +172,7 @@ public class WireMockAssertionsTests : IDisposable } [Fact] - public async Task HaveReceived1Calls_AtAbsolutePathUsingPost_WhenAPostCallWasMadeToAbsolutePath_Should_BeOK() + public async Task HaveReceived1Call_AtAbsolutePathUsingPost_WhenAPostCallWasMadeToAbsolutePath_Should_BeOK() { await _httpClient.PostAsync("anypath", new StringContent(""), _ct); @@ -625,7 +625,7 @@ public class WireMockAssertionsTests : IDisposable } [Fact] - public async Task HaveReceived1Calls_AtAbsoluteUrlUsingPost_ShouldChain() + public async Task HaveReceived1Call_AtAbsoluteUrlUsingPost_ShouldChain() { // Arrange var server = WireMockServer.Start(); diff --git a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj index 988d77fc..b83bfefd 100644 --- a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj +++ b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj @@ -50,6 +50,7 @@ +