using System.Net; using System.Net.Http.Json; using System.Text.Json; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; /// /// Integration tests for authentication flow: login, refresh, logout, and role-based access. /// [Collection("Database")] public class AuthTests : IAsyncLifetime { private readonly ApiFixture _fixture; public AuthTests(ApiFixture fixture) => _fixture = fixture; public async Task InitializeAsync() { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); await DbResetHelper.ResetAsync(db); await DataSeeder.SeedAsync(db); } public Task DisposeAsync() => Task.CompletedTask; [Fact] public async Task Login_ValidCredentials_ReturnsTokenAndProfile() { var client = _fixture.CreateClient(); var response = await client.PostAsJsonAsync("/api/v1/auth/login", new { username = "entry1", password = "password" }); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await response.Content.ReadFromJsonAsync(); var data = body.GetProperty("data"); data.GetProperty("token").GetString().Should().NotBeNullOrEmpty(); data.GetProperty("refreshToken").GetString().Should().NotBeNullOrEmpty(); data.GetProperty("username").GetString().Should().Be("entry1"); data.GetProperty("role").GetString().Should().Be("DATA_ENTRY_CLERK"); } [Fact] public async Task Login_InvalidPassword_Returns422() { var client = _fixture.CreateClient(); var response = await client.PostAsJsonAsync("/api/v1/auth/login", new { username = "entry1", password = "wrongpassword" }); response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); } [Fact] public async Task Login_NonexistentUser_Returns422() { var client = _fixture.CreateClient(); var response = await client.PostAsJsonAsync("/api/v1/auth/login", new { username = "nonexistent", password = "password" }); response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); } [Fact] public async Task ProtectedEndpoint_NoToken_Returns401() { var client = _fixture.CreateClient(); var response = await client.GetAsync("/api/v1/digitization-batches"); response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } [Fact] public async Task RoleRestricted_WrongRole_Returns403() { // entry1 is DATA_ENTRY_CLERK — cannot access admin dashboard var client = await AuthHelper.LoginAsync(_fixture, "entry1"); var response = await client.GetAsync("/api/v1/work-queue/overview"); response.StatusCode.Should().Be(HttpStatusCode.Forbidden); } [Fact] public async Task Refresh_ValidToken_ReturnsNewTokens() { var client = _fixture.CreateClient(); // Login to get refresh token var loginResponse = await client.PostAsJsonAsync("/api/v1/auth/login", new { username = "verifier1", password = "password" }); loginResponse.EnsureSuccessStatusCode(); var loginBody = await loginResponse.Content.ReadFromJsonAsync(); var refreshToken = loginBody.GetProperty("data").GetProperty("refreshToken").GetString(); // Refresh var refreshResponse = await client.PostAsJsonAsync("/api/v1/auth/refresh", new { refreshToken }); refreshResponse.StatusCode.Should().Be(HttpStatusCode.OK); var refreshBody = await refreshResponse.Content.ReadFromJsonAsync(); refreshBody.GetProperty("data").GetProperty("token").GetString().Should().NotBeNullOrEmpty(); refreshBody.GetProperty("data").GetProperty("refreshToken").GetString() .Should().NotBe(refreshToken, "refresh should rotate the token"); } [Fact] public async Task Logout_RevokesRefreshToken() { var client = _fixture.CreateClient(); // Login var loginResponse = await client.PostAsJsonAsync("/api/v1/auth/login", new { username = "intake1", password = "password" }); loginResponse.EnsureSuccessStatusCode(); var loginBody = await loginResponse.Content.ReadFromJsonAsync(); var refreshToken = loginBody.GetProperty("data").GetProperty("refreshToken").GetString(); // Logout var logoutResponse = await client.PostAsJsonAsync("/api/v1/auth/logout", new { refreshToken }); logoutResponse.StatusCode.Should().Be(HttpStatusCode.NoContent); // Attempt to refresh with revoked token var refreshResponse = await client.PostAsJsonAsync("/api/v1/auth/refresh", new { refreshToken }); refreshResponse.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); } [Fact] public async Task Me_Authenticated_ReturnsProfile() { var client = await AuthHelper.LoginAsync(_fixture, "approver1"); var response = await client.GetAsync("/api/v1/auth/me"); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await response.Content.ReadFromJsonAsync(); var data = body.GetProperty("data"); data.GetProperty("username").GetString().Should().Be("approver1"); data.GetProperty("role").GetString().Should().Be("CLINICAL_APPROVER"); } }