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);
}
}