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);
}
_options
must returnnew AuthenticationSchemeOptions()
whenOptionsMonitor.Get(Scheme.Name)
is called inInitializeAsync
. When using NSubtitute, I had to explicitely say so (otherwise the test crashed) – Tyree