using Npgsql; using Testcontainers.PostgreSql; using TUnit.Core.Interfaces; using YesChef.Api.Data; namespace YesChef.Api.IntegrationTests.Infrastructure; /// /// One Postgres container per test session. A migrated "template" database is /// created once; each test asks the fixture for a fresh DB cloned from the /// template via CREATE DATABASE … TEMPLATE …, which is far cheaper than /// replaying migrations. /// public sealed class PostgresFixture : IAsyncInitializer, IAsyncDisposable { private const string TemplateDbName = "yeschef_template"; private readonly PostgreSqlContainer _container = new PostgreSqlBuilder("postgres:17") .WithDatabase("postgres") .WithUsername("postgres") .WithPassword("postgres") .Build(); private string _adminConnectionString = null!; public async Task InitializeAsync() { await _container.StartAsync(); _adminConnectionString = _container.GetConnectionString(); await CreateTemplateAsync(); } private async Task CreateTemplateAsync() { await ExecuteOnPostgresAsync($"""CREATE DATABASE "{TemplateDbName}";"""); var templateConnectionString = BuildConnectionString(TemplateDbName); var options = new DbContextOptionsBuilder() .UseNpgsql(templateConnectionString) .Options; await using (var db = new YesChefDb(options)) { await db.Database.MigrateAsync(); } // Postgres holds idle pooled connections to the template; close them so // the template is free to be used as a CREATE DATABASE source. NpgsqlConnection.ClearAllPools(); } public async Task CreateDatabaseAsync() { var name = $"yeschef_test_{Guid.NewGuid():N}"; await ExecuteOnPostgresAsync($"""CREATE DATABASE "{name}" TEMPLATE "{TemplateDbName}";"""); return new TestDatabase(name, BuildConnectionString(name), this); } internal async Task DropDatabaseAsync(string name) { NpgsqlConnection.ClearAllPools(); await ExecuteOnPostgresAsync($"""DROP DATABASE IF EXISTS "{name}" WITH (FORCE);"""); } private async Task ExecuteOnPostgresAsync(string sql) { await using var conn = new NpgsqlConnection(_adminConnectionString); await conn.OpenAsync(); await using var cmd = new NpgsqlCommand(sql, conn); await cmd.ExecuteNonQueryAsync(); } private string BuildConnectionString(string database) { var builder = new NpgsqlConnectionStringBuilder(_adminConnectionString) { Database = database, // Per-test apps open a small number of connections; the high default // pool keeps the cluster from being flooded under heavy parallelism. MaxPoolSize = 10, Timeout = 60, }; return builder.ConnectionString; } public async ValueTask DisposeAsync() { NpgsqlConnection.ClearAllPools(); await _container.DisposeAsync(); } } /// Handle to a per-test database. Disposing drops it. public sealed class TestDatabase(string name, string connectionString, PostgresFixture fixture) : IAsyncDisposable { public string Name { get; } = name; public string ConnectionString { get; } = connectionString; public async ValueTask DisposeAsync() => await fixture.DropDatabaseAsync(Name); }