Grant Hair
Posted on July 3, 2023
My use case was the following:
Add a delegating handler to add a bearer token to an api call that will either pull a JWT from a 3rd party api endpoint or pull it from memory cache. If we get an unauthorized response we want to bust the cache and repopulate the value with a call to an api
My main issue with testing this logic was in the following code
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
=> MakeRequest(request, false, cancellationToken);
private async Task<HttpResponseMessage> MakeRequest(HttpRequestMessage request, bool isRetry,
CancellationToken cancellationToken)
{
try
{
await SetAuthHeader(request, isRetry);
return await base.SendAsync(request, cancellationToken);
}
catch (ApiException exception) when (exception.StatusCode == HttpStatusCode.Unauthorized)
{
if (!isRetry)
{
// if due to 401 and it wasn't a new token then retry with a new token
return await MakeRequest(request, true, cancellationToken);
}
_logger.LogError(exception, "ApiException making request to {url}", request.RequestUri);
throw;
}
}
When return await base.SendAsync(request, cancellationToken);
was called the base class would then try and send the actual api request which would result in a 404 because my test was setup to send requests to "blah.blah" or something. In my use case it was a 3rd party api to send SMS messages to users not something I wanted to be doing for real life.
My test code looked something kinda like this (Moq-ed Spaghetti) after a couple days and a lot of head banging trying to get stuff mocked out and trying to use Moq Protected().Protected().Protected().Protected().Protected().Protected().Protected().Protected().Protected().Protected().Protected().Protected().Protected().Protected().Protected().Protected().Protected().Protected().Protected().Protected()
or something I can't remember it was a bit of a blur haha
Then I came across this stack overflow answer and stuff started to make sense.
https://stackoverflow.com/a/37219176/9057687
By setting the InnerHandler
of my custom delegating handler to just a stubbed out response like this I was able to avoid the actual sending of the request and just return an Accepted response.
public class TestHandler : DelegatingHandler
{
private readonly SendMessageResponse _sendMessageResponse = new()
{
MessageId = new Guid("AF97201F-F324-4CD1-A513-42811FA962B4")
};
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(HttpStatusCode.Accepted)
{
Content = JsonContent.Create(_sendMessageResponse)
};
return Task.FromResult(response);
}
}
Then my test code could be simplified massively to something like this
public class MyTestHandlerThatIWantToTestShould
{
private const string JwtValue =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCJ9.elSSvs4awH3XEo1yZGTfZWjgCXcfyy_TBRyCSCT3WbM";
private readonly Mock<ILogger<MyTestHandlerThatIWantToTest>> _logger = new();
private readonly IMemoryCache _memoryCache = new MemoryCache(new MemoryCacheOptions());
private readonly MyTestHandlerThatIWantToTest _handlerThatIWantToTest;
private readonly IOptions<SmsApiConfiguration> _config;
private readonly SendMessageResponse _sendMessageResponse = new()
{
MessageId = new Guid("AF97201F-F324-4CD1-A513-42811FA962B4")
};
public MyTestHandlerThatIWantToTestShould()
{
_config = Microsoft.Extensions.Options.Options.Create(new SmsApiConfiguration
{
ApiBaseAddress = "https://any-old-value.website.com/api-test-mocked",
ApiToken = Guid.NewGuid(),
JwtTokenCacheKey = "CacheKey"
});
var mockHttp = new MockHttpMessageHandler();
mockHttp.When("*")
.Respond("application/json", "{'jwt' : '" + JwtValue + "'}");
var client = new HttpClient(mockHttp);
_handlerThatIWantToTest = new MyTestHandlerThatIWantToTest(_logger.Object, _memoryCache, _config, client);
}
[Fact]
public async Task PopulateBearerTokenValue_When_ARequestToSendSmsIsSend()
{
var request = new HttpRequestMessage();
// This is the magic line I'd say
_handlerThatIWantToTest.InnerHandler = new TestHandler();
var invoker = new HttpMessageInvoker(_handlerThatIWantToTest);
// act
var response = await invoker.SendAsync(request, default);
response.StatusCode.Should().Be(HttpStatusCode.Accepted);
var json = await response.Content.ReadAsStringAsync();
var responseObject = JsonConvert.DeserializeObject<SendMessageResponse>(json);
responseObject.MessageId.Should().Be(_sendMessageResponse.MessageId);
request.Headers.Authorization?.Parameter.Should().Be(JwtValue);
_memoryCache.Get(_config.Value.JwtTokenCacheKey).Should().Be(JwtValue);
}
}
In my test I can assert that an auth header was added, the value was added to the memory cache and that an accepted response was returned.
Everyone probably already knows this stuff but it kinda stumped me for a bit I think mainly because we are using Refit for our API calls instead of using HttpClient everywhere so stuff was a wee bit different for me and took a bit to get my head around. We had a bit of chicken and egg situation for a while with the api that both sent the request and obtained the JWT as it was technically part of the same Refit interface but we needed to pass the interface to get the jwt before we could make the request so DI became a bit tricky hence the usage of HttpClient for getting the JWT and refit for sending the actual request.
I'm pretty sure there are a few ways to refactor this code or improve it but the tickets in ready to deploy now so that is a tale for another sprint haha.
Shout out to:
https://github.com/richardszalay/mockhttp
đź‘‹
Posted on July 3, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.