How do you unit test custom middleware that inherits from AuthenticationHandler<AuthenticationSchemeOptions>
?
My custom class that inherits from it is for Basic authentication.
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.");
}
}
Unit testing custom middleware is relatively easy, but when you inherit from AuthenticationHandler
, the base class throws a wrench in it. After looking everywhere and only finding integration tests, I was finally able to figure out how to do it.
Basic setup of the unit test that won't change per test.
[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);
}
Special note on _loggerFactory.Setup(x => x.CreateLogger(It.IsAny<String>())).Returns(logger.Object);
If you do not do this, your unit tests will bomb after your handler finishes on a null reference in code that you cannot debug. It is because the base class calls CreateLogger
in its constructor.
Now, you can setup the context using DefaultHttpContext
to test the logic.
[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);
}
Note that you cannot call HandleAuthenticateAsync
directly as it is protected. The handler must be Initialized first then call AuthenticateAsync
.
I included the rest of the logic to be tested below to give examples on how to manipulate the context and assert on the result for different testing scenarios.
[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);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With