🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
198 lines
6.3 KiB
C#
198 lines
6.3 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Base class for integration tests that require PostgreSQL and Redis
|
|
/// Uses Testcontainers to spin up isolated database instances
|
|
/// </summary>
|
|
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();
|
|
|
|
/// <summary>
|
|
/// Initialize containers before tests
|
|
/// Called by xUnit before any test in the class runs
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cleanup containers after tests
|
|
/// Called by xUnit after all tests in the class complete
|
|
/// </summary>
|
|
public virtual async Task DisposeAsync()
|
|
{
|
|
// Stop containers in parallel
|
|
await Task.WhenAll(
|
|
PostgresContainer.StopAsync(),
|
|
RedisContainer.StopAsync()
|
|
);
|
|
|
|
// Dispose containers
|
|
await PostgresContainer.DisposeAsync();
|
|
await RedisContainer.DisposeAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seed database with test data
|
|
/// Override in derived classes for custom seeding
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create DbContextOptions for Entity Framework Core
|
|
/// </summary>
|
|
protected DbContextOptions<TContext> CreateDbContextOptions<TContext>()
|
|
where TContext : DbContext
|
|
{
|
|
return new DbContextOptionsBuilder<TContext>()
|
|
.UseNpgsql(PostgresConnectionString)
|
|
.EnableSensitiveDataLogging()
|
|
.EnableDetailedErrors()
|
|
.Options;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Execute SQL command on test database
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clean database tables for test isolation
|
|
/// </summary>
|
|
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 $$;
|
|
");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collection fixture for sharing Testcontainers across multiple test classes
|
|
/// Use [Collection("IntegrationTests")] attribute on test classes
|
|
/// </summary>
|
|
[CollectionDefinition("IntegrationTests")]
|
|
public class IntegrationTestCollection : ICollectionFixture<IntegrationTestFixture>
|
|
{
|
|
// This class has no code, and is never created.
|
|
// Its purpose is simply to be the place to apply [CollectionDefinition]
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shared fixture for integration tests
|
|
/// Containers are created once and shared across test classes
|
|
/// </summary>
|
|
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()
|
|
);
|
|
}
|
|
}
|