using FluentAssertions; using foodmarket.Application.Common.Tenancy; using foodmarket.Infrastructure.Integrations.MoySklad; using foodmarket.Infrastructure.Persistence; // Двусмысленность ImportJobStatus: тесты используют только in-process snapshot // API (registry.Create/Save/Get), поэтому ссылаемся на MoySklad-namespace. using ImportJobStatus = foodmarket.Infrastructure.Integrations.MoySklad.ImportJobStatus; using foodmarket.IntegrationTests.Support; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace foodmarket.IntegrationTests; /// TD-5: ImportJob теперь persisted в БД. Тест проверяет, что /// прогресс сохраняется через границу «рестарта реестра» (новый scope = /// новая ConcurrentDictionary в старой версии) — но мы читаем из БД, /// поэтому job остаётся видимым. [Collection(ApiCollection.Name)] public class ImportJobPersistenceTests { private readonly ApiFactory _factory; public ImportJobPersistenceTests(ApiFactory factory) => _factory = factory; [Fact] public async Task Created_job_survives_across_registry_instances() { // 1) Сигнин чтобы получить orgId, потом руками используем registry. var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"impjob-{Guid.NewGuid():N}"); var orgId = await GetOrgIdAsync(api); using var scope1 = _factory.Services.CreateScope(); var registry = _factory.Services.GetRequiredService(); Guid jobId; using (foodmarket.Api.Infrastructure.Tenancy.HttpContextTenantContext.UseOverride(orgId)) { var job = registry.Create("products"); job.Stage = "Импорт страниц 3/10"; job.Total = 100; job.Created = 30; await registry.SaveAsync(job); jobId = job.Id; } // 2) В новом scope (имитация после-рестарта) Get(id) тянет из БД, // не из in-memory ConcurrentDictionary. State сохранён. using (foodmarket.Api.Infrastructure.Tenancy.HttpContextTenantContext.UseOverride(orgId)) { var reloaded = registry.Get(jobId); reloaded.Should().NotBeNull(); reloaded!.Kind.Should().Be("products"); reloaded.Stage.Should().Be("Импорт страниц 3/10"); reloaded.Total.Should().Be(100); reloaded.Created.Should().Be(30); reloaded.Status.Should().Be(ImportJobStatus.Running); } } [Fact] public async Task RecentlyFinished_returns_completed_jobs_from_db() { var api = new ApiActor(_factory.CreateClient()); await api.SignupAndLoginAsync($"impjob-rf-{Guid.NewGuid():N}"); var orgId = await GetOrgIdAsync(api); var registry = _factory.Services.GetRequiredService(); using (foodmarket.Api.Infrastructure.Tenancy.HttpContextTenantContext.UseOverride(orgId)) { var job = registry.Create("products"); job.Status = ImportJobStatus.Succeeded; job.FinishedAt = DateTime.UtcNow; job.Created = 5; await registry.SaveAsync(job); var finished = registry.RecentlyFinished(10); finished.Should().Contain(j => j.Id == job.Id && j.Status == ImportJobStatus.Succeeded); } } [Fact] public async Task Tenant_isolation_for_import_jobs() { var a = new ApiActor(_factory.CreateClient()); var b = new ApiActor(_factory.CreateClient()); await a.SignupAndLoginAsync($"impjob-iso-a-{Guid.NewGuid():N}"); await b.SignupAndLoginAsync($"impjob-iso-b-{Guid.NewGuid():N}"); var orgA = await GetOrgIdAsync(a); var orgB = await GetOrgIdAsync(b); var registry = _factory.Services.GetRequiredService(); Guid jobIdA; using (foodmarket.Api.Infrastructure.Tenancy.HttpContextTenantContext.UseOverride(orgA)) { var job = registry.Create("products"); await registry.SaveAsync(job); jobIdA = job.Id; } // B не видит джоб A через registry.Get (query-filter по OrganizationId). using (foodmarket.Api.Infrastructure.Tenancy.HttpContextTenantContext.UseOverride(orgB)) { registry.Get(jobIdA).Should().BeNull(); } } private static async Task GetOrgIdAsync(ApiActor api) { var me = await api.GetJsonAsync("/api/me"); return Guid.Parse(me.GetProperty("orgId").GetString()!); } }