Files
YesChef/src/backend/YesChef.Api.IntegrationTests/Infrastructure/PostgresFixture.cs
T
Josh Rogers 6d84aad94b Pre-fill list section on product pick; tighten backend warnings
- Adds GET /api/products/{kind}/{id}/section?storeId=... exposing the
  per-store memory the list page mirrors when a product is picked, so the
  section dropdown reflects what the backend would auto-assign on POST.
- Treats backend warnings as errors via Directory.Build.props; fixes the
  surfaced warnings (obsolete PostgreSqlBuilder ctor, nullable string[]
  in IsEquivalentTo, redundant nullable flow).
- Annotates wire-exposed enums (ProductKind, UnitKind, UnitCategory,
  UnitCategoryFlags) with JsonStringEnumConverter so they round-trip as
  strings regardless of caller options. Unblocks the integration tests
  that deserialize DTOs via GetFromJsonAsync without the global converter.
2026-05-15 21:30:00 -05:00

101 lines
3.4 KiB
C#

using Npgsql;
using Testcontainers.PostgreSql;
using TUnit.Core.Interfaces;
using YesChef.Api.Data;
namespace YesChef.Api.IntegrationTests.Infrastructure;
/// <summary>
/// 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 <c>CREATE DATABASE … TEMPLATE …</c>, which is far cheaper than
/// replaying migrations.
/// </summary>
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<YesChefDb>()
.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<TestDatabase> 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();
}
}
/// <summary>Handle to a per-test database. Disposing drops it.</summary>
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);
}