18

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);
    }
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
HeroWong
  • 499
  • 1
  • 5
  • 14

3 Answers3

40

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.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • 4
    Really nice answer. – Chris Pratt Nov 09 '17 at 14:11
  • 2
    I got an error using this code "System.InvalidOperationException : No service for type 'Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionaryFactory' has been registered." I resolved this by also mocking ITempDataDictionary and assigning it to controller.TempData – Jason Learmouth Jan 21 '18 at 22:10
  • @JasonLearmouth this answer targeted the OP's specific target type `IAuthenticationService` and would fail with any other type not setup with the mock. The error you get is the default error when a null is returned fomr the get service call. – Nkosi Jan 21 '18 at 22:16
  • @Nkosi, I agree, which is why I upvoted your answer and didn't edit it. I posted the comment about the error and the solution because it wasn't very easy to figure out what to mock and where to assign it based on the error message alone. – Jason Learmouth Jan 21 '18 at 22:34
  • I don't think you should be mocking out dependencies inside IdentityServer extension methods. If the code changes then your tests break. Much cleaner and safer to create a wrapper over HttpContext and test that the method on your wrapper gets executed instead. To test the fact you are actually signed-in could be done via an integration test – user3154431 May 29 '20 at 08:56
  • @nikosi Thanks for this excellent answer, how would one mock temp data. I am struggling to do that. I can always just write `var tempDataDictMock = new Mock();` and assign that to `Controller.TempData` but not sure if that is the right way to go. The `.GetRequiredService` returns a factory which in turn then returns an instance of type `ITempDataDictionary`. So, just as you have done for `SignInSync` do I follow the same pattern or can I just say `TempData = tempDataDictMock.Object` – Sangeet Agarwal Apr 20 '23 at 10:40
  • 1
    @SangeetAgarwal have a look at the answer I provided here https://stackoverflow.com/a/52182813/5233410 – Nkosi Apr 20 '23 at 14:37
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
marvelTracker
  • 4,691
  • 3
  • 37
  • 49
2

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);
Fredrik Holm
  • 166
  • 9
  • 2
    for me the accepted solution worked beautifully in Core 2.2 – Etienne Charland May 10 '19 at 13:47
  • I personally think that this is a much better approach. I don't think you should be mocking out dependencies inside IdentityServer extension methods. If the code changes then your tests break. Much cleaner and safer to create a wrapper over HttpContext – user3154431 May 29 '20 at 08:55