How to unit test HttpContext.SignInAsync()?

半世苍凉 提交于 2020-01-12 04:44:29

问题


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);
    }
}

回答1:


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 could just as easily mocked the HttpContext as well like the other dependencies.

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




回答2:


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



回答3:


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);


来源:https://stackoverflow.com/questions/47198341/how-to-unit-test-httpcontext-signinasync

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!