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()
);
}
}