C# xunit测试标识服务器快速启动UI-发布模拟登录
我正在尝试为来自QuickStart UI的AccountController创建XUnit测试,并且在弄清楚需要模拟什么才能使登录函数成功时遇到了很多困难。 登录\返回\重定向以以下异常结束:C# xunit测试标识服务器快速启动UI-发布模拟登录,c#,mocking,moq,identityserver4,xunit,C#,Mocking,Moq,Identityserver4,Xunit,我正在尝试为来自QuickStart UI的AccountController创建XUnit测试,并且在弄清楚需要模拟什么才能使登录函数成功时遇到了很多困难。 登录\返回\重定向以以下异常结束: Source: AccountTests.cs line 101 Duration: 2 sec Message: System.NullReferenceException : Object reference not set to an instance of an objec
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);
}
//
///处理从用户名/密码登录的回发
///
[HttpPost]
[ValidateAntiForgeryToken]
公共异步任务登录(LoginInputModel模型,字符串按钮)
{
//检查我们是否在授权请求的上下文中
var context=await\u interaction.GetAuthorizationContextAsync(model.ReturnUrl);
//用户单击了“取消”按钮
如果(按钮!=“登录”)
{
if(上下文!=null)
{
//如果用户取消,则将结果发送回IdentityServer,就像他们
//拒绝同意(即使该客户不需要同意)。
//这将向客户端发回拒绝访问的OIDC错误响应。
wait_interaction.GrantConsentAsync(上下文、同意响应、拒绝);
//我们可以信任model.ReturnUrl,因为GetAuthorizationContextAsync返回了非null
if(wait_clientStore.IsPkceClientAsync(context.ClientId))
{
//如果客户机是PKCE,那么我们假设它是本机的,因此
//返回的响应是为最终用户提供更好的用户体验。
返回视图(“重定向”,新的重定向视图模型{RedirectUrl=model.ReturnUrl});
}
返回重定向(model.ReturnUrl);
}
其他的
{
//因为我们没有有效的上下文,所以我们只需返回主页
返回重定向(“~/”);
}
}
if(ModelState.IsValid)
{
//根据内存存储验证用户名/密码
if(_users.ValidateCredentials(model.Username、model.Password))
{
var user=\u users.FindByUsername(model.Username);
wait_events.RaiseAsync(新用户登录访问事件(user.Username,user.SubjectId,user.Username,clientId:context?.clientId));
//仅当用户选择“记住我”时才在此处设置显式过期。
//否则,我们依赖于cookie中间件中配置的过期。
AuthenticationProperties props=null;
if(AccountOptions.AllowRememberLogin&&model.RememberLogin)
{
props=新的AuthenticationProperties
{
ispersist=true,
ExpiresUtc=DateTimeOffset.UtcNow.Add(AccountOptions.rememberLoginDuration)
};
};
//发出具有主题ID和用户名的身份验证cookie
等待HttpContext.SignInAsync(user.SubjectId、user.Username、props);
if(上下文!=null)
{
if(wait_clientStore.IsPkceClientAsync(context.ClientId))
{
//如果客户机是PKCE,那么我们假设它是本机的,因此
//返回的响应是为最终用户提供更好的用户体验。
返回视图(“重定向”,新的重定向视图模型{RedirectUrl=model.ReturnUrl});
}
//我们可以信任model.ReturnUrl,因为GetAuthorizationContextAsync返回了非null
返回重定向(model.ReturnUrl);
}
//请求本地页面
if(Url.IsLocalUrl(model.ReturnUrl))
{
返回重定向(model.ReturnUrl);
}
else if(string.IsNullOrEmpty(model.ReturnUrl))
{
返回重定向(“~/”);
}
其他的
{
//用户可能单击了恶意链接-应记录
抛出新异常(“无效返回URL”);
}
}
wait_events.RaiseAsync(新用户登录失败事件(model.Username,“无效凭据”,clientId:context?.clientId));
ModelState.AddModelError(string.Empty,AccountOptions.InvalidCredentialsErrorMessage);
}
//出了问题,用错误显示表单
var vm=等待BuildLoginViewModelAsync(模型);
返回视图(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);
}
}
公共类AccountTests
{
私有只读TestUserStore\u用户;
专用只读IIdentialServerInteractionService\u交互;
私有只读IClientStore\u客户端存储;
私有只读IAAuthenticationSchemeProvider\u schemeProvider;
私有只读IEventService事件;
会计控制器(AccountController);;
TestServer\u服务器;
///
///用于设置MVC管道并创建控制器实例的构造函数
///
公共会计测试()
{
var webhost=new WebHostBuilder()
.useURL(“http://*:8000”)
.UseStartup();
_服务器=新的TestServer(webhost);
_users=(TestUserStore)_server.Services.GetService(typeof(TestUserStore));
_交互=(IIdentityServerInteractionService)\ u server.Services.GetService(typeof(IIdentityServerInt