SignInAsync() Source Code
I ran into some problems with unit testing.
DefaultHttpContext.RequestServices
is null
AuthenticationService
object, but I do not know what parameters to passWhat 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);
}
}
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.
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
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With