using Dapper; using Microsoft.EntityFrameworkCore; using Npgsql; using Testcontainers.PostgreSql; using WorkClub.Infrastructure.Data; namespace WorkClub.Tests.Integration.Data; public class MigrationTests : IAsyncLifetime { private PostgreSqlContainer? _container; private string? _connectionString; public async Task InitializeAsync() { _container = new PostgreSqlBuilder() .WithImage("postgres:16-alpine") .Build(); await _container.StartAsync(); _connectionString = _container.GetConnectionString(); } public async Task DisposeAsync() { if (_container != null) { await _container.DisposeAsync(); } } [Fact] public async Task Migration_AppliesSuccessfully_CreatesAllTables() { // Arrange var options = new DbContextOptionsBuilder() .UseNpgsql(_connectionString) .Options; // Act await using var context = new AppDbContext(options); await context.Database.MigrateAsync(); // Assert - verify all expected tables exist await using var connection = new NpgsqlConnection(_connectionString); var tables = (await connection.QueryAsync( @"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name")).ToList(); Assert.Contains("clubs", tables); Assert.Contains("members", tables); Assert.Contains("work_items", tables); Assert.Contains("shifts", tables); Assert.Contains("shift_signups", tables); } [Fact] public async Task Migration_CreatesCorrectIndexes() { // Arrange var options = new DbContextOptionsBuilder() .UseNpgsql(_connectionString) .Options; // Act await using var context = new AppDbContext(options); await context.Database.MigrateAsync(); // Assert - verify critical indexes exist await using var connection = new NpgsqlConnection(_connectionString); var indexes = (await connection.QueryAsync( @"SELECT indexname FROM pg_indexes WHERE schemaname = 'public' ORDER BY indexname")).ToList(); // TenantId indexes Assert.Contains(indexes, i => i.Contains("tenant_id")); // ClubId indexes Assert.Contains(indexes, i => i.Contains("club_id")); // Status indexes for WorkItem Assert.Contains(indexes, i => i.Contains("status")); } [Fact] public async Task Migration_EnablesRowLevelSecurity() { // Arrange var options = new DbContextOptionsBuilder() .UseNpgsql(_connectionString) .Options; // Act await using var context = new AppDbContext(options); await context.Database.MigrateAsync(); // Assert - verify RLS is enabled on tenant tables await using var connection = new NpgsqlConnection(_connectionString); var rlsEnabled = await connection.QueryAsync<(string TableName, bool RlsEnabled)>( @"SELECT relname AS TableName, relrowsecurity AS RlsEnabled FROM pg_class WHERE relnamespace = 'public'::regnamespace AND relname IN ('clubs', 'members', 'work_items', 'shifts', 'shift_signups')"); foreach (var (tableName, enabled) in rlsEnabled) { Assert.True(enabled, $"RLS should be enabled on {tableName}"); } } [Fact] public async Task Migration_CreatesTenantIsolationPolicy() { // Arrange var options = new DbContextOptionsBuilder() .UseNpgsql(_connectionString) .Options; // Act await using var context = new AppDbContext(options); await context.Database.MigrateAsync(); // Assert - verify tenant_isolation policies exist await using var connection = new NpgsqlConnection(_connectionString); var policies = (await connection.QueryAsync( @"SELECT policyname FROM pg_policies WHERE schemaname = 'public' AND policyname = 'tenant_isolation'")).ToList(); // Should have tenant_isolation policy on all tenant tables Assert.True(policies.Count >= 5, "Should have at least 5 tenant_isolation policies"); } }