单元测试自定义 AuthenticationHandler 中间件

Posted

技术标签:

【中文标题】单元测试自定义 AuthenticationHandler 中间件【英文标题】:Unit Test Custom AuthenticationHandler Middleware 【发布时间】:2020-03-16 16:33:06 【问题描述】:

如何对继承自 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.");
        
    

【问题讨论】:

【参考方案1】:

对自定义中间件进行单元测试相对容易,但是当您从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 =&gt; x.CreateLogger(It.IsAny&lt;String&gt;())).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);
        

【讨论】:

请注意,当在 InitializeAsync 中调用 OptionsMonitor.Get(Scheme.Name) 时,_options 必须返回 new AuthenticationSchemeOptions()。在使用 NSubtitute 时,我必须明确说明(否则测试会崩溃) @Luk 我必须为 NSubstitute 做同样的事情,以免它崩溃。但我不确定我是否理解为什么给出这一行:github.com/aspnet/Security/blob/master/src/… - 它应该只创建一个新实例本身?我错过了什么?顺便说一句 - 我只需要在 .NET Core 3.1 中执行此操作(我从 2.1 迁移了一个应用程序,这并不重要)。 谢谢@Luk,你解决了我从 2.2 升级到 3.1 以及创建新的 3.1 项目时遇到的一个谜团。正如@Travis 指出的那样,鉴于InitializeAsync 的代码中的那一行,我不明白为什么这将是一个问题,在我阅读您的评论之前,我完全无法解决。 为避免 Authentication 3.1 library 出现 Null 引用异常,请在 BasicAuthenticationTests() 构造函数的第 2 行添加以下行 _options.Setup(x => x.Get(AuthenticationSchemeOptions.AuthenticationSchemeName)).Returns(new AuthenticationSchemeOptions());

以上是关于单元测试自定义 AuthenticationHandler 中间件的主要内容,如果未能解决你的问题,请参考以下文章

如何使用属性单元测试自定义视图

如何测试自定义单元格是不是已向 tableView 注册?

单元测试 NPE,当我添加片段自定义转换时

对自定义 symfony 约束进行单元测试

单元测试自定义 FxCop 规则

WPF - 对自定义标记扩展进行单元测试