Testing Authenticated Endpoints
Create test authentication handlers and test secured endpoints with mock JWT tokens
Testing Authenticated Endpoints in ASP.NET
Master testing authenticated endpoints in ASP.NET with free flashcards and hands-on practice. This lesson covers authentication setup in tests, mocking JWT tokens, bypassing authentication for integration tests, and validating authorization policiesβessential skills for building secure, reliable web APIs with .NET 10.
Welcome π
Welcome to one of the most critical skills in modern ASP.NET development! Testing authenticated endpoints is where security meets quality assurance. While building authentication is important, verifying that it actually works correctly is equally crucial. In this lesson, you'll learn how to write comprehensive tests for endpoints that require authentication and authorization, ensuring your API security is robust and reliable.
π‘ Why This Matters: Untested authentication logic is a security vulnerability waiting to happen. By the end of this lesson, you'll know how to test everything from JWT validation to role-based authorization, giving you confidence that your secured endpoints work exactly as intended.
Core Concepts π
Understanding the Testing Challenge
When you protect an endpoint with [Authorize] or role-based attributes, testing becomes more complex. Your tests need to simulate authenticated users without actually running a full authentication flow. There are several approaches, each with specific use cases:
| Approach | Use Case | Complexity |
|---|---|---|
| Mocking Authentication | Unit tests, fast feedback | Low |
| WebApplicationFactory with Test Auth | Integration tests, realistic scenarios | Medium |
| Generating Real JWT Tokens | End-to-end tests, full validation | High |
The Authentication Pipeline in ASP.NET
Before diving into testing, let's understand what happens when a request hits an authenticated endpoint:
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β AUTHENTICATION REQUEST FLOW β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
π¨ HTTP Request with Bearer Token
β
β
π Authentication Middleware
β
ββββ β Invalid Token? β 401 Unauthorized
β
β
β
Create ClaimsPrincipal
β
β
π‘οΈ Authorization Middleware
β
ββββ β Insufficient Rights? β 403 Forbidden
β
β
π― Controller Action Executes
β
β
π€ Response Returned
Your tests need to intercept or simulate different parts of this pipeline depending on what you're testing.
WebApplicationFactory: Your Testing Foundation
The WebApplicationFactory class from Microsoft.AspNetCore.Mvc.Testing is the cornerstone of integration testing in ASP.NET. It creates an in-memory test server that hosts your actual application:
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Override services for testing
});
}
}
π‘ Key Insight: This factory creates a real HTTP server in memory, so your tests use actual HTTP requests and responsesβbut without network overhead.
Mocking Authentication with Test Schemes
The most flexible approach for integration tests is creating a test authentication scheme. This bypasses real token validation while still exercising the authorization pipeline:
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder) : base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[]
{
new Claim(ClaimTypes.Name, "TestUser"),
new Claim(ClaimTypes.NameIdentifier, "test-user-id"),
new Claim(ClaimTypes.Role, "Admin")
};
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
What's happening here?
- The handler always succeeds authentication
- It creates a
ClaimsPrincipalwith predefined claims - No token validation occursβperfect for controlled testing
Registering the Test Authentication Scheme
You configure the test scheme in your WebApplicationFactory:
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
}
β οΈ Important: Use ConfigureTestServices instead of ConfigureServices to ensure test services are registered after production services, allowing proper override.
Creating Authenticated HTTP Clients
Once your test authentication is set up, create HTTP clients that include the authentication scheme:
[Fact]
public async Task GetProtectedResource_WithAuthentication_ReturnsSuccess()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("TestScheme");
var response = await client.GetAsync("/api/protected");
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
Testing with Real JWT Tokens
For more realistic testing, you can generate actual JWT tokens:
public class JwtTokenGenerator
{
public string GenerateToken(string userId, string[] roles)
{
var securityKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("your-super-secret-key-min-32-chars"));
var credentials = new SigningCredentials(
securityKey, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, userId),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var token = new JwtSecurityToken(
issuer: "TestIssuer",
audience: "TestAudience",
claims: claims,
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
π§ Mnemonic - JWT Testing: "SICK" tokens need validation:
- Signature
- Issuer
- Claims
- Key (secret)
Testing Authorization Policies
ASP.NET supports policy-based authorization, which you also need to test:
// In Startup/Program.cs
services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdminRole", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("MinimumAge", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(18)));
});
// Controller
[Authorize(Policy = "RequireAdminRole")]
public IActionResult AdminOnly() => Ok("Admin access");
Testing this requires configuring claims that satisfy the policy:
public class TestAuthHandlerWithAge : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly int _age;
public TestAuthHandlerWithAge(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
int age) : base(options, logger, encoder)
{
_age = age;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[]
{
new Claim(ClaimTypes.Name, "TestUser"),
new Claim("age", _age.ToString())
};
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
Testing Unauthorized Access
Don't forget to test the negative casesβwhat happens when authentication fails:
[Fact]
public async Task GetProtectedResource_WithoutAuthentication_Returns401()
{
var client = _factory.CreateClient();
// No authentication header set
var response = await client.GetAsync("/api/protected");
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}
[Fact]
public async Task AdminEndpoint_AsRegularUser_Returns403()
{
// Create client with non-admin user claims
var client = CreateClientWithRole("User");
var response = await client.GetAsync("/api/admin/dashboard");
response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
}
Practical Examples π§
Example 1: Complete Integration Test Setup
Let's build a complete test project structure:
// CustomWebApplicationFactory.cs
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
public string TestUserId { get; set; } = "test-user-123";
public string[] TestUserRoles { get; set; } = new[] { "User" };
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
// Remove existing authentication
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(IAuthenticationService));
if (descriptor != null)
{
services.Remove(descriptor);
}
// Add test authentication
services.AddAuthentication("TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
// Provide test user info to the handler
services.AddSingleton<TestUserProvider>(sp =>
new TestUserProvider
{
UserId = TestUserId,
Roles = TestUserRoles
});
});
}
}
// TestUserProvider.cs
public class TestUserProvider
{
public string UserId { get; set; }
public string[] Roles { get; set; }
}
// TestAuthHandler.cs
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly TestUserProvider _userProvider;
public TestAuthHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
TestUserProvider userProvider) : base(options, logger, encoder)
{
_userProvider = userProvider;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, _userProvider.UserId),
new Claim(ClaimTypes.Name, $"Test User {_userProvider.UserId}")
};
claims.AddRange(_userProvider.Roles.Select(role =>
new Claim(ClaimTypes.Role, role)));
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
// UserProfileTests.cs
public class UserProfileTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly CustomWebApplicationFactory _factory;
private readonly HttpClient _client;
public UserProfileTests(CustomWebApplicationFactory factory)
{
_factory = factory;
_client = _factory.CreateClient();
}
[Fact]
public async Task GetCurrentUser_WithAuthentication_ReturnsUserProfile()
{
// Arrange
_factory.TestUserId = "user-456";
_factory.TestUserRoles = new[] { "Premium" };
_client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("TestScheme");
// Act
var response = await _client.GetAsync("/api/users/me");
var content = await response.Content.ReadAsStringAsync();
var user = JsonSerializer.Deserialize<UserDto>(content);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
user.Id.Should().Be("user-456");
}
}
What makes this great?
- β Reusable factory for all test classes
- β Configurable test user properties
- β Clean separation of concerns
- β Easy to extend for different scenarios
Example 2: Testing Role-Based Authorization
public class AdminEndpointTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly CustomWebApplicationFactory _factory;
public AdminEndpointTests(CustomWebApplicationFactory factory)
{
_factory = factory;
}
private HttpClient CreateAuthenticatedClient(string[] roles)
{
_factory.TestUserRoles = roles;
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("TestScheme");
return client;
}
[Theory]
[InlineData(new[] { "Admin" }, HttpStatusCode.OK)]
[InlineData(new[] { "User" }, HttpStatusCode.Forbidden)]
[InlineData(new[] { "Moderator" }, HttpStatusCode.Forbidden)]
public async Task DeleteUser_WithDifferentRoles_ReturnsExpectedStatus(
string[] roles, HttpStatusCode expectedStatus)
{
// Arrange
var client = CreateAuthenticatedClient(roles);
// Act
var response = await client.DeleteAsync("/api/admin/users/123");
// Assert
response.StatusCode.Should().Be(expectedStatus);
}
[Fact]
public async Task GetAdminDashboard_AsAdmin_ReturnsStatistics()
{
// Arrange
var client = CreateAuthenticatedClient(new[] { "Admin" });
// Act
var response = await client.GetAsync("/api/admin/dashboard");
var content = await response.Content.ReadAsStringAsync();
var stats = JsonSerializer.Deserialize<DashboardStats>(content);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
stats.Should().NotBeNull();
stats.TotalUsers.Should().BeGreaterThan(0);
}
}
π Did you know? ASP.NET evaluates authorization in a specific order: authentication schemes first, then policies, then individual role/claim requirements. Understanding this order helps you debug authorization failures!
Example 3: Testing Custom Authorization Requirements
// Custom Requirement
public class MinimumAccountAgeRequirement : IAuthorizationRequirement
{
public int MinimumDays { get; }
public MinimumAccountAgeRequirement(int minimumDays)
{
MinimumDays = minimumDays;
}
}
// Handler
public class MinimumAccountAgeHandler :
AuthorizationHandler<MinimumAccountAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAccountAgeRequirement requirement)
{
var accountCreatedClaim = context.User.FindFirst("AccountCreated");
if (accountCreatedClaim == null)
{
return Task.CompletedTask;
}
var accountCreated = DateTime.Parse(accountCreatedClaim.Value);
var accountAge = (DateTime.UtcNow - accountCreated).TotalDays;
if (accountAge >= requirement.MinimumDays)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
// Test
public class AccountAgeAuthorizationTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly CustomWebApplicationFactory _factory;
public AccountAgeAuthorizationTests(CustomWebApplicationFactory factory)
{
_factory = factory;
}
[Theory]
[InlineData(-10, HttpStatusCode.Forbidden)] // 10 days in the future (invalid)
[InlineData(5, HttpStatusCode.Forbidden)] // Only 5 days old
[InlineData(30, HttpStatusCode.OK)] // 30 days old - allowed
[InlineData(365, HttpStatusCode.OK)] // 1 year old - allowed
public async Task AccessPremiumFeature_WithDifferentAccountAge_ReturnsExpectedStatus(
int daysOld, HttpStatusCode expectedStatus)
{
// Arrange
var accountCreated = DateTime.UtcNow.AddDays(-daysOld);
var handler = new TestAuthHandlerWithAccountAge(
accountCreated,
Mock.Of<IOptionsMonitor<AuthenticationSchemeOptions>>(),
Mock.Of<ILoggerFactory>(),
Mock.Of<UrlEncoder>());
// Configure factory to use this handler
_factory.TestAccountCreated = accountCreated;
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("TestScheme");
// Act
var response = await client.GetAsync("/api/premium/feature");
// Assert
response.StatusCode.Should().Be(expectedStatus);
}
}
// Modified test handler to support account age
public class TestAuthHandlerWithAccountAge :
AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly DateTime _accountCreated;
public TestAuthHandlerWithAccountAge(
DateTime accountCreated,
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder) : base(options, logger, encoder)
{
_accountCreated = accountCreated;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, "test-user"),
new Claim("AccountCreated", _accountCreated.ToString("O"))
};
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
π‘ Pro Tip: When testing custom authorization requirements, create separate test auth handlers for each scenario. This gives you fine-grained control over the claims being tested.
Example 4: Testing JWT Token Validation
public class JwtAuthenticationTests
{
private readonly WebApplicationFactory<Program> _factory;
private readonly string _secretKey = "super-secret-key-for-testing-minimum-32-characters";
private readonly string _issuer = "TestIssuer";
private readonly string _audience = "TestAudience";
public JwtAuthenticationTests()
{
_factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((context, config) =>
{
config.AddInMemoryCollection(new Dictionary<string, string>
{
["Jwt:Key"] = _secretKey,
["Jwt:Issuer"] = _issuer,
["Jwt:Audience"] = _audience
});
});
});
}
private string GenerateJwtToken(string userId, string[] roles,
DateTime? expiration = null)
{
var securityKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_secretKey));
var credentials = new SigningCredentials(
securityKey, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, userId),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var token = new JwtSecurityToken(
issuer: _issuer,
audience: _audience,
claims: claims,
expires: expiration ?? DateTime.UtcNow.AddHours(1),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
[Fact]
public async Task GetProtectedResource_WithValidJwt_ReturnsSuccess()
{
// Arrange
var token = GenerateJwtToken("user-123", new[] { "User" });
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
// Act
var response = await client.GetAsync("/api/protected");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
[Fact]
public async Task GetProtectedResource_WithExpiredJwt_Returns401()
{
// Arrange
var expiredTime = DateTime.UtcNow.AddHours(-1);
var token = GenerateJwtToken("user-123", new[] { "User" }, expiredTime);
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
// Act
var response = await client.GetAsync("/api/protected");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}
[Fact]
public async Task GetProtectedResource_WithInvalidSignature_Returns401()
{
// Arrange
var wrongKey = "different-secret-key-that-wont-validate-properly";
var securityKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(wrongKey));
var credentials = new SigningCredentials(
securityKey, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _issuer,
audience: _audience,
claims: new[] { new Claim(ClaimTypes.NameIdentifier, "user-123") },
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: credentials);
var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", tokenString);
// Act
var response = await client.GetAsync("/api/protected");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}
}
Common Mistakes β οΈ
Mistake 1: Not Using ConfigureTestServices
β Wrong:
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.AddAuthentication("TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
}
β Correct:
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("TestScheme")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"TestScheme", options => { });
});
}
Why it matters: ConfigureServices runs before the production configuration, meaning your test authentication might be overridden. ConfigureTestServices runs last, ensuring your test setup takes precedence.
Mistake 2: Forgetting to Set the Authorization Header
β Wrong:
[Fact]
public async Task GetProtectedResource_ReturnsSuccess()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/protected");
// Returns 401 because no auth header!
}
β Correct:
[Fact]
public async Task GetProtectedResource_ReturnsSuccess()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("TestScheme");
var response = await client.GetAsync("/api/protected");
}
Mistake 3: Not Testing Both Success and Failure Cases
β Incomplete:
[Fact]
public async Task AdminEndpoint_AsAdmin_ReturnsSuccess()
{
var client = CreateClientWithRole("Admin");
var response = await client.GetAsync("/api/admin");
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
β Complete:
[Theory]
[InlineData("Admin", HttpStatusCode.OK)]
[InlineData("User", HttpStatusCode.Forbidden)]
[InlineData("Guest", HttpStatusCode.Forbidden)]
public async Task AdminEndpoint_WithDifferentRoles_ReturnsExpectedStatus(
string role, HttpStatusCode expectedStatus)
{
var client = CreateClientWithRole(role);
var response = await client.GetAsync("/api/admin");
response.StatusCode.Should().Be(expectedStatus);
}
[Fact]
public async Task AdminEndpoint_WithoutAuth_Returns401()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/admin");
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}
Mistake 4: Hardcoding Claims in Every Test
β Repetitive:
[Fact]
public async Task Test1()
{
var claims = new[] { new Claim(ClaimTypes.Role, "Admin") };
// Setup handler with claims...
}
[Fact]
public async Task Test2()
{
var claims = new[] { new Claim(ClaimTypes.Role, "Admin") };
// Setup handler with claims...
}
β Reusable:
public class AuthenticatedTestBase
{
protected HttpClient CreateClientWithRole(string role)
{
_factory.TestUserRoles = new[] { role };
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("TestScheme");
return client;
}
protected HttpClient CreateClientWithClaims(params Claim[] claims)
{
_factory.TestUserClaims = claims;
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("TestScheme");
return client;
}
}
Mistake 5: Not Isolating Database State Between Tests
β Problematic:
[Fact]
public async Task CreateUser_AsAdmin_CreatesUser()
{
var client = CreateClientWithRole("Admin");
var response = await client.PostAsync("/api/users", content);
// User remains in database for next test!
}
[Fact]
public async Task GetUsers_ReturnsAllUsers()
{
var client = CreateClientWithRole("Admin");
var response = await client.GetAsync("/api/users");
// Might include user from previous test
}
β Isolated:
public class UserTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly CustomWebApplicationFactory _factory;
public UserTests(CustomWebApplicationFactory factory)
{
_factory = factory;
// Reset database before each test
_factory.ResetDatabase();
}
[Fact]
public async Task CreateUser_AsAdmin_CreatesUser()
{
var client = CreateClientWithRole("Admin");
var response = await client.PostAsync("/api/users", content);
// Database is reset before next test
}
}
Key Takeaways π―
WebApplicationFactory is your foundation - Use it to create in-memory test servers that exercise your real application code while maintaining control over authentication.
Test authentication schemes are powerful - Creating a custom
AuthenticationHandlerlets you bypass token validation while still testing authorization logic.Test both positive and negative cases - Don't just test successful authentication; verify that unauthorized and forbidden responses work correctly too.
Use Theory tests for role variations - Testing multiple roles and permissions is cleaner with
[Theory]and[InlineData]attributes.Real JWTs for integration tests - When you need complete validation, generate actual JWT tokens with proper signing keys.
Isolate your tests - Reset database state and use fresh HTTP clients to prevent tests from interfering with each other.
ConfigureTestServices over ConfigureServices - Always use
ConfigureTestServicesto ensure your test setup overrides production configuration.Create reusable helper methods - Build utility methods for creating authenticated clients to reduce test code duplication.
π Further Study
- Microsoft Docs: Integration tests in ASP.NET Core
- Microsoft Docs: Policy-based authorization in ASP.NET Core
- Andrew Lock: Integration Testing with WebApplicationFactory
π Quick Reference Card
| WebApplicationFactory | Creates in-memory test server for integration tests |
| ConfigureTestServices | Override services AFTER production config (use this!) |
| TestAuthHandler | Custom handler that bypasses real authentication |
| ClaimsPrincipal | Represents authenticated user with claims |
| Authorization Header | Set via client.DefaultRequestHeaders.Authorization |
| Test JWT Generation | Use JwtSecurityTokenHandler + SymmetricSecurityKey |
| Status Codes | 401 = Unauthorized, 403 = Forbidden, 200 = OK |
| Theory Tests | Use [Theory] + [InlineData] for multiple roles |