158 lines
5.0 KiB
C#
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";
|
|
}
|