using Microsoft.EntityFrameworkCore; using TeleWave.Infrastructure.Persistence; using Testcontainers.PostgreSql; using Xunit; namespace TeleWave.Integration.Tests; /// /// Поднимает одноразовый Postgres в контейнере и применяет к нему реальные миграции. Даёт свежие /// экземпляры (каждый — своё соединение), чтобы тестировать то, что InMemory /// не умеет: транзакции, advisory-lock, ExecuteDelete, raw SQL. Требует запущенного Docker. /// public sealed class PostgresFixture : IAsyncLifetime { // Контейнер создаётся внутри InitializeAsync: сам билдер бросает, когда Docker недоступен, // и в инициализаторе поля это ронял бы всю коллекцию тестов вместо честного пропуска. private PostgreSqlContainer? _container; /// false, если Docker недоступен (напр. CI-раннер без Docker) — тогда тесты пропускаются. public bool Available { get; private set; } public string ConnectionString => _container?.GetConnectionString() ?? throw new InvalidOperationException("Контейнер не запущен — проверяйте Available."); public async Task InitializeAsync() { try { _container = new PostgreSqlBuilder().WithImage("postgres:16-alpine").Build(); await _container.StartAsync(); await using var db = CreateContext(); await db.Database.MigrateAsync(); Available = true; } catch (Exception) { // Docker не запущен/недоступен — интеграционные тесты будут пропущены (Skip), а не упадут. Available = false; } } public AppDbContext CreateContext() => new(new DbContextOptionsBuilder().UseNpgsql(ConnectionString).Options); public async Task DisposeAsync() { if (_container is not null) await _container.DisposeAsync(); } } [CollectionDefinition("postgres")] public sealed class PostgresCollection : ICollectionFixture; /// Детерминированный источник случайности для планировщика в тестах. internal sealed class SequenceRandom(params int[] sequence) : Domain.Broadcast.Scheduling.IRandomSource { private readonly int[] _sequence = sequence.Length == 0 ? [0] : sequence; private int _i; public int Next(int maxExclusive) { if (maxExclusive <= 0) return 0; var value = _sequence[_i++ % _sequence.Length]; return ((value % maxExclusive) + maxExclusive) % maxExclusive; } }