Testing an AuthenticationHandler in ASP.NET
Our AuthenticationHandler
Let us say we have an AuthenticationHandler
that looks like this to handle authentication. Here is a silly example randomly allowing or denying authentication:
public partial class RandomAuthenticationHandler
: AuthenticationHandler<AuthenticationSchemeOptions>
{
public const string SchemaName = "Random";
readonly bool _enabled;
readonly double _chance;
readonly ILogger<RandomAuthenticationHandler> _logger;
public RandomAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory loggerFactory,
UrlEncoder encoder,
IOptions<RandomSettings> randomSettings
) : base(
options,
loggerFactory,
encoder
)
{
_logger = loggerFactory
.CreateLogger<RandomAuthenticationHandler>();
if (randomSettings?.Value is null)
{
LogMissingConfiguration(_logger);
throw new InvalidOperationException(
"Application misconfigured: No RandomSettings"
)
}
_randomSettings = randomSettings.Value;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!_randomSettings.Enabled)
{
LogDisabled(_logger);
return Task.FromResult(
AuthenticateResult.NoResult()
);
}
if (Random.Shared.NextDouble() < _randomSettings.Chance)
{
LogRandomlyFailing(_logger);
return Task.FromResult(
AuthenticateResult.Fail("Not authenticated")
);
}
LogRandomlySucceeding(_logger);
return Task.FromResult(
AuthenticateResult.Success(
new AuthenticationTicket(
principal: new ClaimsPrincipal(
new ClaimsIdentity(
authenticationType: SchemaName,
claims:
[
new Claim(
ClaimTypes.NameIdentifier,
Guid.NewGuid().ToString()
),
new Claim(
ClaimTypes.Name,
"Random User"
),
]
)
),
authenticationScheme: SchemaName
)
)
);
}
// logging omitted for brevity
}
/**
* Bind this in your IoC setup
**/
public class RandomSettings
{
public bool Enabled { get; set; }
[Range(0.0, 1.0)]
public double Chance { get; set; }
}
How would we test such a thing? Set aside that this is a silly example, and randomness it inherently hard to test. How do we set up the AuthenticationHandler
in a test?
Setting up the test
We need to instantiate our RandomAuthenticationHandler
in a test. We can do that fine.
In the following example I’m using NSubstitute for mocking, and xUnit for the test framework. The logging is set up in a base class called Test_with_logs
that I use in many tests, you can read about that in this post.
public class Given_a_RandomAuthenticationHandler
: Test_with_logs
{
protected IOptionsMonitor<AuthenticationSchemeOptions> _options_monitor;
protected IOptions<RandomSettings> _random_settings;
protected UrlEncoder _url_encoder;
protected RandomAuthenticationHandler _handler;
protected Given_a_RandomAuthenticationHandler()
{
_options_monitor = Substitute
.For<IOptionsMonitor<AuthenticationSchemeOptions>>();
_options_monitor
.Get(Arg.Any<string>())
.Returns(new AuthOptions());
_url_encoder = Substitute.For<UrlEncoder>();
_random_settings = Options.Create(
new RandomSettings
{
Enabled = true,
Chance = 0.2
}
);
_handler = new RandomAuthenticationHandler(
_options_monitor,
TestLoggerFactory,
_url_encoder,
_random_settings
);
}
}
public class When_authenticating_randomly
: Given_a_RandomAuthenticationHandler
{
[Fact]
public async Task It_should_not_crash()
{
await _random_authentication_handler.AuthenticateAsync();
true.Should().BeTrue();
}
}
Our simple test fails, since the AuthenticationHandler
must be initialised before use. We can do that by calling the InitializeAsync
-method. This is the thing that aspnet calls before our HandleAuthenticationAsync
method is called. Let’s add that to our test base-class (the Given
-class):
public class Given_a_RandomAuthenticationHandler
: Test_with_logs
{
// ...previously shown code omitted for brevity...
protected async Task Initialize_with(HttpContext context)
{
await _handler.InitializeAsync(
new AuthenticationScheme(
RandomAuthenticationHandler.SchemaName,
null,
typeof(RandomAuthenticationHandler)
),
context
);
}
}
Now we can call that in our test, and pass in a HttpContext
that we set up for the test. Let’s do that:
public class When_authenticating_randomly
: Given_a_RandomAuthenticationHandler
{
readonly DefaultHttpContext _context = new();
[Fact]
public async Task It_should_return_a_result()
{
await Initialize_with(_context);
var result = await _handler.AuthenticateAsync();
result.Should().NotBeNull();
}
[Fact]
public async Task It_should_log_something()
{
await Initialize_with(_context);
await _handler.AuthenticateAsync();
Logs
.Should()
.NotBeEmpty();
}
// more tests here...
}
And there you have it! Now you can manipulate the HttpContext
as you like before you initialize, and test your AuthenticationHandler
in isolation.
Exercise for the reader: write a test wherein the _random_settings.Value
is null verifying that the InvalidOperationException
is thrown.
Happy testing!