using Dapper; using Microsoft.Data.SqlClient; using Respawn; using SIGCM2.Infrastructure.Persistence; namespace SIGCM2.Application.Tests.Integration; /// /// Integration tests for IUsuarioRepository.UpdatePermisosJsonAsync (UDT-009). /// Uses SIGCM2_Test database directly. /// [Collection("Database")] public sealed class UsuarioRepository_PermisosTests : IAsyncLifetime { private const string ConnectionString = "Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;"; private SqlConnection _connection = null!; private Respawner _respawner = null!; private UsuarioRepository _repository = null!; public async Task InitializeAsync() { _connection = new SqlConnection(ConnectionString); await _connection.OpenAsync(); _respawner = await Respawner.CreateAsync(_connection, new RespawnerOptions { DbAdapter = DbAdapter.SqlServer, TablesToIgnore = [ new Respawn.Graph.Table("dbo", "Rol"), new Respawn.Graph.Table("dbo", "Permiso"), new Respawn.Graph.Table("dbo", "RolPermiso"), // UDT-010: *_History tables are system-versioned — engine rejects direct DELETE. new Respawn.Graph.Table("dbo", "Usuario_History"), new Respawn.Graph.Table("dbo", "Rol_History"), new Respawn.Graph.Table("dbo", "Permiso_History"), new Respawn.Graph.Table("dbo", "RolPermiso_History"), ] }); await _respawner.ResetAsync(_connection); await SeedRolCanonicalAsync(); var factory = new SqlConnectionFactory(ConnectionString); _repository = new UsuarioRepository(factory); // Seed a test user await _connection.ExecuteAsync(""" INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword) VALUES ('testuser', '$2a$12$hash', 'Test', 'User', 'cajero', '{"grant":[],"deny":[]}', 1, 0) """); } public async Task DisposeAsync() { if (_connection is not null) { await _respawner.ResetAsync(_connection); await _connection.CloseAsync(); await _connection.DisposeAsync(); } } // UPJ-01: UpdatePermisosJsonAsync persists PermisosJson and FechaModificacion [Fact] public async Task UpdatePermisosJsonAsync_PersistsJsonAndFechaModificacion() { // Arrange var userId = await _connection.QuerySingleAsync( "SELECT Id FROM dbo.Usuario WHERE Username = 'testuser'"); var newJson = """{"grant":["textos:editar"],"deny":[]}"""; var fechaMod = DateTime.UtcNow; // Act await _repository.UpdatePermisosJsonAsync(userId, newJson, fechaMod); // Assert var row = await _connection.QuerySingleAsync<(string PermisosJson, DateTime? FechaModificacion)>( "SELECT PermisosJson, FechaModificacion FROM dbo.Usuario WHERE Id = @Id", new { Id = userId }); Assert.Equal(newJson, row.PermisosJson); Assert.NotNull(row.FechaModificacion); // Allow 2-second tolerance for DB round-trip Assert.True( Math.Abs((row.FechaModificacion!.Value - fechaMod).TotalSeconds) < 2, $"FechaModificacion {row.FechaModificacion} is too far from {fechaMod}"); } // UPJ-02: UpdatePermisosJsonAsync with non-existent id → no throw (UPDATE affects 0 rows) [Fact] public async Task UpdatePermisosJsonAsync_NonExistentId_NoThrow() { // Should not throw — UPDATE with 0 rows affected is a no-op await _repository.UpdatePermisosJsonAsync(99999, """{"grant":[],"deny":[]}""", DateTime.UtcNow); } // UPJ-03: GetByIdAsync after update reflects new PermisosJson [Fact] public async Task UpdatePermisosJsonAsync_GetByIdReflectsChange() { // Arrange var userId = await _connection.QuerySingleAsync( "SELECT Id FROM dbo.Usuario WHERE Username = 'testuser'"); var newJson = """{"grant":["pauta:azanu:ver"],"deny":["ventas:contado:cobrar"]}"""; // Act await _repository.UpdatePermisosJsonAsync(userId, newJson, DateTime.UtcNow); // Assert — read back through the repo var usuario = await _repository.GetByIdAsync(userId); Assert.NotNull(usuario); Assert.Equal(newJson, usuario!.PermisosJson); } // ── helpers ─────────────────────────────────────────────────────────────── private async Task SeedRolCanonicalAsync() { await _connection.ExecuteAsync(""" SET QUOTED_IDENTIFIER ON; MERGE dbo.Rol AS t USING (VALUES ('admin', N'Administrador', N'Supervisor total'), ('cajero', N'Cajero', N'Mostrador contado') ) 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); """); } }