Backend (.NET 10): - EventFlow CQRS/Event Sourcing with PostgreSQL - GraphQL.NET API with mutations and queries - Custom ReadModelSqlGenerator for snake_case PostgreSQL columns - Hangfire for background job processing - Integration tests with isolated test databases Frontend (React/Vite): - Initial project structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
121 lines
3.7 KiB
C#
121 lines
3.7 KiB
C#
using Npgsql;
|
|
|
|
namespace Books.Api.Tests.Infrastructure;
|
|
|
|
/// <summary>
|
|
/// Creates an isolated PostgreSQL database for each test run.
|
|
/// The database is automatically dropped when disposed.
|
|
/// </summary>
|
|
public class TestDatabase : IDisposable
|
|
{
|
|
public const string DefaultConnectionString =
|
|
"Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=postgres;Include Error Detail=true";
|
|
|
|
private readonly string _masterConnectionString;
|
|
private bool _disposed;
|
|
|
|
public string DatabaseName { get; }
|
|
|
|
public string ConnectionString
|
|
{
|
|
get
|
|
{
|
|
var builder = new NpgsqlConnectionStringBuilder(_masterConnectionString)
|
|
{
|
|
Database = DatabaseName
|
|
};
|
|
return builder.ToString();
|
|
}
|
|
}
|
|
|
|
public TestDatabase(string? masterConnectionString = null)
|
|
{
|
|
_masterConnectionString = masterConnectionString ?? GetConnectionStringFromEnvironment();
|
|
DatabaseName = $"books_test_{Guid.NewGuid():N}";
|
|
|
|
CreateDatabase();
|
|
}
|
|
|
|
private static string GetConnectionStringFromEnvironment()
|
|
{
|
|
return Environment.GetEnvironmentVariable("TEST_CONNECTION_STRING")
|
|
?? DefaultConnectionString;
|
|
}
|
|
|
|
private void CreateDatabase()
|
|
{
|
|
try
|
|
{
|
|
using var connection = new NpgsqlConnection(_masterConnectionString);
|
|
connection.Open();
|
|
|
|
using var command = connection.CreateCommand();
|
|
command.CommandText = $"CREATE DATABASE \"{DatabaseName}\"";
|
|
command.ExecuteNonQuery();
|
|
|
|
Console.WriteLine($"[TestDatabase] Created test database: {DatabaseName}");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Failed to create test database '{DatabaseName}' using connection string: {_masterConnectionString}",
|
|
e);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (_disposed) return;
|
|
|
|
if (disposing)
|
|
{
|
|
try
|
|
{
|
|
// Step 1: Terminate all active connections to prevent "database in use" errors
|
|
using (var connection = new NpgsqlConnection(_masterConnectionString))
|
|
{
|
|
connection.Open();
|
|
|
|
using var terminateCommand = connection.CreateCommand();
|
|
terminateCommand.CommandText = $@"
|
|
SELECT pg_terminate_backend(pg_stat_activity.pid)
|
|
FROM pg_stat_activity
|
|
WHERE pg_stat_activity.datname = '{DatabaseName}'
|
|
AND pid <> pg_backend_pid();
|
|
";
|
|
terminateCommand.ExecuteNonQuery();
|
|
}
|
|
|
|
// Step 2: Drop the database
|
|
using (var connection = new NpgsqlConnection(_masterConnectionString))
|
|
{
|
|
connection.Open();
|
|
|
|
using var dropCommand = connection.CreateCommand();
|
|
dropCommand.CommandText = $"DROP DATABASE IF EXISTS \"{DatabaseName}\"";
|
|
dropCommand.ExecuteNonQuery();
|
|
}
|
|
|
|
Console.WriteLine($"[TestDatabase] Dropped test database: {DatabaseName}");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
// Log but don't throw - we don't want cleanup failures to mask test failures
|
|
Console.WriteLine($"[TestDatabase] Warning: Failed to drop test database {DatabaseName}: {e.Message}");
|
|
}
|
|
}
|
|
|
|
_disposed = true;
|
|
}
|
|
|
|
~TestDatabase()
|
|
{
|
|
Dispose(false);
|
|
}
|
|
}
|