如何对继承自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.");
}
}
单元测试自定义中间件相对容易,但当你从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);
}