up
This commit is contained in:
@@ -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('_');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user