using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using Dapper; using Microsoft.Data.SqlClient; using SIGCM2.TestSupport; namespace SIGCM2.Api.Tests.Roles; /// /// Integration tests for /api/v1/roles (UDT-004). /// [Collection("ApiIntegration")] public sealed class RolesEndpointTests : IAsyncLifetime { private const string TestConnectionString = "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; private const string Endpoint = "/api/v1/roles"; private const string AdminUsername = "admin"; private const string AdminPassword = "@Diego550@"; private readonly HttpClient _client; public RolesEndpointTests(TestWebAppFactory factory) { _client = factory.CreateClient(); } public Task InitializeAsync() => Task.CompletedTask; public Task DisposeAsync() => Task.CompletedTask; private async Task GetBearerTokenAsync(string username, string password) { var response = await _client.PostAsJsonAsync("/api/v1/auth/login", new { username, password }); if (!response.IsSuccessStatusCode) { var body = await response.Content.ReadAsStringAsync(); throw new InvalidOperationException($"Login failed ({(int)response.StatusCode}): {body}"); } var json = await response.Content.ReadFromJsonAsync(); return json.GetProperty("accessToken").GetString()!; } private HttpRequestMessage BuildRequest(HttpMethod method, string url, object? body = null, string? bearerToken = null) { var request = new HttpRequestMessage(method, url); if (bearerToken is not null) request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); if (body is not null) request.Content = JsonContent.Create(body); return request; } private static async Task DeleteRolIfExistsAsync(string codigo) { await using var conn = new SqlConnection(TestConnectionString); await conn.OpenAsync(); await conn.ExecuteAsync("DELETE FROM dbo.Rol WHERE Codigo = @Codigo", new { Codigo = codigo }); } private static async Task DeleteUsuarioIfExistsAsync(string username) { await using var conn = new SqlConnection(TestConnectionString); await conn.OpenAsync(); await conn.ExecuteAsync(""" DELETE rt FROM dbo.RefreshToken rt INNER JOIN dbo.Usuario u ON u.Id = rt.UsuarioId WHERE u.Username = @Username """, new { Username = username }); await conn.ExecuteAsync("DELETE FROM dbo.Usuario WHERE Username = @Username", new { Username = username }); } // ── 401 / 403 guards ──────────────────────────────────────────────────── [Fact] public async Task List_WithoutAuth_Returns401() { var resp = await _client.SendAsync(BuildRequest(HttpMethod.Get, Endpoint)); Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode); } [Fact] public async Task Create_WithNonAdmin_Returns403() { var adminToken = await GetBearerTokenAsync(AdminUsername, AdminPassword); const string nonAdminUser = "rolestest_nonadmin"; // Create a non-admin user via endpoint (admin can still create users). using var mkUser = BuildRequest(HttpMethod.Post, "/api/v1/users", new { username = nonAdminUser, password = "Secure1234!", nombre = "Non", apellido = "Admin", email = (string?)null, rol = "cajero" }, adminToken); var mkUserResp = await _client.SendAsync(mkUser); if (mkUserResp.StatusCode != HttpStatusCode.Created && mkUserResp.StatusCode != HttpStatusCode.Conflict) Assert.Fail($"Seed non-admin user failed: {mkUserResp.StatusCode}"); try { var cajeroToken = await GetBearerTokenAsync(nonAdminUser, "Secure1234!"); using var req = BuildRequest(HttpMethod.Post, Endpoint, new { codigo = "nuevo_test", nombre = "Test", descripcion = (string?)null }, cajeroToken); var resp = await _client.SendAsync(req); Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode); } finally { await DeleteUsuarioIfExistsAsync(nonAdminUser); } } // ── List ──────────────────────────────────────────────────────────────── [Fact] public async Task List_WithAdmin_Returns200WithCanonicalSeeds() { var token = await GetBearerTokenAsync(AdminUsername, AdminPassword); using var req = BuildRequest(HttpMethod.Get, Endpoint, bearerToken: token); var resp = await _client.SendAsync(req); Assert.Equal(HttpStatusCode.OK, resp.StatusCode); var list = await resp.Content.ReadFromJsonAsync(); var codes = list.EnumerateArray().Select(r => r.GetProperty("codigo").GetString()).ToHashSet(); foreach (var c in new[] { "admin", "cajero", "operador_ctacte", "picadora", "jefe_publicidad", "productor", "diagramacion", "reportes" }) Assert.Contains(c, codes); } // ── Get ───────────────────────────────────────────────────────────────── [Fact] public async Task GetByCodigo_Existing_Returns200() { var token = await GetBearerTokenAsync(AdminUsername, AdminPassword); var resp = await _client.SendAsync(BuildRequest(HttpMethod.Get, $"{Endpoint}/cajero", bearerToken: token)); Assert.Equal(HttpStatusCode.OK, resp.StatusCode); var body = await resp.Content.ReadFromJsonAsync(); Assert.Equal("cajero", body.GetProperty("codigo").GetString()); Assert.Equal("Cajero", body.GetProperty("nombre").GetString()); Assert.True(body.GetProperty("activo").GetBoolean()); } [Fact] public async Task GetByCodigo_NonExistent_Returns404() { var token = await GetBearerTokenAsync(AdminUsername, AdminPassword); var resp = await _client.SendAsync(BuildRequest(HttpMethod.Get, $"{Endpoint}/no_existe_xyz", bearerToken: token)); Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode); var body = await resp.Content.ReadFromJsonAsync(); Assert.Equal("rol_not_found", body.GetProperty("error").GetString()); } // ── Create ────────────────────────────────────────────────────────────── [Fact] public async Task Create_NewRol_Returns201() { var token = await GetBearerTokenAsync(AdminUsername, AdminPassword); const string codigo = "endpoint_new_rol"; try { using var req = BuildRequest(HttpMethod.Post, Endpoint, new { codigo, nombre = "Endpoint New", descripcion = "Creado por integration test" }, token); var resp = await _client.SendAsync(req); Assert.Equal(HttpStatusCode.Created, resp.StatusCode); var body = await resp.Content.ReadFromJsonAsync(); Assert.Equal(codigo, body.GetProperty("codigo").GetString()); Assert.True(body.GetProperty("id").GetInt32() > 0); Assert.True(body.GetProperty("activo").GetBoolean()); } finally { await DeleteRolIfExistsAsync(codigo); } } [Fact] public async Task Create_CodigoDuplicado_Returns409() { var token = await GetBearerTokenAsync(AdminUsername, AdminPassword); using var req = BuildRequest(HttpMethod.Post, Endpoint, new { codigo = "cajero", nombre = "Duplicate", descripcion = (string?)null }, token); var resp = await _client.SendAsync(req); Assert.Equal(HttpStatusCode.Conflict, resp.StatusCode); var body = await resp.Content.ReadFromJsonAsync(); Assert.Equal("rol_already_exists", body.GetProperty("error").GetString()); } [Fact] public async Task Create_InvalidCodigoFormat_Returns400() { var token = await GetBearerTokenAsync(AdminUsername, AdminPassword); using var req = BuildRequest(HttpMethod.Post, Endpoint, new { codigo = "Cajero Senior", // uppercase + space — invalid nombre = "Bad", descripcion = (string?)null }, token); var resp = await _client.SendAsync(req); Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode); } // ── Update ────────────────────────────────────────────────────────────── [Fact] public async Task Update_Existing_Returns200WithUpdatedNombre() { var token = await GetBearerTokenAsync(AdminUsername, AdminPassword); const string codigo = "endpoint_upd_rol"; // Seed a rol await using var conn = new SqlConnection(TestConnectionString); await conn.OpenAsync(); await conn.ExecuteAsync( "INSERT INTO dbo.Rol (Codigo, Nombre, Descripcion, Activo) VALUES (@Codigo, N'Viejo', N'Desc vieja', 1);", new { Codigo = codigo }); try { using var req = BuildRequest(HttpMethod.Put, $"{Endpoint}/{codigo}", new { nombre = "Nuevo Nombre", descripcion = "Desc nueva", activo = true }, token); var resp = await _client.SendAsync(req); Assert.Equal(HttpStatusCode.OK, resp.StatusCode); var body = await resp.Content.ReadFromJsonAsync(); Assert.Equal("Nuevo Nombre", body.GetProperty("nombre").GetString()); Assert.Equal("Desc nueva", body.GetProperty("descripcion").GetString()); Assert.Equal(codigo, body.GetProperty("codigo").GetString()); } finally { await DeleteRolIfExistsAsync(codigo); } } [Fact] public async Task Update_NonExistent_Returns404() { var token = await GetBearerTokenAsync(AdminUsername, AdminPassword); using var req = BuildRequest(HttpMethod.Put, $"{Endpoint}/inexistente_abc", new { nombre = "X", descripcion = (string?)null, activo = true }, token); var resp = await _client.SendAsync(req); Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode); } // ── Delete (soft) ─────────────────────────────────────────────────────── [Fact] public async Task Delete_WithoutActiveUsuarios_Returns204() { var token = await GetBearerTokenAsync(AdminUsername, AdminPassword); const string codigo = "endpoint_del_rol"; await using var conn = new SqlConnection(TestConnectionString); await conn.OpenAsync(); await conn.ExecuteAsync( "INSERT INTO dbo.Rol (Codigo, Nombre, Activo) VALUES (@Codigo, N'Temp', 1);", new { Codigo = codigo }); try { var resp = await _client.SendAsync(BuildRequest(HttpMethod.Delete, $"{Endpoint}/{codigo}", bearerToken: token)); Assert.Equal(HttpStatusCode.NoContent, resp.StatusCode); var activo = await conn.ExecuteScalarAsync( "SELECT Activo FROM dbo.Rol WHERE Codigo = @Codigo", new { Codigo = codigo }); Assert.False(activo); } finally { await DeleteRolIfExistsAsync(codigo); } } [Fact] public async Task Delete_WithActiveUsuarios_Returns409() { var token = await GetBearerTokenAsync(AdminUsername, AdminPassword); const string codigo = "endpoint_del_inuse"; const string testUser = "endpoint_del_user"; await using var conn = new SqlConnection(TestConnectionString); await conn.OpenAsync(); await conn.ExecuteAsync( "INSERT INTO dbo.Rol (Codigo, Nombre, Activo) VALUES (@Codigo, N'InUse', 1);", new { Codigo = codigo }); await conn.ExecuteAsync( "INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo) " + "VALUES (@Username, '$2a$12$hash', 'Test', 'User', @Codigo, '[]', 1);", new { Username = testUser, Codigo = codigo }); try { var resp = await _client.SendAsync(BuildRequest(HttpMethod.Delete, $"{Endpoint}/{codigo}", bearerToken: token)); Assert.Equal(HttpStatusCode.Conflict, resp.StatusCode); var body = await resp.Content.ReadFromJsonAsync(); Assert.Equal("rol_in_use", body.GetProperty("error").GetString()); var activo = await conn.ExecuteScalarAsync( "SELECT Activo FROM dbo.Rol WHERE Codigo = @Codigo", new { Codigo = codigo }); Assert.True(activo); } finally { await DeleteUsuarioIfExistsAsync(testUser); await DeleteRolIfExistsAsync(codigo); } } [Fact] public async Task Delete_NonExistent_Returns404() { var token = await GetBearerTokenAsync(AdminUsername, AdminPassword); var resp = await _client.SendAsync(BuildRequest(HttpMethod.Delete, $"{Endpoint}/no_existe_del", bearerToken: token)); Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode); } // ── UDT-006: 403 ProblemDetails shape ──────────────────────────────────── [Fact] public async Task GetRoles_WithCajeroToken_Returns403WithProblemDetailsShape() { const string username = "udt006_roles_403_cajero"; try { var token = await CreateCajeroTokenAsync(username); var resp = await _client.SendAsync(BuildRequest(HttpMethod.Get, Endpoint, bearerToken: token)); Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode); Assert.Contains("problem+json", resp.Content.Headers.ContentType?.MediaType ?? ""); var json = await resp.Content.ReadFromJsonAsync(); Assert.Equal(403, json.GetProperty("status").GetInt32()); Assert.Equal("Acceso denegado", json.GetProperty("title").GetString()); Assert.True(json.TryGetProperty("permisoRequerido", out var perm), "Response must contain 'permisoRequerido'"); // RolesController migra a administracion:roles:gestionar Assert.Equal("administracion:roles:gestionar", perm.GetString()); } finally { await DeleteUsuarioIfExistsAsync(username); } } // Helper: create cajero user via SQL and return token private async Task CreateCajeroTokenAsync(string username) { var adminToken = await GetBearerTokenAsync(AdminUsername, AdminPassword); using var mkUser = BuildRequest(HttpMethod.Post, "/api/v1/users", new { username, password = "Secure1234!", nombre = "Cajero", apellido = "Test", email = (string?)null, rol = "cajero" }, adminToken); var mkResp = await _client.SendAsync(mkUser); if (mkResp.StatusCode != HttpStatusCode.Created && mkResp.StatusCode != HttpStatusCode.Conflict) Assert.Fail($"Seed cajero failed: {mkResp.StatusCode}"); return await GetBearerTokenAsync(username, "Secure1234!"); } }