Inject users and roles dynamically to ASP.NET Core integration tests

After getting fake authenticated user to ASP.NET Core integration tests I made step further and introduced the way to use different user accounts. Using multiple users and roles instead of one test users is very common scenario in web applications. During my carreer I have seen only few business applications that doesn’t use different roles. This blog post demonstrates how to inject users dynamically to ASP.NET Core integration tests.

Source code available! Source code and my sample solution demonstrating unit and integration testing in ASP.NET Core is available in my GitHub repository gpeipman/AspNetCoreTests.

Problem with my test authentication handler

My previous integration testing post introduced TestAuthenticationHandler that is added to request pipeline to provide ASP.NET Core web application with claims based identity. This is how we injected authenticated and authorized user to integration test.

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[]
        {
            new Claim(ClaimTypes.Name, "Test user"),
            new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString())
        };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "Test");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

The handler provides web application with static set of claims. No matter what is the request – claims are always the same. This way it is not possible to test application with different users and roles.

Injecting claims to test authentication handler

To make test authentication handler support different user accounts I invented claims provider. This is the class that carries given set of claims to test authentication provider. I added also some static methods to return already initialized provider with set of claims specific for role.

public class TestClaimsProvider
{
    public IList<Claim> Claims { get; }

    public TestClaimsProvider(IList<Claim> claims)
    {
        Claims = claims;
    }

    public TestClaimsProvider()
    {
        Claims = new List<Claim>();
    }

    public static TestClaimsProvider WithAdminClaims()
    {
        var provider = new TestClaimsProvider();
        provider.Claims.Add(new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()));
        provider.Claims.Add(new Claim(ClaimTypes.Name, "Admin user"));
        provider.Claims.Add(new Claim(ClaimTypes.Role, "Admin"));

        return provider;
    }

    public static TestClaimsProvider WithUserClaims()
    {
        var provider = new TestClaimsProvider();
        provider.Claims.Add(new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString()));
        provider.Claims.Add(new Claim(ClaimTypes.Name, "User"));

        return provider;
    }
}

Instance of claims provider is injected to test authentication handler using framework-level dependency injection.

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    private readonly IList<Claim> _claims;

    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger,
                           UrlEncoder encoder, ISystemClock clock, TestClaimsProvider claimsProvider) : base(options, logger, encoder, clock)
    {
        _claims = claimsProvider.Claims;
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var identity = new ClaimsIdentity(_claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "Test");
       
        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

I also had to modify my shortcut extension methods for WebApplicationFactory that make it easy to have authenticated users in integration tests.

public static class WebApplicationFactoryExtensions
{
    public static WebApplicationFactory<T> WithAuthentication<T>(this WebApplicationFactory<T> factory, TestClaimsProvider claimsProvider) where T : class
    {
        return factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication("Test")
                        .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", op => { });

                services.AddScoped<TestClaimsProvider>(_ => claimsProvider);
            });
        });
    }

    public static HttpClient CreateClientWithTestAuth<T>(this WebApplicationFactory<T> factory, TestClaimsProvider claimsProvider) where T : class
    {
        var client = factory.WithAuthentication(claimsProvider).CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Test");

        return client;
    }
}

Now we are ready to write integration tests for users in different roles.

Role based integration tests

It’s possible to test now if user in given role can access one or another controller action. Here’s the controller I’m using for demo.

[Authorize]
public class CustomersController : Controller
{
    private readonly ICustomerService _customerService;

    public CustomersController(ICustomerService customerService)
    {
        _customerService = customerService;
    }

    public async Task<IActionResult> Index()
    {
        var customers = await _customerService.List();

        return View(customers);
    }

    public async Task<IActionResult> Details(int? id)
    {
        if(id == null)
        {
            return BadRequest();
        }

        var model = await _customerService.GetCustomer(id.Value);
        if(model == null)
        {
            return NotFound();
        }

        return View(model);
    }

    [Authorize(Roles = "Admin")]
    public async Task<IActionResult> Edit(int? id)
    {
        if (id == null)
        {
            return BadRequest();
        }

        var model = await _customerService.GetCustomer(id.Value);
        if (model == null)
        {
            return NotFound();
        }

        return View(model);
    }
}

Using claims provider I can write sample set of integration tests.

public class CustomerControllerTests : TestBase
{
    public CustomerControllerTests(TestApplicationFactory<FakeStartup> factory) : base(factory)
    {
    }

    [Theory]
    [InlineData("/Customers")]
    [InlineData("/Customers/Details")]
    [InlineData("/Customers/Details/1")]
    [InlineData("/Customers/Edit")]
    [InlineData("/Customers/Edit/1")]
    public async Task Get_EndpointsReturnFailToAnonymousUserForRestrictedUrls(string url)
    {
        // Arrange
        var client = Factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });

        // Act
        var response = await client.GetAsync(url);

        // Assert
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    }

    [Theory]
    [InlineData("/Customers")]
    [InlineData("/Customers/Details/1")]
    public async Task Get_EndPointsReturnsSuccessForRegularUser(string url)
    {
        var provider = TestClaimsProvider.WithUserClaims();
        var client = Factory.CreateClientWithTestAuth(provider);

        var response = await client.GetAsync(url);

        response.EnsureSuccessStatusCode();
        Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString());
    }

    [Theory]
    [InlineData("/Customers/Edit/")]
    [InlineData("/Customers/Edit/1")]
    public async Task Get_EditReturnsFailToRegularUser(string url)
    {
        var provider = TestClaimsProvider.WithUserClaims();
        var client = Factory.CreateClientWithTestAuth(provider);

        var response = await client.GetAsync(url);

        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
    }

    [Theory]
    [InlineData("/Customers")]
    [InlineData("/Customers/Details/1")]
    [InlineData("/Customers/Edit/1")]
    public async Task Get_EndPointsReturnsSuccessForAdmin(string url)
    {
        var provider = TestClaimsProvider.WithAdminClaims();
        var client = Factory.CreateClientWithTestAuth(provider);

        var response = await client.GetAsync(url);

        response.EnsureSuccessStatusCode();
        Assert.Equal("text/html; charset=utf-8", response.Content.Headers.ContentType.ToString());
    }
}

These tests try out the following:

  1. Anonymous user cannot access actions of customers controller
  2. Regular application user can access customers list and details page
  3. Regular application user cannot access customer edit view
  4. Administrator can access list, details and edit view of customers controller

It’s time to run these tests and check out results.

ASP.NET Core integration tests with injected user pass

We are lucky – all tests passed.

Wrapping up

Having something already done made it easy for us to extend the current solution to inject multiple users and roles to integration tests. We had to create claims provider and for convenience we added static methods that return claims sets for known roles like user and administrator. We injected claims provider to test authentication handler that returns them to ASP.NET Core when handler is called. We also updated shortcut extension methods to make it more convenient to get users to integration tests.

Gunnar Peipman

Gunnar Peipman is ASP.NET, Azure and SharePoint fan, Estonian Microsoft user group leader, blogger, conference speaker, teacher, and tech maniac. Since 2008 he is Microsoft MVP specialized on ASP.NET.

    8 thoughts on “Inject users and roles dynamically to ASP.NET Core integration tests

    Leave a Reply

    Your email address will not be published. Required fields are marked *