Files
SIG-CM2.0/tests/SIGCM2.Api.Tests/Roles/RolesEndpointTests.cs

354 lines
14 KiB
C#
Raw Normal View History

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 =
"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<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);
}
}