TIL: How to test an ASP.NET Authorization Filter

This post is part of my Advent of Code 2022.

These days I needed to work with a microservice for one of my clients. In that microservice, instead of validating incoming requests with the built-in model validations or FluentValidation, they use authorization filters. I needed to write some tests for that filter. This is what I learned.

Apart from validating the integrity of the incoming requests, the filter also validated that the referenced object in the request body matched the same “client.”

A weird filter scenario

The filter looked something like this,

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Newtonsoft.Json;
using MyWeirdFilterScenario.Controllers;

namespace MyWeirdFilterScenario.Filters;

public class MyAuthorizationFilter : IAsyncAuthorizationFilter
{
    private readonly Dictionary<string, Func<AuthorizationFilterContext, Task<bool>>> _validationsPerEndpoint;

    private readonly IClientRepository _clientRepository;
    private readonly IOtherEntityRepository _otherEntityRepository;

    public MyAuthorizationFilter(IClientRepository clientRepository,
                                 IOtherEntityRepository otherEntityRepository)
    {
        _clientRepository = clientRepository;
        _otherEntityRepository = otherEntityRepository;

        // Register validations per action name here
        // vvvvv
        _validationsPerEndpoint = new Dictionary<string, Func<AuthorizationFilterContext, Task<bool>>>(StringComparer.OrdinalIgnoreCase)
        {
            { nameof(SomethingController.Post),  ValidatePostAsync },
            // Register validations for other methods here...
        };
    }

    public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        var actionName = ((ControllerActionDescriptor)context.ActionDescriptor).ActionName;

        try
        {
            var validation = _validationsPerEndpoint[actionName];
            var isValid = await validation(context);
            //                  ^^^^^^^^^^
            // Grab and run the validation for the called endpoint
            if (!isValid)
            {
                context.Result = new BadRequestResult();
                return;
            }
        }
        catch (Exception)
        {
            // Log bad things here...
            context.Result = new BadRequestResult();
        }
    }

    private async Task<bool> ValidatePostAsync(AuthorizationFilterContext context)
    {
        var request = await GetRequestBodyAsync<AnyPostRequest>(context);
        //                  ^^^^^^^^^^^^^^^^^^^
        // Grab the request body
        if (request == null || request.ClientId == default)
        {
            return false;
        }

        var client = await _clientRepository.GetByIdAsync(request.ClientId);
        //  ^^^^^^
        // Check our client exists...
        if (client == null)
        {
            return false;
        }

        var otherEntity = await _otherEntityRepository.GetByIdAsync(request.OtherEntityId);
        if (otherEntity == null || otherEntity.ClientId != client.Id)
        //  ^^^^^^^^^^^
        // Check we're updating our own entity...
        {
            return false;
        }

        // Doing something else here...

        return true;
    }

    // A helper method to grab the request body from the AuthorizationFilterContext
    private static async Task<T?> GetRequestBodyAsync<T>(AuthorizationFilterContext context)
    {
        var request = context.HttpContext.Request;
        request.EnableBuffering();
        request.Body.Position = 0;

        var body = new StreamReader(request.Body);
        var requestBodyJson = await body.ReadToEndAsync();

        request.Body.Position = 0;

        if (string.IsNullOrEmpty(requestBodyJson))
        {
            return default;
        }

        var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore };
        var requestBody = JsonConvert.DeserializeObject<T>(requestBodyJson, settings);
        return requestBody;
    }
}

On the OnAuthorizationAsync() method, this filter grabbed the validation method based on the called method name. And, inside the validation method, it checked that the request had a valid “clientId” and the referenced entity belonged to the same client. This is to prevent any client from updating somebody else’s entities.

Also, notice we needed to use the EnableBuffering() and reset the body’s position before and after reading the body from the AuthorizationFilterContext.

On the controller side, we registered the filter with an attribute like this,

using Microsoft.AspNetCore.Mvc;
using RecreatingFilterScenario.Filters;

namespace MyAuthorizationFilter.Controllers;

[ApiController]
[Route("[controller]")]
[ServiceFilter(typeof(MyAuthorizationFilter))]
//                    ^^^^^^^^^^^
public class SomethingController : ControllerBase
{
    [HttpPost]
    public void Post(AnyPostRequest request)
    {
        // Beep, beep, boop...
        // Doing something with request
    }

    // Other methods here...
}

And, to make it work, we also need to register our filter in the dependencies container.

Morning Brew
Photo by Kris Gerhard on Unsplash

How to test an ASP.NET async authorization filter

To test an ASP.NET async filter, create a new instance of the filter passing the needed dependencies as stubs. Then, when calling the OnAuthorizationAsync() method, create a AuthorizationFilterContext instance attaching the request body inside a DefaultHttpContext.

Like this,

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Moq;
using Newtonsoft.Json;
using RecreatingFilterScenario.Controllers;
using RecreatingFilterScenario.Filters;
using System.Text;

namespace MyWeirdFilterScenario.Tests;

[TestClass]
public class MyAuthorizationFilterTests
{
    [TestMethod]
    public async Task OnAuthorizationAsync_OtherEntityWithoutTheSameClient_ReturnsBadRequest()
    {
        var sameClientId = 1;
        var otherClientId = 2;
        var otherEntityId = 123456;

        var fakeClientRepository = new Mock<IClientRepository>();
        fakeClientRepository
            .Setup(t => t.GetByIdAsync(sameClientId))
            .ReturnsAsync(new Client(sameClientId));

        var fakeOtherEntityRepository = new Mock<IOtherEntityRepository>();
        fakeOtherEntityRepository
            .Setup(t => t.GetByIdAsync(otherEntityId))
            .ReturnsAsync(new OtherEntity(otherClientId));

        var filter = new MyAuthorizationFilter(fakeClientRepository.Object, fakeOtherEntityRepository.Object);
        //  ^^^^^^
        // Create an instance of our filter with two fake dependencies

        var request = new AnyPostRequest(sameClientId, otherEntityId);
        var context = BuildContext(request);
        //            ^^^^^^^^^^^^
        // Create an AuthorizationFilterContext
        await filter.OnAuthorizationAsync(context);

        Assert.IsNotNull(context.Result);
        Assert.AreEqual(typeof(BadRequestResult), context.Result.GetType());
    }

    private AuthorizationFilterContext BuildContext(AnyPostRequest? request)
    {
        var httpContext = new DefaultHttpContext();

        var json = JsonConvert.SerializeObject(request);
        var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
        httpContext.Request.Body = stream;
        httpContext.Request.ContentLength = stream.Length;
        httpContext.Request.ContentType = "application/json";
        // ^^^^^^^^
        // Attach a JSON body

        var actionDescriptor = new ControllerActionDescriptor
        {
            ActionName = nameof(SomethingController.Post)
            // ^^^^^^^
            // Use the endpoint name
        };
        var actionContext = new ActionContext(httpContext, new RouteData(), actionDescriptor);
        return new AuthorizationFilterContext(actionContext, new List<IFilterMetadata>());
    }
}

Let’s unwrap it. First, we created an instance of MyAuthorizationFilter passing the dependencies as fakes using Moq. As stubs, to be precise.

To call the OnAuthorizationAsync() method, we needed to create an AuthorizationFilterContext. This context required an ActionContext. We used a Builder method, BuildContext(), to keep things clean.

Then, to create an ActionContext, we needed to attach the request body as JSON to a DefaultHttpContext and set the action descriptor with our method name. Since we didn’t read any route information, we passed a default RouteData instance.

Notice that we needed to use a MemoryStream to pass our request object as JSON and set the content length and type. Source.

With the BuildContext() method in place, we got the Arrange and Act parts of our sample test. The next step was to assert on the context result.

Voilà! That’s what I learned about unit testing ASP.NET authorization filters. Again, a Builder method helped to keep things simple and easier to reuse.

If you want to read more about unit testing, check How to write tests for HttpClient using Moq and my Unit Testing 101 series where we cover from what a unit test is, to fakes and mocks, to best practices.

Want to write readable and maintainable unit tests in C#? Join my course Mastering C# Unit Testing with Real-world Examples on Udemy and learn unit testing best practices while refactoring real unit tests from my past projects. No more tests for a Calculator class.

Happy testing!

TIL: Always check for missing configuration values inside constructors

This post is part of my Advent of Code 2022.

This is a lesson I learned after trying to use a shared NuGet package in one of my client’s projects and getting an ArgumentNullException. I had no clue that I needed some configuration values in my appsettings.json file. This is what I learned.

Always check for missing configuration values inside constructors. In case they’re not set, throw a human-friendly exception message showing the name of the expected configuration value. For example: ‘Missing Section:Subsection:Value in config file’.

A missing configuration value

This is what happened. I needed to import a feature from a shared Nuget package. It had a method to register its dependencies. Something like services.AddFeature().

When calling an API endpoint that used that feature, I got an ArgumentNullException: “Value cannot be null. (Parameter ‘uriString’).” It seemed that I was missing a URI. But what URI?

Without any XML docstrings on the AddFeature() method, I had no other solution than to decompile that DLL. I found a service like this one,

public class SomeService : ISomeService
{
    private readonly Uri _anyUri;

    public SomeService(IOptions<AnyConfigOptions> options, OtherParam otherParam)
    {
        _anyUri = new Uri(options.Value.AnyConfigValue);
        //                ^^^^^^^
        // System.ArgumentNullException: Value cannot be null. (Parameter 'uriString')
    }

    public async Task DoSomethingAsync()
    {
        // Beep, beep, boop...
        // Doing something here...
    }
}

There it was! The service used the IOptions pattern to read configuration values. And I needed an URL inside a section in the appsettings.json file. How was I supposed to know?

Black and brown jigsaw puzzle
Missing one value... Photo by Sigmund on Unsplash

A better exception message

Then I realized that a validation inside the constructor with a human-friendly message would have saved me (and any other future developer using that NuGet package) some time. And it would have pointed me in the right direction. I mean having something like,

public class SomeService : ISomeService
{
    private readonly Uri _anyUri;

    public SomeService(IOptions<AnyConfigOptions> options, OtherParam otherParam)
    {
        //  vvvvvvv
        if (string.IsNullOrEmpty(options?.Value?.AnyConfigValue))
        {
            throw new ArgumentNullException("Missing 'AnyConfigOptions:AnyConfigValue' in config file.");
            //                              ^^^^^^^^
            // I think this would be a better message
        }

        _anyUri = new Uri(options.Value.AnyConfigValue);
    }

    public async Task DoSomethingAsync()
    {
        // Beep, beep, boop...
        // Doing something here again...
    }
}

Even better, what if the AddFeature() method had an overload that receives the expected configuration value? Something like AddFeature(AnyConfigOptions options). This way, the client of that package could decide the source of those options. Either read them from a configuration file or hardcode them.

The book “Growing Object-Oriented Software Guided by Tests” suggests having a StupidProgrammerMistakeException or a specific exception for this type of scenario: missing configuration values. This would be a good use case for that exception type.

Voilà! That’s what I learned today: always validate configuration values inside constructors and use explicit error messages when implementing the Options pattern. It reminded me of “The given key was not present in the dictionary” and other obscure error messages. Do you write friendly and clear error messages?

To read more content about ASP.NET Core, check how to add caching with Redis and how to read configuration values.

Happy coding!

How to write tests for HttpClient using Moq

This post is part of my Advent of Code.

These days I needed to unit test a service that used the built-in HttpClient. It wasn’t as easy as creating a fake for HttpClient. This is how to write tests for HttpClient with Moq and a set of extension methods to make it easier.

To write tests for a service that requires a HttpClient, create a fake for HttpMessageHandler and set up the protected SendAsync() method to return a HttpResponseMessage. Then, create a new HttpClient passing the fake instance of HttpMessageHandler created before.

How to Create a Testable HttpClient

For example, let’s write a test for a AnyService class that receives a HttpClient, using MSTest and Moq,

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Moq.Protected;
using Newtonsoft.Json;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace MyProject.Services.Tests;

[TestClass]
public class AnyServiceTests
{
    [TestMethod]
    public async Task DoSomethingAsync_ByDefault_ReturnsSomethingElse()
    {
        var fakeHttpMessageHandler = new Mock<HttpMessageHandler>();
        fakeHttpMessageHandler
                .Protected()
                // ^^^^^^^
                .Setup<Task<HttpResponseMessage>>(
                    "SendAsync",
                    ItExpr.IsAny<HttpRequestMessage>(),
                    ItExpr.IsAny<CancellationToken>()
                )
                .ReturnsAsync(new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.OK,
                    Content = new StringContent(JsonConvert.SerializeObject(new AnyResponseViewModel()))
                    // We add the expected response here:                   ^^^^^
                });
        using var httpClient = new HttpClient(fakeHttpMessageHandler.Object);
        //                                    ^^^^^
        var service = new AnyService(client);

        var someResult = await service.DoSomethingAsync();

        // Assert something here...
        Assert.IsNotNull(someResult);
    }
}

Notice how we used the Protected() and Setup() methods from Moq to create a fake for HtttpMessageHandler. Then, inside the ReturnsAsync() method, we created a response message with a response object. And, finally, we used the fake handler to create a new HttpClient to pass it to our AnyService instance.

That’s how we created a fake HttpClient. But, as soon as we start to write more tests, all of them get bloated with lots of duplicated code. Especially, if we create new tests by copy-pasting an existing one.

We should reduce the noise in our tests using factory methods or builders to make our tests more readable. Let’s do that!

Some extensions methods to set up the faked HttpClient

It would be great if we could reduce the Arrange phase of our sample test to one or two lines. Something like this,

[TestMethod]
public async Task DoSomethingAsync_ByDefault_ReturnsSomethingElse()
{
    using var client = new Mock<HttpMessageHandler>()
                  .WithSuccessfulResponse(new AnyResponseViewModel())
                  //                      ^^^^^
                  // Alternatively,
                  // .WithUnauthorizedResponse()
                  // or
                  // .WithException<HttpRequestException>()
                  .ToHttpClient();
    var service = new AnyService(client);

    var someResult = await service.DoSomethingAsync();

    // Assert something here...
    Assert.IsNotNull(someResult);
}

It’s not that difficult to write some extension methods on top of the Mock<HttpMessageHandler> to simplify the creation of testable HttpClient instances.

In fact, here they are,

using Moq;
using Moq.Language.Flow;
using Moq.Protected;
using Newtonsoft.Json;
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace HttpMessageHandlerTests.Extensions;

public static class MockHttpMessageHandlerExtensions
{
    public static Mock<HttpMessageHandler> WithSuccessfulResponse<T>(
        this Mock<HttpMessageHandler> fakeHttpMessageHandler,
        T responseContent)
    {
        fakeHttpMessageHandler
            .GetProtectedSetup()
            .ReturnsAsync(new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.OK,
                Content = new StringContent(JsonConvert.SerializeObject(responseContent))
            });

        return fakeHttpMessageHandler;
    }

    public static Mock<HttpMessageHandler> WithUnauthorizedResponse(
        this Mock<HttpMessageHandler> fakeHttpMessageHandler)
    {
        fakeHttpMessageHandler
            .GetProtectedSetup()
            .ReturnsAsync(new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.Unauthorized,
                RequestMessage = new HttpRequestMessage()
            });

        return fakeHttpMessageHandler;
    }

    public static Mock<HttpMessageHandler> WithDelegate(
        this Mock<HttpMessageHandler> fakeHttpMessageHandler,
        Func<HttpRequestMessage, CancellationToken, HttpResponseMessage> func)
    {
        fakeHttpMessageHandler
            .GetProtectedSetup()
            .ReturnsAsync(func);

        return fakeHttpMessageHandler;
    }

    public static Mock<HttpMessageHandler> WithException<TException>(
        this Mock<HttpMessageHandler> fakeHttpMessageHandler)
        where TException : Exception, new()
    {
        fakeHttpMessageHandler
            .GetProtectedSetup()
            .Throws<TException>();

        return fakeHttpMessageHandler;
    }

    public static HttpClient ToHttpClient(this Mock<HttpMessageHandler> fakeHttpMessageHandler)
    {
        return new HttpClient(fakeHttpMessageHandler.Object);
    }

    private static ISetup<HttpMessageHandler, Task<HttpResponseMessage>> GetProtectedSetup(
        this Mock<HttpMessageHandler> fakeHttpMessageHandler)
    {
        return fakeHttpMessageHandler
            .Protected()
            .Setup<Task<HttpResponseMessage>>(
                "SendAsync",
                ItExpr.IsAny<HttpRequestMessage>(),
                ItExpr.IsAny<CancellationToken>());
    }
}

We can add other methods like WithNotFoundResponse(), WithInternalServerResponse() or WithTooManyRequestsResponse() to cover other response codes. Even, we can setup the fake HttpMessageHandler passing an Uri with a method ForUri(), for example.

Voilà! That’s how to write tests with HttpClient and Moq. With some extension methods, we could have a small DSL to write more readable tests. For a more fully-featured alternative to write tests for HttpClient, check mockhttp, “a testing layer for Microsoft’s HttpClient library.”

If you want to read more about unit testing, check my Unit Testing 101 series where we cover from what a unit test is, to fakes and mocks, to best practices.

Happy testing!

Advent of Code 2022

This year, inspired by C# Advent and 24 Pull Requests, I decided to do my own Christmas challenge: my own Advent of Code. I prefer to call it: Advent of Posts. Starting on December 1st, I’m publishing 24 posts, one post per day.

The challenge is to write an article per day in about 2 hours, including proof-reading and banner design. I’ve written some of the post in advance to avoid content pressure.

Here is my Advent of Posts:

Happy coding!

Monday Links: 40-year Programmer, Work and Burnout

Another five interesting links I found in past weeks. This time, I found a recurring theme while reading Hacker News: burnout, caring and statisfaction.

The Forty-Year Programmer

This post contains all the insights of 40-years a lifetime working as a programmer. These are some of my favorites:

  • “Advice is expertise with all the most important bits removed.”
  • “The work is good. If it stops being good, you’ll stop too. If it stops being good, that’s an emergency: you need to take a vacation…“
  • “Don’t confuse work with your career. They’re not the same thing. They’re only barely related.”

Read full article

Developers Don’t Fight The Last War

This post reminded me about an ex-coworker that says that using the same skills, architecture, and tools is like being an architect who always design the same house over and over. What worked yesterady won’t work tomorrow, I guess.

“…Technology is changing all the time, but developers like to create software with the knowledge and skills they already have.”

Read full article

How to communicate effectively as a developer

Apart from social interaction, this is another challenge when working remotely. In the old days, we just tapped somebody else’s shoulders. These days of remote and asynchronous working, communication is more challenging. And we’re not teaching that when onboarding new hires. I still get plain “Hello” messages. I just ignore them.

This article shows a solution: “high resolution writing.” But, there’s another challenge with that. Often, we work with non-native speakers of the language used at work (pretty much, English everywhere). And, writing is a separate skill to master. Even for native speakers writing in his native language.

Read full article

Caring about jobs and enjoyment after burnout

I’ve seen an increasing amount of Hacker News posts about burnout and job-related problems. These are some of them:

Tips to relearn how to care about my job?: “I got a high paying job at a recognizable tech company, but plagued with exhaustion and lack of motivation, which is killing me daily life. Anything you guys have done to dig out of a rut?”

Has anyone managed to find enjoyment in their work after burnout?: “Has anyone come back from being burnt out to love what they do again? If so, how did you manage to do it?”

How to deal with burnout and its consequences?: Speaking about burnout: “I really don’t know how to get over this and how to move past it. I feel quite literally incapable of working…I’m trying to figure out what my future even looks like and how to move past this and any advice would be really appreciated.”

A lot of good advice in there.

What “Work” Looks Like

I think we’ve all been to those meetings where nobody cares or even listens to what the organizer says. Especially, those where the organizer reads a document, he could have shared in the first place. This article shows an alternative to collaborative brainstorming. Spoiler alert: it’s away from computers. Read full article

Voilà! Another Monday Links. Have you experience burnout? How did you overcome it? What’s different about work after burnout? What are you doing to prevent burnout?

Want to receive an email with curated links like these? Get 4 more delivered straight to your inbox every Friday. Don’t miss out on next week’s links. Subscribe to my Friday Links here.

Happy coding!