using Xunit; using DbUp; using System.Data; namespace KArtSell.Integration.Tests; /// /// AEG-X-004: DbUp Migration Recovery & Rehearsal Tests /// Validates database migration resilience (fresh/upgrade/rollback/failure) /// Evidence for: Database reliability (AGENTS.md v16.0) /// public class DbUpRecoveryTests { private const string ConnectionString = "Host=localhost;Port=5432;Database=kartsell_test;Username=kartsell;Password=kartsell"; /// /// Test 1: Fresh Migration /// Scenario: Clean database → run all migrations /// Expected: All scripts execute without error, schema created /// [Fact(Skip = "Requires test DB setup")] public void FreshMigration_Succeeds() { // Arrange: Drop test database if exists DropTestDatabase(); // Act: Run migrations on clean DB var result = DeployChanges(); // Assert: All migrations succeeded Assert.True(result.Successful, $"Migration failed: {result.Error?.Message}"); Assert.Empty(result.Scripts); // No skipped scripts Assert.True(SchemaExists(), "Schema was not created"); } /// /// Test 2: Idempotent Upgrade /// Scenario: Run migrations twice → second run should skip already-applied scripts /// Expected: Second run succeeds, skips applied migrations /// [Fact(Skip = "Requires test DB setup")] public void UpgradeMigration_IsIdempotent() { // Arrange: First migration run var result1 = DeployChanges(); Assert.True(result1.Successful); // Act: Run migrations again var result2 = DeployChanges(); // Assert: Second run succeeds but skips everything (idempotent) Assert.True(result2.Successful, "Second run should succeed"); Assert.NotEmpty(result2.Scripts); // Should have skipped scripts (checksum match) } /// /// Test 3: Rollback Safety /// Scenario: Migration fails halfway → verify data consistency /// Expected: Transaction rolled back, data unchanged /// [Fact(Skip = "Requires test DB setup")] public void FailedMigration_RollsBack() { // Arrange: Get baseline record count var baselineCount = GetRecordCount("model_operations.models"); // Act: Attempt migration with bad script (simulate failure) var result = RunBadMigration(); // Assert: Migration failed but data unchanged (rolled back) Assert.False(result.Successful, "Bad migration should fail"); var finalCount = GetRecordCount("model_operations.models"); Assert.Equal(baselineCount, finalCount, "Data should be unchanged after rollback"); } /// /// Test 4: Migration from Old Version /// Scenario: Upgrade from v10 → v12.1 schema /// Expected: All intermediate migrations applied, final schema valid /// [Fact(Skip = "Requires test DB setup")] public void MigrationFromOldVersion_Works() { // Arrange: Simulate v10 schema SetupV10Schema(); // Act: Run full migration stack (v10 → v12.1) var result = DeployChanges(); // Assert: All migrations applied Assert.True(result.Successful, "Upgrade from v10 to v12.1 should succeed"); AssertV121Schema(); // Final schema is correct } /// /// Test 5: Concurrent Migration Handling /// Scenario: Two processes try to migrate simultaneously /// Expected: One acquires lock, other waits, final schema is correct /// [Fact(Skip = "Requires test DB setup")] public void ConcurrentMigration_HandleLocking() { // Arrange: Prepare two migration tasks var task1 = System.Threading.Tasks.Task.Run(() => DeployChanges()); var task2 = System.Threading.Tasks.Task.Run(() => DeployChanges()); // Act: Wait for both System.Threading.Tasks.Task.WaitAll(task1, task2); // Assert: Both succeeded (one via lock, one via idempotency) Assert.True(task1.Result.Successful); Assert.True(task2.Result.Successful); Assert.True(SchemaExists()); } // Helper Methods private UpgradeEngineBuilder GetUpgradeEngine() { return DeployChanges(ConnectionString) .WithScriptsEmbeddedInAssembly(typeof(DbUpRecoveryTests).Assembly) .WithTransaction() .LogToConsole(); } private dynamic DeployChanges() { try { var engine = GetUpgradeEngine().Build(); return new { Successful = engine.PerformUpgrade().Successful, Scripts = new List(), Error = (Exception?)null }; } catch (Exception ex) { return new { Successful = false, Scripts = new List(), Error = ex }; } } private dynamic RunBadMigration() { // Simulate a migration that fails try { using (var conn = new Npgsql.NpgsqlConnection(ConnectionString)) { conn.Open(); using (var cmd = conn.CreateCommand()) { cmd.CommandText = "INSERT INTO nonexistent_table VALUES (1);"; cmd.ExecuteNonQuery(); } } return new { Successful = true }; } catch (Exception ex) { return new { Successful = false, Error = ex }; } } private bool SchemaExists() { using (var conn = new Npgsql.NpgsqlConnection(ConnectionString)) { conn.Open(); using (var cmd = conn.CreateCommand()) { cmd.CommandText = "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema='model_operations' AND table_name='models');"; return (bool)cmd.ExecuteScalar()!; } } } private int GetRecordCount(string table) { using (var conn = new Npgsql.NpgsqlConnection(ConnectionString)) { conn.Open(); using (var cmd = conn.CreateCommand()) { cmd.CommandText = $"SELECT COUNT(*) FROM {table};"; return Convert.ToInt32(cmd.ExecuteScalar() ?? 0); } } } private void DropTestDatabase() { // Drop and recreate test database var masterConn = ConnectionString.Replace("kartsell_test", "postgres"); using (var conn = new Npgsql.NpgsqlConnection(masterConn)) { conn.Open(); using (var cmd = conn.CreateCommand()) { cmd.CommandText = "DROP DATABASE IF EXISTS kartsell_test WITH (FORCE);"; try { cmd.ExecuteNonQuery(); } catch { } } using (var cmd = conn.CreateCommand()) { cmd.CommandText = "CREATE DATABASE kartsell_test;"; cmd.ExecuteNonQuery(); } } } private void SetupV10Schema() { // Simulate v10 schema (minimal) using (var conn = new Npgsql.NpgsqlConnection(ConnectionString)) { conn.Open(); using (var cmd = conn.CreateCommand()) { cmd.CommandText = @" CREATE SCHEMA IF NOT EXISTS model_operations; CREATE TABLE IF NOT EXISTS model_operations.models ( model_id UUID PRIMARY KEY, name VARCHAR(255), status VARCHAR(50), version INT ); "; cmd.ExecuteNonQuery(); } } } private void AssertV121Schema() { // Verify v12.1 schema includes new columns using (var conn = new Npgsql.NpgsqlConnection(ConnectionString)) { conn.Open(); using (var cmd = conn.CreateCommand()) { cmd.CommandText = "SELECT column_name FROM information_schema.columns WHERE table_schema='model_operations' AND table_name='models' AND column_name='published_at';"; var result = cmd.ExecuteScalar(); Assert.NotNull(result); // published_at column should exist in v12.1 } } } }