Skip to content

Commit c084e83

Browse files
committed
Clone HttpRequestMessage so the copy is asserted and the original request can be disposed properly
1 parent c785c2c commit c084e83

File tree

6 files changed

+109
-5
lines changed

6 files changed

+109
-5
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1010
- .NET Framework 4.6.2, 4.7.0 and 4.7.2, since these can't be tested using xUnit v3
1111
### Added
1212
- Support for .NET 9.0
13-
- support for .NET 10.0
13+
- Support for .NET 10.0
14+
### Changed
15+
- The TestableHttpMessageHandler now makes a clone of the original request, so that the original request can be disposed.
1416

1517
## [0.11] - 2024-06-15
1618
### Removed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-

1+
override TestableHttpClient.TestableHttpMessageHandler.Dispose(bool disposing) -> void

src/TestableHttpClient/TestableHttpMessageHandler.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,20 @@ public class TestableHttpMessageHandler : HttpMessageHandler
1717
/// </summary>
1818
public IEnumerable<HttpRequestMessage> Requests => httpRequestMessages;
1919

20+
protected override void Dispose(bool disposing)
21+
{
22+
DisposeRequestMessages();
23+
base.Dispose(disposing);
24+
}
25+
26+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "It gets disposed in the dispose method")]
2027
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
2128
{
22-
httpRequestMessages.Enqueue(request);
29+
#if NET8_0_OR_GREATER
30+
ArgumentNullException.ThrowIfNull(request);
31+
#endif
32+
33+
httpRequestMessages.Enqueue(await HttpRequestMessageCloner.ClonaAsync(request, cancellationToken).ConfigureAwait(false));
2334

2435
HttpResponseMessage responseMessage = new();
2536
HttpResponseContext context = new(request, httpRequestMessages, responseMessage, Options);
@@ -55,6 +66,15 @@ public void RespondWith(IResponse response)
5566
/// <remarks>The configuration itself (Options and the configured IResponse) will not be cleared or reset.</remarks>
5667
public void ClearRequests()
5768
{
69+
DisposeRequestMessages();
5870
httpRequestMessages.Clear();
5971
}
72+
73+
private void DisposeRequestMessages()
74+
{
75+
foreach (HttpRequestMessage request in httpRequestMessages)
76+
{
77+
request.Dispose();
78+
}
79+
}
6080
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace TestableHttpClient.Utils;
4+
5+
internal static class HttpRequestMessageCloner
6+
{
7+
internal static async Task<HttpRequestMessage> ClonaAsync(HttpRequestMessage request, CancellationToken cancellationToken)
8+
{
9+
HttpRequestMessage clone = new()
10+
{
11+
Method = request.Method,
12+
RequestUri = request.RequestUri,
13+
Version = request.Version,
14+
};
15+
16+
foreach (var item in request.Headers)
17+
{
18+
clone.Headers.TryAddWithoutValidation(item.Key, item.Value);
19+
}
20+
21+
// Copy content (buffered)
22+
if (request.Content is not null)
23+
{
24+
var bytes = await request.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
25+
var contentClone = new ByteArrayContent(bytes);
26+
27+
// copy content headers
28+
foreach (var header in request.Content.Headers)
29+
{
30+
contentClone.Headers.TryAddWithoutValidation(header.Key, header.Value);
31+
}
32+
33+
clone.Content = contentClone;
34+
}
35+
36+
return clone;
37+
}
38+
}

test/TestableHttpClient.IntegrationTests/AssertingRequests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,30 @@ public async Task AssertingContent()
162162
using StringContent content = new("my special content");
163163
_ = await client.PostAsync("https://httpbin.org/post", content, TestContext.Current.CancellationToken);
164164

165+
#if NETFRAMEWORK
166+
// On .NET Framework the HttpClient disposes the content automatically. So we can't perform the same test.
167+
testHandler.ShouldHaveMadeRequests();
168+
#else
169+
testHandler.ShouldHaveMadeRequests().WithContent("my special content");
170+
testHandler.ShouldHaveMadeRequests().WithContent("my*content");
171+
testHandler.ShouldHaveMadeRequests().WithContent("*");
172+
173+
Assert.Throws<HttpRequestMessageAssertionException>(() => testHandler.ShouldHaveMadeRequests().WithContent(""));
174+
Assert.Throws<HttpRequestMessageAssertionException>(() => testHandler.ShouldHaveMadeRequests().WithContent("my"));
175+
#endif
176+
}
177+
178+
[Fact]
179+
public async Task AssertingContent_WhenOriginalContentIsDisposed()
180+
{
181+
using TestableHttpMessageHandler testHandler = new();
182+
using HttpClient client = new(testHandler);
183+
184+
using (StringContent content = new("my special content"))
185+
{
186+
_ = await client.PostAsync("https://httpbin.org/post", content, TestContext.Current.CancellationToken);
187+
}
188+
165189
#if NETFRAMEWORK
166190
// On .NET Framework the HttpClient disposes the content automatically. So we can't perform the same test.
167191
testHandler.ShouldHaveMadeRequests();

test/TestableHttpClient.Tests/TestableHttpMessageHandlerTests.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public async Task SendAsync_WhenRequestsAreMade_LogsRequests()
1616

1717
_ = await client.SendAsync(request, TestContext.Current.CancellationToken);
1818

19-
Assert.Contains(request, sut.Requests);
19+
Assert.Contains(request, sut.Requests, new SimpleHttpRequestMessageComparer());
2020
}
2121

2222
[Fact]
@@ -34,7 +34,7 @@ public async Task SendAsync_WhenMultipleRequestsAreMade_AllRequestsAreLogged()
3434
_ = await client.SendAsync(request3, TestContext.Current.CancellationToken);
3535
_ = await client.SendAsync(request4, TestContext.Current.CancellationToken);
3636

37-
Assert.Equal([request1, request2, request3, request4], sut.Requests);
37+
Assert.Equal([request1, request2, request3, request4], sut.Requests, new SimpleHttpRequestMessageComparer());
3838
}
3939

4040
[Fact]
@@ -210,4 +210,24 @@ public void Dispose()
210210
GC.SuppressFinalize(this);
211211
}
212212
}
213+
214+
private sealed class SimpleHttpRequestMessageComparer : IEqualityComparer<HttpRequestMessage>
215+
{
216+
public bool Equals(HttpRequestMessage? x, HttpRequestMessage? y)
217+
{
218+
if (x is null && y is null)
219+
{
220+
return true;
221+
}
222+
223+
if (x is null || y is null)
224+
{
225+
return false;
226+
}
227+
228+
return x.Method == y.Method && x.RequestUri == y.RequestUri && x.Version == y.Version;
229+
}
230+
231+
public int GetHashCode([DisallowNull] HttpRequestMessage obj) => HashCode.Combine(obj.Method, obj.RequestUri, obj.Version);
232+
}
213233
}

0 commit comments

Comments
 (0)