add: Integration test gaps for deferred promotion and retry
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for authentication flow: login, refresh, logout, and role-based access.
|
||||
/// </summary>
|
||||
[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<AppDbContext>();
|
||||
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<JsonElement>();
|
||||
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<JsonElement>();
|
||||
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<JsonElement>();
|
||||
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<JsonElement>();
|
||||
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<JsonElement>();
|
||||
var data = body.GetProperty("data");
|
||||
data.GetProperty("username").GetString().Should().Be("approver1");
|
||||
data.GetProperty("role").GetString().Should().Be("CLINICAL_APPROVER");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user