Skip to content

Instantly share code, notes, and snippets.

@stevebrownlee
Created September 25, 2025 14:39
Show Gist options
  • Save stevebrownlee/bdacdb396f0ee5196f10b9417282ae47 to your computer and use it in GitHub Desktop.
Save stevebrownlee/bdacdb396f0ee5196f10b9417282ae47 to your computer and use it in GitHub Desktop.
Xunit Tests with authentication

Integration Tests with Cookie Authentication and Role Authorization

Overview

Setting up integration tests for your .NET minimal API with Identity Framework cookie authentication requires creating a test server, seeding test users with appropriate roles, and authenticating requests in your tests.

1. Test Project Setup

First, ensure your test project has the necessary packages:

<!-- In your test project's .csproj file -->
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />

2. Custom WebApplicationFactory

Create a custom WebApplicationFactory to configure your test environment:

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.AspNetCore.Identity;
using PetPal.API.Data; // Adjust namespace as needed
using PetPal.API.Models; // Adjust namespace as needed

public class PetPalWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram> 
    where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Remove the existing DbContext registration
            var descriptor = services.SingleOrDefault(d => 
                d.ServiceType == typeof(DbContextOptions<YourDbContext>));
            if (descriptor != null)
                services.Remove(descriptor);

            // Add in-memory database for testing
            services.AddDbContext<YourDbContext>(options =>
            {
                options.UseInMemoryDatabase("TestDatabase");
            });

            // Configure Identity for testing
            services.Configure<IdentityOptions>(options =>
            {
                // Simplified password requirements for testing
                options.Password.RequireDigit = false;
                options.Password.RequiredLength = 6;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireUppercase = false;
                options.Password.RequireLowercase = false;
            });
        });

        builder.UseEnvironment("Testing");
    }
}

3. Base Integration Test Class

Create a base class that handles authentication setup:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Identity;
using System.Net.Http;
using Xunit;
using PetPal.API.Models; // Adjust namespace

public class IntegrationTestBase : IClassFixture<PetPalWebApplicationFactory<Program>>
{
    protected readonly HttpClient _client;
    protected readonly PetPalWebApplicationFactory<Program> _factory;

    public IntegrationTestBase(PetPalWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = _factory.CreateClient();
        
        // Initialize the database and seed data
        SeedDatabase().GetAwaiter().GetResult();
    }

    protected virtual async Task SeedDatabase()
    {
        using var scope = _factory.Services.CreateScope();
        var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
        var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
        var dbContext = scope.ServiceProvider.GetRequiredService<YourDbContext>();

        // Ensure database is created
        await dbContext.Database.EnsureCreatedAsync();

        // Create roles
        if (!await roleManager.RoleExistsAsync("Admin"))
        {
            await roleManager.CreateAsync(new IdentityRole("Admin"));
        }
        if (!await roleManager.RoleExistsAsync("User"))
        {
            await roleManager.CreateAsync(new IdentityRole("User"));
        }

        // Create test users
        await CreateTestUser(userManager, "[email protected]", "TestPassword123!", "User");
        await CreateTestUser(userManager, "[email protected]", "AdminPassword123!", "Admin");
    }

    private async Task CreateTestUser(UserManager<ApplicationUser> userManager, 
        string email, string password, string role)
    {
        var user = await userManager.FindByEmailAsync(email);
        if (user == null)
        {
            user = new ApplicationUser
            {
                UserName = email,
                Email = email,
                EmailConfirmed = true
            };

            var result = await userManager.CreateAsync(user, password);
            if (result.Succeeded)
            {
                await userManager.AddToRoleAsync(user, role);
            }
        }
    }

    protected async Task<HttpClient> GetAuthenticatedClientAsync(string email = "[email protected]", string password = "TestPassword123!")
    {
        var client = _factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

        // Login and get authentication cookie
        var loginData = new
        {
            Email = email,
            Password = password
        };

        var loginResponse = await client.PostAsJsonAsync("/auth/login", loginData);
        
        // Extract cookies from login response
        if (loginResponse.Headers.TryGetValues("Set-Cookie", out var cookies))
        {
            foreach (var cookie in cookies)
            {
                client.DefaultRequestHeaders.Add("Cookie", cookie.Split(';')[0]);
            }
        }

        return client;
    }

    protected async Task<HttpClient> GetAdminClientAsync()
    {
        return await GetAuthenticatedClientAsync("[email protected]", "AdminPassword123!");
    }
}

4. Example Integration Tests

Here are examples of integration tests for different authorization scenarios:

public class PetControllerTests : IntegrationTestBase
{
    public PetControllerTests(PetPalWebApplicationFactory<Program> factory) : base(factory)
    {
    }

    [Fact]
    public async Task GetUserPets_WithoutAuthentication_ReturnsUnauthorized()
    {
        // Act
        var response = await _client.GetAsync("/user/pets");

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

    [Fact]
    public async Task GetUserPets_WithAuthentication_ReturnsOk()
    {
        // Arrange
        var authenticatedClient = await GetAuthenticatedClientAsync();

        // Act
        var response = await authenticatedClient.GetAsync("/user/pets");

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

    [Fact]
    public async Task CreatePet_WithValidData_ReturnsCreated()
    {
        // Arrange
        var authenticatedClient = await GetAuthenticatedClientAsync();
        var newPet = new
        {
            Name = "Buddy",
            Species = "Dog",
            Breed = "Golden Retriever",
            Age = 3
        };

        // Act
        var response = await authenticatedClient.PostAsJsonAsync("/pets", newPet);

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

    [Fact]
    public async Task DeletePet_AsNonOwner_ReturnsForbidden()
    {
        // Arrange
        var adminClient = await GetAdminClientAsync();
        var userClient = await GetAuthenticatedClientAsync();
        
        // First, create a pet as admin
        var newPet = new { Name = "AdminPet", Species = "Cat" };
        var createResponse = await adminClient.PostAsJsonAsync("/pets", newPet);
        var createdPet = await createResponse.Content.ReadFromJsonAsync<dynamic>();
        var petId = createdPet.id;

        // Act - Try to delete as regular user
        var response = await userClient.DeleteAsync($"/pets/{petId}");

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

// Example test for admin-only endpoints
public class AdminControllerTests : IntegrationTestBase
{
    public AdminControllerTests(PetPalWebApplicationFactory<Program> factory) : base(factory)
    {
    }

    [Fact]
    public async Task AdminEndpoint_WithRegularUser_ReturnsForbidden()
    {
        // Arrange
        var userClient = await GetAuthenticatedClientAsync();

        // Act
        var response = await userClient.GetAsync("/admin/some-admin-endpoint");

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

    [Fact]
    public async Task AdminEndpoint_WithAdminUser_ReturnsOk()
    {
        // Arrange
        var adminClient = await GetAdminClientAsync();

        // Act
        var response = await adminClient.GetAsync("/admin/some-admin-endpoint");

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

5. Testing Specific Roles

For endpoints that require specific roles, create helper methods:

protected async Task<HttpClient> GetClientWithRoleAsync(string role)
{
    using var scope = _factory.Services.CreateScope();
    var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
    
    var email = $"{role.ToLower()}@test.com";
    var user = await userManager.FindByEmailAsync(email);
    
    if (user == null)
    {
        await CreateTestUser(userManager, email, "Password123!", role);
    }
    
    return await GetAuthenticatedClientAsync(email, "Password123!");
}

[Fact]
public async Task VetEndpoint_WithVetRole_ReturnsOk()
{
    // Arrange
    var vetClient = await GetClientWithRoleAsync("Veterinarian");

    // Act
    var response = await vetClient.GetAsync("/vet/appointments");

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

6. Testing with Custom Claims

If you need to test with specific claims:

private async Task AddClaimToUser(string email, string claimType, string claimValue)
{
    using var scope = _factory.Services.CreateScope();
    var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
    
    var user = await userManager.FindByEmailAsync(email);
    if (user != null)
    {
        await userManager.AddClaimAsync(user, new Claim(claimType, claimValue));
    }
}

7. Cleanup and Best Practices

  • Use IClassFixture for sharing the same instance of WebApplicationFactory across tests in a class
  • Use ICollectionFixture if you need to share across multiple test classes
  • Consider using IAsyncLifetime for async setup/cleanup
  • Use descriptive test names that clearly indicate what's being tested
  • Group related tests in separate classes
  • Use in-memory database for faster test execution

8. Example Test Class Structure

[Collection("Integration Tests")]
public class AuthorizedEndpointTests : IntegrationTestBase
{
    public AuthorizedEndpointTests(PetPalWebApplicationFactory<Program> factory) 
        : base(factory)
    {
    }

    [Theory]
    [InlineData("/user/pets")]
    [InlineData("/pets/1")]
    public async Task AuthorizedEndpoints_WithoutAuth_ReturnUnauthorized(string endpoint)
    {
        var response = await _client.GetAsync(endpoint);
        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
    }

    [Theory]
    [InlineData("/user/pets")]
    [InlineData("/pets")]
    public async Task AuthorizedEndpoints_WithAuth_ReturnSuccess(string endpoint)
    {
        var client = await GetAuthenticatedClientAsync();
        var response = await client.GetAsync(endpoint);
        Assert.True(response.IsSuccessStatusCode);
    }
}

This setup provides a robust foundation for testing your cookie-based authentication and role-based authorization in your .NET minimal API integration tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment