using NUnit.Framework; using Npgsql; using Testcontainers.PostgreSql; namespace Strata.SqlTools.Tests.PostgreSql.TestContainers; /// /// Base class for PostgreSQL testcontainer tests that handles container lifecycle. /// [TestFixture] [Category("Integration")] public abstract class PostgreSqlTestContainerFixture { protected PostgreSqlContainer? Container { get; set; } protected NpgsqlDataSource? DataSource { get; set; } [OneTimeSetUp] [Timeout(120000)] // 2 minutes for container startup + schema initialization public async Task OneTimeSetup() { // Create and start PostgreSQL container Container = new PostgreSqlBuilder() .WithImage("postgres:16-alpine") .WithDatabase("testdb") .WithUsername("testuser") .WithPassword("testpassword") .WithCleanUp(true) .Build(); await Container.StartAsync(); // Create data source for connection pooling var connectionString = Container.GetConnectionString(); DataSource = NpgsqlDataSource.Create(connectionString); // Initialize test database schema await InitializeSchema(); } [OneTimeTearDown] public async Task OneTimeTearDown() { if (DataSource != null) { await DataSource.DisposeAsync(); } if (Container != null) { await Container.StopAsync(); await Container.DisposeAsync(); } } /// /// Override to initialize the test database schema with tables, etc. /// protected virtual async Task InitializeSchema() { if (DataSource == null) return; using var command = DataSource.CreateCommand(@" CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, active BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS orders ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id), order_total DECIMAL(10, 2) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS products ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, price DECIMAL(10, 2) NOT NULL, in_stock BOOLEAN DEFAULT true ); "); await command.ExecuteNonQueryAsync(); } /// /// Executes a SQL query and returns the result set. /// protected async Task>> ExecuteQuery(string sql) { if (DataSource == null) throw new InvalidOperationException("DataSource is not initialized"); var results = new List>(); using var command = DataSource.CreateCommand(sql); using var reader = await command.ExecuteReaderAsync(); while (await reader.ReadAsync()) { var row = new Dictionary(); for (int i = 0; i < reader.FieldCount; i++) { row[reader.GetName(i)] = reader.GetValue(i); } results.Add(row); } return results; } /// /// Clears all test data from tables. /// protected async Task ClearTestData() { if (DataSource == null) return; using var command = DataSource.CreateCommand(@" TRUNCATE TABLE orders, users, products RESTART IDENTITY CASCADE; "); await command.ExecuteNonQueryAsync(); } }