Files
SIG-CM2.0/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs

489 lines
19 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.Permisos;
/// <summary>
/// Integration tests for /api/v1/permisos and /api/v1/roles/{codigo}/permisos (UDT-005).
/// RED: written before PermisosController exists.
/// </summary>
[Collection("ApiIntegration")]
public sealed class PermisosEndpointTests : IAsyncLifetime
{
private const string TestConnectionString =
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
private const string AdminUsername = "admin";
private const string AdminPassword = "@Diego550@";
private readonly HttpClient _client;
public PermisosEndpointTests(TestWebAppFactory factory)
{
_client = factory.CreateClient();
}
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync() => Task.CompletedTask;
// ── Helpers ──────────────────────────────────────────────────────────────
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 RestoreCajeroPermisosAsync()
{
await using var conn = new SqlConnection(TestConnectionString);
await conn.OpenAsync();
// Remove any test-added permisos from cajero
await conn.ExecuteAsync("""
DELETE rp FROM dbo.RolPermiso rp
JOIN dbo.Rol r ON r.Id = rp.RolId
JOIN dbo.Permiso p ON p.Id = rp.PermisoId
WHERE r.Codigo = 'cajero'
AND p.Codigo NOT IN (
'ventas:contado:crear','ventas:contado:modificar',
'ventas:contado:cobrar','ventas:contado:facturar'
);
""");
// Re-add missing canonical cajero permisos
await conn.ExecuteAsync("""
SET QUOTED_IDENTIFIER ON;
MERGE dbo.RolPermiso AS t
USING (
SELECT r.Id AS RolId, p.Id AS PermisoId
FROM (VALUES
('cajero','ventas:contado:crear'),
('cajero','ventas:contado:modificar'),
('cajero','ventas:contado:cobrar'),
('cajero','ventas:contado:facturar')
) AS x (RolCodigo, PermisoCodigo)
JOIN dbo.Rol r ON r.Codigo = x.RolCodigo
JOIN dbo.Permiso p ON p.Codigo = x.PermisoCodigo
) AS s ON t.RolId = s.RolId AND t.PermisoId = s.PermisoId
WHEN NOT MATCHED BY TARGET THEN
INSERT (RolId, PermisoId) VALUES (s.RolId, s.PermisoId);
""");
}
private async Task<string> CreateNonAdminUserAndGetTokenAsync(string username, string rol = "cajero")
{
var adminToken = await GetBearerTokenAsync(AdminUsername, AdminPassword);
// Create non-admin user via API
using var mkUser = BuildRequest(HttpMethod.Post, "/api/v1/users", new
{
username,
password = "Secure1234!",
nombre = "Non",
apellido = "Admin",
email = (string?)null,
rol
}, 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}");
return await GetBearerTokenAsync(username, "Secure1234!");
}
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 });
}
// ── GET /api/v1/permisos — catalog ───────────────────────────────────────
[Fact]
public async Task GetPermisos_WithAdmin_Returns200With22Items()
{
var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
using var req = BuildRequest(HttpMethod.Get, "/api/v1/permisos", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
var list = await resp.Content.ReadFromJsonAsync<JsonElement>();
// V011 (ADM-001) adds 'administracion:secciones:gestionar' → 22 total
Assert.Equal(22, list.GetArrayLength());
}
[Fact]
public async Task GetPermisos_WithoutToken_Returns401()
{
var resp = await _client.SendAsync(BuildRequest(HttpMethod.Get, "/api/v1/permisos"));
Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
}
[Fact]
public async Task GetPermisos_WithNonAdminToken_Returns403()
{
const string username = "perm_nonadmin_list";
try
{
var token = await CreateNonAdminUserAndGetTokenAsync(username);
using var req = BuildRequest(HttpMethod.Get, "/api/v1/permisos", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode);
}
finally
{
await DeleteUsuarioIfExistsAsync(username);
}
}
[Fact]
public async Task GetPermisos_ResponseContainsCodigoNombreFields()
{
var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
using var req = BuildRequest(HttpMethod.Get, "/api/v1/permisos", bearerToken: token);
var resp = await _client.SendAsync(req);
var list = await resp.Content.ReadFromJsonAsync<JsonElement>();
var first = list.EnumerateArray().First();
Assert.True(first.TryGetProperty("codigo", out _), "Response item missing 'codigo' field");
Assert.True(first.TryGetProperty("nombre", out _), "Response item missing 'nombre' field");
}
// ── GET /api/v1/roles/{codigo}/permisos ──────────────────────────────────
[Fact]
public async Task GetRolPermisos_AdminRol_Returns200With22Items()
{
var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
using var req = BuildRequest(HttpMethod.Get, "/api/v1/roles/admin/permisos", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
var list = await resp.Content.ReadFromJsonAsync<JsonElement>();
// V011 (ADM-001) adds 'administracion:secciones:gestionar' → 22 total
Assert.Equal(22, list.GetArrayLength());
}
[Fact]
public async Task GetRolPermisos_CajeroRol_Returns200With4Items()
{
var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
using var req = BuildRequest(HttpMethod.Get, "/api/v1/roles/cajero/permisos", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
var list = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal(4, list.GetArrayLength());
}
[Fact]
public async Task GetRolPermisos_ReportesRol_Returns200WithEmptyArray()
{
var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
using var req = BuildRequest(HttpMethod.Get, "/api/v1/roles/reportes/permisos", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
var list = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal(0, list.GetArrayLength());
}
[Fact]
public async Task GetRolPermisos_InexistentRol_Returns404()
{
var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
using var req = BuildRequest(HttpMethod.Get, "/api/v1/roles/rol_inexistente_xyz/permisos", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
}
[Fact]
public async Task GetRolPermisos_WithoutToken_Returns401()
{
var resp = await _client.SendAsync(BuildRequest(HttpMethod.Get, "/api/v1/roles/admin/permisos"));
Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
}
[Fact]
public async Task GetRolPermisos_WithNonAdminToken_Returns403()
{
const string username = "perm_nonadmin_getRol";
try
{
var token = await CreateNonAdminUserAndGetTokenAsync(username);
using var req = BuildRequest(HttpMethod.Get, "/api/v1/roles/admin/permisos", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode);
}
finally
{
await DeleteUsuarioIfExistsAsync(username);
}
}
[Fact]
public async Task GetRolPermisos_InvalidCodigoFormat_Returns400()
{
var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
// "ROL-INVALIDO" no matchea ^[a-z][a-z0-9_]*$ (tiene guion y mayúsculas)
using var req = BuildRequest(HttpMethod.Get, "/api/v1/roles/ROL-INVALIDO/permisos", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
}
// ── PUT /api/v1/roles/{codigo}/permisos ──────────────────────────────────
[Fact]
public async Task PutRolPermisos_ValidAssignment_Returns200WithUpdatedSet()
{
var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
try
{
using var req = BuildRequest(
HttpMethod.Put,
"/api/v1/roles/cajero/permisos",
new { codigos = new[] { "ventas:contado:crear" } },
token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
var list = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal(1, list.GetArrayLength());
Assert.Equal("ventas:contado:crear", list[0].GetProperty("codigo").GetString());
}
finally
{
await RestoreCajeroPermisosAsync();
}
}
[Fact]
public async Task PutRolPermisos_ThenGet_ReturnsUpdatedSet()
{
var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
try
{
// Assign 1 permiso to cajero
using var putReq = BuildRequest(
HttpMethod.Put,
"/api/v1/roles/cajero/permisos",
new { codigos = new[] { "textos:editar" } },
token);
var putResp = await _client.SendAsync(putReq);
Assert.Equal(HttpStatusCode.OK, putResp.StatusCode);
// GET should now return 1 item
using var getReq = BuildRequest(HttpMethod.Get, "/api/v1/roles/cajero/permisos", bearerToken: token);
var getResp = await _client.SendAsync(getReq);
Assert.Equal(HttpStatusCode.OK, getResp.StatusCode);
var list = await getResp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal(1, list.GetArrayLength());
Assert.Equal("textos:editar", list[0].GetProperty("codigo").GetString());
}
finally
{
await RestoreCajeroPermisosAsync();
}
}
[Fact]
public async Task PutRolPermisos_Idempotent_TwoCallsSameResult()
{
var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
try
{
var body = new { codigos = new[] { "ventas:contado:crear", "textos:editar" } };
using var req1 = BuildRequest(HttpMethod.Put, "/api/v1/roles/cajero/permisos", body, token);
var resp1 = await _client.SendAsync(req1);
Assert.Equal(HttpStatusCode.OK, resp1.StatusCode);
using var req2 = BuildRequest(HttpMethod.Put, "/api/v1/roles/cajero/permisos", body, token);
var resp2 = await _client.SendAsync(req2);
Assert.Equal(HttpStatusCode.OK, resp2.StatusCode);
var list2 = await resp2.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal(2, list2.GetArrayLength());
}
finally
{
await RestoreCajeroPermisosAsync();
}
}
[Fact]
public async Task PutRolPermisos_AdminWithEmptyList_Returns400()
{
var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
using var req = BuildRequest(
HttpMethod.Put,
"/api/v1/roles/admin/permisos",
new { codigos = Array.Empty<string>() },
token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
}
[Fact]
public async Task PutRolPermisos_NonExistentPermiso_Returns404()
{
var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
using var req = BuildRequest(
HttpMethod.Put,
"/api/v1/roles/cajero/permisos",
new { codigos = new[] { "permiso:no:existe" } },
token);
var resp = await _client.SendAsync(req);
// Validator rejects unknown codes with 400 (not in catalog) before handler can 404
// The validator checks Permiso.Todos — if code not in static catalog → 400
Assert.True(
resp.StatusCode == HttpStatusCode.BadRequest || resp.StatusCode == HttpStatusCode.NotFound,
$"Expected 400 or 404 but got {resp.StatusCode}");
}
[Fact]
public async Task PutRolPermisos_InexistentRol_Returns404()
{
var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
using var req = BuildRequest(
HttpMethod.Put,
"/api/v1/roles/rol_inexistente_xyz/permisos",
new { codigos = new[] { "ventas:contado:crear" } },
token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
}
[Fact]
public async Task PutRolPermisos_WithoutToken_Returns401()
{
var resp = await _client.SendAsync(BuildRequest(
HttpMethod.Put,
"/api/v1/roles/cajero/permisos",
new { codigos = new[] { "ventas:contado:crear" } }));
Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
}
[Fact]
public async Task PutRolPermisos_WithNonAdminToken_Returns403()
{
const string username = "perm_nonadmin_put";
try
{
var token = await CreateNonAdminUserAndGetTokenAsync(username);
using var req = BuildRequest(
HttpMethod.Put,
"/api/v1/roles/cajero/permisos",
new { codigos = new[] { "ventas:contado:crear" } },
token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode);
}
finally
{
await DeleteUsuarioIfExistsAsync(username);
}
}
// ── UDT-006: 403 ProblemDetails shape ─────────────────────────────────────
[Fact]
public async Task GetPermisos_WithCajeroToken_Returns403WithProblemDetailsShape()
{
const string username = "udt006_permisos_403_cajero";
try
{
var token = await CreateNonAdminUserAndGetTokenAsync(username);
using var req = BuildRequest(HttpMethod.Get, "/api/v1/permisos", bearerToken: token);
var resp = await _client.SendAsync(req);
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'");
// GET /permisos migra a administracion:permisos:ver
Assert.Equal("administracion:permisos:ver", perm.GetString());
}
finally
{
await DeleteUsuarioIfExistsAsync(username);
}
}
[Fact]
public async Task PutRolPermisos_WithCajeroToken_Returns403WithProblemDetailsShape()
{
const string username = "udt006_put_permisos_403";
try
{
var token = await CreateNonAdminUserAndGetTokenAsync(username);
using var req = BuildRequest(
HttpMethod.Put,
"/api/v1/roles/cajero/permisos",
new { codigos = new[] { "ventas:contado:crear" } },
token);
var resp = await _client.SendAsync(req);
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.True(json.TryGetProperty("permisoRequerido", out var perm),
"Response must contain 'permisoRequerido'");
// PUT /roles/{c}/permisos migra a administracion:roles_permisos:gestionar
Assert.Equal("administracion:roles_permisos:gestionar", perm.GetString());
}
finally
{
await DeleteUsuarioIfExistsAsync(username);
}
}
}