diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.ForMock.Extensions.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.ForMock.Extensions.cs index aa7fcb40..2d64cc2c 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.ForMock.Extensions.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.ForMock.Extensions.cs @@ -403,34 +403,7 @@ private static void AppendMethodSetup(Class @class, StringBuilder sb, Method met sb.AppendLine(); sb.AppendLine("\t\t{"); - - if (@class is { ClassName: "HttpClient", ClassFullName: "System.Net.Http.HttpClient", } && - method.Name.StartsWith("Send")) - { - sb.Append("\t\t\tif (setup is Mock httpClientMock &&").AppendLine(); - sb.Append( - "\t\t\t httpClientMock.ConstructorParameters[0] is IMockSubject httpMessageHandlerMock &&") - .AppendLine(); - sb.Append( - "\t\t\t httpMessageHandlerMock.Mock is IMockMethodSetup httpMessageHandlerSetup)") - .AppendLine(); - sb.Append("\t\t\t{").AppendLine(); - AppendMethodSetupBody(sb, method, useParameters, - "\t\t\t\t", - method.GetUniqueNameString().Replace("System.Net.Http.HttpMessageInvoker", - "System.Net.Http.HttpMessageHandler"), - "httpMessageHandlerSetup"); - sb.Append("\t\t\t}").AppendLine(); - sb.Append("\t\t\telse").AppendLine(); - sb.Append("\t\t\t{").AppendLine(); - AppendMethodSetupBody(sb, method, useParameters, "\t\t\t\t"); - sb.Append("\t\t\t}").AppendLine(); - } - else - { - AppendMethodSetupBody(sb, method, useParameters); - } - + AppendMethodSetupBody(sb, method, useParameters); sb.AppendLine("\t\t}"); } diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.ForMock.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.ForMock.cs index fba782d8..9f1d5fe0 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.ForMock.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.ForMock.cs @@ -75,7 +75,7 @@ namespace Mockolate.Generated; sb.Append("\t\tvar ").Append(resultVarName).Append(" = _mock.Registrations.InvokeMethod(") .Append(mockClass.Delegate.GetUniqueNameString()); } - + foreach (MethodParameter p in mockClass.Delegate.Parameters) { sb.Append(", new NamedParameterValue(\"").Append(p.Name).Append("\", ").Append(p.RefKind switch @@ -291,7 +291,7 @@ private static void AppendMockSubject_ImplementClass(StringBuilder sb, Class @cl if (mockMethods?.All(m => !Method.EqualityComparer.Equals(method, m)) != false) { AppendMockSubject_ImplementClass_AddMethod(sb, method, className, mockClass is not null, - @class.IsInterface); + @class.IsInterface, @class); } } @@ -607,7 +607,7 @@ property.IndexerParameters is not null } private static void AppendMockSubject_ImplementClass_AddMethod(StringBuilder sb, Method method, string className, - bool explicitInterfaceImplementation, bool isClassInterface) + bool explicitInterfaceImplementation, bool isClassInterface, Class @class) { sb.Append("\t/// ( : IHttpRequestMessageParameter { public bool Matches(HttpRequestMessage value) - => ((IParameter)parameter).Matches(valueSelector(value)); + { + if (parameter is IHttpRequestMessagePropertyParameter httpRequestMessageParameter) + { + return httpRequestMessageParameter.Matches(valueSelector(value), value); + } + + return ((IParameter)parameter).Matches(valueSelector(value)); + } public void InvokeCallbacks(HttpRequestMessage value) { diff --git a/Source/Mockolate/Web/IHttpRequestMessagePropertyParameter.cs b/Source/Mockolate/Web/IHttpRequestMessagePropertyParameter.cs new file mode 100644 index 00000000..61816616 --- /dev/null +++ b/Source/Mockolate/Web/IHttpRequestMessagePropertyParameter.cs @@ -0,0 +1,16 @@ +using System.Net.Http; +using Mockolate.Parameters; + +namespace Mockolate.Web; + +/// +/// A parameter of type that also gets a . +/// +internal interface IHttpRequestMessagePropertyParameter : IParameter +{ + /// + /// Matches the property of type while also considering the + /// . + /// + bool Matches(T value, HttpRequestMessage? requestMessage); +} diff --git a/Source/Mockolate/Web/ItExtensions.HttpContent.cs b/Source/Mockolate/Web/ItExtensions.HttpContent.cs index f483c224..d20f98e3 100644 --- a/Source/Mockolate/Web/ItExtensions.HttpContent.cs +++ b/Source/Mockolate/Web/ItExtensions.HttpContent.cs @@ -120,7 +120,8 @@ public interface IHttpContentParameter : IParameter, IHttpHeaderPa } private sealed class HttpContentParameter - : IParameter, IStringContentBodyMatchingParameter, IFormDataContentParameter + : IParameter, IStringContentBodyMatchingParameter, IFormDataContentParameter, + IHttpRequestMessagePropertyParameter { private List>? _callbacks; private IContentMatcher? _contentMatcher; @@ -137,9 +138,38 @@ public IFormDataContentParameter Exactly() return this; } + /// + public bool Matches(HttpContent? value, HttpRequestMessage? requestMessage) + { + if (value is null) + { + return false; + } + + if (_mediaType is not null && + value.Headers.ContentType?.MediaType?.Equals(_mediaType, StringComparison.OrdinalIgnoreCase) != true) + { + return false; + } + + if (_headers is not null && + !_headers.Matches(value.Headers)) + { + return false; + } + + if (_contentMatcher is not null && + !_contentMatcher.Matches(value, requestMessage)) + { + return false; + } + + return true; + } + /// public bool Matches(object? value) - => value is HttpContent typedValue && Matches(typedValue); + => value is HttpContent typedValue && Matches(typedValue, null); /// public void InvokeCallbacks(object? value) @@ -251,33 +281,7 @@ public IStringContentBodyParameter IgnoringCase() return this; } - /// - /// Checks whether the given matches the expectations. - /// - private bool Matches(HttpContent value) - { - if (_mediaType is not null && - value.Headers.ContentType?.MediaType?.Equals(_mediaType, StringComparison.OrdinalIgnoreCase) != true) - { - return false; - } - - if (_headers is not null && - !_headers.Matches(value.Headers)) - { - return false; - } - - if (_contentMatcher is not null && - !_contentMatcher.Matches(value)) - { - return false; - } - - return true; - } - - private static string GetStringFromHttpContent(HttpContent content) + private static string GetStringFromHttpContent(HttpContent content, HttpRequestMessage? message) { static Encoding GetEncodingFromCharset(string? charset) { @@ -300,18 +304,32 @@ static Encoding GetEncodingFromCharset(string? charset) Encoding encoding = GetEncodingFromCharset(charset); #if NET8_0_OR_GREATER Stream stream = content.ReadAsStream(); + long position = stream.Position; using StreamReader reader = new(stream, encoding, leaveOpen: true); + string stringContent = reader.ReadToEnd(); + stream.Position = position; #else - Stream stream = content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult(); - using StreamReader reader = new(stream, encoding); + string stringContent; + if (message?.Properties.TryGetValue("Mockolate:HttpContent", out object value) == true + && value is byte[] bytes) + { + stringContent = encoding.GetString(bytes); + } + else + { + Stream stream = content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + long position = stream.Position; + using StreamReader reader = new(stream, encoding, true, 1024, true); + stringContent = reader.ReadToEnd(); + stream.Position = position; + } #endif - string stringContent = reader.ReadToEnd(); return stringContent; } private interface IContentMatcher { - bool Matches(HttpContent content); + bool Matches(HttpContent content, HttpRequestMessage? message); } private sealed class StringMatcher : IContentMatcher @@ -328,9 +346,9 @@ public StringMatcher(string value, bool isExact) _bodyMatchType = isExact ? BodyMatchType.Exact : BodyMatchType.Wildcard; } - public bool Matches(HttpContent content) + public bool Matches(HttpContent content, HttpRequestMessage? message) { - string stringContent = GetStringFromHttpContent(content); + string stringContent = GetStringFromHttpContent(content, message); switch (_bodyMatchType) { case BodyMatchType.Exact when @@ -387,9 +405,9 @@ public PredicateStringMatcher(Func predicate) _predicate = predicate; } - public bool Matches(HttpContent content) + public bool Matches(HttpContent content, HttpRequestMessage? message) { - string stringContent = GetStringFromHttpContent(content); + string stringContent = GetStringFromHttpContent(content, message); return _predicate.Invoke(stringContent); } } @@ -403,16 +421,32 @@ public BinaryMatcher(Func predicate) _predicate = predicate; } - public bool Matches(HttpContent content) + public bool Matches(HttpContent content, HttpRequestMessage? message) { #if NET8_0_OR_GREATER Stream stream = content.ReadAsStream(); -#else - Stream stream = content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult(); -#endif + long position = stream.Position; using MemoryStream ms = new(); stream.CopyTo(ms); byte[] bytes = ms.ToArray(); + stream.Position = position; +#else + byte[] bytes; + if (message?.Properties.TryGetValue("Mockolate:HttpContent", out object value) == true + && value is byte[] b) + { + bytes = b; + } + else + { + Stream stream = content.ReadAsStreamAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + long position = stream.Position; + using MemoryStream ms = new(); + stream.CopyTo(ms); + bytes = ms.ToArray(); + stream.Position = position; + } +#endif return _predicate.Invoke(bytes); } } @@ -439,7 +473,7 @@ public FormDataMatcher(string formDataParameters) .Select(pair => (pair.Key, new HttpFormDataValue(pair.Value)))); } - public bool Matches(HttpContent content) + public bool Matches(HttpContent content, HttpRequestMessage? message) { List<(string Key, string Value)> formDataParameters = GetFormData(content).ToList(); return _isExactly diff --git a/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.PatchTests.cs b/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.PatchTests.cs index a0d8768d..9798dc0c 100644 --- a/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.PatchTests.cs +++ b/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.PatchTests.cs @@ -23,12 +23,12 @@ public async Task StringUri_ShouldVerifyHttpContent(string mediaType, int expect HttpClient httpClient = Mock.Create(); await httpClient.PatchAsync("https://www.aweXpect.com", - new StringContent("", Encoding.UTF8, mediaType), + new StringContent("{}", Encoding.UTF8, mediaType), CancellationToken.None); await That(httpClient.VerifyMock.Invoked.PatchAsync( It.IsAny(), - It.IsHttpContent("application/json"))) + It.IsHttpContent("application/json").WithString("{}"))) .Exactly(expected); } @@ -111,12 +111,12 @@ public async Task Uri_ShouldVerifyHttpContent(string mediaType, int expected) HttpClient httpClient = Mock.Create(); await httpClient.PatchAsync("https://www.aweXpect.com", - new StringContent("", Encoding.UTF8, mediaType), + new StringContent("{}", Encoding.UTF8, mediaType), CancellationToken.None); await That(httpClient.VerifyMock.Invoked.PatchAsync( It.IsUri("*aweXpect.com*"), - It.IsHttpContent("application/json"))) + It.IsHttpContent("application/json").WithString("{}"))) .Exactly(expected); } diff --git a/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.PostTests.cs b/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.PostTests.cs index 48b6659e..3d66b2ab 100644 --- a/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.PostTests.cs +++ b/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.PostTests.cs @@ -22,12 +22,12 @@ public async Task StringUri_ShouldVerifyHttpContent(string mediaType, int expect HttpClient httpClient = Mock.Create(); await httpClient.PostAsync("https://www.aweXpect.com", - new StringContent("", Encoding.UTF8, mediaType), + new StringContent("{}", Encoding.UTF8, mediaType), CancellationToken.None); await That(httpClient.VerifyMock.Invoked.PostAsync( It.IsAny(), - It.IsHttpContent("application/json"))) + It.IsHttpContent("application/json").WithString("{}"))) .Exactly(expected); } @@ -109,12 +109,12 @@ public async Task Uri_ShouldVerifyHttpContent(string mediaType, int expected) HttpClient httpClient = Mock.Create(); await httpClient.PostAsync("https://www.aweXpect.com", - new StringContent("", Encoding.UTF8, mediaType), + new StringContent("{}", Encoding.UTF8, mediaType), CancellationToken.None); await That(httpClient.VerifyMock.Invoked.PostAsync( It.IsUri("*aweXpect.com*"), - It.IsHttpContent("application/json"))) + It.IsHttpContent("application/json").WithString("{}"))) .Exactly(expected); } diff --git a/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.PutTests.cs b/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.PutTests.cs index bdd9ac91..3e329362 100644 --- a/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.PutTests.cs +++ b/Tests/Mockolate.Tests/Web/HttpClientExtensionsTests.Verify.PutTests.cs @@ -22,12 +22,12 @@ public async Task StringUri_ShouldVerifyHttpContent(string mediaType, int expect HttpClient httpClient = Mock.Create(); await httpClient.PutAsync("https://www.aweXpect.com", - new StringContent("", Encoding.UTF8, mediaType), + new StringContent("{}", Encoding.UTF8, mediaType), CancellationToken.None); await That(httpClient.VerifyMock.Invoked.PutAsync( It.IsAny(), - It.IsHttpContent("application/json"))) + It.IsHttpContent("application/json").WithString("{}"))) .Exactly(expected); } @@ -109,12 +109,12 @@ public async Task Uri_ShouldVerifyHttpContent(string mediaType, int expected) HttpClient httpClient = Mock.Create(); await httpClient.PutAsync("https://www.aweXpect.com", - new StringContent("", Encoding.UTF8, mediaType), + new StringContent("{}", Encoding.UTF8, mediaType), CancellationToken.None); await That(httpClient.VerifyMock.Invoked.PutAsync( It.IsUri("*aweXpect.com*"), - It.IsHttpContent("application/json"))) + It.IsHttpContent("application/json").WithString("{}"))) .Exactly(expected); } diff --git a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithBytesTests.cs b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithBytesTests.cs index e6d2b735..25c35aaf 100644 --- a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithBytesTests.cs +++ b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithBytesTests.cs @@ -53,6 +53,24 @@ public async Task ShouldCheckForEquality(byte[] body, byte[] expected, bool expe await That(result.StatusCode) .IsEqualTo(expectSuccess ? HttpStatusCode.OK : HttpStatusCode.NotImplemented); } + + [Fact] + public async Task WhenValidatedAndSetup_ShouldResetStreamPosition() + { + byte[] body = [0x66, 0x67,]; + HttpClient httpClient = Mock.Create(); + httpClient.SetupMock.Method + .PostAsync(It.IsAny(), It.IsHttpContent().WithBytes(body)) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); + + HttpResponseMessage result = await httpClient + .PostAsync("https://www.aweXpect.com", new ByteArrayContent(body), CancellationToken.None); + + await That(httpClient.VerifyMock.Invoked + .PostAsync(It.IsAny(), It.IsHttpContent().WithBytes(body))) + .Once(); + await That(result.StatusCode).IsEqualTo(HttpStatusCode.OK); + } } } } diff --git a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringTests.cs b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringTests.cs index fb058fbb..b3252c9d 100644 --- a/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringTests.cs +++ b/Tests/Mockolate.Tests/Web/ItExtensionsTests.IsHttpContentTests.WithStringTests.cs @@ -139,6 +139,23 @@ await That(result.StatusCode) .IsEqualTo(expectSuccess ? HttpStatusCode.OK : HttpStatusCode.NotImplemented); } + [Fact] + public async Task WhenValidatedAndSetup_ShouldResetStreamPosition() + { + HttpClient httpClient = Mock.Create(); + httpClient.SetupMock.Method + .PostAsync(It.IsAny(), It.IsHttpContent().WithString("foo")) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)); + + HttpResponseMessage result = await httpClient + .PostAsync("https://www.aweXpect.com", new StringContent("foo"), CancellationToken.None); + + await That(httpClient.VerifyMock.Invoked + .PostAsync(It.IsAny(), It.IsHttpContent().WithString("foo"))) + .Once(); + await That(result.StatusCode).IsEqualTo(HttpStatusCode.OK); + } + [Theory] [InlineData("foo")] public async Task WithInvalidCharsetHeader_ShouldFallbackToUtf8(string charsetHeader)