Skip to content

Commit

Permalink
Add handler to generate a more dynamic response on a configured request
Browse files Browse the repository at this point in the history
  • Loading branch information
sandermvanvliet committed Apr 2, 2024
1 parent 7f9fbed commit 6413070
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 6 deletions.
17 changes: 17 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Codenizer.HttpClient.Testable Changelog

## 2.8.0.0

Add support to supply a lambda to create the response that's sent to the client. It allows you to generate a more dynamic response if the path you're configuring requires that.

For example:

```csharp
handler
.RespondTo(HttpMethod.Get, "/api/entity/blah")
.With(HttpStatusCode.OK)
.AndContent(
"application/json",
req => $@"{{""path"":""{req.RequestUri!.PathAndQuery}""}}");
```

When calling that endpoint the response will contain the path taken from the request.

## 2.7.0.0

Add the option to specify a lambda to configure the `HttpClient` as it's being created by the `TestableHttpClientFactory`.
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

<Project>
<PropertyGroup>
<Version>2.7.0.0</Version>
<Version>2.8.0.0</Version>
<Authors>Sander van Vliet</Authors>
<Company>Codenizer BV</Company>
<Copyright>2024 Sander van Vliet</Copyright>
Expand Down
19 changes: 18 additions & 1 deletion src/Codenizer.HttpClient.Testable/IResponseBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace Codenizer.HttpClient.Testable
Expand All @@ -15,12 +16,28 @@ public interface IResponseBuilder
/// </summary>
/// <param name="mimeType">The MIME type of the response</param>
/// <param name="data">The response to return</param>
/// <returns>The current <see cref="IRequestBuilder"/> instance</returns>
/// <returns>The current <see cref="IResponseBuilder"/> instance</returns>
/// <remarks>
/// Depending on the <paramref name="mimeType"/> the supplied data can be a string, byte[] or object. When a byte[] is given the content will be a <see cref="ByteArrayContent"/>,
/// for strings a <see cref="StringContent"/> is used. For object the MIME type needs to be set to application/json otherwise an <see cref="InvalidOperationException" /> will be thrown.</remarks>
IResponseBuilder AndContent(string mimeType, object data);

/// <summary>
/// Invoke a callback to generate the response
/// </summary>
/// <param name="mimeType">The MIME type of the response</param>
/// <param name="callback">A lambda to generate the response to send to the called</param>
/// <returns>The current <see cref="IResponseBuilder"/> instance</returns>
IResponseBuilder AndContent(string mimeType, Func<HttpRequestMessage, object> callback);

/// <summary>
/// Invoke a callback to generate the response
/// </summary>
/// <param name="mimeType">The MIME type of the response</param>
/// <param name="callback">A lambda to generate the response to send to the called</param>
/// <returns>The current <see cref="IResponseBuilder"/> instance</returns>
IResponseBuilder AndContent(string mimeType, Func<HttpRequestMessage, Task<object>> callback);

/// <summary>
/// Add the given HTTP headers to the response
/// </summary>
Expand Down
24 changes: 23 additions & 1 deletion src/Codenizer.HttpClient.Testable/RequestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace Codenizer.HttpClient.Testable
Expand Down Expand Up @@ -73,6 +74,11 @@ internal RequestBuilder(HttpMethod method, string pathAndQuery, string? contentT
/// </summary>
public object? Data { get; private set; }
/// <summary>
/// Optional. The callback to invoke when generating the response to a request.
/// </summary>
public Func<HttpRequestMessage, object>? ResponseCallback { get; private set; }
public Func<HttpRequestMessage, Task<object>>? AsyncResponseCallback { get; private set; }
/// <summary>
/// Optional. The MIME type of the content to respond with. Only applicable if <see cref="Data"/> is also provided, otherwise ignored.
/// </summary>
public string? MediaType { get; private set; }
Expand Down Expand Up @@ -249,7 +255,23 @@ public IResponseBuilder AndContent(string mimeType, object data)

return this;
}


public IResponseBuilder AndContent(string mimeType, Func<HttpRequestMessage, object> callback)
{
MediaType = mimeType;
ResponseCallback = callback;

return this;
}

public IResponseBuilder AndContent(string mimeType, Func<HttpRequestMessage, Task<object>> callback)
{
MediaType = mimeType;
AsyncResponseCallback = callback;

return this;
}

/// <inheritdoc />
public IResponseBuilder AndHeaders(Dictionary<string, string> headers)
{
Expand Down
18 changes: 15 additions & 3 deletions src/Codenizer.HttpClient.Testable/TestableMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,25 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
StatusCode = responseBuilder.StatusCode
};

if (responseBuilder.Data != null)
var responseBuilderData = responseBuilder.Data;

if (responseBuilderData == null && responseBuilder.ResponseCallback != null)
{
responseBuilderData = responseBuilder.ResponseCallback(request);
}

if (responseBuilderData == null && responseBuilder.AsyncResponseCallback != null)
{
responseBuilderData = await responseBuilder.AsyncResponseCallback(request);
}

if (responseBuilderData != null)
{
if (responseBuilder.Data is byte[] buffer)
if (responseBuilderData is byte[] buffer)
{
response.Content = new ByteArrayContent(buffer);
}
else if (responseBuilder.Data is string content)
else if (responseBuilderData is string content)
{
response.Content = new StringContent(content, Encoding.UTF8, responseBuilder.MediaType);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
Expand Down Expand Up @@ -537,5 +538,79 @@ public async void GivenHandlerIsConfiguredToReturnByteArray_ResponseContentIsByt
.Should()
.ContainInOrder(new byte[] {0x1, 0x2, 0x3 });
}

[Fact]
public async Task GivenHandlerOnRequest_HandlerIsInvoked()
{
var handler = new TestableMessageHandler();
var client = new System.Net.Http.HttpClient(handler);

handler
.RespondTo(HttpMethod.Get, "/api/entity/blah")
.With(HttpStatusCode.OK)
.AndContent(
"application/json",
req => $@"{{""path"":""{req.RequestUri!.PathAndQuery}""}}");

var response = await client.GetAsync("https://tempuri.org/api/entity/blah");

var serializedContent = await response.Content.ReadAsStringAsync();

serializedContent.Should().Be(@"{""path"":""/api/entity/blah""}");
}

[Fact]
public async Task GivenAsyncHandlerOnRequest_HandlerIsInvoked()
{
var handler = new TestableMessageHandler();
var client = new System.Net.Http.HttpClient(handler);

handler
.RespondTo(HttpMethod.Get, "/api/entity/blah")
.With(HttpStatusCode.OK)
.AndContent(
"application/json",
async req =>
{
await Task.Delay(10);

return $@"{{""path"":""{req.RequestUri!.PathAndQuery}""}}";
});

var response = await client.GetAsync("https://tempuri.org/api/entity/blah");

var serializedContent = await response.Content.ReadAsStringAsync();

serializedContent.Should().Be(@"{""path"":""/api/entity/blah""}");
}

[Fact]
public async Task GivenAsyncHandlerOnRequestThatReadsRequestContent_ContentCanStillBeInspectedInAssertion()
{
var handler = new TestableMessageHandler();
var client = new System.Net.Http.HttpClient(handler);

handler
.RespondTo(HttpMethod.Post, "/api/entity/blah")
.With(HttpStatusCode.OK)
.AndContent(
"text/plain",
async req =>
{
var postedContent = await req.Content!.ReadAsStringAsync();

return postedContent;
});

await client.PostAsync("https://tempuri.org/api/entity/blah", new StringContent("HELLO WORLD!"));

var content = await handler
.Requests
.Single()
.Content!
.ReadAsStringAsync();

content.Should().Be("HELLO WORLD!");
}
}
}

0 comments on commit 6413070

Please sign in to comment.