diff --git a/Source/Mockolate/MockRegistration.Setup.cs b/Source/Mockolate/MockRegistration.Setup.cs index 86fa86c9..86797a05 100644 --- a/Source/Mockolate/MockRegistration.Setup.cs +++ b/Source/Mockolate/MockRegistration.Setup.cs @@ -307,6 +307,7 @@ public ValueStorage GetOrAdd(NamedParameterValue key, Func valueGe } } + [ExcludeFromCodeCoverage] private sealed class NamedParameterValueComparer : IEqualityComparer { public static readonly NamedParameterValueComparer Instance = new(); diff --git a/Source/Mockolate/Web/HttpClientExtensions.cs b/Source/Mockolate/Web/HttpClientExtensions.cs index 77e32995..c759344c 100644 --- a/Source/Mockolate/Web/HttpClientExtensions.cs +++ b/Source/Mockolate/Web/HttpClientExtensions.cs @@ -83,12 +83,8 @@ public bool Matches(HttpRequestMessage value) } string requestUri1 = value.RequestUri.ToString(); - string requestUri2 = requestUri1.EndsWith('/') - ? requestUri1.Substring(0, requestUri1.Length - 1) - : requestUri1 + '/'; - return invokableParameter.Matches(requestUri1) || - invokableParameter.Matches(requestUri2); + (requestUri1.EndsWith('/') && invokableParameter.Matches(requestUri1.TrimEnd('/'))); } public void InvokeCallbacks(HttpRequestMessage value) diff --git a/Source/Mockolate/Web/ItExtensions.Uri.cs b/Source/Mockolate/Web/ItExtensions.Uri.cs index 3aedfb86..9a31bc3a 100644 --- a/Source/Mockolate/Web/ItExtensions.Uri.cs +++ b/Source/Mockolate/Web/ItExtensions.Uri.cs @@ -79,12 +79,9 @@ public bool Matches(object? value) if (pattern is not null) { string requestUri1 = uri.ToString(); - string requestUri2 = requestUri1.EndsWith('/') - ? requestUri1.Substring(0, requestUri1.Length - 1) - : requestUri1 + '/'; Wildcard wildcard = Wildcard.Pattern(pattern, true); if (!wildcard.Matches(requestUri1) && - !wildcard.Matches(requestUri2)) + (!requestUri1.EndsWith('/') || !wildcard.Matches(requestUri1.TrimEnd('/')))) { return false; } diff --git a/Tests/Mockolate.Tests/MockMethods/VerifyInvokedTests.cs b/Tests/Mockolate.Tests/MockMethods/VerifyInvokedTests.cs index a45b095a..864cd50f 100644 --- a/Tests/Mockolate.Tests/MockMethods/VerifyInvokedTests.cs +++ b/Tests/Mockolate.Tests/MockMethods/VerifyInvokedTests.cs @@ -17,25 +17,25 @@ public async Task Equals_ShouldWork() } [Fact] - public async Task Equals_WithOtherOverload_ShouldWork() + public async Task Equals_ShouldWorkWithNull() { - object obj = new(); + object? obj = null; IMethodService mock = Mock.Create(); - _ = mock.Equals(3); + _ = mock.Equals(null); - await That(mock.VerifyMock.Invoked.Equals(It.Is(obj))).Never(); + await That(mock.VerifyMock.Invoked.Equals(It.Is(obj))).Once(); } [Fact] - public async Task Equals_ShouldWorkWithNull() + public async Task Equals_WithOtherOverload_ShouldWork() { - object? obj = null; + object obj = new(); IMethodService mock = Mock.Create(); - _ = mock.Equals(null); + _ = mock.Equals(3); - await That(mock.VerifyMock.Invoked.Equals(It.Is(obj))).Once(); + await That(mock.VerifyMock.Invoked.Equals(It.Is(obj))).Never(); } [Fact] @@ -59,6 +59,17 @@ public async Task MethodWithDifferentName_ShouldNotMatch() await That(sut.VerifyMock.Invoked.Subtract(It.IsAny(), It.IsAny())).Never(); } + [Fact] + public async Task MethodWithDifferentName_WithParameters_ShouldNotMatch() + { + MockTests.IMyService sut = Mock.Create(); + + sut.Subtract(1, 4); + sut.Subtract(2, 4); + + await That(sut.VerifyMock.Invoked.Multiply(Match.AnyParameters())).Never(); + } + [Fact] public async Task MethodWithDifferentOverload_ShouldNotMatch() { diff --git a/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.cs b/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.cs index 27f79d08..3420aa48 100644 --- a/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.cs +++ b/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.cs @@ -48,6 +48,60 @@ public async Task NullUri_ShouldReturnFalse() await That(result).IsFalse(); } + [Fact] + public async Task ShouldSupportMonitoring() + { + int callbackCount = 0; + HttpClient httpClient = Mock.Create(); + httpClient.SetupMock.Method + .GetAsync(It.Matches("*") + .Do(_ => callbackCount++) + .Monitor(out IParameterMonitor monitor)) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); + + await httpClient.GetAsync("https://www.aweXpect.com/foo", CancellationToken.None); + await httpClient.PostAsync("https://www.aweXpect.com/bar", null, CancellationToken.None); + await httpClient.GetAsync("https://www.aweXpect.com/baz", CancellationToken.None); + + await That(monitor.Values).IsEqualTo([ + "https://www.awexpect.com/foo", + "https://www.awexpect.com/baz", + ]); + await That(callbackCount).IsEqualTo(2); + } + + [Theory] + [InlineData("*aweXpect.com")] + [InlineData("*aweXpect.com/")] + public async Task TrailingSlash_ShouldBeIgnored(string matchPattern) + { + HttpClient httpClient = Mock.Create(); + httpClient.SetupMock.Method + .GetAsync(It.Matches(matchPattern)) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); + + HttpResponseMessage result = + await httpClient.GetAsync("https://www.aweXpect.com", CancellationToken.None); + + await That(result.StatusCode) + .IsEqualTo(HttpStatusCode.OK); + } + + [Fact] + public async Task TrailingSlash_WhenNotPresent_ShouldNotBeAdded() + { + HttpClient httpClient = Mock.Create(); + httpClient.SetupMock.Method + .GetAsync(It.Matches("*www.aweXpect.com/foo/")) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); + + HttpResponseMessage result = + await httpClient.GetAsync("https://www.aweXpect.com/foo", CancellationToken.None); + + await That(result.StatusCode) + .IsEqualTo(HttpStatusCode.NotImplemented); + } + private sealed class InvalidParameter : IParameter { public IParameter Do(Action callback) diff --git a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsBinaryContentTests.cs b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsBinaryContentTests.cs index 28a9fbc6..3b2aa0b3 100644 --- a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsBinaryContentTests.cs +++ b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsBinaryContentTests.cs @@ -58,11 +58,12 @@ public async Task EqualTo_ShouldCheckForEquality(byte[] body, byte[] expected, b await That(result.StatusCode).IsEqualTo(expectSuccess ? HttpStatusCode.OK : HttpStatusCode.NotImplemented); } - + #if !NETFRAMEWORK [Fact] public async Task ShouldSupportMonitoring() { + int callbackCount = 0; List responses = [ new([]), @@ -71,7 +72,9 @@ public async Task ShouldSupportMonitoring() ]; HttpClient httpClient = Mock.Create(); httpClient.SetupMock.Method.PostAsync(It.IsAny(), - It.IsBinaryContent().Monitor(out IParameterMonitor monitor)); + It.IsBinaryContent() + .Do(_ => callbackCount++) + .Monitor(out IParameterMonitor monitor)); foreach (ByteArrayContent response in responses) { @@ -81,6 +84,7 @@ public async Task ShouldSupportMonitoring() await That( (await Task.WhenAll(monitor.Values.Select(c => c!.ReadAsByteArrayAsync()))).Select(x => x.Length)) .IsEqualTo([0, 1, 3,]); + await That(callbackCount).IsEqualTo(3); } #endif diff --git a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsStringContentTests.cs b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsStringContentTests.cs index 51813752..a737ef99 100644 --- a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsStringContentTests.cs +++ b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsStringContentTests.cs @@ -18,6 +18,7 @@ public sealed class IsStringContentTests [Fact] public async Task ShouldSupportMonitoring() { + int callbackCount = 0; List responses = [ new("", Encoding.UTF8, "application/json"), @@ -26,7 +27,9 @@ public async Task ShouldSupportMonitoring() ]; HttpClient httpClient = Mock.Create(); httpClient.SetupMock.Method.PostAsync(It.IsAny(), - It.IsStringContent("application/json").Monitor(out IParameterMonitor monitor)); + It.IsStringContent("application/json") + .Do(_ => callbackCount++) + .Monitor(out IParameterMonitor monitor)); foreach (StringContent response in responses) { @@ -35,6 +38,7 @@ public async Task ShouldSupportMonitoring() await That(await Task.WhenAll(monitor.Values.Select(c => c!.ReadAsStringAsync()))) .IsEqualTo(["", "foo", "bar",]); + await That(callbackCount).IsEqualTo(3); } #endif diff --git a/Tests/Mockolate.Tests/Web/ItExtensionsTests.UriTests.cs b/Tests/Mockolate.Tests/Web/ItExtensionsTests.UriTests.cs index b761dc2a..f0b5c8d2 100644 --- a/Tests/Mockolate.Tests/Web/ItExtensionsTests.UriTests.cs +++ b/Tests/Mockolate.Tests/Web/ItExtensionsTests.UriTests.cs @@ -46,8 +46,11 @@ public async Task ForHttps_ShouldVerifyHttpsScheme(string uri, bool expectMatch) [Fact] public async Task ShouldSupportMonitoring() { + int callbackCount = 0; HttpClient httpClient = Mock.Create(); - httpClient.SetupMock.Method.GetAsync(It.IsUri().Monitor(out IParameterMonitor monitor)); + httpClient.SetupMock.Method.GetAsync(It.IsUri() + .Do(_ => callbackCount++) + .Monitor(out IParameterMonitor monitor)); await httpClient.GetAsync("https://www.aweXpect.com", CancellationToken.None); await httpClient.GetAsync("https://www.aweXpect.com/foo", CancellationToken.None); @@ -59,6 +62,7 @@ await That(monitor.Values.Select(u => u?.ToString())) "https://www.aweXpect.com/foo", "https://www.aweXpect.com/bar", ]).IgnoringCase(); + await That(callbackCount).IsEqualTo(3); } [Theory] @@ -84,9 +88,42 @@ public async Task ShouldVerifyFullUriWithWildcardMatch(string uri, string patter await That(result.StatusCode).IsEqualTo(expectMatch ? HttpStatusCode.OK : HttpStatusCode.NotImplemented); } + [Theory] + [InlineData("*aweXpect.com")] + [InlineData("*aweXpect.com/")] + public async Task TrailingSlash_ShouldBeIgnored(string matchPattern) + { + HttpClient httpClient = Mock.Create(); + httpClient.SetupMock.Method + .GetAsync(It.IsUri(matchPattern)) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); + + HttpResponseMessage result = + await httpClient.GetAsync("https://www.aweXpect.com", CancellationToken.None); + + await That(result.StatusCode) + .IsEqualTo(HttpStatusCode.OK); + } + + [Fact] + public async Task TrailingSlash_WhenNotPresent_ShouldNotBeAdded() + { + HttpClient httpClient = Mock.Create(); + httpClient.SetupMock.Method + .GetAsync(It.IsUri("*www.aweXpect.com/foo/")) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); + + HttpResponseMessage result = + await httpClient.GetAsync("https://www.aweXpect.com/foo", CancellationToken.None); + + await That(result.StatusCode) + .IsEqualTo(HttpStatusCode.NotImplemented); + } + [Theory] [InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "www.awexpect.com", true)] [InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "*awexpect*", true)] + [InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "*aweXpect*", true)] [InlineData("http://www.aweXpect.com/foo/bar?x=123&y=234", "mockolate.com", false)] public async Task WithHost_ShouldVerifyHost(string uri, string hostPattern, bool expectMatch) { @@ -103,6 +140,7 @@ public async Task WithHost_ShouldVerifyHost(string uri, string hostPattern, bool [Theory] [InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "/foo/bar", true)] [InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "*foo*", true)] + [InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "*FOO*", true)] [InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "*bar*", true)] [InlineData("http://www.aweXpect.com/foo/bar?x=123&y=234", "*baz*", false)] public async Task WithPath_ShouldVerifyPath(string uri, string pathPattern, bool expectMatch) @@ -139,6 +177,7 @@ public async Task WithPort_ShouldVerifyPort(string uri, int port, bool expectMat [InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "?x=123&y=234", true)] [InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "*123*", true)] [InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "*x=*y=*", true)] + [InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "*X=*Y=*", true)] [InlineData("http://www.aweXpect.com/foo/bar?x=123&y=234", "*z=*", false)] public async Task WithQuery_ShouldVerifyQuery(string uri, string queryPattern, bool expectMatch) {