diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..a4203ccc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +* text=auto +*.cs text diff=csharp +*.cshtml text diff=html +*.csx text diff=csharp +*.sln text eol=crlf +*.slnx text eol=crlf +*.csproj text eol=crlf diff --git a/TestableHttpClient.sln b/TestableHttpClient.sln deleted file mode 100644 index 4f29e03d..00000000 --- a/TestableHttpClient.sln +++ /dev/null @@ -1,83 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.3.32901.215 -MinimumVisualStudioVersion = 15.0.26124.0 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4C8914F8-D732-462B-978E-3BB5DBE547D7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestableHttpClient", "src\TestableHttpClient\TestableHttpClient.csproj", "{FD5111E1-2970-4DC4-84DD-E4966E17DCB3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{BBCED492-E92B-4FA8-A4A5-B5A76091F25E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestableHttpClient.Tests", "test\TestableHttpClient.Tests\TestableHttpClient.Tests.csproj", "{70673E72-C346-4AC2-946D-D9F99816FC72}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{800147F1-758C-406D-AF75-CB20EB7CDB18}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - CHANGELOG.md = CHANGELOG.md - LICENSE = LICENSE - README.md = README.md - version.json = version.json - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestableHttpClient.IntegrationTests", "test\TestableHttpClient.IntegrationTests\TestableHttpClient.IntegrationTests.csproj", "{37A6C1C0-1117-43DE-BD15-290BC8AD32BE}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {FD5111E1-2970-4DC4-84DD-E4966E17DCB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FD5111E1-2970-4DC4-84DD-E4966E17DCB3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FD5111E1-2970-4DC4-84DD-E4966E17DCB3}.Debug|x64.ActiveCfg = Debug|Any CPU - {FD5111E1-2970-4DC4-84DD-E4966E17DCB3}.Debug|x64.Build.0 = Debug|Any CPU - {FD5111E1-2970-4DC4-84DD-E4966E17DCB3}.Debug|x86.ActiveCfg = Debug|Any CPU - {FD5111E1-2970-4DC4-84DD-E4966E17DCB3}.Debug|x86.Build.0 = Debug|Any CPU - {FD5111E1-2970-4DC4-84DD-E4966E17DCB3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FD5111E1-2970-4DC4-84DD-E4966E17DCB3}.Release|Any CPU.Build.0 = Release|Any CPU - {FD5111E1-2970-4DC4-84DD-E4966E17DCB3}.Release|x64.ActiveCfg = Release|Any CPU - {FD5111E1-2970-4DC4-84DD-E4966E17DCB3}.Release|x64.Build.0 = Release|Any CPU - {FD5111E1-2970-4DC4-84DD-E4966E17DCB3}.Release|x86.ActiveCfg = Release|Any CPU - {FD5111E1-2970-4DC4-84DD-E4966E17DCB3}.Release|x86.Build.0 = Release|Any CPU - {70673E72-C346-4AC2-946D-D9F99816FC72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {70673E72-C346-4AC2-946D-D9F99816FC72}.Debug|Any CPU.Build.0 = Debug|Any CPU - {70673E72-C346-4AC2-946D-D9F99816FC72}.Debug|x64.ActiveCfg = Debug|Any CPU - {70673E72-C346-4AC2-946D-D9F99816FC72}.Debug|x64.Build.0 = Debug|Any CPU - {70673E72-C346-4AC2-946D-D9F99816FC72}.Debug|x86.ActiveCfg = Debug|Any CPU - {70673E72-C346-4AC2-946D-D9F99816FC72}.Debug|x86.Build.0 = Debug|Any CPU - {70673E72-C346-4AC2-946D-D9F99816FC72}.Release|Any CPU.ActiveCfg = Release|Any CPU - {70673E72-C346-4AC2-946D-D9F99816FC72}.Release|Any CPU.Build.0 = Release|Any CPU - {70673E72-C346-4AC2-946D-D9F99816FC72}.Release|x64.ActiveCfg = Release|Any CPU - {70673E72-C346-4AC2-946D-D9F99816FC72}.Release|x64.Build.0 = Release|Any CPU - {70673E72-C346-4AC2-946D-D9F99816FC72}.Release|x86.ActiveCfg = Release|Any CPU - {70673E72-C346-4AC2-946D-D9F99816FC72}.Release|x86.Build.0 = Release|Any CPU - {37A6C1C0-1117-43DE-BD15-290BC8AD32BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {37A6C1C0-1117-43DE-BD15-290BC8AD32BE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {37A6C1C0-1117-43DE-BD15-290BC8AD32BE}.Debug|x64.ActiveCfg = Debug|Any CPU - {37A6C1C0-1117-43DE-BD15-290BC8AD32BE}.Debug|x64.Build.0 = Debug|Any CPU - {37A6C1C0-1117-43DE-BD15-290BC8AD32BE}.Debug|x86.ActiveCfg = Debug|Any CPU - {37A6C1C0-1117-43DE-BD15-290BC8AD32BE}.Debug|x86.Build.0 = Debug|Any CPU - {37A6C1C0-1117-43DE-BD15-290BC8AD32BE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {37A6C1C0-1117-43DE-BD15-290BC8AD32BE}.Release|Any CPU.Build.0 = Release|Any CPU - {37A6C1C0-1117-43DE-BD15-290BC8AD32BE}.Release|x64.ActiveCfg = Release|Any CPU - {37A6C1C0-1117-43DE-BD15-290BC8AD32BE}.Release|x64.Build.0 = Release|Any CPU - {37A6C1C0-1117-43DE-BD15-290BC8AD32BE}.Release|x86.ActiveCfg = Release|Any CPU - {37A6C1C0-1117-43DE-BD15-290BC8AD32BE}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {FD5111E1-2970-4DC4-84DD-E4966E17DCB3} = {4C8914F8-D732-462B-978E-3BB5DBE547D7} - {70673E72-C346-4AC2-946D-D9F99816FC72} = {BBCED492-E92B-4FA8-A4A5-B5A76091F25E} - {37A6C1C0-1117-43DE-BD15-290BC8AD32BE} = {BBCED492-E92B-4FA8-A4A5-B5A76091F25E} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {CD31CAB7-6661-4E80-9A70-BC8BA6B9B764} - EndGlobalSection -EndGlobal diff --git a/TestableHttpClient.slnx b/TestableHttpClient.slnx new file mode 100644 index 00000000..c2c7d77e --- /dev/null +++ b/TestableHttpClient.slnx @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/global.json b/global.json index 3b9f8d5b..0b3d6623 100644 --- a/global.json +++ b/global.json @@ -3,5 +3,8 @@ "version": "10.0.100", "allowPrerelease": false, "rollForward": "latestMajor" + }, + "test": { + "runner": "Microsoft.Testing.Platform" } } diff --git a/src/TestableHttpClient/HttpHeadersExtensions.cs b/src/TestableHttpClient/HttpHeadersExtensions.cs index 8b3d152f..9ea4490c 100644 --- a/src/TestableHttpClient/HttpHeadersExtensions.cs +++ b/src/TestableHttpClient/HttpHeadersExtensions.cs @@ -7,14 +7,13 @@ internal static bool HasHeader(this HttpHeaders headers, string headerName) return headers.TryGetValues(headerName, out _); } - internal static bool HasHeader(this HttpHeaders headers, string headerName, string headerValue) + internal static bool HasHeader(this HttpHeaders headers, string headerName, Value headerValue) { if (headers.TryGetValues(headerName, out var values)) { var value = string.Join(" ", values); - return StringMatcher.Matches(value, headerValue); + return headerValue.Matches(value, false); } - return false; } } diff --git a/src/TestableHttpClient/HttpRequestMessageAsserter.cs b/src/TestableHttpClient/HttpRequestMessageAsserter.cs index 3559981d..5b1dc1d0 100644 --- a/src/TestableHttpClient/HttpRequestMessageAsserter.cs +++ b/src/TestableHttpClient/HttpRequestMessageAsserter.cs @@ -7,6 +7,8 @@ internal sealed class HttpRequestMessageAsserter : IHttpRequestMessagesCheck { private readonly List _expectedConditions = new(); + private readonly RequestBuilder expectedRequestBuilder; + /// /// Construct a new HttpRequestMessageAsserter. /// @@ -16,6 +18,7 @@ public HttpRequestMessageAsserter(IEnumerable httpRequestMes { Requests = httpRequestMessages ?? throw new ArgumentNullException(nameof(httpRequestMessages)); Options = options ?? new TestableHttpMessageHandlerOptions(); + expectedRequestBuilder = new RequestBuilder(Options.UriPatternMatchingOptions); } /// @@ -27,9 +30,28 @@ public HttpRequestMessageAsserter(IEnumerable httpRequestMes /// public TestableHttpMessageHandlerOptions Options { get; } - private void Assert(int? expectedCount = null) + private HttpRequestMessageAsserter Assert(int? expectedCount = null, string condition = "") { - var actualCount = Requests.Count(); + if (!string.IsNullOrEmpty(condition)) + { + _expectedConditions.Add(condition); + } + return Assert(expectedCount); + } + + private HttpRequestMessageAsserter Assert(int? expectedCount = null) + { + int actualCount; + Request expectedRequest = expectedRequestBuilder.Build(); + try + { + actualCount = Requests.Count(expectedRequest.Equals); + } + catch (ObjectDisposedException) + { + throw new HttpRequestMessageAssertionException("Can't validate requests, because one or more requests have content that is already disposed."); + } + var pass = expectedCount switch { null => actualCount > 0, @@ -38,9 +60,11 @@ private void Assert(int? expectedCount = null) if (!pass) { - var message = MessageBuilder.BuildMessage(expectedCount, actualCount, _expectedConditions); + var message = MessageBuilder.BuildMessage(expectedCount, actualCount, expectedRequest, _expectedConditions); throw new HttpRequestMessageAssertionException(message); } + + return this; } /// @@ -50,6 +74,7 @@ private void Assert(int? expectedCount = null) /// The name of the condition, used in the exception message. /// The for further assertions. [AssertionMethod] + [Obsolete("WithFilter will be made internal, since it should no longer be necesary to use.")] public IHttpRequestMessagesCheck WithFilter(Func requestFilter, string condition) => WithFilter(requestFilter, null, condition); /// @@ -60,9 +85,11 @@ private void Assert(int? expectedCount = null) /// The name of the condition, used in the exception message. /// The for further assertions. [AssertionMethod] + [Obsolete("WithFilter will be made internal, since it should no longer be necesary to use.")] public IHttpRequestMessagesCheck WithFilter(Func requestFilter, int expectedNumberOfRequests, string condition) => WithFilter(requestFilter, (int?)expectedNumberOfRequests, condition); [AssertionMethod] + [Obsolete("WithFilter will be made internal, since it should no longer be necesary to use.")] public IHttpRequestMessagesCheck WithFilter(Func requestFilter, int? expectedNumberOfRequests, string condition) { if (!string.IsNullOrEmpty(condition)) @@ -70,15 +97,9 @@ public IHttpRequestMessagesCheck WithFilter(Func reque _expectedConditions.Add(condition); } - try - { - Requests = Requests.Where(requestFilter); - Assert(expectedNumberOfRequests); - } - catch (ObjectDisposedException) - { - throw new HttpRequestMessageAssertionException("Can't validate requests, because one or more requests have content that is already disposed."); - } + Requests = Requests.Where(requestFilter); + Assert(expectedNumberOfRequests); + return this; } @@ -97,19 +118,13 @@ public IHttpRequestMessagesCheck WithFilter(Func reque /// The for further assertions. public IHttpRequestMessagesCheck WithRequestUri(string pattern, int expectedNumberOfRequests) => WithRequestUri(pattern, (int?)expectedNumberOfRequests); - private IHttpRequestMessagesCheck WithRequestUri(string pattern, int? expectedNumberOfRequests) + private HttpRequestMessageAsserter WithRequestUri(string pattern, int? expectedNumberOfRequests) { Guard.ThrowIfNullOrEmpty(pattern); - var condition = string.Empty; - if (pattern != "*") - { - condition = $"uri pattern '{pattern}'"; - } + expectedRequestBuilder.WithRequestUri(pattern); - UriPattern uriPattern = UriPatternParser.Parse(pattern); - - return WithFilter(x => x.RequestUri is not null && uriPattern.Matches(x.RequestUri, Options.UriPatternMatchingOptions), expectedNumberOfRequests, condition); + return Assert(expectedNumberOfRequests); } /// @@ -127,11 +142,13 @@ private IHttpRequestMessagesCheck WithRequestUri(string pattern, int? expectedNu /// The for further assertions. public IHttpRequestMessagesCheck WithHttpMethod(HttpMethod httpMethod, int expectedNumberOfRequests) => WithHttpMethod(httpMethod, (int?)expectedNumberOfRequests); - private IHttpRequestMessagesCheck WithHttpMethod(HttpMethod httpMethod, int? expectedNumberOfRequests) + private HttpRequestMessageAsserter WithHttpMethod(HttpMethod httpMethod, int? expectedNumberOfRequests) { Guard.ThrowIfNull(httpMethod); - return WithFilter(x => x.HasHttpMethod(httpMethod), expectedNumberOfRequests, $"HTTP Method '{httpMethod}'"); + expectedRequestBuilder.WithMethod(httpMethod); + + return Assert(expectedNumberOfRequests); } /// @@ -150,11 +167,13 @@ private IHttpRequestMessagesCheck WithHttpMethod(HttpMethod httpMethod, int? exp /// The for further assertions. public IHttpRequestMessagesCheck WithHttpVersion(Version httpVersion, int expectedNumberOfRequests) => WithHttpVersion(httpVersion, (int?)expectedNumberOfRequests); - private IHttpRequestMessagesCheck WithHttpVersion(Version httpVersion, int? expectedNumberOfRequests) + private HttpRequestMessageAsserter WithHttpVersion(Version httpVersion, int? expectedNumberOfRequests) { Guard.ThrowIfNull(httpVersion); - return WithFilter(x => x.HasHttpVersion(httpVersion), expectedNumberOfRequests, $"HTTP Version '{httpVersion}'"); + expectedRequestBuilder.WithVersion(httpVersion); + + return Assert(expectedNumberOfRequests, $"HTTP Version '{httpVersion}'"); } /// @@ -172,11 +191,13 @@ private IHttpRequestMessagesCheck WithHttpVersion(Version httpVersion, int? expe /// The for further assertions. public IHttpRequestMessagesCheck WithHeader(string headerName, int expectedNumberOfRequests) => WithHeader(headerName, (int?)expectedNumberOfRequests); - private IHttpRequestMessagesCheck WithHeader(string headerName, int? expectedNumberOfRequests) + private HttpRequestMessageAsserter WithHeader(string headerName, int? expectedNumberOfRequests) { Guard.ThrowIfNullOrEmpty(headerName); - return WithFilter(x => x.HasHeader(headerName), expectedNumberOfRequests, $"header '{headerName}'"); + expectedRequestBuilder.WithHeader(headerName); + + return Assert(expectedNumberOfRequests); } /// @@ -196,12 +217,14 @@ private IHttpRequestMessagesCheck WithHeader(string headerName, int? expectedNum /// The for further assertions. public IHttpRequestMessagesCheck WithHeader(string headerName, string headerValue, int expectedNumberOfRequests) => WithHeader(headerName, headerValue, (int?)expectedNumberOfRequests); - private IHttpRequestMessagesCheck WithHeader(string headerName, string headerValue, int? expectedNumberOfRequests) + private HttpRequestMessageAsserter WithHeader(string headerName, string headerValue, int? expectedNumberOfRequests) { Guard.ThrowIfNullOrEmpty(headerName); Guard.ThrowIfNullOrEmpty(headerValue); - return WithFilter(x => x.HasHeader(headerName, headerValue), expectedNumberOfRequests, $"header '{headerName}' and value '{headerValue}'"); + expectedRequestBuilder.WithHeader(headerName, headerValue); + + return Assert(expectedNumberOfRequests); } /// @@ -219,10 +242,12 @@ private IHttpRequestMessagesCheck WithHeader(string headerName, string headerVal /// The for further assertions. public IHttpRequestMessagesCheck WithContent(string pattern, int expectedNumberOfRequests) => WithContent(pattern, (int?)expectedNumberOfRequests); - private IHttpRequestMessagesCheck WithContent(string pattern, int? expectedNumberOfRequests) + private HttpRequestMessageAsserter WithContent(string pattern, int? expectedNumberOfRequests) { Guard.ThrowIfNull(pattern); - return WithFilter(x => x.HasContent(pattern), expectedNumberOfRequests, $"content '{pattern}'"); + expectedRequestBuilder.WithContent(pattern); + + return Assert(expectedNumberOfRequests); } } diff --git a/src/TestableHttpClient/HttpRequestMessageExtensions.cs b/src/TestableHttpClient/HttpRequestMessageExtensions.cs deleted file mode 100644 index 26abc9a2..00000000 --- a/src/TestableHttpClient/HttpRequestMessageExtensions.cs +++ /dev/null @@ -1,129 +0,0 @@ -namespace TestableHttpClient; - -/// -/// A set of static methods for checking values on a . -/// -internal static class HttpRequestMessageExtensions -{ - /// - /// Determines whether a specific HttpVersion is set on a request. - /// - /// A to check the correct version on. - /// The expected version. - /// true when the HttpVersion matches; otherwise, false. - internal static bool HasHttpVersion(this HttpRequestMessage httpRequestMessage, Version httpVersion) - { - Guard.ThrowIfNull(httpRequestMessage); - Guard.ThrowIfNull(httpVersion); - - return httpRequestMessage.Version == httpVersion; - } - - /// - /// Determines whether a specific HttpMethod is set on a request. - /// - /// A to check the correct method on. - /// The expected method. - /// true when the HttpMethod matches; otherwise, false. - internal static bool HasHttpMethod(this HttpRequestMessage httpRequestMessage, HttpMethod httpMethod) - { - Guard.ThrowIfNull(httpRequestMessage); - Guard.ThrowIfNull(httpMethod); - - return httpRequestMessage.Method == httpMethod; - } - - internal static bool HasHeader(this HttpRequestMessage httpRequestMessage, string headerName) - { - Guard.ThrowIfNull(httpRequestMessage); - Guard.ThrowIfNullOrEmpty(headerName); - - return httpRequestMessage.Headers.HasHeader(headerName) || (httpRequestMessage.Content is not null && httpRequestMessage.Content.Headers.HasHeader(headerName)); - } - - internal static bool HasHeader(this HttpRequestMessage httpRequestMessage, string headerName, string headerValue) - { - Guard.ThrowIfNull(httpRequestMessage); - Guard.ThrowIfNullOrEmpty(headerName); - Guard.ThrowIfNullOrEmpty(headerValue); - - return httpRequestMessage.Headers.HasHeader(headerName, headerValue) || (httpRequestMessage.Content is not null && httpRequestMessage.Content.Headers.HasHeader(headerName, headerValue)); - } - - [Obsolete("Use HasHeader instead.")] - internal static bool HasRequestHeader(this HttpRequestMessage httpRequestMessage, string headerName) - { - Guard.ThrowIfNull(httpRequestMessage); - Guard.ThrowIfNullOrEmpty(headerName); - - return httpRequestMessage.Headers.HasHeader(headerName); - } - - [Obsolete("Use HasHeader instead.")] - internal static bool HasRequestHeader(this HttpRequestMessage httpRequestMessage, string headerName, string headerValue) - { - Guard.ThrowIfNull(httpRequestMessage); - Guard.ThrowIfNullOrEmpty(headerName); - Guard.ThrowIfNullOrEmpty(headerValue); - - return httpRequestMessage.Headers.HasHeader(headerName, headerValue); - } - - [Obsolete("Use HasHeader instead.")] - internal static bool HasContentHeader(this HttpRequestMessage httpRequestMessage, string headerName) - { - Guard.ThrowIfNull(httpRequestMessage); - Guard.ThrowIfNullOrEmpty(headerName); - - if (httpRequestMessage.Content == null) - { - return false; - } - - return httpRequestMessage.Content.Headers.HasHeader(headerName); - } - - [Obsolete("Use HasHeader instead.")] - internal static bool HasContentHeader(this HttpRequestMessage httpRequestMessage, string headerName, string headerValue) - { - Guard.ThrowIfNull(httpRequestMessage); - Guard.ThrowIfNullOrEmpty(headerName); - Guard.ThrowIfNullOrEmpty(headerValue); - - if (httpRequestMessage.Content == null) - { - return false; - } - - return httpRequestMessage.Content.Headers.HasHeader(headerName, headerValue); - } - - /// - /// Determines whether the request content matches a string pattern. - /// - /// A to check the correct content on. - /// A pattern to match the request content, supports * as wildcards. - /// true when the request content matches the pattern; otherwise, false. - internal static bool HasContent(this HttpRequestMessage httpRequestMessage, string pattern) - { - Guard.ThrowIfNull(httpRequestMessage); - Guard.ThrowIfNull(pattern); - - if (httpRequestMessage.Content == null) - { - return false; - } - - var stringContent = httpRequestMessage.Content.ReadAsStringAsync() - .ConfigureAwait(false) - .GetAwaiter() - .GetResult(); - - return pattern switch - { - "" => stringContent == pattern, - "*" => true, - _ => StringMatcher.Matches(stringContent, pattern), - }; - } -} diff --git a/src/TestableHttpClient/PublicAPI.Unshipped.txt b/src/TestableHttpClient/PublicAPI.Unshipped.txt index f9efd0a5..568d353b 100644 --- a/src/TestableHttpClient/PublicAPI.Unshipped.txt +++ b/src/TestableHttpClient/PublicAPI.Unshipped.txt @@ -10,4 +10,11 @@ TestableHttpClient.IHttpRequestMessagesCheck.WithHttpMethod(System.Net.Http.Http TestableHttpClient.IHttpRequestMessagesCheck.WithHttpVersion(System.Version! httpVersion) -> TestableHttpClient.IHttpRequestMessagesCheck! TestableHttpClient.IHttpRequestMessagesCheck.WithHttpVersion(System.Version! httpVersion, int expectedNumberOfRequests) -> TestableHttpClient.IHttpRequestMessagesCheck! TestableHttpClient.IHttpRequestMessagesCheck.WithRequestUri(string! pattern) -> TestableHttpClient.IHttpRequestMessagesCheck! -TestableHttpClient.IHttpRequestMessagesCheck.WithRequestUri(string! pattern, int expectedNumberOfRequests) -> TestableHttpClient.IHttpRequestMessagesCheck! \ No newline at end of file +TestableHttpClient.IHttpRequestMessagesCheck.WithRequestUri(string! pattern, int expectedNumberOfRequests) -> TestableHttpClient.IHttpRequestMessagesCheck! +TestableHttpClient.RequestBuilder +TestableHttpClient.RequestBuilder.WithContent(string! pattern) -> TestableHttpClient.RequestBuilder! +TestableHttpClient.RequestBuilder.WithHeader(string! headerName) -> TestableHttpClient.RequestBuilder! +TestableHttpClient.RequestBuilder.WithHeader(string! headerName, string! pattern) -> TestableHttpClient.RequestBuilder! +TestableHttpClient.RequestBuilder.WithMethod(System.Net.Http.HttpMethod! httpMethod) -> TestableHttpClient.RequestBuilder! +TestableHttpClient.RequestBuilder.WithRequestUri(string! pattern) -> TestableHttpClient.RequestBuilder! +TestableHttpClient.RequestBuilder.WithVersion(System.Version! httpVersion) -> TestableHttpClient.RequestBuilder! diff --git a/src/TestableHttpClient/Request.cs b/src/TestableHttpClient/Request.cs new file mode 100644 index 00000000..1c9d0319 --- /dev/null +++ b/src/TestableHttpClient/Request.cs @@ -0,0 +1,98 @@ +namespace TestableHttpClient; + +internal record Request : IEquatable +{ + public Request(UriPatternMatchingOptions uriPatternMatchingOptions) + { + UriPatternMatchingOptions = uriPatternMatchingOptions; + } + + public UriPatternMatchingOptions UriPatternMatchingOptions { get; } + + public HttpMethod? Method { get; init; } + public UriPattern? RequestUri { get; init; } + public Version? Version { get; init; } + + public Dictionary? Headers { get; init; } + + public string? Content { get; init; } + + public Request AddHeader(string headerName) => AddHeader(headerName, Value.Any()); + + public Request AddHeader(string headerName, string headerValue) => AddHeader(headerName, Value.Pattern(headerValue)); + + public Request AddHeader(string headerName, Value headerValue) + { + if (Headers is null) + { + Dictionary headerValues = new() { [headerName] = headerValue }; + return this with { Headers = headerValues }; + } + else + { + Headers[headerName] = headerValue; + return this; + } + } + + public bool Equals(HttpRequestMessage? other) + { + if (other is null) + { + return false; + } + + if (Method is not null && other.Method != Method) + { + return false; + } + + if (RequestUri is not null && other.RequestUri is not null && !RequestUri.Matches(other.RequestUri, UriPatternMatchingOptions)) + { + return false; + } + + if (Version is not null && other.Version != Version) + { + return false; + } + + if (Headers is not null) + { + foreach (var keyValuePair in Headers) + { + if (!other.Headers.HasHeader(keyValuePair.Key, keyValuePair.Value) && (other.Content is null || !other.Content.Headers.HasHeader(keyValuePair.Key, keyValuePair.Value))) + { + return false; + } + } + } + + if (Content is not null) + { + if (other.Content is null) + { + return false; + } + + var stringContent = other.Content.ReadAsStringAsync() + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + + var contentMatches = Content switch + { + "" => stringContent == Content, + "*" => true, + _ => StringMatcher.Matches(stringContent, Content), + }; + + if (!contentMatches) + { + return false; + } + } + + return true; + } +} diff --git a/src/TestableHttpClient/RequestBuilder.cs b/src/TestableHttpClient/RequestBuilder.cs new file mode 100644 index 00000000..430ee6f8 --- /dev/null +++ b/src/TestableHttpClient/RequestBuilder.cs @@ -0,0 +1,62 @@ +namespace TestableHttpClient; + +public sealed class RequestBuilder +{ + private Request request; + + internal RequestBuilder(UriPatternMatchingOptions uriPatternMatchingOptions) + { + request = new(uriPatternMatchingOptions); + } + + public RequestBuilder WithMethod(HttpMethod httpMethod) + { + Guard.ThrowIfNull(httpMethod); + + request = request with { Method = httpMethod }; + return this; + } + + public RequestBuilder WithRequestUri(string pattern) + { + Guard.ThrowIfNullOrEmpty(pattern); + + request = request with { RequestUri = UriPatternParser.Parse(pattern) }; + return this; + } + + public RequestBuilder WithVersion(Version httpVersion) + { + Guard.ThrowIfNull(httpVersion); + + request = request with { Version = httpVersion }; + return this; + } + + public RequestBuilder WithHeader(string headerName) + { + Guard.ThrowIfNullOrEmpty(headerName); + + request = request.AddHeader(headerName, Value.Any()); + return this; + } + + public RequestBuilder WithHeader(string headerName, string pattern) + { + Guard.ThrowIfNullOrEmpty(headerName); + Guard.ThrowIfNullOrEmpty(pattern); + + request = request.AddHeader(headerName, Value.Pattern(pattern)); + return this; + } + + public RequestBuilder WithContent(string pattern) + { + Guard.ThrowIfNull(pattern); + + request = request with { Content = pattern }; + return this; + } + + internal Request Build() => request; +} diff --git a/src/TestableHttpClient/Utils/MessageBuilder.cs b/src/TestableHttpClient/Utils/MessageBuilder.cs index 888908b6..527f84c8 100644 --- a/src/TestableHttpClient/Utils/MessageBuilder.cs +++ b/src/TestableHttpClient/Utils/MessageBuilder.cs @@ -1,25 +1,68 @@ -namespace TestableHttpClient.Utils; +using System.Diagnostics; +using System.Globalization; +using System.Net.Mime; + +namespace TestableHttpClient.Utils; internal static class MessageBuilder { - internal static string BuildMessage(int? expectedCount, int actualCount, IEnumerable conditions) + internal static string BuildMessage(int? expectedCount, int actualCount, Request expectedRequest, IEnumerable conditions) { var pass = expectedCount switch { null => actualCount > 0, _ => expectedCount == actualCount }; + var method = expectedRequest.Method switch + { + null => "", + _ => $"{expectedRequest.Method} " + }; var expectedMessage = expectedCount switch { - null => "at least one request", - 0 => "no requests", - 1 => "one request", - _ => $"{expectedCount} requests" + null => $"at least one {method}request", + 0 => $"no {method}requests", + 1 => $"one {method}request", + _ => $"{expectedCount} {method}requests" + }; + + var requestUri = expectedRequest.RequestUri switch + { + null => "", + _ => BuildRequestUri(expectedRequest.RequestUri) + }; + + var headers = expectedRequest.Headers switch + { + null => "", + _ => BuildHeaders(expectedRequest.Headers) }; + string content = string.Empty; + + if (!string.IsNullOrEmpty(expectedRequest.Content)) + { + StringBuilder contentBuilder = new(); + if (string.IsNullOrEmpty(headers)) + { + contentBuilder.AppendLine(" with content:"); + } + else + { + contentBuilder.AppendLine("and content:"); + } + + string[] splitcontent = expectedRequest.Content!.Split(['\n']); + foreach (var line in splitcontent) + { + contentBuilder.AppendLine(CultureInfo.InvariantCulture, $" {line.Trim('\r')}"); + } + content = contentBuilder.ToString(); + } + var expectedConditions = string.Empty; - if (conditions.Any()) + if (conditions is not null && conditions.Any()) { expectedConditions = $" with {string.Join(", ", conditions)}"; } @@ -33,8 +76,98 @@ internal static string BuildMessage(int? expectedCount, int actualCount, IEnumer return pass switch { - true => $"Expected {expectedMessage} to be made{expectedConditions}, and {actualMessage}.", - false => $"Expected {expectedMessage} to be made{expectedConditions}, but {actualMessage}." + true => $"Expected {expectedMessage} to be made{requestUri}{headers}{content}{expectedConditions}, and {actualMessage}.", + false => $"Expected {expectedMessage} to be made{requestUri}{headers}{content}{expectedConditions}, but {actualMessage}." + }; + } + + private static string BuildRequestUri(UriPattern requestUri) + { + string scheme = BuildValue(requestUri.Scheme); + string host = BuildValue(requestUri.Host); + string port = BuildValue(requestUri.Port); + string path = BuildValue(requestUri.Path); + string query = BuildValue(requestUri.Query); + + StringBuilder uriBuilder = new(); + if (!string.IsNullOrEmpty(scheme)) + { + uriBuilder.Append(scheme).Append("://"); + } + + if (!string.IsNullOrEmpty(host)) + { + uriBuilder.Append(host); + } + else if (uriBuilder.Length > 0) + { + uriBuilder.Append(""); + } + + if (!string.IsNullOrEmpty(port)) + { + uriBuilder.Append(':').Append(port); + } + + if (!string.IsNullOrEmpty(path)) + { + uriBuilder.Append(path); + } + + if (!string.IsNullOrEmpty(query)) + { + uriBuilder.Append('?').Append(query); + } + + var result = uriBuilder.ToString(); + if (string.IsNullOrEmpty(result)) + { + return result; + } + else + { + return $" to '{result}'"; + } + } + + private static string BuildHeaders(Dictionary headerValues) + { + StringBuilder headers = new(); + + foreach (var header in headerValues) + { + var value = BuildValue(header.Value); + if (string.IsNullOrEmpty(value)) + { + headers.AppendLine(CultureInfo.InvariantCulture, $" {header.Key}: "); + } + else + { + headers.AppendLine(CultureInfo.InvariantCulture, $" {header.Key}: '{value}'"); + } + } + + var result = headers.ToString(); + if (string.IsNullOrEmpty(result)) + { + return result; + } + else + { + StringBuilder headervalue = new(); + headervalue.AppendLine(" with headers:").Append(result); + return headervalue.ToString(); ; + } + } + + private static string BuildValue(Value value) + { + return value switch + { + AnyValue => "", + ExactValue exactValue => exactValue.ExpectedValue, + PatternValue patternValue => patternValue.ExpectedValue, + _ => throw new UnreachableException() }; } } diff --git a/src/TestableHttpClient/Utils/NetStandardPollyFill.cs b/src/TestableHttpClient/Utils/NetStandardPollyFill.cs index f3d1acc3..d6f3626c 100644 --- a/src/TestableHttpClient/Utils/NetStandardPollyFill.cs +++ b/src/TestableHttpClient/Utils/NetStandardPollyFill.cs @@ -1,5 +1,7 @@ #if NETSTANDARD +using System.Runtime.CompilerServices; + namespace TestableHttpClient.Utils; internal static class NetStandardPollyFill @@ -13,6 +15,16 @@ public static string Replace(this string input, string oldValue, string newValue { return input.Replace(oldValue, newValue); } + + public static StringBuilder Append(this StringBuilder builder, IFormatProvider? provider, string handler) + { + return builder.Append(handler); + } + + public static StringBuilder AppendLine(this StringBuilder builder, IFormatProvider? provider, string handler) + { + return builder.AppendLine(handler); + } } #endif diff --git a/src/TestableHttpClient/Utils/RequestFormatOptions.cs b/src/TestableHttpClient/Utils/RequestFormatOptions.cs new file mode 100644 index 00000000..756710df --- /dev/null +++ b/src/TestableHttpClient/Utils/RequestFormatOptions.cs @@ -0,0 +1,13 @@ +namespace TestableHttpClient.Utils; + +[Flags] +internal enum RequestFormatOptions +{ + HttpMethod = 1, + RequestUri = 2, + HttpVersion = 4, + RequestLine = HttpMethod | RequestUri | HttpVersion, + Headers = 8, + Content = 16, + All = RequestLine | Headers | Content +} diff --git a/src/TestableHttpClient/Utils/UriPattern.cs b/src/TestableHttpClient/Utils/UriPattern.cs index 3c2cc39a..f4ac5ef4 100644 --- a/src/TestableHttpClient/Utils/UriPattern.cs +++ b/src/TestableHttpClient/Utils/UriPattern.cs @@ -2,7 +2,7 @@ namespace TestableHttpClient.Utils; -internal sealed class UriPattern +internal sealed record UriPattern { public static UriPattern Any { get; } = new UriPattern(); diff --git a/src/TestableHttpClient/Utils/Value.cs b/src/TestableHttpClient/Utils/Value.cs index 27569edb..bba79836 100644 --- a/src/TestableHttpClient/Utils/Value.cs +++ b/src/TestableHttpClient/Utils/Value.cs @@ -12,23 +12,19 @@ internal abstract record Value } [DebuggerDisplay("Any value")] -file sealed record AnyValue : Value +internal sealed record AnyValue : Value { internal override bool Matches(string value, bool ignoreCase) => true; } -[DebuggerDisplay("Exact value: {expectedValue}")] -file sealed record ExactValue : Value +[DebuggerDisplay("Exact value: {ExpectedValue}")] +internal sealed record ExactValue(string ExpectedValue) : Value { - private readonly string expectedValue; - public ExactValue(string expectedValue) => this.expectedValue = expectedValue; - internal override bool Matches(string value, bool ignoreCase) => expectedValue.Equals(value, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + internal override bool Matches(string value, bool ignoreCase) => ExpectedValue.Equals(value, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } -[DebuggerDisplay("Pattern value: {pattern}")] -file sealed record PatternValue : Value +[DebuggerDisplay("Pattern value: {ExpectedValue}")] +internal sealed record PatternValue(string ExpectedValue) : Value { - private readonly string pattern; - public PatternValue(string pattern) => this.pattern = pattern; - internal override bool Matches(string value, bool ignoreCase) => StringMatcher.Matches(value, pattern, ignoreCase); + internal override bool Matches(string value, bool ignoreCase) => StringMatcher.Matches(value, ExpectedValue, ignoreCase); } diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets index 69c311b3..b40c836a 100644 --- a/test/Directory.Build.targets +++ b/test/Directory.Build.targets @@ -1,5 +1,5 @@ - + diff --git a/test/TestableHttpClient.Tests/HttpRequestMessageAsserterTests/WithFilter.cs b/test/TestableHttpClient.Tests/HttpRequestMessageAsserterTests/WithFilter.cs index a8c5a664..3aa86ed6 100644 --- a/test/TestableHttpClient.Tests/HttpRequestMessageAsserterTests/WithFilter.cs +++ b/test/TestableHttpClient.Tests/HttpRequestMessageAsserterTests/WithFilter.cs @@ -1,5 +1,6 @@ namespace TestableHttpClient.Tests.HttpRequestMessageAsserterTests; +[Obsolete("WithFilter will be made internal, since it should no longer be necesary to use.")] public sealed class WithFilter { [Fact] diff --git a/test/TestableHttpClient.Tests/HttpRequestMessagesCheckExtensionsTests/WithFormUrlEncodedContent.cs b/test/TestableHttpClient.Tests/HttpRequestMessagesCheckExtensionsTests/WithFormUrlEncodedContent.cs index 3d981229..279ef4d4 100644 --- a/test/TestableHttpClient.Tests/HttpRequestMessagesCheckExtensionsTests/WithFormUrlEncodedContent.cs +++ b/test/TestableHttpClient.Tests/HttpRequestMessagesCheckExtensionsTests/WithFormUrlEncodedContent.cs @@ -88,7 +88,12 @@ public void WithFormUrlEncodedContent_WithoutNumberOfRequests_RequestWithNotMatc var exception = Assert.Throws(() => sut.WithFormUrlEncodedContent([new KeyValuePair("username", "alice")])); - Assert.Equal("Expected at least one request to be made with content 'username=alice', but no requests were made.", exception.Message); + Assert.Equal(""" + Expected at least one request to be made with content: + username=alice + , but no requests were made. + """, exception.Message); + } [Fact] @@ -103,6 +108,12 @@ public void WithFormUrlEncodedContent_WithoutNumberOfRequests_RequestWithNotMatc var exception = Assert.Throws(() => sut.WithFormUrlEncodedContent([new KeyValuePair("username", "alice")])); - Assert.Equal("Expected at least one request to be made with content 'username=alice', header 'Content-Type' and value 'application/x-www-form-urlencoded*', but no requests were made.", exception.Message); + Assert.Equal(""" + Expected at least one request to be made with headers: + Content-Type: 'application/x-www-form-urlencoded*' + and content: + username=alice + , but no requests were made. + """, exception.Message); } } diff --git a/test/TestableHttpClient.Tests/HttpRequestMessagesCheckExtensionsTests/WithJsonContent.cs b/test/TestableHttpClient.Tests/HttpRequestMessagesCheckExtensionsTests/WithJsonContent.cs index 29b31c19..a6d72462 100644 --- a/test/TestableHttpClient.Tests/HttpRequestMessagesCheckExtensionsTests/WithJsonContent.cs +++ b/test/TestableHttpClient.Tests/HttpRequestMessagesCheckExtensionsTests/WithJsonContent.cs @@ -47,7 +47,11 @@ public void WithJsonContent_RequestWithDifferentContent_ThrowsHttpRequestMessage HttpRequestMessageAsserter sut = new([request]); var exception = Assert.Throws(() => sut.WithJsonContent(null)); - Assert.Equal("Expected at least one request to be made with content 'null', but no requests were made.", exception.Message); + Assert.Equal(""" + Expected at least one request to be made with content: + null + , but no requests were made. + """, exception.Message); } [Fact] @@ -60,6 +64,12 @@ public void WithJsonContent_RequestWithDifferentContentType_ThrowsHttpRequestMes HttpRequestMessageAsserter sut = new([request]); var exception = Assert.Throws(() => sut.WithJsonContent(null)); - Assert.Equal("Expected at least one request to be made with content 'null', header 'Content-Type' and value 'application/json*', but no requests were made.", exception.Message); + Assert.Equal(""" + Expected at least one request to be made with headers: + Content-Type: 'application/json*' + and content: + null + , but no requests were made. + """, exception.Message); } } diff --git a/test/TestableHttpClient.Tests/RequestBuilderTests.cs b/test/TestableHttpClient.Tests/RequestBuilderTests.cs new file mode 100644 index 00000000..17b19eff --- /dev/null +++ b/test/TestableHttpClient.Tests/RequestBuilderTests.cs @@ -0,0 +1,148 @@ +using TestableHttpClient.Utils; + +namespace TestableHttpClient.Tests; + +public sealed class RequestBuilderTests +{ + private readonly RequestBuilder sut = new(new UriPatternMatchingOptions()); + + [Fact] + public void Build_ByDefault_CreatesEmptyRequest() + { + Request request = sut.Build(); + + Assert.Null(request.Method); + Assert.Null(request.RequestUri); + Assert.Null(request.Version); + Assert.Null(request.Headers); + Assert.Null(request.Content); + } + + [Fact] + public void WithMethod_NullMethod_ThrowsArgumentNullException() + { + Assert.Throws(() => sut.WithMethod(null!)); + } + + [Fact] + public void WithMethod_CreatesRequestWithMethod() + { + Request request = sut.WithMethod(HttpMethod.Post).Build(); + + Assert.Equal(HttpMethod.Post, request.Method); + Assert.Null(request.RequestUri); + Assert.Null(request.Version); + Assert.Null(request.Headers); + Assert.Null(request.Content); + } + + [Fact] + public void WithRequestUri_NullUri_ThrowsArgumentNullException() + { + Assert.Throws(() => sut.WithRequestUri(null!)); + } + + [Fact] + public void WithRequestUri_EmptyUri_ThrowsArgumentException() + { + Assert.Throws(() => sut.WithRequestUri(string.Empty)); + } + + [Fact] + public void WithRequestUri_UriPattern_ShouldSetRequestUri() + { + Request request = sut.WithRequestUri("http*//test.example").Build(); + + Assert.Null(request.Method); + Assert.Equal(UriPatternParser.Parse("http*//test.example"), request.RequestUri); + Assert.Null(request.Version); + Assert.Null(request.Headers); + Assert.Null(request.Content); + } + + [Fact] + public void WithVersion_NullVersion_ThrowsArgumentNullException() + { + Assert.Throws(() => sut.WithVersion(null!)); + } + + [Fact] + public void WithVersion_CreatesRequestWithVersion() + { + Request request = sut.WithVersion(HttpVersion.Version11).Build(); + + Assert.Null(request.Method); + Assert.Null(request.RequestUri); + Assert.Equal(HttpVersion.Version11, request.Version); + Assert.Null(request.Headers); + Assert.Null(request.Content); + } + + [Fact] + public void WithHeader_NullHeaderName_ThrowsArgumentNullException() + { + Assert.Throws(() => sut.WithHeader(null!)); + Assert.Throws(() => sut.WithHeader(null!, "test")); + } + + [Fact] + public void WithHeader_EmptyHeaderName_ThrowsArgumentException() + { + Assert.Throws(() => sut.WithHeader(string.Empty)); + Assert.Throws(() => sut.WithHeader(string.Empty, "test")); + } + + [Fact] + public void WithHeader_ValidHeaderNameNoValue_CreatesRequestWithHeaderWithAnyValue() + { + Request request = sut.WithHeader("Content-Length").Build(); + + Assert.Null(request.Method); + Assert.Null(request.RequestUri); + Assert.Null(request.Version); + Assert.Equal(new Dictionary() { ["Content-Length"] = Value.Any() }, request.Headers); + Assert.Null(request.Content); + } + + [Fact] + public void WithHeader_NullPattern_ThrowsArgumentNullException() + { + Assert.Throws(() => sut.WithHeader("Content-Length", null!)); + } + + [Fact] + public void WithHeader_EmptyPattern_ThrowsArgumentException() + { + Assert.Throws(() => sut.WithHeader("Content-Length", string.Empty)); + } + + [Fact] + public void WithHeader_ValidHeaderNamePatternValue_CreatesRequestWithHeaderWithAnyValue() + { + Request request = sut.WithHeader("Content-Length", "*").Build(); + + Assert.Null(request.Method); + Assert.Null(request.RequestUri); + Assert.Null(request.Version); + Assert.Equal(new Dictionary() { ["Content-Length"] = Value.Pattern("*") }, request.Headers); + Assert.Null(request.Content); + } + + [Fact] + public void WithContent_NullContent_ThrowsArgumentNullException() + { + Assert.Throws(() => sut.WithContent(null!)); + } + + [Fact] + public void WithContent_CreatesRequestWithContent() + { + Request request = sut.WithContent("content").Build(); + + Assert.Null(request.Method); + Assert.Null(request.RequestUri); + Assert.Null(request.Version); + Assert.Null(request.Headers); + Assert.Equal("content", request.Content); + } +} diff --git a/test/TestableHttpClient.Tests/TestableHttpMessageHandlerExtensionsTests/CreateClientWithConfigurer.cs b/test/TestableHttpClient.Tests/TestableHttpMessageHandlerExtensionsTests/CreateClientWithConfigurer.cs index b9597838..91115556 100644 --- a/test/TestableHttpClient.Tests/TestableHttpMessageHandlerExtensionsTests/CreateClientWithConfigurer.cs +++ b/test/TestableHttpClient.Tests/TestableHttpMessageHandlerExtensionsTests/CreateClientWithConfigurer.cs @@ -61,7 +61,7 @@ public void CreateClientWithConfigurer_WhenConfiguringBaseAddress_DoesNotOverrid [Fact] public void CreateClientWithConfigurer_CallsConfigureClientWithClientToReturn() { - HttpClient? capturedClient= null; + HttpClient? capturedClient = null; using TestableHttpMessageHandler sut = new(); void configureClient(HttpClient client) => capturedClient = client; diff --git a/test/TestableHttpClient.Tests/TestableHttpMessageHandlerExtensionsTests/CreateClientWithConfigurerAndHttpMessageHandlers.cs b/test/TestableHttpClient.Tests/TestableHttpMessageHandlerExtensionsTests/CreateClientWithConfigurerAndHttpMessageHandlers.cs index 87cbfdf1..1ab23966 100644 --- a/test/TestableHttpClient.Tests/TestableHttpMessageHandlerExtensionsTests/CreateClientWithConfigurerAndHttpMessageHandlers.cs +++ b/test/TestableHttpClient.Tests/TestableHttpMessageHandlerExtensionsTests/CreateClientWithConfigurerAndHttpMessageHandlers.cs @@ -8,7 +8,7 @@ public void CreateClientWithConfigurerAndHttpMessageHandlers_NullTestableHttpMes TestableHttpMessageHandler sut = null!; static void configureClient(HttpClient _) { } - + var exception = Assert.Throws(() => sut.CreateClient(configureClient, [])); Assert.Equal("handler", exception.ParamName); } @@ -18,7 +18,7 @@ public void CreateClientWithConfigurerAndHttpMessageHandlers_NullConfigureAction { using TestableHttpMessageHandler sut = new(); Action configureClient = null!; - + var exception = Assert.Throws(() => sut.CreateClient(configureClient, [])); Assert.Equal("configureClient", exception.ParamName); } @@ -41,7 +41,7 @@ public void CreateClientWithConfigurerAndHttpMessageHandlers_CallsConfigureClien HttpClient? capturedClient = null; using TestableHttpMessageHandler sut = new(); void configureClient(HttpClient client) => capturedClient = client; - + using var client = sut.CreateClient(configureClient, []); Assert.Same(client, capturedClient); diff --git a/test/TestableHttpClient.Tests/Utils/MessageBuilderTests.cs b/test/TestableHttpClient.Tests/Utils/MessageBuilderTests.cs index 5f429638..e26de18b 100644 --- a/test/TestableHttpClient.Tests/Utils/MessageBuilderTests.cs +++ b/test/TestableHttpClient.Tests/Utils/MessageBuilderTests.cs @@ -5,9 +5,10 @@ namespace TestableHttpClient.Tests.Utils; public class MessageBuilderTests { [Fact] - public void BuildMessage_NoExpectedCountZeroActualCountNoConditions() + public void BuildMessage_NoExpectedCountZeroActualCountDefaultExpectedRequest() { - var result = MessageBuilder.BuildMessage(null, 0, []); + Request request = new(new()); + var result = MessageBuilder.BuildMessage(null, 0, request, []); Assert.Equal("Expected at least one request to be made, but no requests were made.", result); } @@ -16,17 +17,19 @@ public void BuildMessage_NoExpectedCountZeroActualCountNoConditions() [InlineData(1, "one request was made")] [InlineData(2, "2 requests were made")] [InlineData(10, "10 requests were made")] - public void BuildMessage_NoExpectedCountVariableActualCountNoConditions(int actualCount, string expectedMessage) + public void BuildMessage_NoExpectedCountVariableActualCountDefaultExpectedRequest(int actualCount, string expectedMessage) { - var result = MessageBuilder.BuildMessage(null, actualCount, []); + Request request = new(new()); + var result = MessageBuilder.BuildMessage(null, actualCount, request, []); Assert.Equal($"Expected at least one request to be made, and {expectedMessage}.", result); } [Fact] - public void BuildMessage_ZeroExpectedCountZeroActualCountNoConditions() + public void BuildMessage_ZeroExpectedCountZeroActualCountDefaultExpectedRequest() { - var result = MessageBuilder.BuildMessage(0, 0, []); + Request request = new(new()); + var result = MessageBuilder.BuildMessage(0, 0, request, []); Assert.Equal($"Expected no requests to be made, and no requests were made.", result); } @@ -35,20 +38,125 @@ public void BuildMessage_ZeroExpectedCountZeroActualCountNoConditions() [InlineData(1, "one request")] [InlineData(2, "2 requests")] [InlineData(10, "10 requests")] - public void BuildMessage_VariableExpectedCountZeroActualCountNoConditions(int expectedCount, string expectedMessage) + public void BuildMessage_VariableExpectedCountZeroActualCountDefaultExpectedRequest(int expectedCount, string expectedMessage) { - var result = MessageBuilder.BuildMessage(expectedCount, 0, []); + Request request = new(new()); + var result = MessageBuilder.BuildMessage(expectedCount, 0, request, []); Assert.Equal($"Expected {expectedMessage} to be made, but no requests were made.", result); } + [Theory] + [InlineData(null, "at least one GET request")] + [InlineData(1, "one GET request")] + [InlineData(2, "2 GET requests")] + [InlineData(10, "10 GET requests")] + public void BuildMessage_VariableExpectedCountZeroActualCountRequestSpecifyingMethod(int? expectedCount, string expectedMessage) + { + Request request = new(new()) + { + Method = HttpMethod.Get, + }; + + var result = MessageBuilder.BuildMessage(expectedCount, 0, request, []); + + Assert.Equal($"Expected {expectedMessage} to be made, but no requests were made.", result); + } + + [Theory] + [InlineData("/test", "/test")] + [InlineData("https://*/test", "https:///test")] + [InlineData("/test?Hello", "/test?Hello")] + public void BuildMessage_OnexpectedCountZeroActualCountRequestSpecifyingUri(string uri, string expectedRepresentation) + { + Request request = new(new()) + { + RequestUri = UriPatternParser.Parse(uri) + }; + + var result = MessageBuilder.BuildMessage(1, 0, request, []); + + Assert.Equal($"Expected one request to be made to '{expectedRepresentation}', but no requests were made.", result); + } + + [Fact] + public void BuildMessage_OnexpectedCountZeroActualCountRequestSpecifyingHeaders() + { + Request request = new(new()) + { + Headers = new Dictionary() + { + ["Header1"] = Value.Any(), + ["Header2"] = Value.Pattern("Value2"), + ["Header3"] = Value.Exact("Value3"), + } + }; + + var result = MessageBuilder.BuildMessage(1, 0, request, []); + + Assert.Equal(""" + Expected one request to be made with headers: + Header1: + Header2: 'Value2' + Header3: 'Value3' + , but no requests were made. + """, result); + } + + [Fact] + public void BuildMessage_OnexpectedCountZeroActualCountRequestSpecifyingContent() + { + Request request = new(new()) + { + Content = "test" + }; + + var result = MessageBuilder.BuildMessage(1, 0, request, []); + + Assert.Equal(""" + Expected one request to be made with content: + test + , but no requests were made. + """, result); + } + + [Fact] + public void BuildMessage_OnexpectedCountZeroActualCountRequestSpecifyingHeadersSpecifyingContent() + { + Request request = new(new()) + { + Headers = new Dictionary() + { + ["Content-Type"] = Value.Exact("application/json") + }, + Content = """ + { + "test": "value" + } + """ + }; + + var result = MessageBuilder.BuildMessage(1, 0, request, []); + + Assert.Equal(""" + Expected one request to be made with headers: + Content-Type: 'application/json' + and content: + { + "test": "value" + } + , but no requests were made. + """, result); + } + [Theory] [InlineData("with condition 1", "condition 1")] [InlineData("with condition 1, condition 2", "condition 1", "condition 2")] [InlineData("with condition 1, condition 2, condition 3", "condition 1", "condition 2", "condition 3")] public void BuildMessage_NullExpectedCountZeroActualCountVariableAmountOfConditions(string expectedCondition, params string[] conditions) { - var result = MessageBuilder.BuildMessage(null, 0, conditions); + Request request = new(new()); + var result = MessageBuilder.BuildMessage(null, 0, request, conditions); Assert.Equal($"Expected at least one request to be made {expectedCondition}, but no requests were made.", result); } @@ -59,7 +167,8 @@ public void BuildMessage_NullExpectedCountZeroActualCountVariableAmountOfConditi [InlineData("with condition 1, condition 2, condition 3", "condition 1", "condition 2", "condition 3")] public void BuildMessage_NullExpectedCountOneActualCountVariableAmountOfConditions(string expectedCondition, params string[] conditions) { - var result = MessageBuilder.BuildMessage(null, 1, conditions); + Request request = new(new()); + var result = MessageBuilder.BuildMessage(null, 1, request, conditions); Assert.Equal($"Expected at least one request to be made {expectedCondition}, and one request was made.", result); }