我正在尝试为来自QuickStart UI的AccountController创建XUnit测试,并且在弄清楚我需要模拟什么才能使Login函数成功时遇到很多麻烦。 Login_return_Redirect最终出现以下异常:
Source: AccountTests.cs line 101
Duration: 2 sec
Message:
System.NullReferenceException : Object reference not set to an instance of an object.
Stack Trace:
AuthenticationHandlerProvider.GetHandlerAsync(HttpContext context, String authenticationScheme)
FederatedSignoutAuthenticationHandlerProvider.GetHandlerAsync(HttpContext context, String authenticationScheme)
DefaultUserSession.AuthenticateAsync()
DefaultUserSession.GetUserAsync()
DefaultUserSession.CreateSessionIdAsync(ClaimsPrincipal principal, AuthenticationProperties properties)
IdentityServerAuthenticationService.SignInAsync(HttpContext context, String scheme, ClaimsPrincipal principal, AuthenticationProperties properties)
AuthenticationManagerExtensions.SignInAsync(HttpContext context, IdentityServerUser user, AuthenticationProperties properties)
AuthenticationManagerExtensions.SignInAsync(HttpContext context, String subject, String name, AuthenticationProperties properties, Claim[] claims)
AccountController.Login(LoginInputModel model, String button) line 130
AccountTests.Login_return_Redirect() line 104
--- End of stack trace from previous location where exception was thrown ---
我正在测试的功能:
/// <summary>
/// Handle postback from username/password login
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model, string button)
{
// check if we are in the context of an authorization request
var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
// the user clicked the "cancel" button
if (button != "login")
{
if (context != null)
{
// if the user cancels, send a result back into IdentityServer as if they
// denied the consent (even if this client does not require consent).
// this will send back an access denied OIDC error response to the client.
await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
if (await _clientStore.IsPkceClientAsync(context.ClientId))
{
// if the client is PKCE then we assume it's native, so this change in how to
// return the response is for better UX for the end user.
return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
}
return Redirect(model.ReturnUrl);
}
else
{
// since we don't have a valid context, then we just go back to the home page
return Redirect("~/");
}
}
if (ModelState.IsValid)
{
// validate username/password against in-memory store
if (_users.ValidateCredentials(model.Username, model.Password))
{
var user = _users.FindByUsername(model.Username);
await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username, clientId: context?.ClientId));
// only set explicit expiration here if user chooses "remember me".
// otherwise we rely upon expiration configured in cookie middleware.
AuthenticationProperties props = null;
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
};
// issue authentication cookie with subject ID and username
await HttpContext.SignInAsync(user.SubjectId, user.Username, props);
if (context != null)
{
if (await _clientStore.IsPkceClientAsync(context.ClientId))
{
// if the client is PKCE then we assume it's native, so this change in how to
// return the response is for better UX for the end user.
return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
}
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
return Redirect(model.ReturnUrl);
}
// request for a local page
if (Url.IsLocalUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
else if (string.IsNullOrEmpty(model.ReturnUrl))
{
return Redirect("~/");
}
else
{
// user might have clicked on a malicious link - should be logged
throw new Exception("invalid return URL");
}
}
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId:context?.ClientId));
ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
}
// something went wrong, show form with error
var vm = await BuildLoginViewModelAsync(model);
return View(vm);
}
我当前的测试设置:
public class AccountTests
{
private readonly TestUserStore _users;
private readonly IIdentityServerInteractionService _interaction;
private readonly IClientStore _clientStore;
private readonly IAuthenticationSchemeProvider _schemeProvider;
private readonly IEventService _events;
AccountController _controller;
TestServer _server;
/// <summary>
/// Constructor that sets up the MVC pipeline and creates the controller instance
/// </summary>
public AccountTests()
{
var webhost = new WebHostBuilder()
.UseUrls("http://*:8000")
.UseStartup<Startup>();
_server = new TestServer(webhost);
_users = (TestUserStore)_server.Services.GetService(typeof(TestUserStore));
_interaction = (IIdentityServerInteractionService)_server.Services.GetService(typeof(IIdentityServerInteractionService));
_clientStore = (IClientStore)_server.Services.GetService(typeof(IClientStore));
_schemeProvider = (IAuthenticationSchemeProvider)_server.Services.GetService(typeof(IAuthenticationSchemeProvider));
_events = (IEventService)_server.Services.GetService(typeof(IEventService));
_controller = new AccountController(_interaction, _clientStore, _schemeProvider, _events, _users);
// configuring the HTTP context and user principal,
// in order to be able to use the User.Identity.Name property in the controller action
var validPrincipal = new ClaimsPrincipal(
new[]
{
new ClaimsIdentity(
new[] {new Claim(ClaimTypes.Name, "testsuser@testinbox.com") })
});
var mockHttpContext = new Mock<HttpContext>(MockBehavior.Strict);
mockHttpContext.SetupGet(hc => hc.User).Returns(validPrincipal);
mockHttpContext.SetupGet(c => c.Items).Returns(new Dictionary<object, object>());
mockHttpContext.SetupGet(ctx => ctx.RequestServices)
.Returns(_server.Services);
var collection = Mock.Of<IFormCollection>();
var request = new Mock<HttpRequest>();
request.Setup(f => f.ReadFormAsync(CancellationToken.None)).Returns(Task.FromResult(collection));
var mockHeader = new Mock<IHeaderDictionary>();
mockHeader.Setup(h => h["X-Requested-With"]).Returns("XMLHttpRequest");
request.SetupGet(r => r.Headers).Returns(mockHeader.Object);
mockHttpContext.SetupGet(c => c.Request).Returns(request.Object);
var response = new Mock<HttpResponse>();
response.SetupProperty(it => it.StatusCode);
mockHttpContext.Setup(c => c.Response).Returns(response.Object);
_controller.ControllerContext = new ControllerContext()
{
HttpContext = mockHttpContext.Object
};
}
/// <summary>
///
/// </summary>
/// <returns>Task</returns>
[Fact]
public async Task Login_returns_ViewResult()
{
var request = await _controller.Login("");
Assert.IsAssignableFrom<ViewResult>(request);
}
[Fact]
public async Task Login_return_Redirect()
{
var request = await _controller.Login(new LoginInputModel { Username = "alice", Password = "alice", RememberLogin = false, ReturnUrl = "" }, "login");
Assert.IsAssignableFrom<RedirectResult>(request);
}
}