Integration tests need to be reliable. If you’re hitting your actual identity provider with an [Authorize] attribute, your tests will fail if that server ever goes down or makes backwards-incompatible changes. The purpose of an integration test in ASP.NET is to test your application pipeline; not the availability of an external server. Here’s how to bypass the [Authorize] attribute and inject your own claims for use in integration tests.
Normally, OAuth (json web token) authentication is implemented like this:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = MyAuthority;
options.Audience = MyAudience;
});
The trick to bypassing [Authorize] (and eventually injecting mocks) is to have override-able startup. I break up ConfigureServices into 3 sections: ConfigureAuth, ConfigureAppInsights, and ConfigureDependencies.
///
/// Configures authentication for the web app. This is abstracted out
/// so that we can override the authentication middleware for an
/// integration test, and thus, don't have a dependency on the
/// identity server for the test.
///
protected virtual void ConfigureAuth(IServiceCollection services)
{
///
/// Configures app insights for the web app. Under test, we probably
/// don't want real telemtery, so provide the option to turn it off.
///
protected virtual void ConfigureAppInsights(IServiceCollection services)
{
///
/// Configures dependencies for the web app. Under test, we likely
/// want to use mocks, so this provides a convenient way to register
/// different implementations. Additionally or alternatively, calling
/// services.addSingleton multiple times, when resolved, returns the
/// last registered instance.
///
protected virtual void ConfigureDependencies(IServiceCollection services)
{
///
/// Set up dependency injection, configuration bindings, etc.
///
public void ConfigureServices(IServiceCollection services)
{
this.ConfigureAuth(services);
this.ConfigureAppInsights(services);
this.ConfigureDependencies(services);
Then we need to write custom authentication middleware that will pass any authentication attempt and inject any claims we care about. Here’s the source for a “LocalAuthHandler” that injects a custom UserId claim.
https://gist.github.com/michaeltnguyen/718f801fba2fdf370e2e8c5a5e763e08
Now we need to make the asp net core authentication middleware use our custom auth handler.
///
/// For instrumented tests, causes the authorization middleware to
/// bypass identity server and use the LocalAuthenticationHandler to
/// authorize requests and inject a default company id claim.
///
public static AuthenticationBuilder AddLocalAuthentication(this IServiceCollection services)
{
return services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = LocalAuthenticationHandler.AuthScheme;
options.DefaultChallengeScheme = LocalAuthenticationHandler.AuthScheme;
}).AddScheme(
LocalAuthenticationHandler.AuthScheme, _ => { });
}
And here’s what an overridden startup might look like:
https://gist.github.com/michaeltnguyen/f4f36e1d92661b60fa1183bc5cb8760b
And now you can write integration tests that run through the whole http application pipeline.
One problem here is that the controller is still hitting the actual database, so there’s still an external dependency that can cause the test to fail. (mocks to the rescue!) We added a ConfigureDependencies method to our startup class, so now we have a way to inject mocks crafted specifically for the integration tests.
protected override void ConfigureDependencies(IServiceCollection services)
{
services.AddSingleton(new Mock(MockBehavior.Strict).Object);
}
Moq provides a method Mock.Get
///
/// Retrieves a Mocked service for an integration test. The type
/// must be registered in LocalAuthStartup.ConfigureDependencies, or
/// this call will throw an exception.
///
/// The mock service.
public static Mock GetMockService(this IServiceProvider provider)
where T : class
{
return Mock.Get(provider.GetService());
}
So a full blown integration test with local authentication and injected database mocks looks like:
protected IServiceProvider ServiceProvider { get; }
protected HttpClient Client { get; }
public LocalAuthControllerTest()
{
var webhost = new WebHostBuilder().UseStartup();
var server = new TestServer(webhost);
Client = server.CreateClient();
ServiceProvider = server.Host.Services;
}
[Fact]
public async Task TestClaimedQuantityReport_ReturnsExpectedJson()
{
var dbManager = ServiceProvider.GetMockService();
// set up mock here
var result = await Client.GetAsync("/api/v1/employee?name=bob");
var body = await result.Content.ReadAsStringAsync();
var expectedResponse = File.ReadAllText("Responses/employees.json");
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.True(JToken.DeepEquals(JToken.Parse(body), JToken.Parse(expectedResponse)));
dbManager.VerifyAll();
}
Tada! Now you have 100% reliable integration tests that run through the entire HTTP application pipeline without hitting network at all!