6d84aad94b
- 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.
101 lines
3.4 KiB
C#
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);
|
|
}
|