food-market/tests/food-market.IntegrationTests/Support/ApiFactory.cs
nns f2dad91e05 test(integration): Testcontainers.PostgreSql + WebApplicationFactory, 10 тестов (P1-21)
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>
2026-05-27 03:14:01 +05:00

60 lines
3.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(),
});
});
}
}