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?

Noah Ispas
  • 170
  • 2
  • 12
  • You are most likely still mocking one of the extensions check to make sure you are mocking the instance member. – Nkosi Mar 09 '20 at 14:54
  • 2
    Also try using a DefaultHttpContext like in the linked example. reduces the amounts of mocking to be done. – Nkosi Mar 09 '20 at 14:54
  • @Nkosi, I am a little bit confused as [this link](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.iauthenticationservice?view=aspnetcore-3.1) says that SignInAsync is no extension method. For me it looks like that it reason it works with SignInAsync. – Noah Ispas Mar 09 '20 at 15:11
  • According to your exception message you are still trying to mock an extension `Unsupported expression: _ => _.GetTokenAsync(It.IsAny(), It.IsAny())` – Nkosi Mar 09 '20 at 15:12
  • 1
    If you follow the source code breadcrumbs it leads you to [here](https://github.com/dotnet/aspnetcore/blob/425c196cba530b161b120a57af8f1dd513b96f67/src/Http/Authentication.Abstractions/src/TokenExtensions.cs#L135). Mock the instance members being used by the extension and you should get the desired behavior – Nkosi Mar 09 '20 at 15:18
  • 1
    As already stated in the provided answer you need to be mocking `AuthenticateAsync`, which is what is ultimately called by the extension. – Nkosi Mar 09 '20 at 15:19

3 Answers3

12

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);
    }
Stacey
  • 281
  • 1
  • 11
  • This setup '.Setup(x => x.HttpContext.RequestServices.GetService(typeof(IAuthenticationService)))' is not work on my side – Tho Ho Oct 06 '20 at 04:47
  • You should write code to return .Setup(x => x.HttpContext.RequestServices.GetService(typeof(IAuthenticationService))).Returns(authenticationService.Object); – Trung Oct 12 '22 at 08:56
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.

Zer0
  • 7,191
  • 1
  • 20
  • 34
5

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
};
Dharman
  • 30,962
  • 25
  • 85
  • 135
Dawid Dragan
  • 61
  • 1
  • 1