2026-04-15 15:39:25 -03:00
|
|
|
using Dapper;
|
|
|
|
|
using Microsoft.Data.SqlClient;
|
|
|
|
|
using SIGCM2.Infrastructure.Persistence;
|
|
|
|
|
|
|
|
|
|
namespace SIGCM2.Application.Tests.Integration;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Integration tests for RolPermisoRepository against SIGCM2_Test.
|
|
|
|
|
/// RED: written before the repository implementation exists.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[Collection("Database")]
|
|
|
|
|
public class RolPermisoRepositoryTests : IAsyncLifetime
|
|
|
|
|
{
|
2026-04-18 21:44:36 -03:00
|
|
|
private const string ConnectionString = TestConnectionStrings.AppTestDb;
|
2026-04-15 15:39:25 -03:00
|
|
|
|
|
|
|
|
private SqlConnection _connection = null!;
|
|
|
|
|
private RolPermisoRepository _repository = null!;
|
|
|
|
|
|
|
|
|
|
public async Task InitializeAsync()
|
|
|
|
|
{
|
|
|
|
|
_connection = new SqlConnection(ConnectionString);
|
|
|
|
|
await _connection.OpenAsync();
|
|
|
|
|
|
|
|
|
|
// Ensure the 18 canonical permisos exist — idempotent MERGE.
|
|
|
|
|
// Other test classes call Respawn.ResetAsync which may have cleared Permiso.
|
|
|
|
|
await SeedPermisosCanonicalAsync();
|
|
|
|
|
|
|
|
|
|
// Ensure canonical RolPermiso seeds are present.
|
|
|
|
|
await SeedRolPermisosCanonicalAsync();
|
|
|
|
|
|
|
|
|
|
// Restore RolPermiso seeds for 'cajero' (4 permisos) in case prior test modified them.
|
|
|
|
|
await RestoreCajeroPermisosAsync();
|
|
|
|
|
|
|
|
|
|
var factory = new SqlConnectionFactory(ConnectionString);
|
|
|
|
|
_repository = new RolPermisoRepository(factory);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task SeedPermisosCanonicalAsync()
|
|
|
|
|
{
|
|
|
|
|
const string sql = """
|
|
|
|
|
SET QUOTED_IDENTIFIER ON;
|
|
|
|
|
MERGE dbo.Permiso AS t
|
|
|
|
|
USING (VALUES
|
|
|
|
|
('ventas:contado:crear', N'Cargar orden contado', NULL, 'ventas'),
|
|
|
|
|
('ventas:contado:modificar', N'Modificar orden contado', NULL, 'ventas'),
|
|
|
|
|
('ventas:contado:cobrar', N'Cobrar orden contado', NULL, 'ventas'),
|
|
|
|
|
('ventas:contado:facturar', N'Facturar orden contado', NULL, 'ventas'),
|
|
|
|
|
('ventas:ctacte:crear', N'Cargar orden cuenta corriente', NULL, 'ventas'),
|
|
|
|
|
('ventas:ctacte:facturar', N'Facturar lote cuenta corriente', NULL, 'ventas'),
|
|
|
|
|
('textos:editar', N'Editar textos', NULL, 'textos'),
|
|
|
|
|
('textos:reclamos:ver', N'Ver reclamos de textos', NULL, 'textos'),
|
|
|
|
|
('pauta:azanu:ver', N'Ver AZANU en pauta', NULL, 'pauta'),
|
|
|
|
|
('pauta:limpiar', N'Limpieza de pauta', NULL, 'pauta'),
|
|
|
|
|
('pauta:recursos:fueradehora', N'Recursos fuera de hora', NULL, 'pauta'),
|
|
|
|
|
('productores:deuda:ver', N'Ver deuda propia de productores', NULL, 'productores'),
|
|
|
|
|
('productores:pendientes:crear', N'Cargar pendientes de productores', NULL, 'productores'),
|
|
|
|
|
('productores:deuda:bypass', N'Bypass de deuda de productores', NULL, 'productores'),
|
|
|
|
|
('administracion:usuarios:gestionar', N'Gestionar usuarios del sistema', N'Crear, editar y desactivar usuarios', 'administracion'),
|
|
|
|
|
('administracion:tarifarios:gestionar', N'Gestionar tarifarios', N'Crear y modificar tarifarios de publicidad', 'administracion'),
|
|
|
|
|
('administracion:medios:gestionar', N'Gestionar medios publicitarios', N'Alta y configuracion de medios', 'administracion'),
|
|
|
|
|
('administracion:auditoria:ver', N'Ver logs de auditoria', N'Acceso al dashboard de auditoria', 'administracion')
|
|
|
|
|
) AS s (Codigo, Nombre, Descripcion, Modulo)
|
|
|
|
|
ON t.Codigo = s.Codigo
|
|
|
|
|
WHEN NOT MATCHED BY TARGET THEN
|
|
|
|
|
INSERT (Codigo, Nombre, Descripcion, Modulo)
|
|
|
|
|
VALUES (s.Codigo, s.Nombre, s.Descripcion, s.Modulo);
|
|
|
|
|
""";
|
|
|
|
|
await _connection.ExecuteAsync(sql);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task SeedRolPermisosCanonicalAsync()
|
|
|
|
|
{
|
|
|
|
|
const string sql = """
|
|
|
|
|
SET QUOTED_IDENTIFIER ON;
|
|
|
|
|
MERGE dbo.RolPermiso AS t
|
|
|
|
|
USING (
|
|
|
|
|
SELECT r.Id AS RolId, p.Id AS PermisoId
|
|
|
|
|
FROM (VALUES
|
|
|
|
|
('admin', 'ventas:contado:crear'),
|
|
|
|
|
('admin', 'ventas:contado:modificar'),
|
|
|
|
|
('admin', 'ventas:contado:cobrar'),
|
|
|
|
|
('admin', 'ventas:contado:facturar'),
|
|
|
|
|
('admin', 'ventas:ctacte:crear'),
|
|
|
|
|
('admin', 'ventas:ctacte:facturar'),
|
|
|
|
|
('admin', 'textos:editar'),
|
|
|
|
|
('admin', 'textos:reclamos:ver'),
|
|
|
|
|
('admin', 'pauta:azanu:ver'),
|
|
|
|
|
('admin', 'pauta:limpiar'),
|
|
|
|
|
('admin', 'pauta:recursos:fueradehora'),
|
|
|
|
|
('admin', 'productores:deuda:ver'),
|
|
|
|
|
('admin', 'productores:pendientes:crear'),
|
|
|
|
|
('admin', 'productores:deuda:bypass'),
|
|
|
|
|
('admin', 'administracion:usuarios:gestionar'),
|
|
|
|
|
('admin', 'administracion:tarifarios:gestionar'),
|
|
|
|
|
('admin', 'administracion:medios:gestionar'),
|
|
|
|
|
('admin', 'administracion:auditoria:ver'),
|
|
|
|
|
('cajero', 'ventas:contado:crear'),
|
|
|
|
|
('cajero', 'ventas:contado:modificar'),
|
|
|
|
|
('cajero', 'ventas:contado:cobrar'),
|
|
|
|
|
('cajero', 'ventas:contado:facturar'),
|
|
|
|
|
('operador_ctacte', 'ventas:ctacte:crear'),
|
|
|
|
|
('operador_ctacte', 'ventas:ctacte:facturar'),
|
|
|
|
|
('picadora', 'textos:editar'),
|
|
|
|
|
('picadora', 'textos:reclamos:ver'),
|
|
|
|
|
('jefe_publicidad', 'textos:editar'),
|
|
|
|
|
('jefe_publicidad', 'textos:reclamos:ver'),
|
|
|
|
|
('jefe_publicidad', 'pauta:azanu:ver'),
|
|
|
|
|
('jefe_publicidad', 'pauta:limpiar'),
|
|
|
|
|
('jefe_publicidad', 'pauta:recursos:fueradehora'),
|
|
|
|
|
('jefe_publicidad', 'productores:deuda:ver'),
|
|
|
|
|
('jefe_publicidad', 'productores:deuda:bypass'),
|
|
|
|
|
('productor', 'productores:deuda:ver'),
|
|
|
|
|
('productor', 'productores:pendientes:crear'),
|
|
|
|
|
('diagramacion', 'pauta:azanu:ver')
|
|
|
|
|
) 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);
|
|
|
|
|
""";
|
|
|
|
|
await _connection.ExecuteAsync(sql);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task DisposeAsync()
|
|
|
|
|
{
|
|
|
|
|
// Restore cajero permisos so TablesToIgnore still reflects clean seed state.
|
|
|
|
|
await RestoreCajeroPermisosAsync();
|
|
|
|
|
await _connection.CloseAsync();
|
|
|
|
|
await _connection.DisposeAsync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Restores the 4 canonical cajero permisos to match V006 seed state.
|
|
|
|
|
/// Uses MERGE so it's idempotent.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task RestoreCajeroPermisosAsync()
|
|
|
|
|
{
|
|
|
|
|
// Delete any extra permisos assigned to cajero by tests
|
|
|
|
|
await _connection.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 the 4 canonical cajero permisos if missing
|
|
|
|
|
await _connection.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);
|
|
|
|
|
""");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── GetByRolCodigoAsync ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
[Fact]
|
2026-04-19 09:46:31 -03:00
|
|
|
public async Task GetByRolCodigoAsync_Admin_Returns26Permisos()
|
2026-04-15 15:39:25 -03:00
|
|
|
{
|
2026-04-16 19:04:06 -03:00
|
|
|
// admin has 18 permisos from V006 + 3 new admin permisos from V007 (UDT-006)
|
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
|
|
|
// + 1 from V011 (ADM-001): 'administracion:secciones:gestionar'
|
2026-04-17 17:41:30 -03:00
|
|
|
// + 1 from V013 (ADM-008): 'administracion:puntos_de_venta:gestionar'
|
2026-04-19 07:49:18 -03:00
|
|
|
// + 1 from V014 (ADM-009): 'administracion:fiscal:gestionar'
|
2026-04-19 09:46:31 -03:00
|
|
|
// + 1 from V016 (CAT-001): 'catalogo:rubros:gestionar'
|
|
|
|
|
// + 1 from V017 (PRD-001): 'catalogo:tipos:gestionar' = 26 total
|
2026-04-15 15:39:25 -03:00
|
|
|
var permisos = await _repository.GetByRolCodigoAsync("admin");
|
|
|
|
|
|
2026-04-19 09:46:31 -03:00
|
|
|
Assert.Equal(26, permisos.Count);
|
2026-04-15 15:39:25 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetByRolCodigoAsync_Admin_ContainsAllModules()
|
|
|
|
|
{
|
|
|
|
|
var permisos = await _repository.GetByRolCodigoAsync("admin");
|
|
|
|
|
var codigos = permisos.Select(p => p.Codigo).ToHashSet();
|
|
|
|
|
|
|
|
|
|
Assert.Contains("ventas:contado:crear", codigos);
|
|
|
|
|
Assert.Contains("administracion:auditoria:ver", codigos);
|
|
|
|
|
Assert.Contains("pauta:limpiar", codigos);
|
|
|
|
|
Assert.Contains("productores:deuda:bypass", codigos);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetByRolCodigoAsync_Cajero_Returns4Permisos()
|
|
|
|
|
{
|
|
|
|
|
// cajero: ventas:contado:crear, :modificar, :cobrar, :facturar
|
|
|
|
|
var permisos = await _repository.GetByRolCodigoAsync("cajero");
|
|
|
|
|
|
|
|
|
|
Assert.Equal(4, permisos.Count);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetByRolCodigoAsync_Cajero_OnlyVentasContadoPermisos()
|
|
|
|
|
{
|
|
|
|
|
var permisos = await _repository.GetByRolCodigoAsync("cajero");
|
|
|
|
|
var codigos = permisos.Select(p => p.Codigo).ToHashSet();
|
|
|
|
|
|
|
|
|
|
Assert.Contains("ventas:contado:crear", codigos);
|
|
|
|
|
Assert.Contains("ventas:contado:modificar", codigos);
|
|
|
|
|
Assert.Contains("ventas:contado:cobrar", codigos);
|
|
|
|
|
Assert.Contains("ventas:contado:facturar", codigos);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetByRolCodigoAsync_Reportes_ReturnsEmpty()
|
|
|
|
|
{
|
|
|
|
|
// 'reportes' rol has 0 permisos in V006 seed
|
|
|
|
|
var permisos = await _repository.GetByRolCodigoAsync("reportes");
|
|
|
|
|
|
|
|
|
|
Assert.Empty(permisos);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetByRolCodigoAsync_NonExistentRol_ReturnsEmpty()
|
|
|
|
|
{
|
|
|
|
|
// Unknown rol código — returns empty list, not an exception
|
|
|
|
|
var permisos = await _repository.GetByRolCodigoAsync("rol_inexistente_xyz");
|
|
|
|
|
|
|
|
|
|
Assert.Empty(permisos);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetByRolCodigoAsync_ReturnsFullPermisoEntities()
|
|
|
|
|
{
|
|
|
|
|
var permisos = await _repository.GetByRolCodigoAsync("cajero");
|
|
|
|
|
|
|
|
|
|
var primero = permisos.First();
|
|
|
|
|
// All entity fields must be populated
|
|
|
|
|
Assert.True(primero.Id > 0);
|
|
|
|
|
Assert.False(string.IsNullOrWhiteSpace(primero.Codigo));
|
|
|
|
|
Assert.False(string.IsNullOrWhiteSpace(primero.Nombre));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── ReplaceForRolAsync ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task ReplaceForRolAsync_ReplacesExistingSetWithNewSet()
|
|
|
|
|
{
|
|
|
|
|
// Get cajero's rol ID and a different permiso ID
|
|
|
|
|
var cajeroId = await _connection.ExecuteScalarAsync<int>(
|
|
|
|
|
"SELECT Id FROM dbo.Rol WHERE Codigo = 'cajero'");
|
|
|
|
|
var textoPermisoId = await _connection.ExecuteScalarAsync<int>(
|
|
|
|
|
"SELECT Id FROM dbo.Permiso WHERE Codigo = 'textos:editar'");
|
|
|
|
|
|
|
|
|
|
// Replace cajero's 4 permisos with just 1
|
|
|
|
|
await _repository.ReplaceForRolAsync(cajeroId, new[] { textoPermisoId });
|
|
|
|
|
|
|
|
|
|
var permisos = await _repository.GetByRolCodigoAsync("cajero");
|
|
|
|
|
Assert.Single(permisos);
|
|
|
|
|
Assert.Equal("textos:editar", permisos[0].Codigo);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task ReplaceForRolAsync_Idempotent_SameCallTwiceProducesSameResult()
|
|
|
|
|
{
|
|
|
|
|
var cajeroId = await _connection.ExecuteScalarAsync<int>(
|
|
|
|
|
"SELECT Id FROM dbo.Rol WHERE Codigo = 'cajero'");
|
|
|
|
|
var permisoId = await _connection.ExecuteScalarAsync<int>(
|
|
|
|
|
"SELECT Id FROM dbo.Permiso WHERE Codigo = 'ventas:contado:crear'");
|
|
|
|
|
|
|
|
|
|
await _repository.ReplaceForRolAsync(cajeroId, new[] { permisoId });
|
|
|
|
|
await _repository.ReplaceForRolAsync(cajeroId, new[] { permisoId });
|
|
|
|
|
|
|
|
|
|
var permisos = await _repository.GetByRolCodigoAsync("cajero");
|
|
|
|
|
Assert.Single(permisos);
|
|
|
|
|
Assert.Equal("ventas:contado:crear", permisos[0].Codigo);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task ReplaceForRolAsync_WithEmptyList_DeletesAllPermisos()
|
|
|
|
|
{
|
|
|
|
|
var cajeroId = await _connection.ExecuteScalarAsync<int>(
|
|
|
|
|
"SELECT Id FROM dbo.Rol WHERE Codigo = 'cajero'");
|
|
|
|
|
|
|
|
|
|
await _repository.ReplaceForRolAsync(cajeroId, Array.Empty<int>());
|
|
|
|
|
|
|
|
|
|
var permisos = await _repository.GetByRolCodigoAsync("cajero");
|
|
|
|
|
Assert.Empty(permisos);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task ReplaceForRolAsync_PostReplace_GetReflectsNewSet()
|
|
|
|
|
{
|
|
|
|
|
var cajeroId = await _connection.ExecuteScalarAsync<int>(
|
|
|
|
|
"SELECT Id FROM dbo.Rol WHERE Codigo = 'cajero'");
|
|
|
|
|
|
|
|
|
|
// Get IDs of 2 specific permisos
|
|
|
|
|
var rows = await _connection.QueryAsync<(int Id, string Codigo)>(
|
|
|
|
|
"SELECT Id, Codigo FROM dbo.Permiso WHERE Codigo IN ('pauta:azanu:ver', 'pauta:limpiar')");
|
|
|
|
|
var ids = rows.Select(r => r.Id).ToArray();
|
|
|
|
|
|
|
|
|
|
await _repository.ReplaceForRolAsync(cajeroId, ids);
|
|
|
|
|
|
|
|
|
|
var permisos = await _repository.GetByRolCodigoAsync("cajero");
|
|
|
|
|
var codigos = permisos.Select(p => p.Codigo).ToHashSet();
|
|
|
|
|
|
|
|
|
|
Assert.Equal(2, permisos.Count);
|
|
|
|
|
Assert.Contains("pauta:azanu:ver", codigos);
|
|
|
|
|
Assert.Contains("pauta:limpiar", codigos);
|
|
|
|
|
}
|
|
|
|
|
}
|