C# xunit测试标识服务器快速启动UI-发布模拟登录

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

我正在尝试为来自QuickStart UI的AccountController创建XUnit测试,并且在弄清楚需要模拟什么才能使登录函数成功时遇到了很多困难。 登录\返回\重定向以以下异常结束:

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