using ItWorx.KnowledgeHub.Infrastructure.Persistence; using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure; using Microsoft.EntityFrameworkCore; using Npgsql; namespace ItWorx.KnowledgeHub.IntegrationTests.Operations; /// Proves M7-A02 ("Startup refuses unsafe schema drift") against a real, /// freshly migrated PostgreSQL database — forward migration (the normal path) must /// stay silent, and a database with an applied migration this build's assembly does /// not know about must be refused before any data-mutating call. [Collection(PostgresTestGroup.Name)] public sealed class SchemaGuardTests(PostgresContainerFixture postgres) { [Fact] public async Task A_freshly_migrated_database_with_only_known_migrations_is_accepted() { await using var target = await RestoreTargetDatabase.CreateAsync(postgres); var options = new DbContextOptionsBuilder() .UseNpgsql(target.ConnectionString, npgsql => npgsql.MigrationsAssembly(typeof(KnowledgeHubDbContext).Assembly.GetName().Name)) .UseSnakeCaseNamingConvention() .Options; await using var db = new KnowledgeHubDbContext(options); await SchemaGuard.EnsureCompatibleAsync(db.Database, CancellationToken.None); } [Fact] public async Task An_unmigrated_brand_new_database_is_accepted_nothing_can_be_ahead_of_an_empty_history() { var databaseName = $"schema_guard_fresh_{Guid.NewGuid():N}"; await using (var admin = new NpgsqlConnection(postgres.AdminConnectionString)) { await admin.OpenAsync(); await using var create = new NpgsqlCommand($"CREATE DATABASE \"{databaseName}\"", admin); await create.ExecuteNonQueryAsync(); } try { var connectionString = new NpgsqlConnectionStringBuilder(postgres.AdminConnectionString) { Database = databaseName }.ConnectionString; var options = new DbContextOptionsBuilder() .UseNpgsql(connectionString, npgsql => npgsql.MigrationsAssembly(typeof(KnowledgeHubDbContext).Assembly.GetName().Name)) .UseSnakeCaseNamingConvention() .Options; await using var db = new KnowledgeHubDbContext(options); // No exception, even though the migrations-history table does not exist yet. await SchemaGuard.EnsureCompatibleAsync(db.Database, CancellationToken.None); } finally { await using var admin = new NpgsqlConnection(postgres.AdminConnectionString); await admin.OpenAsync(); await using var drop = new NpgsqlCommand($"DROP DATABASE IF EXISTS \"{databaseName}\" WITH (FORCE)", admin); await drop.ExecuteNonQueryAsync(); } } [Fact] public async Task A_database_with_an_applied_migration_unknown_to_this_build_is_refused() { await using var target = await RestoreTargetDatabase.CreateAsync(postgres); // Simulate a database that is ahead of this build (e.g. rolled back to an // older image against a newer database) by recording a migration this // assembly has never heard of. await using (var connection = new NpgsqlConnection(target.ConnectionString)) { await connection.OpenAsync(); await using var insert = new NpgsqlCommand( "INSERT INTO \"public\".\"__EFMigrationsHistory\" (migration_id, product_version) VALUES (@id, @version)", connection); insert.Parameters.AddWithValue("id", "99999999999999_FutureMigrationFromAnUnreleasedBuild"); insert.Parameters.AddWithValue("version", "99.0.0"); await insert.ExecuteNonQueryAsync(); } var options = new DbContextOptionsBuilder() .UseNpgsql(target.ConnectionString, npgsql => npgsql.MigrationsAssembly(typeof(KnowledgeHubDbContext).Assembly.GetName().Name)) .UseSnakeCaseNamingConvention() .Options; await using var db = new KnowledgeHubDbContext(options); var exception = await Assert.ThrowsAsync( () => SchemaGuard.EnsureCompatibleAsync(db.Database, CancellationToken.None)); Assert.Contains("FutureMigrationFromAnUnreleasedBuild", exception.Message, StringComparison.Ordinal); } }