Memoires of a http request - How I got unit tested

ASP .NET Core xunit indentitymodel MOQ

8 March. 2021

Recently, I was working on a project where I needed to retrieve the access token from HttpContext and use this token to perform an HTTP request to get additional user info from an OpenID Connect (OIDC) UserInfo endpoint.
All this logic was encapsulated in a single service class.
Unit testing this class was giving me all sorts of issues and headaches, so I decided to document my findings and share them with you.

Used frameworks & libraries

The initial setup

Initially, the UserInfoService class was implemented as follows:

internal class UserInfoService : IUserInfoService
{
 private readonly IHttpContextWrapper _httpContextWrapper;
 private readonly string _authorityUri;

 public UserInfoService(IHttpContextWrapper httpContextWrapper, string authorityUri)
 {
 _httpContextWrapper = httpContextWrapper;
 _authorityUri = authorityUri;
 }

 public async Task<UserInfoResponse> GetUserInfoAsync()
 {
 try
 {
 var token = await _httpContextWrapper.GetTokenAsync("access_token");

 using var client = new HttpClient();

 var userInfoRequest = new UserInfoRequest
 {
 Address = $"{_authorityUri}/connect/userinfo",
 Token = token
 };

 return await client.GetUserInfoAsync(userInfoRequest);
 }
 catch (Exception ex)
 {
 throw new UserInfoServiceException("Could not get user info from context", ex);
 }
 }
}

https://gitlab.com/-/snippets/2087658

Pretty straightforward, right?
First we get the access token from HttpContext using the GetTokenAsync extension method in the Microsoft.AspNetCore.Authentication namespace.
Once we have the token, we get the additional info from the OIDC UserInfo endpoint using an HttpClient.
The userinfo endpoint is actually hosted on an identity server project. I won’t go into specifics regarding this setup, since this is beyond the scope of this article. Just know that the GetUserInfoAsync method is an extension method provided by the IdentityModel package that retrieves user info claims from an OIDC userinfo endpoint.

We have a perfectly working class, so what’s wrong? This will become clear when we start writing some unit tests. Let’s get to it!
Our first test looks like this:

https://gitlab.com/-/snippets/2087663

Looking good… We’re simply mocking the HttpContext using the DefaultHttpContext class from ASP.NET Core and set up the GetTokenAsync method to return a fake token.

This should give us a perfectly valid UserInfoResponse.
Ok, let’s run the test.

Image1

Boom, that just blew up in our face!

Image2

What happened? Basically, Moq is telling us that it can’t mock sealed classes like DefaultHttpContext. Additionally, GetTokenAsync is an extension method which Moq also can’t mock. We’ll resolve this in the next section.

Wrap it up

No, we’re not wrapping up this post just yet. We are going to wrap the HttpContext in a wrapper class, enabling us to mock our method.

https://gitlab.com/-/snippets/1841695

Naturally, we’ll need to change the UserInfoService as well. Here’s the resulting GetUserInfoAsync method.

https://gitlab.com/-/snippets/2088343

Now we can update our test accordingly:

https://gitlab.com/-/snippets/2088344

We removed the DefaultHttpContext and are now using our IHttpContextWrapper instead. Running this gives us the following result:

Image3

The exception has been resolved, but the test still doesn’t pass.

Image6

This actually makes sense, doesn’t it? We’re basically doing an actual call to an endpoint on http://fake.noest.it. This host doesn’t exist, thus the IsError property on our result object is true.
When we inspect the object while debugging, this is exactly what we see:

Image8

At the moment, we’re not able to mock the call from HttpClient that’s being used in our UserInfoService. This is our next step.

[Fact]ory

In ASP.NET Core, there’s a useful interface at our disposal that can help us to extract the object initialization of an HttpClient out of the UserInfoService, the IHttpClientFactory.
Let’s go back to our UserInfoService and create the HttpClient using the interface.

https://gitlab.com/-/snippets/2088346

You’ll notice that the interface is injected in the constructor using Dependency Injection. In order for this to work, you’ll have to register the IHttpClientFactory in the IoC container.
If you’re using the ASP.NET Core built-in service container, you can use the AddHttpClient extension method on the IServiceCollection. More information about this extension method can be found here.

Now we have injected the interface, we can use it to create the HttpClient using the CreateClient method and we can now also easily setup a mock for the factory in our test.

https://gitlab.com/-/snippets/2088353

Running this test gives us the exact same result as before.

Image5

Why? Well, because we’re still doing the same thing as before. Setting up the mock for the IHttpClientFactory was only the first step to mocking a call through an HttpClient.
You may have already noticed, but we’re still returning a new HttpClient from our mock which will still do an actual request to the unknown host.
On to the next section… Bear with me, we’re almost there :)

Panda 2 ad24e2a662a33412d3cd9339a99f71d0

Mock the response not the call

Alright, the final step. We want to be able to create an HttpClient that doesn’t send requests across the wire. Why not just mock the HttpClient and setup the GetUserInfoAsync method like the following example?

https://gitlab.com/-/snippets/1841701

Because the GetUserInfoAsync is also an extension method and we’ll run into the same issue as before. So we need our HttpClient to be able to return a fake response. To accomplish this, we’ll create a MessageHandler that we can set up to return any response to our liking. You can do this by creating a class that inherits from the abstract class DelegatingHandler and overriding the SendAsync method.

https://gitlab.com/-/snippets/1841703

Special thanks to Anthony Giretti, his article has been a big help for the creation of this handler.
And again, let’s update our test.

https://gitlab.com/-/snippets/2088354

Notice that we’re setting the content of the response to a JSON string. The GetUserInfoAsync extension method expects a JSON claims response and will parse this into the UserInfoResponse class. With this in mind, you could even perform an additional assertion.

https://gitlab.com/-/snippets/2088355

The resulting claims are now validated as well.
Let’s give our test a spin!

Image7

Hooray! It passes!

Hooray 1 ad24e2a662a33412d3cd9339a99f71d0

Wrapping up, for real this time

Ok, we’ve covered several topics in order to be able to create a unit test for our call to an OIDC UserInfo endpoint.
First we’ve set up a wrapper for our HttpContext, so we could mock the GetTokenAsync extension method.
After that, we’ve created the HttpClient using the IHttpClientFactory interface. This enabled us to extract the HttpClient object initialization out of our service class. That way we can set up a testable client in our unit test.
As a final step, a DelegatingHandler was added so that we can simulate a response for an HTTP call.
That’s it!

Thank you for reading this article. Hopefully it can be of use to you.
Feel free to share your thoughts and comments. Feedback can be sent to thomas.vervaeke@noest.it

Be sure to check out the side notes and useful links below for extra reading material.

DSC 2005

-

Side notes

Some of you may have already noticed, throughout this post we’ve been doing multiple assertions in our test. I know that this isn’t considered a best practice, but to keep this post as simple and short as possible, I decided to work with a single test method.

Be sure to use the CreateClient method with the string name parameter on the IHttpClientFactory interface. There’s also a parameterless extension method in the System.Net.Http namespace from the Microsoft.Extensions.Http assembly.
But again, you guessed it, Moq will be unable to setup this method since it’s an extension method.

It is actually possible to mock the HttpContext without creating a wrapper around it, as follows.

https://gitlab.com/-/snippets/1841708

Thanks to my colleague Wouter Huysentruit for the code snippet.
However, after discussing this, we agreed that this isn’t the best approach. It requires a lot of extra setup and is more prone to revision when the inner workings of HttpContext are changed in future updates of ASP.NET Core.
Adding a wrapper is just easier and future proof.

Useful links
https://openid.net/connect/
http://docs.identityserver.io/en/latest/
http://anthonygiretti.com/2018/09/06/how-to-unit-test-a-class-that-consumes-an-httpclient-with-ihttpclientfactory-in-asp-net-core/
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0

Group Created with Sketch.
Group 2 Created with Sketch.
Group Created with Sketch.
Group Created with Sketch.
Group Created with Sketch.