using Dapper; using SIGCM2.TestSupport; namespace SIGCM2.Application.Tests.Integration; /// /// SUITE-B-MIGRATION-V009 — M-01 a M-07 (UDT-009) /// Validates the V009 migration SQL and SqlTestFixture.EnsureV009SchemaAsync. /// Uses SIGCM2_Test_App database via shared SqlTestFixture. /// [Collection("Database")] public sealed class V009MigrationTests : IAsyncLifetime { private readonly SqlTestFixture _db; public V009MigrationTests(SqlTestFixture db) { _db = db; } public async Task InitializeAsync() { await _db.ResetAndSeedAsync(); } public Task DisposeAsync() => Task.CompletedTask; // M-01: migration file exists on filesystem [Fact] public void MigrationFile_Exists() { // Walk up from test assembly looking for the repo root var dir = new DirectoryInfo(AppContext.BaseDirectory); string? repoRoot = null; while (dir is not null) { if (dir.GetFiles("SIGCM2.slnx").Length > 0) { repoRoot = dir.FullName; break; } dir = dir.Parent; } // Known fallback if (repoRoot is null && Directory.Exists(@"E:\SIG-CM2.0")) repoRoot = @"E:\SIG-CM2.0"; Assert.NotNull(repoRoot); var migrationPath = Path.Combine(repoRoot!, "database", "migrations", "V009__activate_permisos_overrides.sql"); Assert.True(File.Exists(migrationPath), $"Migration file not found at: {migrationPath}"); } // M-02: re-run idempotent (EnsureV009SchemaAsync twice → no error) [Fact] public async Task EnsureV009SchemaAsync_IsIdempotent() { await EnsureV009SchemaAsync(); await EnsureV009SchemaAsync(); // second call must not throw } // M-03: after migration, DEFAULT constraint is the new shape [Fact] public async Task EnsureV009SchemaAsync_DefaultConstraint_IsNewShape() { await EnsureV009SchemaAsync(); const string sql = """ SELECT object_definition(default_object_id) AS DefaultDef FROM sys.columns WHERE object_id = OBJECT_ID('dbo.Usuario') AND name = 'PermisosJson' """; var definition = await _db.Connection.QuerySingleOrDefaultAsync(sql); Assert.NotNull(definition); Assert.Contains(@"{""grant"":[]", definition); Assert.Contains(@"""deny"":[]}", definition); } // M-04: rows with '[]' are migrated to new shape [Fact] public async Task EnsureV009SchemaAsync_MigratesLegacyEmptyArray() { await EnsureV009SchemaAsync(); await _db.Connection.ExecuteAsync(""" INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword) VALUES ('legacyempty', '$2a$12$hash', 'L', 'E', 'admin', '[]', 1, 0) """); // Run migration again to migrate the newly inserted row await EnsureV009SchemaAsync(); var permisosJson = await _db.Connection.QuerySingleAsync( "SELECT PermisosJson FROM dbo.Usuario WHERE Username = 'legacyempty'"); Assert.Equal("""{"grant":[],"deny":[]}""", permisosJson); } // M-05: rows with '["*"]' are migrated [Fact] public async Task EnsureV009SchemaAsync_MigratesLegacyWildcard() { await EnsureV009SchemaAsync(); await _db.Connection.ExecuteAsync(""" INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword) VALUES ('legacywild', '$2a$12$hash', 'L', 'W', 'admin', '["*"]', 1, 0) """); await EnsureV009SchemaAsync(); var permisosJson = await _db.Connection.QuerySingleAsync( "SELECT PermisosJson FROM dbo.Usuario WHERE Username = 'legacywild'"); Assert.Equal("""{"grant":[],"deny":[]}""", permisosJson); } // M-06: the migration UPDATE statement includes NULL / empty-string conditions // The column is NOT NULL (V001 constraint), so we verify the UPDATE logic covers // all the WHERE conditions syntactically and that rows with '' are migrated. [Fact] public async Task EnsureV009SchemaAsync_MigratesEmptyStringRows() { // First apply V009 schema so the constraint is updated await EnsureV009SchemaAsync(); // Temporarily drop and re-add without the DEFAULT so we can insert '' await _db.Connection.ExecuteAsync(""" IF EXISTS ( SELECT 1 FROM sys.default_constraints WHERE name = 'DF_Usuario_Permisos' AND parent_object_id = OBJECT_ID('dbo.Usuario') ) ALTER TABLE dbo.Usuario DROP CONSTRAINT DF_Usuario_Permisos; """); await _db.Connection.ExecuteAsync(""" INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword) VALUES ('emptystruser', '$2a$12$hash', 'E', 'S', 'admin', '', 1, 0) """); // Re-apply V009 (which restores constraint and migrates '' rows) await EnsureV009SchemaAsync(); var permisosJson = await _db.Connection.QuerySingleAsync( "SELECT PermisosJson FROM dbo.Usuario WHERE Username = 'emptystruser'"); Assert.Equal("""{"grant":[],"deny":[]}""", permisosJson); } // M-07: admin seed in SqlTestFixture uses new shape [Fact] public async Task SqlTestFixture_SeedAdmin_UsesNewPermisosJsonShape() { await EnsureV009SchemaAsync(); // Seed admin as TestFixture does post-V009 await _db.Connection.ExecuteAsync(""" IF NOT EXISTS (SELECT 1 FROM dbo.Usuario WHERE Username = 'admin') INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword) VALUES ( 'admin', '$2a$12$rmq6tlSAQ8WXhR2CwLCSeuwCJKz/.8Eab95UQCUNfwe4dokeOqMcW', 'Administrador', 'Sistema', 'admin', '{"grant":[],"deny":[]}', 1, 0 ) """); var permisosJson = await _db.Connection.QuerySingleAsync( "SELECT PermisosJson FROM dbo.Usuario WHERE Username = 'admin'"); Assert.Equal("""{"grant":[],"deny":[]}""", permisosJson); } // ── helpers ─────────────────────────────────────────────────────────────── /// /// Replicates V009 migration idempotently — mirrors SqlTestFixture.EnsureV009SchemaAsync. /// private async Task EnsureV009SchemaAsync() { const string dropConstraint = """ IF EXISTS ( SELECT 1 FROM sys.default_constraints WHERE name = 'DF_Usuario_Permisos' AND parent_object_id = OBJECT_ID('dbo.Usuario') ) BEGIN ALTER TABLE dbo.Usuario DROP CONSTRAINT DF_Usuario_Permisos; END """; const string addConstraint = """ IF NOT EXISTS ( SELECT 1 FROM sys.default_constraints WHERE name = 'DF_Usuario_Permisos' AND parent_object_id = OBJECT_ID('dbo.Usuario') ) BEGIN ALTER TABLE dbo.Usuario ADD CONSTRAINT DF_Usuario_Permisos DEFAULT('{"grant":[],"deny":[]}') FOR PermisosJson; END """; const string migrateRows = """ UPDATE dbo.Usuario SET PermisosJson = '{"grant":[],"deny":[]}' WHERE PermisosJson IN ('[]', '["*"]', '') OR PermisosJson IS NULL OR LTRIM(RTRIM(PermisosJson)) = '' """; await _db.Connection.ExecuteAsync(dropConstraint); await _db.Connection.ExecuteAsync(addConstraint); await _db.Connection.ExecuteAsync(migrateRows); } }