Files
SIG-CM2.0/tests/SIGCM2.Api.Tests/Roles/RolesEndpointTests.cs
dmolinari e5b6c06f64 refactor(tests): Api.Tests apunta a SIGCM2_Test_Api via TestConnectionStrings
Todos los archivos de Api.Tests reemplazan la connection string hardcodeada
por TestConnectionStrings.ApiTestDb. Cada proyecto de tests ahora tiene su
propia base de datos aislada, eliminando la contención entre Application.Tests
y Api.Tests que causaba flakiness.
2026-04-18 21:44:40 -03:00

400 lines
16 KiB
C#

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;
/// <summary>
/// Integration tests for /api/v1/roles (UDT-004).
/// </summary>
[Collection("ApiIntegration")]
public sealed class RolesEndpointTests : IAsyncLifetime
{
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
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<string> 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<JsonElement>();
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<JsonElement>();
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<JsonElement>();
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<JsonElement>();
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<JsonElement>();
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<JsonElement>();
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<JsonElement>();
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<bool>(
"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<JsonElement>();
Assert.Equal("rol_in_use", body.GetProperty("error").GetString());
var activo = await conn.ExecuteScalarAsync<bool>(
"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<JsonElement>();
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<string> 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!");
}
}