using Npgsql; namespace Books.Api.Tests.Infrastructure; /// /// Creates an isolated PostgreSQL database for each test run. /// The database is automatically dropped when disposed. /// public class TestDatabase : IDisposable { public const string DefaultConnectionString = "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=postgres;Include Error Detail=true"; private readonly string _masterConnectionString; private bool _disposed; public string DatabaseName { get; } public string ConnectionString { get { var builder = new NpgsqlConnectionStringBuilder(_masterConnectionString) { Database = DatabaseName }; return builder.ToString(); } } public TestDatabase(string? masterConnectionString = null) { _masterConnectionString = masterConnectionString ?? GetConnectionStringFromEnvironment(); DatabaseName = $"books_test_{Guid.NewGuid():N}"; CreateDatabase(); } private static string GetConnectionStringFromEnvironment() { return Environment.GetEnvironmentVariable("TEST_CONNECTION_STRING") ?? DefaultConnectionString; } private void CreateDatabase() { try { using var connection = new NpgsqlConnection(_masterConnectionString); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = $"CREATE DATABASE \"{DatabaseName}\""; command.ExecuteNonQuery(); Console.WriteLine($"[TestDatabase] Created test database: {DatabaseName}"); } catch (Exception e) { throw new InvalidOperationException( $"Failed to create test database '{DatabaseName}' using connection string: {_masterConnectionString}", e); } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { try { // Step 1: Terminate all active connections to prevent "database in use" errors using (var connection = new NpgsqlConnection(_masterConnectionString)) { connection.Open(); using var terminateCommand = connection.CreateCommand(); terminateCommand.CommandText = $@" SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '{DatabaseName}' AND pid <> pg_backend_pid(); "; terminateCommand.ExecuteNonQuery(); } // Step 2: Drop the database using (var connection = new NpgsqlConnection(_masterConnectionString)) { connection.Open(); using var dropCommand = connection.CreateCommand(); dropCommand.CommandText = $"DROP DATABASE IF EXISTS \"{DatabaseName}\""; dropCommand.ExecuteNonQuery(); } Console.WriteLine($"[TestDatabase] Dropped test database: {DatabaseName}"); } catch (Exception e) { // Log but don't throw - we don't want cleanup failures to mask test failures Console.WriteLine($"[TestDatabase] Warning: Failed to drop test database {DatabaseName}: {e.Message}"); } } _disposed = true; } ~TestDatabase() { Dispose(false); } }