Files
git.stella-ops.org/src/__Tests/__Libraries/StellaOps.Infrastructure.Postgres.Testing/PostgresIntegrationFixture.cs
2025-12-26 01:48:24 +02:00

158 lines
5.0 KiB
C#

using System.Reflection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Testcontainers.PostgreSql;
using Xunit;
using Xunit.Sdk;
namespace StellaOps.Infrastructure.Postgres.Testing;
/// <summary>
/// Base class for PostgreSQL integration test fixtures.
/// Uses Testcontainers to spin up a real PostgreSQL instance.
/// </summary>
/// <remarks>
/// Inherit from this class and override <see cref="GetMigrationAssembly"/> and <see cref="GetModuleName"/>
/// to provide module-specific migrations.
/// </remarks>
public abstract class PostgresIntegrationFixture : IAsyncLifetime
{
private PostgreSqlContainer? _container;
private PostgresFixture? _fixture;
/// <summary>
/// Gets the PostgreSQL connection string for tests.
/// </summary>
public string ConnectionString => _container?.GetConnectionString()
?? throw new InvalidOperationException("Container not initialized");
/// <summary>
/// Gets the schema name for test isolation.
/// </summary>
public string SchemaName => _fixture?.SchemaName
?? throw new InvalidOperationException("Fixture not initialized");
/// <summary>
/// Gets the PostgreSQL test fixture.
/// </summary>
public PostgresFixture Fixture => _fixture
?? throw new InvalidOperationException("Fixture not initialized");
/// <summary>
/// Gets the logger for this fixture.
/// </summary>
protected virtual ILogger Logger => NullLogger.Instance;
/// <summary>
/// Gets the PostgreSQL Docker image to use.
/// </summary>
protected virtual string PostgresImage => "postgres:16-alpine";
/// <summary>
/// Gets the assembly containing embedded SQL migrations.
/// </summary>
/// <returns>Assembly with embedded migration resources, or null if no migrations.</returns>
protected abstract Assembly? GetMigrationAssembly();
/// <summary>
/// Gets the module name for logging and schema naming.
/// </summary>
protected abstract string GetModuleName();
/// <summary>
/// Gets the resource prefix for filtering embedded resources.
/// </summary>
protected virtual string? GetResourcePrefix() => null;
/// <summary>
/// Initializes the PostgreSQL container and runs migrations.
/// </summary>
public virtual async Task InitializeAsync()
{
try
{
_container = new PostgreSqlBuilder()
.WithImage(PostgresImage)
.Build();
await _container.StartAsync();
}
catch (ArgumentException ex) when (ShouldSkipForMissingDocker(ex))
{
try
{
if (_container is not null)
{
await _container.DisposeAsync();
}
}
catch
{
// Ignore cleanup failures during skip.
}
_container = null;
throw SkipException.ForSkip(
$"Postgres integration tests require Docker/Testcontainers. Skipping because the container failed to start: {ex.Message}");
}
var moduleName = GetModuleName();
_fixture = PostgresFixtureFactory.Create(ConnectionString, moduleName, Logger);
await _fixture.InitializeAsync();
var migrationAssembly = GetMigrationAssembly();
if (migrationAssembly != null)
{
await _fixture.RunMigrationsFromAssemblyAsync(
migrationAssembly,
moduleName,
GetResourcePrefix());
}
}
/// <summary>
/// Cleans up the PostgreSQL container and fixture.
/// </summary>
public virtual async Task DisposeAsync()
{
if (_fixture != null)
{
await _fixture.DisposeAsync();
}
if (_container != null)
{
await _container.DisposeAsync();
}
}
/// <summary>
/// Truncates all tables in the test schema for test isolation between test methods.
/// </summary>
public Task TruncateAllTablesAsync(CancellationToken cancellationToken = default)
=> Fixture.TruncateAllTablesAsync(cancellationToken);
/// <summary>
/// Executes raw SQL for test setup.
/// </summary>
public Task ExecuteSqlAsync(string sql, CancellationToken cancellationToken = default)
=> Fixture.ExecuteSqlAsync(sql, cancellationToken);
private static bool ShouldSkipForMissingDocker(ArgumentException exception)
{
return string.Equals(exception.ParamName, "DockerEndpointAuthConfig", StringComparison.Ordinal)
|| exception.Message.Contains("Docker is either not running", StringComparison.OrdinalIgnoreCase);
}
}
/// <summary>
/// PostgreSQL integration fixture without migrations.
/// Useful for testing the infrastructure itself or creating schemas dynamically.
/// </summary>
public sealed class PostgresIntegrationFixtureWithoutMigrations : PostgresIntegrationFixture
{
protected override Assembly? GetMigrationAssembly() => null;
protected override string GetModuleName() => "Test";
}