xunit测试IdentityServer快速入门UI-问题模拟登录

时间:2019-12-20 13:39:39

标签: c# mocking moq identityserver4 xunit

我正在尝试为来自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);


        }


    }

0 个答案:

没有答案