using System; using System.Threading.Tasks; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; using Microsoft.EntityFrameworkCore; using Npgsql; using Testcontainers.PostgreSql; using Testcontainers.Redis; using Xunit; namespace ColaFlow.IntegrationTests.Infrastructure; /// /// Base class for integration tests that require PostgreSQL and Redis /// Uses Testcontainers to spin up isolated database instances /// public abstract class IntegrationTestBase : IAsyncLifetime { // PostgreSQL Container protected PostgreSqlContainer PostgresContainer { get; private set; } = null!; // Redis Container protected RedisContainer RedisContainer { get; private set; } = null!; // Connection Strings protected string PostgresConnectionString => PostgresContainer.GetConnectionString(); protected string RedisConnectionString => RedisContainer.GetConnectionString(); /// /// Initialize containers before tests /// Called by xUnit before any test in the class runs /// public virtual async Task InitializeAsync() { // Create PostgreSQL container PostgresContainer = new PostgreSqlBuilder() .WithImage("postgres:16-alpine") .WithDatabase("colaflow_test") .WithUsername("colaflow_test") .WithPassword("colaflow_test_password") .WithCleanUp(true) .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(5432)) .Build(); // Create Redis container RedisContainer = new RedisBuilder() .WithImage("redis:7-alpine") .WithCleanUp(true) .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(6379)) .Build(); // Start containers in parallel await Task.WhenAll( PostgresContainer.StartAsync(), RedisContainer.StartAsync() ); // Optional: Run migrations or seed data await SeedDatabaseAsync(); } /// /// Cleanup containers after tests /// Called by xUnit after all tests in the class complete /// public virtual async Task DisposeAsync() { // Stop containers in parallel await Task.WhenAll( PostgresContainer.StopAsync(), RedisContainer.StopAsync() ); // Dispose containers await PostgresContainer.DisposeAsync(); await RedisContainer.DisposeAsync(); } /// /// Seed database with test data /// Override in derived classes for custom seeding /// protected virtual async Task SeedDatabaseAsync() { // Example: Create tables, seed data, etc. await using var connection = new NpgsqlConnection(PostgresConnectionString); await connection.OpenAsync(); // Create extensions await using var command = connection.CreateCommand(); command.CommandText = @" CREATE EXTENSION IF NOT EXISTS ""uuid-ossp""; CREATE EXTENSION IF NOT EXISTS ""pg_trgm""; "; await command.ExecuteNonQueryAsync(); } /// /// Create DbContextOptions for Entity Framework Core /// protected DbContextOptions CreateDbContextOptions() where TContext : DbContext { return new DbContextOptionsBuilder() .UseNpgsql(PostgresConnectionString) .EnableSensitiveDataLogging() .EnableDetailedErrors() .Options; } /// /// Execute SQL command on test database /// protected async Task ExecuteSqlAsync(string sql) { await using var connection = new NpgsqlConnection(PostgresConnectionString); await connection.OpenAsync(); await using var command = connection.CreateCommand(); command.CommandText = sql; await command.ExecuteNonQueryAsync(); } /// /// Clean database tables for test isolation /// protected async Task CleanDatabaseAsync() { await ExecuteSqlAsync(@" DO $$ DECLARE r RECORD; BEGIN -- Disable triggers FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE'; END LOOP; END $$; "); } } /// /// Collection fixture for sharing Testcontainers across multiple test classes /// Use [Collection("IntegrationTests")] attribute on test classes /// [CollectionDefinition("IntegrationTests")] public class IntegrationTestCollection : ICollectionFixture { // This class has no code, and is never created. // Its purpose is simply to be the place to apply [CollectionDefinition] } /// /// Shared fixture for integration tests /// Containers are created once and shared across test classes /// public class IntegrationTestFixture : IAsyncLifetime { public PostgreSqlContainer PostgresContainer { get; private set; } = null!; public RedisContainer RedisContainer { get; private set; } = null!; public string PostgresConnectionString => PostgresContainer.GetConnectionString(); public string RedisConnectionString => RedisContainer.GetConnectionString(); public async Task InitializeAsync() { // Create containers PostgresContainer = new PostgreSqlBuilder() .WithImage("postgres:16-alpine") .WithDatabase("colaflow_test") .WithUsername("colaflow_test") .WithPassword("colaflow_test_password") .WithCleanUp(true) .Build(); RedisContainer = new RedisBuilder() .WithImage("redis:7-alpine") .WithCleanUp(true) .Build(); // Start containers await Task.WhenAll( PostgresContainer.StartAsync(), RedisContainer.StartAsync() ); } public async Task DisposeAsync() { await Task.WhenAll( PostgresContainer.DisposeAsync().AsTask(), RedisContainer.DisposeAsync().AsTask() ); } }