Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to unit test HttpContext.SignInAsync()?

SignInAsync() Source Code

I ran into some problems with unit testing.

  1. DefaultHttpContext.RequestServices is null
  2. I tried to create the AuthenticationService object, but I do not know what parameters to pass

What should I do? How to unit test HttpContext.SignInAsync()?

Method under test

public async Task<IActionResult> Login(LoginViewModel vm, [FromQuery]string returnUrl)
{
    if (ModelState.IsValid)
    {
        var user = await context.Users.FirstOrDefaultAsync(u => u.UserName == vm.UserName && u.Password == vm.Password);
        if (user != null)
        {
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, user.UserName)
            };
            var identity = new ClaimsIdentity(claims, "HappyDog");

            // here
            await HttpContext.SignInAsync(new ClaimsPrincipal(identity));
            return Redirect(returnUrl ?? Url.Action("Index", "Goods"));
        }
    }
    return View(vm);
}

What I have tried so far.

[TestMethod]
public async Task LoginTest()
{
    using (var context = new HappyDogContext(_happyDogOptions))
    {
        await context.Users.AddAsync(new User { Id = 1, UserName = "test", Password = "password", FacePicture = "FacePicture" });
        await context.SaveChangesAsync();

        var controller = new UserController(svc, null)
        {
            ControllerContext = new ControllerContext
            {
                HttpContext = new DefaultHttpContext
                {
                    // How mock RequestServices?
                    // RequestServices = new AuthenticationService()?
                }
            }
        };
        var vm = new LoginViewModel { UserName = "test", Password = "password" };
        var result = await controller.Login(vm, null) as RedirectResult;
        Assert.AreEqual("/Goods", result.Url);
    }
}
like image 905
HeroWong Avatar asked Nov 09 '17 09:11

HeroWong


3 Answers

HttpContext.SignInAsync is an extension method that uses RequestServices, which is IServiceProvider. That is what you must mock.

context.RequestServices
    .GetRequiredService<IAuthenticationService>()
    .SignInAsync(context, scheme, principal, properties);

You can either create a fake/mock manually by creating classes that derive from the used interfaces or use a mocking framework like Moq

//...code removed for brevity

var authServiceMock = new Mock<IAuthenticationService>();
authServiceMock
    .Setup(_ => _.SignInAsync(It.IsAny<HttpContext>(), It.IsAny<string>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<AuthenticationProperties>()))
    .Returns(Task.FromResult((object)null));

var serviceProviderMock = new Mock<IServiceProvider>();
serviceProviderMock
    .Setup(_ => _.GetService(typeof(IAuthenticationService)))
    .Returns(authServiceMock.Object);

var controller = new UserController(svc, null) {
    ControllerContext = new ControllerContext {
        HttpContext = new DefaultHttpContext {
            // How mock RequestServices?
            RequestServices = serviceProviderMock.Object
        }
    }
};

//...code removed for brevity

You can read up on how to use Moq here at their Quick start

You could just as easily mocked the HttpContext as well like the other dependencies but if a default implementation exists that causes no undesired behavior, then using that can make things a lot simpler to arrange

For example, an actual IServiceProvider could have been used by building one via ServiceCollection

//...code removed for brevity

var authServiceMock = new Mock<IAuthenticationService>();
authServiceMock
    .Setup(_ => _.SignInAsync(It.IsAny<HttpContext>(), It.IsAny<string>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<AuthenticationProperties>()))
    .Returns(Task.FromResult((object)null));

var services = new ServiceCollection();
services.AddSingleton<IAuthenticationService>(authServiceMock.Object);

var controller = new UserController(svc, null) {
    ControllerContext = new ControllerContext {
        HttpContext = new DefaultHttpContext {
            // How mock RequestServices?
            RequestServices = services.BuildServiceProvider();
        }
    }
};

//...code removed for brevity

That way if there are other dependencies, they can be mocked and registered with the service collection so that they can be resolved as needed.

like image 134
Nkosi Avatar answered Sep 20 '22 08:09

Nkosi


In case you guys are looking for NSubstitue example (Asp.net core).

    IAuthenticationService authenticationService = Substitute.For<IAuthenticationService>();

        authenticationService
            .SignInAsync(Arg.Any<HttpContext>(), Arg.Any<string>(), Arg.Any<ClaimsPrincipal>(),
                Arg.Any<AuthenticationProperties>()).Returns(Task.FromResult((object) null));

        var serviceProvider = Substitute.For<IServiceProvider>();
        var authSchemaProvider = Substitute.For<IAuthenticationSchemeProvider>();
        var systemClock = Substitute.For<ISystemClock>();

        authSchemaProvider.GetDefaultAuthenticateSchemeAsync().Returns(Task.FromResult
        (new AuthenticationScheme("idp", "idp", 
            typeof(IAuthenticationHandler))));

        serviceProvider.GetService(typeof(IAuthenticationService)).Returns(authenticationService);
        serviceProvider.GetService(typeof(ISystemClock)).Returns(systemClock);
        serviceProvider.GetService(typeof(IAuthenticationSchemeProvider)).Returns(authSchemaProvider);

        context.RequestServices.Returns(serviceProvider);


        // Your act goes here

        // Your assert goes here
like image 38
marvelTracker Avatar answered Sep 18 '22 08:09

marvelTracker


This didn't work for me in .NET Core 2.2 - it still expects another interface: ISystemClock. So I simply decided to take another approach, namely to wrap the entire thing, like this:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;

namespace Utilities.HttpContext
{
    public interface IHttpContextWrapper
    {
        Task SignInAsync(Controller controller, string subject, string name, AuthenticationProperties props);
    }
}

...and then I have one implementation for normal use and on for test.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;

namespace Utilities.HttpContext
{
    public class DefaultHttpContextWrapper : IHttpContextWrapper
    {
        public async Task SignInAsync(Controller controller, string subject, string name, AuthenticationProperties props)
        {
            await controller.HttpContext.SignInAsync(subject, name, props);
        }
    }
}

...and the fake implementation:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;

namespace Utilities.HttpContext
{
    public class FakeHttpContextWrapper : IHttpContextWrapper
    {
        public Task SignInAsync(Controller controller, string subject, string name, AuthenticationProperties props)
        {
            return Task.CompletedTask;
        }
    }
}

Then I just inject the desired implementation as the interface in the controller's constructor using .NET Core's native DI container (in Startup.cs).

services.AddScoped<IHttpContextWrapper, DefaultHttpContextWrapper>();

Finally, the call looks like this (passing in my controller with this):

await _httpContextWrapper.SignInAsync(this, user.SubjectId, user.Username, props);
like image 38
Fredrik Holm Avatar answered Sep 21 '22 08:09

Fredrik Holm