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