Using HttpContext.GetTokenAsync in C# Unit Tests
Asked Answered
P

3

6

I am trying to write unit tests for my controller class which retrieves a token with the following command:

string token = await HttpContext.GetTokenAsync("access_token");

Therefore I mocked the HttpContext with the following code:

public static HttpContext MakeFakeContext()
{
    var serviceProvider = new Mock<IServiceProvider>();
    var authservice = new Mock<IAuthenticationService>();

    authservice.Setup(_ => _.GetTokenAsync(It.IsAny<HttpContext>(), It.IsAny<string>())).Returns(Task.FromResult("token"));
    serviceProvider.Setup(_ => _.GetService(typeof(IAuthenticationService))).Returns(authservice);

    return new DefaultHttpContext
    {
        RequestServices = serviceProvider.Object
    };
}

I am setting the mocked context with:

var mockcontext = MakeFakeContext();

unitUnderTest.ControllerContext = new ControllerContext
{
    HttpContext = mockcontext
};

Now when I run the unit test, I am getting the following error:

System.NotSupportedException : Unsupported expression: _ => _.GetTokenAsync(It.IsAny(), It.IsAny()) Extension methods (here: AuthenticationTokenExtensions.GetTokenAsync) may not be used in setup / verification expressions.

During my research I stumbled across solutions where you can mock specific parts that are involved under the hood which are not part of extensions. These are some of them: Moq IServiceProvider / IServiceScope, How to unit test HttpContext.SignInAsync()?. The second one shows a similar problem, which seems to work after I tried. But for some reason it does not work for the GetTokenAsync method.

Do you guys have any hint out there?

Purree answered 9/3, 2020 at 13:53 Comment(6)
You are most likely still mocking one of the extensions check to make sure you are mocking the instance member.Smetana
Also try using a DefaultHttpContext like in the linked example. reduces the amounts of mocking to be done.Smetana
@Nkosi, I am a little bit confused as this link says that SignInAsync is no extension method. For me it looks like that it reason it works with SignInAsync.Purree
According to your exception message you are still trying to mock an extension Unsupported expression: _ => _.GetTokenAsync(It.IsAny(), It.IsAny())Smetana
If you follow the source code breadcrumbs it leads you to here. Mock the instance members being used by the extension and you should get the desired behaviorSmetana
As already stated in the provided answer you need to be mocking AuthenticateAsync, which is what is ultimately called by the extension.Smetana
E
8

Here's the source code for that extension method:

public static Task<string> GetTokenAsync(this HttpContext context, string scheme, 
string tokenName) =>
    context.RequestServices.GetRequiredService<IAuthenticationService> 
    ().GetTokenAsync(context, scheme, tokenName);

Which in turn calls this extension method:

public static async Task<string> GetTokenAsync(this IAuthenticationService auth, HttpContext context, string scheme, string tokenName)
{
    if (auth == null)
    {
        throw new ArgumentNullException(nameof(auth));
    }
    if (tokenName == null)
    {
        throw new ArgumentNullException(nameof(tokenName));
    }
    var result = await auth.AuthenticateAsync(context, scheme);
    return result?.Properties?.GetTokenValue(tokenName);
}

The end result is calling AuthenticateAsync as seen in the line var result = await auth.AuthenticateAsync(context, scheme);.

Since you can't modify extension methods, maybe you can write your own mocked ones?

I'm not exactly sure what the best practice is when doing a mock on an object that has extension methods so maybe someone can expand on this answer.

It is worth noting AuthenticateAsync is not an extension method and you can find the code here.

As mentioned by @Nkosi:

Mock IServiceProvider and IAuthenticationService.

IMHO, seeing the real code is always useful so you can identify and understand what it's doing under the covers and fully mock out all the pieces needed, so I'll leave the above up.

Epitomize answered 9/3, 2020 at 14:14 Comment(0)
J
14

Building on from Zer0's answer here is an example using Moq:

    private void MockHttpContextGetToken(
        Mock<IHttpContextAccessor> httpContextAccessorMock,
        string tokenName, string tokenValue, string scheme = null)
    {
        var authenticationServiceMock = new Mock<IAuthenticationService>();
        httpContextAccessorMock
            .Setup(x => x.HttpContext.RequestServices.GetService(typeof(IAuthenticationService)))
            .Returns(authenticationServiceMock.Object);

        var authResult = AuthenticateResult.Success(
            new AuthenticationTicket(new ClaimsPrincipal(), scheme));

        authResult.Properties.StoreTokens(new[]
        {
            new AuthenticationToken { Name = tokenName, Value = tokenValue }
        });

        authenticationServiceMock
            .Setup(x => x.AuthenticateAsync(httpContextAccessorMock.Object.HttpContext, scheme))
            .ReturnsAsync(authResult);
    }
J answered 6/5, 2020 at 9:3 Comment(3)
This setup '.Setup(x => x.HttpContext.RequestServices.GetService(typeof(IAuthenticationService)))' is not work on my sideSestet
You should write code to return .Setup(x => x.HttpContext.RequestServices.GetService(typeof(IAuthenticationService))).Returns(authenticationService.Object);Celebrant
Spent a whole day tying to write a test for a simple class that used this extension method and it was a huge pain in the bones. Your solution works perfectly in .Net 6.Glyceryl
E
8

Here's the source code for that extension method:

public static Task<string> GetTokenAsync(this HttpContext context, string scheme, 
string tokenName) =>
    context.RequestServices.GetRequiredService<IAuthenticationService> 
    ().GetTokenAsync(context, scheme, tokenName);

Which in turn calls this extension method:

public static async Task<string> GetTokenAsync(this IAuthenticationService auth, HttpContext context, string scheme, string tokenName)
{
    if (auth == null)
    {
        throw new ArgumentNullException(nameof(auth));
    }
    if (tokenName == null)
    {
        throw new ArgumentNullException(nameof(tokenName));
    }
    var result = await auth.AuthenticateAsync(context, scheme);
    return result?.Properties?.GetTokenValue(tokenName);
}

The end result is calling AuthenticateAsync as seen in the line var result = await auth.AuthenticateAsync(context, scheme);.

Since you can't modify extension methods, maybe you can write your own mocked ones?

I'm not exactly sure what the best practice is when doing a mock on an object that has extension methods so maybe someone can expand on this answer.

It is worth noting AuthenticateAsync is not an extension method and you can find the code here.

As mentioned by @Nkosi:

Mock IServiceProvider and IAuthenticationService.

IMHO, seeing the real code is always useful so you can identify and understand what it's doing under the covers and fully mock out all the pieces needed, so I'll leave the above up.

Epitomize answered 9/3, 2020 at 14:14 Comment(0)
M
7

This works for me

controller.ControllerContext = new ControllerContext();
var serviceProvider = new Mock<IServiceProvider>();
var authenticationServiceMock = new Mock<IAuthenticationService>();
var authResult = AuthenticateResult.Success(
    new AuthenticationTicket(new ClaimsPrincipal(), null));

authResult.Properties.StoreTokens(new[]
{
    new AuthenticationToken { Name = "access_token", Value = "accessTokenValue" }
});

authenticationServiceMock
    .Setup(x => x.AuthenticateAsync(It.IsAny<HttpContext>(), null))
    .ReturnsAsync(authResult);

serviceProvider.Setup(_ => _.GetService(typeof(IAuthenticationService))).Returns(authenticationServiceMock.Object);

controller.ControllerContext.HttpContext = new DefaultHttpContext
{
    User = user,
    RequestServices = serviceProvider.Object
};
Meara answered 13/10, 2020 at 12:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.