Files
SIG-CM2.0/tests/SIGCM2.Application.Tests/Integration/PermisoRepositoryTests.cs
dmolinari 65787db272 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

197 lines
8.8 KiB
C#

using Dapper;
using Microsoft.Data.SqlClient;
using SIGCM2.Infrastructure.Persistence;
namespace SIGCM2.Application.Tests.Integration;
/// <summary>
/// Integration tests for PermisoRepository against SIGCM2_Test.
/// RED: written before the repository implementation exists.
/// </summary>
[Collection("Database")]
public class PermisoRepositoryTests : IAsyncLifetime
{
private const string ConnectionString =
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
private SqlConnection _connection = null!;
private PermisoRepository _repository = null!;
public async Task InitializeAsync()
{
_connection = new SqlConnection(ConnectionString);
await _connection.OpenAsync();
// Ensure the 18 canonical permisos are present — idempotent MERGE.
// Needed because other test classes (RefreshTokenRepositoryTests, UsuarioRepositoryTests)
// may call Respawn.ResetAsync before us, which would clear Permiso even if listed in
// TablesToIgnore of the central SqlTestFixture (each class configures its own Respawner).
await SeedPermisosCanonicalAsync();
var factory = new SqlConnectionFactory(ConnectionString);
_repository = new PermisoRepository(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);
}
public async Task DisposeAsync()
{
await _connection.CloseAsync();
await _connection.DisposeAsync();
}
// ── ListAsync ────────────────────────────────────────────────────────────
[Fact]
public async Task ListAsync_Returns22CanonicalSeeds()
{
var list = await _repository.ListAsync();
// V005 seeds 18 canonical permisos + V007 (UDT-006) adds 3 admin permisos
// + V011 (ADM-001) adds 'administracion:secciones:gestionar'
// + V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' = 23 total
Assert.Equal(23, list.Count);
}
[Fact]
public async Task ListAsync_ContainsExpectedCodigos()
{
var list = await _repository.ListAsync();
var codigos = list.Select(p => p.Codigo).ToHashSet();
Assert.Contains("ventas:contado:crear", codigos);
Assert.Contains("ventas:contado:facturar", codigos);
Assert.Contains("administracion:usuarios:gestionar", codigos);
Assert.Contains("administracion:auditoria:ver", codigos);
}
[Fact]
public async Task ListAsync_AllItemsHaveNonEmptyCodigoAndNombre()
{
var list = await _repository.ListAsync();
foreach (var p in list)
{
Assert.False(string.IsNullOrWhiteSpace(p.Codigo));
Assert.False(string.IsNullOrWhiteSpace(p.Nombre));
}
}
// ── GetByCodigoAsync ─────────────────────────────────────────────────────
[Fact]
public async Task GetByCodigoAsync_ExistingCodigo_ReturnsPermiso()
{
var permiso = await _repository.GetByCodigoAsync("ventas:contado:crear");
Assert.NotNull(permiso);
Assert.Equal("ventas:contado:crear", permiso!.Codigo);
Assert.False(string.IsNullOrWhiteSpace(permiso.Nombre));
}
[Fact]
public async Task GetByCodigoAsync_AnotherExistingCodigo_ReturnsCorrectPermiso()
{
var permiso = await _repository.GetByCodigoAsync("administracion:usuarios:gestionar");
Assert.NotNull(permiso);
Assert.Equal("administracion:usuarios:gestionar", permiso!.Codigo);
}
[Fact]
public async Task GetByCodigoAsync_NonExistentCodigo_ReturnsNull()
{
var permiso = await _repository.GetByCodigoAsync("permiso:inexistente:xyz");
Assert.Null(permiso);
}
// ── GetByCodigosAsync ────────────────────────────────────────────────────
[Fact]
public async Task GetByCodigosAsync_ThreeValidCodigos_ReturnsThreeEntities()
{
var codigos = new[]
{
"ventas:contado:crear",
"ventas:contado:facturar",
"textos:editar"
};
var result = await _repository.GetByCodigosAsync(codigos);
Assert.Equal(3, result.Count);
var returnedCodigos = result.Select(p => p.Codigo).ToHashSet();
foreach (var c in codigos)
Assert.Contains(c, returnedCodigos);
}
[Fact]
public async Task GetByCodigosAsync_MixedExistingAndNonExisting_ReturnsOnlyExisting()
{
var codigos = new[]
{
"ventas:contado:crear",
"permiso:no:existe",
"textos:editar"
};
var result = await _repository.GetByCodigosAsync(codigos);
// Only 2 of 3 exist
Assert.Equal(2, result.Count);
var returnedCodigos = result.Select(p => p.Codigo).ToHashSet();
Assert.Contains("ventas:contado:crear", returnedCodigos);
Assert.Contains("textos:editar", returnedCodigos);
}
[Fact]
public async Task GetByCodigosAsync_EmptyList_ReturnsEmpty()
{
var result = await _repository.GetByCodigosAsync(Array.Empty<string>());
Assert.Empty(result);
}
[Fact]
public async Task GetByCodigosAsync_AllNonExisting_ReturnsEmpty()
{
var codigos = new[] { "no:existe:uno", "no:existe:dos" };
var result = await _repository.GetByCodigosAsync(codigos);
Assert.Empty(result);
}
}