up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-28 20:55:22 +02:00
parent d040c001ac
commit 2548abc56f
231 changed files with 47468 additions and 68 deletions

View File

@@ -0,0 +1,211 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Npgsql;
using StellaOps.Infrastructure.Postgres.Migrations;
using StellaOps.Infrastructure.Postgres.Options;
namespace StellaOps.Infrastructure.Postgres.Testing;
/// <summary>
/// Test fixture for PostgreSQL integration tests.
/// Provides connection management and schema setup for tests.
/// </summary>
/// <remarks>
/// Use with Testcontainers or a local PostgreSQL instance.
/// Each test class should create its own schema for isolation.
/// </remarks>
public sealed class PostgresFixture : IAsyncDisposable
{
private readonly string _connectionString;
private readonly string _schemaName;
private readonly ILogger _logger;
private bool _disposed;
/// <summary>
/// Creates a new PostgreSQL test fixture.
/// </summary>
/// <param name="connectionString">PostgreSQL connection string.</param>
/// <param name="schemaName">Unique schema name for test isolation.</param>
/// <param name="logger">Optional logger.</param>
public PostgresFixture(
string connectionString,
string schemaName,
ILogger? logger = null)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
_schemaName = schemaName ?? throw new ArgumentNullException(nameof(schemaName));
_logger = logger ?? NullLogger.Instance;
}
/// <summary>
/// Connection string for the test database.
/// </summary>
public string ConnectionString => _connectionString;
/// <summary>
/// Schema name for test isolation.
/// </summary>
public string SchemaName => _schemaName;
/// <summary>
/// Creates PostgreSQL options for the test fixture.
/// </summary>
public PostgresOptions CreateOptions() => new()
{
ConnectionString = _connectionString,
SchemaName = _schemaName,
CommandTimeoutSeconds = 30,
AutoMigrate = false
};
/// <summary>
/// Initializes the test schema.
/// </summary>
public async Task InitializeAsync(CancellationToken cancellationToken = default)
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
// Create schema
await using var createSchemaCmd = new NpgsqlCommand(
$"CREATE SCHEMA IF NOT EXISTS {_schemaName};", connection);
await createSchemaCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Created test schema: {Schema}", _schemaName);
}
/// <summary>
/// Runs migrations for the test schema.
/// </summary>
/// <param name="migrationsPath">Path to migration SQL files.</param>
/// <param name="moduleName">Module name for logging.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task RunMigrationsAsync(
string migrationsPath,
string moduleName,
CancellationToken cancellationToken = default)
{
var runner = new MigrationRunner(
_connectionString,
_schemaName,
moduleName,
_logger);
await runner.RunAsync(migrationsPath, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Executes raw SQL for test setup.
/// </summary>
public async Task ExecuteSqlAsync(string sql, CancellationToken cancellationToken = default)
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var command = new NpgsqlCommand(sql, connection);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Truncates all tables in the test schema for test isolation.
/// </summary>
public async Task TruncateAllTablesAsync(CancellationToken cancellationToken = default)
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
// Get all tables in the schema
await using var getTablesCmd = new NpgsqlCommand(
"""
SELECT table_name FROM information_schema.tables
WHERE table_schema = @schema AND table_type = 'BASE TABLE'
AND table_name != 'schema_migrations';
""",
connection);
getTablesCmd.Parameters.AddWithValue("schema", _schemaName);
var tables = new List<string>();
await using (var reader = await getTablesCmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false))
{
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
tables.Add(reader.GetString(0));
}
}
if (tables.Count == 0) return;
// Truncate all tables
var truncateSql = $"TRUNCATE TABLE {string.Join(", ", tables.Select(t => $"{_schemaName}.{t}"))} CASCADE;";
await using var truncateCmd = new NpgsqlCommand(truncateSql, connection);
await truncateCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Truncated {Count} tables in schema {Schema}", tables.Count, _schemaName);
}
/// <summary>
/// Cleans up the test schema.
/// </summary>
public async ValueTask DisposeAsync()
{
if (_disposed) return;
_disposed = true;
try
{
await using var connection = new NpgsqlConnection(_connectionString);
await connection.OpenAsync().ConfigureAwait(false);
await using var dropSchemaCmd = new NpgsqlCommand(
$"DROP SCHEMA IF EXISTS {_schemaName} CASCADE;", connection);
await dropSchemaCmd.ExecuteNonQueryAsync().ConfigureAwait(false);
_logger.LogInformation("Dropped test schema: {Schema}", _schemaName);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to drop test schema: {Schema}", _schemaName);
}
}
}
/// <summary>
/// Factory for creating PostgreSQL test fixtures.
/// </summary>
public static class PostgresFixtureFactory
{
/// <summary>
/// Creates a fixture with a unique schema name based on the test class name.
/// </summary>
/// <param name="connectionString">PostgreSQL connection string.</param>
/// <param name="testClassName">Test class name for schema naming.</param>
/// <param name="logger">Optional logger.</param>
public static PostgresFixture Create(
string connectionString,
string testClassName,
ILogger? logger = null)
{
// Create a unique schema name based on test class and timestamp
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var schemaName = $"test_{SanitizeIdentifier(testClassName)}_{timestamp}";
return new PostgresFixture(connectionString, schemaName, logger);
}
/// <summary>
/// Creates a fixture with a random schema name.
/// </summary>
public static PostgresFixture CreateRandom(string connectionString, ILogger? logger = null)
{
var schemaName = $"test_{Guid.NewGuid():N}";
return new PostgresFixture(connectionString, schemaName, logger);
}
private static string SanitizeIdentifier(string name)
{
// Convert to lowercase and replace non-alphanumeric with underscore
return string.Concat(name.ToLowerInvariant()
.Select(c => char.IsLetterOrDigit(c) ? c : '_'))
.TrimEnd('_');
}
}