90 lines
4.3 KiB
C#
90 lines
4.3 KiB
C#
using ItWorx.KnowledgeHub.Infrastructure.Persistence;
|
|
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Npgsql;
|
|
|
|
namespace ItWorx.KnowledgeHub.IntegrationTests.Operations;
|
|
|
|
/// <summary>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.</summary>
|
|
[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<KnowledgeHubDbContext>()
|
|
.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<KnowledgeHubDbContext>()
|
|
.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<KnowledgeHubDbContext>()
|
|
.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<SchemaDriftException>(
|
|
() => SchemaGuard.EnsureCompatibleAsync(db.Database, CancellationToken.None));
|
|
Assert.Contains("FutureMigrationFromAnUnreleasedBuild", exception.Message, StringComparison.Ordinal);
|
|
}
|
|
}
|