18

您如何对继承自的自定义中间件进行单元测试AuthenticationHandler<AuthenticationSchemeOptions>

我从它继承的自定义类用于基本身份验证。

    public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        private readonly IProvidePrincipal _principalProvider;

        public BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IProvidePrincipal principalProvider)
            : base(options, logger, encoder, clock)
        {
            _principalProvider = principalProvider;
        }

        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (Request.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorizationHeader))
            {
                if (Credentials.TryParse(authorizationHeader, out Credentials credentials))
                {
                    var principal = await _principalProvider.GetClaimsPrincipalAsync(credentials.Username, credentials.Password, Scheme.Name);

                    if (principal != null)
                    {
                        var ticket = new AuthenticationTicket(principal, Scheme.Name);

                        return AuthenticateResult.Success(ticket);
                    }
                    else
                    {
                        return AuthenticateResult.Fail("Basic authentication failed.  Invalid username and password.");
                    }
                }
                else
                {
                    return AuthenticateResult.Fail("Basic authentication failed.  Unable to parse username and password.");
                }
            }

            return AuthenticateResult.Fail("Basic authentication failed.  Authorization header is missing.");
        }
    }
4

1 回答 1

44

单元测试自定义中间件相对容易,但是当你从 继承时AuthenticationHandler,基类会在其中抛出一个扳手。在到处寻找并且只找到集成测试之后,我终于能够弄清楚如何去做。

单元测试的基本设置,每次测试都不会改变。

    [TestClass]
    public class BasicAuthenticationTests
    {
        private readonly Mock<IOptionsMonitor<AuthenticationSchemeOptions>> _options;
        private readonly Mock<ILoggerFactory> _loggerFactory;
        private readonly Mock<UrlEncoder> _encoder;
        private readonly Mock<ISystemClock> _clock;
        private readonly Mock<IProvidePrincipal> _principalProvider;
        private readonly BasicAuthenticationHandler _handler;

        public BasicAuthenticationTests()
        {
            _options = new Mock<IOptionsMonitor<AuthenticationSchemeOptions>>();
            
            // This Setup is required for .NET Core 3.1 onwards.
            _options
                .Setup(x => x.Get(It.IsAny<string>()))
                .Returns(new AuthenticationSchemeOptions());
            
            var logger = new Mock<ILogger<BasicAuthenticationHandler>>();
            _loggerFactory = new Mock<ILoggerFactory>();
            _loggerFactory.Setup(x => x.CreateLogger(It.IsAny<String>())).Returns(logger.Object);

            _encoder = new Mock<UrlEncoder>();
            _clock = new Mock<ISystemClock>();
            _principalProvider = new Mock<IProvidePrincipal>();

            _handler = new BasicAuthenticationHandler(_options.Object, _loggerFactory.Object, _encoder.Object, _clock.Object, _principalProvider.Object);
        }

特别注意如果_loggerFactory.Setup(x => x.CreateLogger(It.IsAny<String>())).Returns(logger.Object); 您不这样做,您的单元测试将在您的处理程序完成您无法调试的代码中的空引用后爆炸。这是因为基类CreateLogger在其构造函数中调用。

现在,您可以设置DefaultHttpContext用于测试逻辑的上下文。

        [TestMethod]
        public async Task HandleAuthenticateAsync_NoAuthorizationHeader_ReturnsAuthenticateResultFail()
        {
            var context = new DefaultHttpContext();

            await _handler.InitializeAsync(new AuthenticationScheme(BasicAuthenticationHandler.SchemeName, null, typeof(BasicAuthenticationHandler)), context);
            var result = await _handler.AuthenticateAsync();

            Assert.IsFalse(result.Succeeded);
            Assert.AreEqual("Basic authentication failed.  Authorization header is missing.", result.Failure.Message);
        }

请注意,您不能HandleAuthenticateAsync直接调用,因为它是受保护的。处理程序必须先初始化,然后调用AuthenticateAsync

我在下面包含了要测试的其余逻辑,以举例说明如何操作上下文并对不同测试场景的结果进行断言。

        [TestMethod]
        public async Task HandleAuthenticateAsync_CredentialsTryParseFails_ReturnsAuthenticateResultFail()
        {
            var context = new DefaultHttpContext();
            var authorizationHeader = new StringValues(String.Empty);
            context.Request.Headers.Add(HeaderNames.Authorization, authorizationHeader);

            await _handler.InitializeAsync(new AuthenticationScheme(BasicAuthenticationHandler.SchemeName, null, typeof(BasicAuthenticationHandler)), context);
            var result = await _handler.AuthenticateAsync();

            Assert.IsFalse(result.Succeeded);
            Assert.AreEqual("Basic authentication failed.  Unable to parse username and password.", result.Failure.Message);
        }

        [TestMethod]
        public async Task HandleAuthenticateAsync_PrincipalIsNull_ReturnsAuthenticateResultFail()
        {
            _principalProvider.Setup(m => m.GetClaimsPrincipalAsync(It.IsAny<String>(), It.IsAny<String>(), It.IsAny<String>())).ReturnsAsync((ClaimsPrincipal)null);

            var context = new DefaultHttpContext();
            var authorizationHeader = new StringValues("Basic VGVzdFVzZXJOYW1lOlRlc3RQYXNzd29yZA==");
            context.Request.Headers.Add(HeaderNames.Authorization, authorizationHeader);

            await _handler.InitializeAsync(new AuthenticationScheme(BasicAuthenticationHandler.SchemeName, null, typeof(BasicAuthenticationHandler)), context);
            var result = await _handler.AuthenticateAsync();

            Assert.IsFalse(result.Succeeded);
            Assert.AreEqual("Basic authentication failed.  Invalid username and password.", result.Failure.Message);
        }

        [TestMethod]
        public async Task HandleAuthenticateAsync_PrincipalIsNull_ReturnsAuthenticateResultSuccessWithPrincipalInTicket()
        {
            var username = "TestUserName";
            var claims = new[] { new Claim(ClaimTypes.Name, username) };
            var identity = new ClaimsIdentity(claims, BasicAuthenticationHandler.SchemeName);
            var claimsPrincipal = new ClaimsPrincipal(identity);
            _principalProvider.Setup(m => m.GetClaimsPrincipalAsync(It.IsAny<String>(), It.IsAny<String>(), It.IsAny<String>())).ReturnsAsync(claimsPrincipal);

            var context = new DefaultHttpContext();
            var authorizationHeader = new StringValues("Basic VGVzdFVzZXJOYW1lOlRlc3RQYXNzd29yZA==");
            context.Request.Headers.Add(HeaderNames.Authorization, authorizationHeader);

            await _handler.InitializeAsync(new AuthenticationScheme(BasicAuthenticationHandler.SchemeName, null, typeof(BasicAuthenticationHandler)), context);
            var result = await _handler.AuthenticateAsync();

            Assert.IsTrue(result.Succeeded);
            Assert.AreEqual(BasicAuthenticationHandler.SchemeName, result.Ticket.AuthenticationScheme);
            Assert.AreEqual(username, result.Ticket.Principal.Identity.Name);
        }
于 2019-11-20T20:47:53.833 回答