Hangfire.PostgreSql storage (тот же ConnectionString:Default). Сервер стартует только когда Hangfire:Enabled (true по умолчанию) — в интеграционных тестах выключаем через env Hangfire__Enabled=false, чтобы тесты не плодили служебные таблицы в одноразовом контейнере. Dashboard на /hangfire с авторизационным фильтром SuperAdminHangfireFilter — требует роли SuperAdmin (стандартный OpenIddict-токен валидируется аутентификационным middleware'ом перед этим). Recurring jobs (HangfireJobsConfigurator): • prune-stock-movements — ежедневно 03:30 UTC, удаляет StockMovement старше 730 дней (Hangfire:Retention:StockMovementDays). За 30 минут до бэкапа, чтобы pg_dump не цеплял временные блокировки. • prune-audit-log — ежедневно 03:45 UTC, удаляет super_admin_audit_log старше 90 дней (Hangfire:Retention:AuditLogDays). Логика очистки в HousekeepingJobs (scoped, использует AppDbContext с IgnoreQueryFilters — это межтенантная задача). Тесты: 1 unit (PruneStockMovements удаляет только старые), 1 интеграционный (dashboard не отвечает без Hangfire-сервера). Полный прогон: 24 unit + 32 integration = 56 зелёных. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
64 lines
3.5 KiB
C#
64 lines
3.5 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");
|
||
// Hangfire-сервер не нужен в интеграционных тестах: он бы создавал свои
|
||
// таблицы в одноразовом контейнере и удерживал коннекшен. Сам клиент
|
||
// (AddHangfire) остаётся — recurring jobs не регистрируются.
|
||
Environment.SetEnvironmentVariable("Hangfire__Enabled", "false");
|
||
}
|
||
|
||
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(),
|
||
});
|
||
});
|
||
}
|
||
}
|