DEV Community

Cover image for How delegating handlers bit me in the ass and how I tested them
Grant Hair
Grant Hair

Posted on

How delegating handlers bit me in the ass and how I tested them

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;
            }
        }



Enter fullscreen mode Exit fullscreen mode

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.

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

Moq-ed Spaghetti

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);
        }
    }


Enter fullscreen mode Exit fullscreen mode

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);
        }
    }


Enter fullscreen mode Exit fullscreen mode

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.

My test passed

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

https://github.com/moq/moq

https://fluentassertions.com

๐Ÿ‘‹

Top comments (0)