ApiFactory поднимает реальный API на одноразовом postgres:16-alpine (Ryuk off — сеть к Docker Hub нестабильна, образ закэширован; RateLimiting off через env, т.к. лимитер читает конфиг эагерно). Program сделан public partial для фабрики. Сценарии (10 зелёных): - signup-flow: signup→token→/api/me с org; дубль-signup 400; слабый пароль 400. - tenant isolation A vs B: контрагент A не виден B (список + прямой GET 404). - permission: кастомная роль без ProductsEdit → PUT товара 403, GET 200; админ не 403. - supply post→unpost: остаток 0→10, Cost=70 (скользящее среднее), unpost→0; двойной post 409. - retail overselling: продажа сверх остатка → 409; недоплата → 400. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
60 lines
3.1 KiB
C#
60 lines
3.1 KiB
C#
using Microsoft.AspNetCore.Hosting;
|
||
using Microsoft.AspNetCore.Mvc.Testing;
|
||
using Microsoft.Extensions.Configuration;
|
||
using Testcontainers.PostgreSql;
|
||
using Xunit;
|
||
|
||
namespace foodmarket.IntegrationTests.Support;
|
||
|
||
/// <summary>Поднимает реальный API (WebApplicationFactory) поверх одноразового
|
||
/// Postgres-контейнера (Testcontainers). Миграции применяются на старте host'а
|
||
/// (Program.Migrate), сидеры наполняют справочники и SuperAdmin. Запускается
|
||
/// один раз на всю коллекцию тестов.</summary>
|
||
public sealed class ApiFactory : WebApplicationFactory<Program>, IAsyncLifetime
|
||
{
|
||
static ApiFactory()
|
||
{
|
||
// Ryuk (reaper) тянет образ из Docker Hub — на этом хосте сеть к внешним
|
||
// реестрам нестабильна, а postgres:16-alpine уже закэширован. Выключаем.
|
||
Environment.SetEnvironmentVariable("TESTCONTAINERS_RYUK_DISABLED", "true");
|
||
// Лимитер читает конфиг ЭАГЕРНО при регистрации сервисов — поэтому
|
||
// переопределяем через env-переменную (её CreateBuilder подхватывает до
|
||
// регистрации), а не через ConfigureAppConfiguration (применяется позже).
|
||
// Тесты логинятся десятки раз с одного loopback-IP — иначе 429.
|
||
Environment.SetEnvironmentVariable("RateLimiting__Enabled", "false");
|
||
// Тише логи в тестовом прогоне (OpenIddict сыплет Debug-событиями).
|
||
Environment.SetEnvironmentVariable("Serilog__MinimumLevel__Default", "Warning");
|
||
}
|
||
|
||
private readonly PostgreSqlContainer _db = new PostgreSqlBuilder()
|
||
.WithImage("postgres:16-alpine")
|
||
.WithDatabase("food_market")
|
||
.WithUsername("food_market")
|
||
.WithPassword("food_market_test")
|
||
.Build();
|
||
|
||
public async Task InitializeAsync() => await _db.StartAsync();
|
||
|
||
public new async Task DisposeAsync()
|
||
{
|
||
await _db.DisposeAsync();
|
||
await base.DisposeAsync();
|
||
}
|
||
|
||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||
{
|
||
// Development — простые dev-ключи OpenIddict (без генерации сертификатов),
|
||
// 3-сегментный токен. Лимитер выключаем: тесты логинятся много раз с одного IP.
|
||
builder.UseEnvironment("Development");
|
||
builder.ConfigureAppConfiguration((_, cfg) =>
|
||
{
|
||
// Строку подключения AddDbContext читает лениво — здесь override
|
||
// срабатывает. RateLimiting выключён через env (см. static ctor).
|
||
cfg.AddInMemoryCollection(new Dictionary<string, string?>
|
||
{
|
||
["ConnectionStrings:Default"] = _db.GetConnectionString(),
|
||
});
|
||
});
|
||
}
|
||
}
|