2026-04-15 15:42:03 -03:00
|
|
|
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
|
|
|
|
|
{
|
2026-04-18 21:44:40 -03:00
|
|
|
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
|
2026-04-15 15:42:03 -03:00
|
|
|
|
|
|
|
|
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]
|
2026-04-19 13:17:31 -03:00
|
|
|
public async Task GetPermisos_WithAdmin_Returns200With27Items()
|
2026-04-15 15:42:03 -03:00
|
|
|
{
|
|
|
|
|
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>();
|
fix(adm-008): correcciones del verify loop
Seis ajustes post-verify detectados durante la corrida full de tests:
1. PuntoDeVentaRepository: UQ_PuntoDeVenta_Medio_AFIP (no _MedioId_NumeroAFIP)
— el catch de unique violation no disparaba → 500 en race duplicado.
2. Application.DependencyInjection: registro de 8 handlers PuntosDeVenta
— sin esto, dispatcher arrojaba "No service registered" → 500.
3. ReservarNumeroCommandHandler: backoff ampliado a 5 retries
[25, 75, 200, 500, 1200]ms para soportar 50 threads concurrentes.
4. SecuenciaComprobante: SYSTEM_VERSIONING = OFF (AD8 revisitado).
Under UPDATE concurrente sobre misma fila, el engine arroja
"transaction time earlier than period start time" — limitación
conocida de Temporal Tables con alta contención de UPDATEs.
Decisión: secuencia es operacional, no configuración → sin history.
V013 y SqlTestFixture actualizados para ser idempotentes.
5. SqlTestFixture: EnsureV013SchemaAsync idempotente + PuntoDeVenta_History
en TablesToIgnore + permiso administracion:puntos_de_venta:gestionar
en seed canónico + asignación a rol admin.
6. Tests: conteos 22→23 permisos (V013 agrega uno); repository fixtures
ignoran PuntoDeVenta_History; test UpdatePdv_WhenPdvInactive eliminado
(over-specified — spec no bloquea update en PdV inactivo, solo en Medio
padre inactivo; alineado con frontend que permite editar PdV inactivo).
Resultado: 190/190 Api.Tests y tests específicos ADM-008 verdes
(Domain 13, Application 42, Api 21 = 76 tests nuevos). El único failure
residual (AuditEventRepositoryTests.QueryAsync_Limit_EmitsCursor) es
pre-existente y no relacionado a ADM-008.
Covers: verify report CRITICAL (UQ name mismatch) + WARNINGs descubiertos
durante la ejecución (DI registro, temporal tables concurrency, permiso
fixture, counts de tests pre-existentes).
2026-04-17 13:02:35 -03:00
|
|
|
// V011 (ADM-001) adds 'administracion:secciones:gestionar' → 22
|
2026-04-17 17:41:25 -03:00
|
|
|
// V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' → 23
|
2026-04-19 07:49:18 -03:00
|
|
|
// V014 (ADM-009) adds 'administracion:fiscal:gestionar' → 24
|
2026-04-19 09:57:11 -03:00
|
|
|
// V016 (CAT-001) adds 'catalogo:rubros:gestionar' → 25
|
2026-04-19 13:17:31 -03:00
|
|
|
// V017 (PRD-001) adds 'catalogo:tipos:gestionar' → 26
|
2026-04-20 12:01:49 -03:00
|
|
|
// V018 (PRD-002) adds 'catalogo:productos:gestionar' → 27
|
|
|
|
|
// V020 (PRC-001) adds 'tasacion:caracteres_especiales:gestionar' → 28 total
|
|
|
|
|
Assert.Equal(28, list.GetArrayLength());
|
2026-04-15 15:42:03 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[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]
|
2026-04-19 13:17:31 -03:00
|
|
|
public async Task GetRolPermisos_AdminRol_Returns200With27Items()
|
2026-04-15 15:42:03 -03:00
|
|
|
{
|
|
|
|
|
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>();
|
fix(adm-008): correcciones del verify loop
Seis ajustes post-verify detectados durante la corrida full de tests:
1. PuntoDeVentaRepository: UQ_PuntoDeVenta_Medio_AFIP (no _MedioId_NumeroAFIP)
— el catch de unique violation no disparaba → 500 en race duplicado.
2. Application.DependencyInjection: registro de 8 handlers PuntosDeVenta
— sin esto, dispatcher arrojaba "No service registered" → 500.
3. ReservarNumeroCommandHandler: backoff ampliado a 5 retries
[25, 75, 200, 500, 1200]ms para soportar 50 threads concurrentes.
4. SecuenciaComprobante: SYSTEM_VERSIONING = OFF (AD8 revisitado).
Under UPDATE concurrente sobre misma fila, el engine arroja
"transaction time earlier than period start time" — limitación
conocida de Temporal Tables con alta contención de UPDATEs.
Decisión: secuencia es operacional, no configuración → sin history.
V013 y SqlTestFixture actualizados para ser idempotentes.
5. SqlTestFixture: EnsureV013SchemaAsync idempotente + PuntoDeVenta_History
en TablesToIgnore + permiso administracion:puntos_de_venta:gestionar
en seed canónico + asignación a rol admin.
6. Tests: conteos 22→23 permisos (V013 agrega uno); repository fixtures
ignoran PuntoDeVenta_History; test UpdatePdv_WhenPdvInactive eliminado
(over-specified — spec no bloquea update en PdV inactivo, solo en Medio
padre inactivo; alineado con frontend que permite editar PdV inactivo).
Resultado: 190/190 Api.Tests y tests específicos ADM-008 verdes
(Domain 13, Application 42, Api 21 = 76 tests nuevos). El único failure
residual (AuditEventRepositoryTests.QueryAsync_Limit_EmitsCursor) es
pre-existente y no relacionado a ADM-008.
Covers: verify report CRITICAL (UQ name mismatch) + WARNINGs descubiertos
durante la ejecución (DI registro, temporal tables concurrency, permiso
fixture, counts de tests pre-existentes).
2026-04-17 13:02:35 -03:00
|
|
|
// V011 (ADM-001) adds 'administracion:secciones:gestionar' → 22
|
2026-04-17 17:41:25 -03:00
|
|
|
// V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' → 23
|
2026-04-19 07:49:18 -03:00
|
|
|
// V014 (ADM-009) adds 'administracion:fiscal:gestionar' → 24
|
2026-04-19 09:57:11 -03:00
|
|
|
// V016 (CAT-001) adds 'catalogo:rubros:gestionar' → 25
|
2026-04-19 13:17:31 -03:00
|
|
|
// V017 (PRD-001) adds 'catalogo:tipos:gestionar' → 26
|
2026-04-20 12:01:49 -03:00
|
|
|
// V018 (PRD-002) adds 'catalogo:productos:gestionar' → 27
|
|
|
|
|
// V020 (PRC-001) adds 'tasacion:caracteres_especiales:gestionar' → 28 total
|
|
|
|
|
Assert.Equal(28, list.GetArrayLength());
|
2026-04-15 15:42:03 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 15:56:49 -03:00
|
|
|
[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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 15:42:03 -03:00
|
|
|
// ── 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-15 16:34:32 -03:00
|
|
|
|
|
|
|
|
// ── 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-15 15:42:03 -03:00
|
|
|
}
|