/
afanasevn
/
MaxSystems
Обзор
Документация
Войти
/
afanasevn
/
MaxSystems
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
tests/MaxSystems.IntegrationTests/IntegrationTestFixture.cs
253 строки
9 KB
IBS\NAfanasev
Update solution references
23 июн 2026, 16:24
23 июн 2026, 16:24
02b1783
Код
Авторство
О чём код?
using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using MaxSystems.IntegrationTests.Infrastructure; using MaxSystems.Worker.Handlers; using Testcontainers.PostgreSql; using Testcontainers.RabbitMq; namespace MaxSystems.IntegrationTests; /// <summary> /// Fixture: только API (publisher/outbox), без Worker — journal остаётся Pending. /// </summary> public sealed class IntegrationPublisherTestFixture : IntegrationTestFixtureBase { /// <inheritdoc /> protected override bool StartWorker => false; /// <inheritdoc /> protected override IServiceDeskHandlerBehavior? CreateServiceDeskBehavior() => null; /// <inheritdoc /> protected override INotificationHandlerBehavior? CreateNotificationBehavior() => null; } /// <summary> /// E2E fixture: API + Worker с успешным demo Service Desk handler и SMTP-заглушкой. /// </summary> public sealed class IntegrationTestFixture : IntegrationTestFixtureBase { /// <inheritdoc /> protected override bool StartWorker => true; /// <inheritdoc /> protected override IServiceDeskHandlerBehavior CreateServiceDeskBehavior() => new DefaultServiceDeskHandlerBehavior(); /// <inheritdoc /> protected override INotificationHandlerBehavior CreateNotificationBehavior() => new DefaultNotificationHandlerBehavior(); } /// <summary> /// Fixture для тестов DLQ Service Desk: Worker с принудительным сбоем SD на Work2. /// </summary> public sealed class IntegrationFailureTestFixture : IntegrationTestFixtureBase { /// <inheritdoc /> protected override bool StartWorker => true; /// <inheritdoc /> protected override IServiceDeskHandlerBehavior CreateServiceDeskBehavior() => new FailingServiceDeskHandlerBehavior(TestSeedIds.Work2); /// <inheritdoc /> protected override INotificationHandlerBehavior CreateNotificationBehavior() => new DefaultNotificationHandlerBehavior(); } /// <summary> /// Fixture для тестов DLQ notifications: Worker с принудительным сбоем SMTP на Work1. /// </summary> public sealed class IntegrationNotificationFailureTestFixture : IntegrationTestFixtureBase { /// <inheritdoc /> protected override bool StartWorker => true; /// <inheritdoc /> protected override IServiceDeskHandlerBehavior CreateServiceDeskBehavior() => new DefaultServiceDeskHandlerBehavior(); /// <inheritdoc /> protected override INotificationHandlerBehavior CreateNotificationBehavior() => new FailingNotificationHandlerBehavior(TestSeedIds.Work1); } /// <summary> /// Общая инфраструктура: контейнеры, API factory, опционально Worker host. /// </summary> public abstract class IntegrationTestFixtureBase : IAsyncLifetime { private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder() .WithImage("postgres:16-alpine") .WithDatabase("MaxSystems") .WithUsername("maxsystems") .WithPassword("maxsystems") .Build(); private readonly RabbitMqContainer _rabbitMq = new RabbitMqBuilder() .WithImage("rabbitmq:3-management-alpine") .WithUsername("guest") .WithPassword("guest") .Build(); private WorkerTestHost? _workerHost; /// <summary>Запущенный Worker host (E2E).</summary> public WorkerTestHost WorkerHost => _workerHost ?? throw new InvalidOperationException("Worker is not started for this fixture."); /// <summary>Фабрика хоста API с connection strings из контейнеров.</summary> public MaxSystemsWebApplicationFactory Factory { get; private set; } = null!; /// <summary>HTTP-клиент для вызовов /api/*.</summary> public HttpClient Client { get; private set; } = null!; /// <summary>Строка подключения к PostgreSQL (127.0.0.1 + mapped port).</summary> public string PostgresConnectionString { get; private set; } = string.Empty; /// <summary>AMQP URI RabbitMQ (127.0.0.1 + mapped port).</summary> public string RabbitMqAmqpUri { get; private set; } = string.Empty; /// <summary>Запускать ли Worker после API.</summary> protected abstract bool StartWorker { get; } /// <summary>Behavior Service Desk; null, если Worker не стартует.</summary> protected abstract IServiceDeskHandlerBehavior? CreateServiceDeskBehavior(); /// <summary>Behavior notifications; null, если Worker не стартует.</summary> protected abstract INotificationHandlerBehavior? CreateNotificationBehavior(); /// <inheritdoc /> public async Task InitializeAsync() { await Task.WhenAll( _postgres.StartAsync(), _rabbitMq.StartAsync()); PostgresConnectionString = BuildPostgresConnectionString(); RabbitMqAmqpUri = BuildRabbitMqAmqpUri(); Factory = new MaxSystemsWebApplicationFactory(PostgresConnectionString, RabbitMqAmqpUri); Client = Factory.CreateClient(); _ = Factory.Services; await WaitForApiReadyAsync(); if (StartWorker) { _workerHost = new WorkerTestHost(); await _workerHost.StartAsync( PostgresConnectionString, RabbitMqAmqpUri, CreateServiceDeskBehavior(), CreateNotificationBehavior()); } } /// <summary> /// Логин manager@corp.local и установка Bearer-токена на Client. /// </summary> public async Task<string> LoginAsManagerAsync() { return await LoginAsync("manager@corp.local"); } /// <summary> /// Логин spec1@corp.local (Specialist) и установка Bearer-токена на Client. /// </summary> public async Task<string> LoginAsSpecialistAsync() { return await LoginAsync("spec1@corp.local"); } /// <summary>Логин по email и установка Bearer-токена на Client.</summary> private async Task<string> LoginAsync(string email) { var response = await Client.PostAsJsonAsync( "/api/auth/login", new { email, password = "demo123" }); response.EnsureSuccessStatusCode(); using var json = await response.Content.ReadFromJsonAsync<JsonDocument>(); var token = json!.RootElement.GetProperty("token").GetString() ?? throw new InvalidOperationException("Login response missing token."); Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return token; } /// <inheritdoc /> public async Task DisposeAsync() { Client.Dispose(); if (_workerHost is not null) await _workerHost.DisposeAsync(); await Factory.DisposeAsync(); await Task.WhenAll( _postgres.DisposeAsync().AsTask(), _rabbitMq.DisposeAsync().AsTask()); } /// <summary> /// Ждёт migrate/seed, Wolverine AutoProvision и доступность auth/БД. /// </summary> private async Task WaitForApiReadyAsync() { var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(120); Exception? lastError = null; while (DateTime.UtcNow < deadline) { try { await LoginAsManagerAsync(); Client.DefaultRequestHeaders.Authorization = null; _ = await IntegrationTestDatabase.CountDeliveryLogsAsync(PostgresConnectionString); await RabbitMqTestTopology.EnsureExchangeExistsAsync( RabbitMqAmqpUri, MaxSystems.Application.Integration.IntegrationTopology.ExchangeName); return; } catch (Exception ex) { lastError = ex; await Task.Delay(TimeSpan.FromSeconds(2)); } } throw new InvalidOperationException("API host did not become ready in time.", lastError); } /// <summary>На Windows используем 127.0.0.1 вместо localhost (IPv6).</summary> private string BuildPostgresConnectionString() { var builder = new Npgsql.NpgsqlConnectionStringBuilder(_postgres.GetConnectionString()) { Host = "127.0.0.1", Port = _postgres.GetMappedPublicPort(5432), }; return builder.ConnectionString; } private string BuildRabbitMqAmqpUri() { var port = _rabbitMq.GetMappedPublicPort(5672); return $"amqp://guest:guest@127.0.0.1:{port}"; } } /// <summary>Fixture publisher-тестов (без Worker).</summary> [CollectionDefinition(Name)] public sealed class IntegrationPublisherTestCollection : ICollectionFixture<IntegrationPublisherTestFixture> { public const string Name = "Integration Event Bus Publisher"; }