using System.Net; using System.Net.Http.Json; using System.Text.Json; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; [Collection("Database")] public class UserManagementTests : IAsyncLifetime { private readonly ApiFixture _fixture; private HttpClient _client = null!; public UserManagementTests(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); _client = await AuthHelper.LoginAsync(_fixture, "admin1"); } public Task DisposeAsync() => Task.CompletedTask; [Fact] public async Task CreateUser_ValidRequest_Returns201WithUser() { var request = new { Username = "newclerk1", Password = "Secure1Pass", FullName = "New Clerk", Role = "DATA_ENTRY_CLERK" }; var response = await _client.PostAsJsonAsync("/api/v1/users", request); response.StatusCode.Should().Be(HttpStatusCode.Created); var body = await response.Content.ReadFromJsonAsync(); var data = body.GetProperty("data"); data.GetProperty("username").GetString().Should().Be("newclerk1"); data.GetProperty("fullName").GetString().Should().Be("New Clerk"); data.GetProperty("role").GetString().Should().Be("DATA_ENTRY_CLERK"); data.GetProperty("id").GetGuid().Should().NotBeEmpty(); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var user = await db.Users.FirstOrDefaultAsync(u => u.Username == "newclerk1"); user.Should().NotBeNull(); user!.IsActive.Should().BeTrue(); } [Fact] public async Task CreateUser_DuplicateUsername_Returns409() { var request = new { Username = "entry1", Password = "Secure1Pass", FullName = "Duplicate Entry", Role = "DATA_ENTRY_CLERK" }; var response = await _client.PostAsJsonAsync("/api/v1/users", request); response.StatusCode.Should().Be(HttpStatusCode.Conflict); var body = await response.Content.ReadFromJsonAsync(); body.GetProperty("error").GetProperty("code").GetString().Should().Be("USERNAME_TAKEN"); } [Fact] public async Task CreateUser_WeakPassword_Returns422() { var request = new { Username = "weakuser", Password = "short", FullName = "Weak Password User", Role = "VERIFIER" }; var response = await _client.PostAsJsonAsync("/api/v1/users", request); response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); } [Fact] public async Task CreateUser_NonAdmin_Returns403() { var entryClient = await AuthHelper.LoginAsync(_fixture, "entry1"); var request = new { Username = "unauthorized", Password = "Secure1Pass", FullName = "Unauthorized Create", Role = "VERIFIER" }; var response = await entryClient.PostAsJsonAsync("/api/v1/users", request); response.StatusCode.Should().Be(HttpStatusCode.Forbidden); } [Fact] public async Task UpdateUser_ChangeRoleAndName_Returns200() { // First create a user to update var createReq = new { Username = "toupdate1", Password = "Secure1Pass", FullName = "Original Name", Role = "DATA_ENTRY_CLERK" }; var createResp = await _client.PostAsJsonAsync("/api/v1/users", createReq); createResp.StatusCode.Should().Be(HttpStatusCode.Created); var created = await createResp.Content.ReadFromJsonAsync(); var userId = created.GetProperty("data").GetProperty("id").GetGuid(); // Update var updateReq = new { FullName = "Updated Name", Role = "VERIFIER" }; var response = await _client.PatchAsJsonAsync($"/api/v1/users/{userId}", updateReq); response.StatusCode.Should().Be(HttpStatusCode.OK); var body = await response.Content.ReadFromJsonAsync(); var data = body.GetProperty("data"); data.GetProperty("fullName").GetString().Should().Be("Updated Name"); data.GetProperty("role").GetString().Should().Be("VERIFIER"); } [Fact] public async Task UpdateUser_Deactivate_Returns200() { var createReq = new { Username = "todeactivate1", Password = "Secure1Pass", FullName = "Will Be Deactivated", Role = "INTAKE_CLERK" }; var createResp = await _client.PostAsJsonAsync("/api/v1/users", createReq); createResp.StatusCode.Should().Be(HttpStatusCode.Created); var created = await createResp.Content.ReadFromJsonAsync(); var userId = created.GetProperty("data").GetProperty("id").GetGuid(); var response = await _client.PatchAsJsonAsync( $"/api/v1/users/{userId}", new { IsActive = false }); response.StatusCode.Should().Be(HttpStatusCode.OK); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var user = await db.Users.FindAsync(userId); user!.IsActive.Should().BeFalse(); } [Fact] public async Task UpdateUser_NotFound_Returns404() { var response = await _client.PatchAsJsonAsync( $"/api/v1/users/{Guid.NewGuid()}", new { FullName = "Ghost" }); response.StatusCode.Should().Be(HttpStatusCode.NotFound); } [Fact] public async Task ResetPassword_AdminResets_Returns204() { var createReq = new { Username = "resetme1", Password = "OldPass1Word", FullName = "Reset Target", Role = "DATA_ENTRY_CLERK" }; var createResp = await _client.PostAsJsonAsync("/api/v1/users", createReq); createResp.StatusCode.Should().Be(HttpStatusCode.Created); var created = await createResp.Content.ReadFromJsonAsync(); var userId = created.GetProperty("data").GetProperty("id").GetGuid(); var response = await _client.PostAsJsonAsync( $"/api/v1/users/{userId}/reset-password", new { NewPassword = "NewPass1Word" }); response.StatusCode.Should().Be(HttpStatusCode.NoContent); // Verify new password works by logging in var loginClient = _fixture.CreateClient(); var loginResp = await loginClient.PostAsJsonAsync("/api/v1/auth/login", new { username = "resetme1", password = "NewPass1Word" }); loginResp.StatusCode.Should().Be(HttpStatusCode.OK); } [Fact] public async Task ChangePassword_CorrectCurrent_Returns204() { // Create a user, then log in as them and change their own password var createReq = new { Username = "selfchange1", Password = "Original1Pass", FullName = "Self Changer", Role = "VERIFIER" }; await _client.PostAsJsonAsync("/api/v1/users", createReq); var userClient = await AuthHelper.LoginAsync(_fixture, "selfchange1", "Original1Pass"); var response = await userClient.PostAsJsonAsync("/api/v1/users/me/change-password", new { CurrentPassword = "Original1Pass", NewPassword = "Changed1Pass" }); response.StatusCode.Should().Be(HttpStatusCode.NoContent); // Verify new password works var loginClient = _fixture.CreateClient(); var loginResp = await loginClient.PostAsJsonAsync("/api/v1/auth/login", new { username = "selfchange1", password = "Changed1Pass" }); loginResp.StatusCode.Should().Be(HttpStatusCode.OK); } [Fact] public async Task ChangePassword_WrongCurrent_Returns422() { var createReq = new { Username = "wrongpw1", Password = "Correct1Pass", FullName = "Wrong Password", Role = "VERIFIER" }; await _client.PostAsJsonAsync("/api/v1/users", createReq); var userClient = await AuthHelper.LoginAsync(_fixture, "wrongpw1", "Correct1Pass"); var response = await userClient.PostAsJsonAsync("/api/v1/users/me/change-password", new { CurrentPassword = "Wrong1Pass", NewPassword = "New1Pass" }); response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); var body = await response.Content.ReadFromJsonAsync(); body.GetProperty("error").GetProperty("code").GetString().Should().Be("INVALID_PASSWORD"); } }