Follow-up of B1 (V010 migration). Issues found when running the full suite
cross-assembly:
1. Respawn 'Cannot delete rows from a temporal history table' error:
4 per-class Respawner configs in SIGCM2.Application.Tests did not
include the newly-created *_History tables introduced by V010
(Usuario_History / Rol_History / Permiso_History / RolPermiso_History).
The engine rejects direct DELETE on system-versioned history tables.
Extended TablesToIgnore in all 4 configs.
2. FK_RefreshToken_Usuario violation in RolRepositoryTests.InitializeAsync:
Manual 'DELETE FROM Usuario' failed when residual RefreshTokens from
prior suites existed. Added 'DELETE FROM RefreshToken' before the
Usuario cleanup to respect FK order. Latent bug surfaced by a new
test-run ordering — not UDT-010 specific, but fixed in scope.
3. UQ_Usuario_Username duplicate admin race:
TransactionScopeSpikeTests (B0) and V010MigrationTests (B1) were
missing [Collection("ApiIntegration")], causing them to run in
parallel with the rest of SIGCM2.Api.Tests and race on SeedAdmin.
Serialized by adding the Collection attribute.
Suite now passes cross-assembly: 130/130 Api.Tests + 336/336 Application.Tests.
Refs: sdd/udt-010-auditoria-trazabilidad/apply-progress (B1 follow-up)
243 lines
10 KiB
C#
243 lines
10 KiB
C#
using Dapper;
|
|
using Microsoft.Data.SqlClient;
|
|
using SIGCM2.Domain.Entities;
|
|
using SIGCM2.Infrastructure.Persistence;
|
|
|
|
namespace SIGCM2.Application.Tests.Integration;
|
|
|
|
[Collection("Database")]
|
|
public class RolRepositoryTests : IAsyncLifetime
|
|
{
|
|
private const string ConnectionString =
|
|
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
|
|
|
private SqlConnection _connection = null!;
|
|
private RolRepository _repository = null!;
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
_connection = new SqlConnection(ConnectionString);
|
|
await _connection.OpenAsync();
|
|
|
|
// Clean RefreshToken first (FK to Usuario), then Usuario (FK to Rol), then custom Rol codes.
|
|
// Residual RefreshTokens from prior test suites would violate FK_RefreshToken_Usuario otherwise.
|
|
await _connection.ExecuteAsync("DELETE FROM dbo.RefreshToken;");
|
|
await _connection.ExecuteAsync("DELETE FROM dbo.Usuario;");
|
|
await _connection.ExecuteAsync("DELETE FROM dbo.Rol WHERE Codigo NOT IN ('admin','cajero','operador_ctacte','picadora','jefe_publicidad','productor','diagramacion','reportes');");
|
|
// Ensure canonical Rol seeds exist (idempotent — previous test classes may have wiped them via Respawn).
|
|
await SeedRolCanonicalAsync();
|
|
// Reset any mutations applied to canonical seeds during prior tests.
|
|
await _connection.ExecuteAsync("UPDATE dbo.Rol SET Activo = 1, FechaModificacion = NULL WHERE Codigo IN ('admin','cajero','operador_ctacte','picadora','jefe_publicidad','productor','diagramacion','reportes');");
|
|
// Seed admin usuario (needed by HasActiveUsuariosAsync test expecting admin active).
|
|
await _connection.ExecuteAsync(
|
|
"INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo) " +
|
|
"VALUES ('admin', '$2a$12$hash', 'Administrador', 'Sistema', 'admin', '[\"*\"]', 1);");
|
|
|
|
var factory = new SqlConnectionFactory(ConnectionString);
|
|
_repository = new RolRepository(factory);
|
|
}
|
|
|
|
public async Task DisposeAsync()
|
|
{
|
|
await _connection.CloseAsync();
|
|
await _connection.DisposeAsync();
|
|
}
|
|
|
|
private async Task SeedRolCanonicalAsync()
|
|
{
|
|
const string sql = """
|
|
SET QUOTED_IDENTIFIER ON;
|
|
MERGE dbo.Rol AS t
|
|
USING (VALUES
|
|
('admin', N'Administrador', N'Supervisor total'),
|
|
('cajero', N'Cajero', N'Mostrador contado'),
|
|
('operador_ctacte', N'Operador Cta Cte', N'Cuenta corriente'),
|
|
('picadora', N'Picadora/Correctora', N'Edición de textos'),
|
|
('jefe_publicidad', N'Jefe de Publicidad', N'Supervisión de pauta'),
|
|
('productor', N'Productor', N'Carga restringida'),
|
|
('diagramacion', N'Diagramación/Taller', N'Solo lectura pauta'),
|
|
('reportes', N'Reportes', N'Solo lectura reportes')
|
|
) AS s (Codigo, Nombre, Descripcion)
|
|
ON t.Codigo = s.Codigo
|
|
WHEN NOT MATCHED BY TARGET THEN
|
|
INSERT (Codigo, Nombre, Descripcion, Activo)
|
|
VALUES (s.Codigo, s.Nombre, s.Descripcion, 1);
|
|
""";
|
|
await _connection.ExecuteAsync(sql);
|
|
}
|
|
|
|
// ── ListAsync ────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task ListAsync_ReturnsAllCanonicalSeeds()
|
|
{
|
|
var list = await _repository.ListAsync();
|
|
|
|
var codes = list.Select(r => r.Codigo).ToHashSet();
|
|
Assert.Contains("admin", codes);
|
|
Assert.Contains("cajero", codes);
|
|
Assert.Contains("operador_ctacte", codes);
|
|
Assert.Contains("picadora", codes);
|
|
Assert.Contains("jefe_publicidad", codes);
|
|
Assert.Contains("productor", codes);
|
|
Assert.Contains("diagramacion", codes);
|
|
Assert.Contains("reportes", codes);
|
|
Assert.True(list.Count >= 8);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ListAsync_IncludesInactiveRoles()
|
|
{
|
|
// Triangulation: list must include deactivated rows too.
|
|
await _connection.ExecuteAsync("INSERT INTO dbo.Rol (Codigo, Nombre, Activo) VALUES ('listtest_inactive', N'Inactivo de test', 0);");
|
|
|
|
var list = await _repository.ListAsync();
|
|
|
|
var inactive = list.Single(r => r.Codigo == "listtest_inactive");
|
|
Assert.False(inactive.Activo);
|
|
}
|
|
|
|
// ── GetByCodigoAsync ────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task GetByCodigoAsync_ExistingCodigo_ReturnsRol()
|
|
{
|
|
var rol = await _repository.GetByCodigoAsync("cajero");
|
|
|
|
Assert.NotNull(rol);
|
|
Assert.Equal("cajero", rol!.Codigo);
|
|
Assert.Equal("Cajero", rol.Nombre);
|
|
Assert.True(rol.Activo);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetByCodigoAsync_NonExistentCodigo_ReturnsNull()
|
|
{
|
|
var rol = await _repository.GetByCodigoAsync("no_existe");
|
|
Assert.Null(rol);
|
|
}
|
|
|
|
// ── ExistsActiveByCodigoAsync ───────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task ExistsActiveByCodigoAsync_ActiveCodigo_ReturnsTrue()
|
|
{
|
|
Assert.True(await _repository.ExistsActiveByCodigoAsync("admin"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExistsActiveByCodigoAsync_InactiveCodigo_ReturnsFalse()
|
|
{
|
|
await _connection.ExecuteAsync("INSERT INTO dbo.Rol (Codigo, Nombre, Activo) VALUES ('exists_inactive', N'Test inactivo', 0);");
|
|
|
|
Assert.False(await _repository.ExistsActiveByCodigoAsync("exists_inactive"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExistsActiveByCodigoAsync_MissingCodigo_ReturnsFalse()
|
|
{
|
|
Assert.False(await _repository.ExistsActiveByCodigoAsync("missing_codigo_xyz"));
|
|
}
|
|
|
|
// ── AddAsync ────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task AddAsync_NewRol_PersistsAndReturnsId()
|
|
{
|
|
var rol = Rol.ForCreation("addtest_new", "Add Test", "Rol de prueba add");
|
|
|
|
var newId = await _repository.AddAsync(rol);
|
|
|
|
Assert.True(newId > 0);
|
|
|
|
var persisted = await _connection.QuerySingleAsync<(string Codigo, string Nombre, string? Descripcion, bool Activo)>(
|
|
"SELECT Codigo, Nombre, Descripcion, Activo FROM dbo.Rol WHERE Id = @Id",
|
|
new { Id = newId });
|
|
|
|
Assert.Equal("addtest_new", persisted.Codigo);
|
|
Assert.Equal("Add Test", persisted.Nombre);
|
|
Assert.Equal("Rol de prueba add", persisted.Descripcion);
|
|
Assert.True(persisted.Activo);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddAsync_WithNullDescripcion_PersistsNull()
|
|
{
|
|
var rol = Rol.ForCreation("addtest_nulldesc", "Null Desc", null);
|
|
|
|
var newId = await _repository.AddAsync(rol);
|
|
|
|
var desc = await _connection.ExecuteScalarAsync<string?>(
|
|
"SELECT Descripcion FROM dbo.Rol WHERE Id = @Id", new { Id = newId });
|
|
Assert.Null(desc);
|
|
}
|
|
|
|
// ── UpdateAsync ─────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task UpdateAsync_ExistingCodigo_UpdatesMutableFieldsAndSetsFechaModificacion()
|
|
{
|
|
await _connection.ExecuteAsync(
|
|
"INSERT INTO dbo.Rol (Codigo, Nombre, Descripcion, Activo) VALUES ('updtest_one', N'Nombre Viejo', N'Desc vieja', 1);");
|
|
|
|
var updated = await _repository.UpdateAsync("updtest_one", "Nombre Nuevo", "Desc nueva", activo: true);
|
|
|
|
Assert.True(updated);
|
|
|
|
var row = await _connection.QuerySingleAsync<(string Nombre, string? Descripcion, bool Activo, DateTime? FechaModificacion)>(
|
|
"SELECT Nombre, Descripcion, Activo, FechaModificacion FROM dbo.Rol WHERE Codigo = 'updtest_one'");
|
|
|
|
Assert.Equal("Nombre Nuevo", row.Nombre);
|
|
Assert.Equal("Desc nueva", row.Descripcion);
|
|
Assert.True(row.Activo);
|
|
Assert.NotNull(row.FechaModificacion);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateAsync_NonExistentCodigo_ReturnsFalse()
|
|
{
|
|
var updated = await _repository.UpdateAsync("updtest_missing", "X", null, true);
|
|
Assert.False(updated);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateAsync_DoesNotChangeCodigo()
|
|
{
|
|
await _connection.ExecuteAsync(
|
|
"INSERT INTO dbo.Rol (Codigo, Nombre, Activo) VALUES ('updtest_codigo', N'Test Codigo', 1);");
|
|
|
|
await _repository.UpdateAsync("updtest_codigo", "Nombre Cambiado", null, true);
|
|
|
|
var stillExists = await _connection.ExecuteScalarAsync<int>(
|
|
"SELECT COUNT(*) FROM dbo.Rol WHERE Codigo = 'updtest_codigo';");
|
|
Assert.Equal(1, stillExists);
|
|
}
|
|
|
|
// ── HasActiveUsuariosAsync ──────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task HasActiveUsuariosAsync_WithActiveUsuario_ReturnsTrue()
|
|
{
|
|
// 'admin' Usuario is seeded active and references Rol.admin.
|
|
Assert.True(await _repository.HasActiveUsuariosAsync("admin"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task HasActiveUsuariosAsync_NoUsuariosReferencing_ReturnsFalse()
|
|
{
|
|
// 'reportes' seed has no Usuario referencing it in a clean test DB.
|
|
Assert.False(await _repository.HasActiveUsuariosAsync("reportes"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task HasActiveUsuariosAsync_OnlyInactiveUsuarioReferencing_ReturnsFalse()
|
|
{
|
|
// Insert an INACTIVE usuario referencing 'cajero'.
|
|
await _connection.ExecuteAsync(
|
|
"INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo) " +
|
|
"VALUES ('inactivo1', '$2a$12$hash', 'Test', 'Inactivo', 'cajero', '[]', 0);");
|
|
|
|
Assert.False(await _repository.HasActiveUsuariosAsync("cajero"));
|
|
}
|
|
}
|