using FluentAssertions; using foodmarket.Api.Background; using foodmarket.Domain.Organizations; using foodmarket.Infrastructure.Persistence; using foodmarket.IntegrationTests.Support; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace foodmarket.IntegrationTests; /// Sprint 28: тест для /// (введён в Sprint 25). Требует реальный PostgreSQL — метод использует /// information_schema + DO $$ блоки, которых нет в SQLite. /// /// Проверяет: /// - Только org'и с именем quality-% старше N часов удаляются. /// - Свежие quality-* не трогаются. /// - Не-quality org'и (включая Test-*, реальные имена) не трогаются. /// - FK-loop ретрая работает (нет foreign_key_violation при множественных /// зависимостях employees ↔ employee_roles). /// [Collection(ApiCollection.Name)] public class PruneQualityTestOrgsTests { private readonly ApiFactory _factory; public PruneQualityTestOrgsTests(ApiFactory factory) => _factory = factory; private static IConfiguration CfgWith(int hours) => new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["Cleanup:QualityTestOrgHours"] = hours.ToString(), }).Build(); [Fact] public async Task Deletes_only_quality_orgs_older_than_threshold() { using var scope = _factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); // Сетап: 4 org'и — quality-old (старая), quality-new (свежая), // real-old (реальное имя, не трогаем), quality-edge (на грани). var oldQuality = new Organization { Id = Guid.NewGuid(), Name = "quality-old-test-1", CreatedAt = DateTime.UtcNow.AddHours(-48), }; var newQuality = new Organization { Id = Guid.NewGuid(), Name = "quality-new-test-2", CreatedAt = DateTime.UtcNow.AddMinutes(-30), }; var realOld = new Organization { Id = Guid.NewGuid(), Name = "Real Shop LLC", CreatedAt = DateTime.UtcNow.AddDays(-30), }; var edge = new Organization { Id = Guid.NewGuid(), Name = "quality-edge-test-3", // На самой границе threshold (24h) с небольшим запасом. CreatedAt = DateTime.UtcNow.AddHours(-25), }; db.Organizations.AddRange(oldQuality, newQuality, realOld, edge); await db.SaveChangesAsync(); // Threshold = 24 часа. var jobs = new HousekeepingJobs(db, CfgWith(hours: 24), NullLogger.Instance); var deleted = await jobs.PruneQualityTestOrgsAsync(); // Удалены должны быть oldQuality + edge (24h+ старые с quality-* префиксом). deleted.Should().Be(2); using var scope2 = _factory.Services.CreateScope(); var db2 = scope2.ServiceProvider.GetRequiredService(); var remaining = await db2.Organizations .IgnoreQueryFilters() .Where(o => new[] { oldQuality.Id, newQuality.Id, realOld.Id, edge.Id }.Contains(o.Id)) .Select(o => o.Name) .ToListAsync(); remaining.Should().Contain("quality-new-test-2", "свежий quality-* не удаляется"); remaining.Should().Contain("Real Shop LLC", "реальная org не удаляется"); remaining.Should().NotContain("quality-old-test-1", "старая quality-* удалена"); remaining.Should().NotContain("quality-edge-test-3", "edge-старше-threshold удалена"); } [Fact] public async Task Returns_zero_when_no_candidates_match() { using var scope = _factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); // Только свежая quality-* — она НЕ должна удаляться. var fresh = new Organization { Id = Guid.NewGuid(), Name = $"quality-fresh-{Guid.NewGuid():N}", CreatedAt = DateTime.UtcNow, }; db.Organizations.Add(fresh); await db.SaveChangesAsync(); var jobs = new HousekeepingJobs(db, CfgWith(hours: 24), NullLogger.Instance); var deleted = await jobs.PruneQualityTestOrgsAsync(); deleted.Should().Be(0); using var scope2 = _factory.Services.CreateScope(); var db2 = scope2.ServiceProvider.GetRequiredService(); var stillExists = await db2.Organizations .IgnoreQueryFilters() .AnyAsync(o => o.Id == fresh.Id); stillExists.Should().BeTrue(); } }