food-market/tests/food-market.IntegrationTests/PruneQualityTestOrgsTests.cs
nns ed140cb819
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
docs(s28): api-reference 195→240 + observability + integration #7 + CI
Overnight progress while 4h-soak runs in background:

1. ApiReferenceDocsJob.cs + scripts/gen-api-reference.py — return-type
   regex теперь ловит nested generics любой глубины. Было 195
   endpoint'ов в auto-gen reference; стало 240 (+45). EmployeesController
   GET /api/organization/employees был пропущен из-за
   Task<ActionResult<PagedResult<EmployeeDto>>>.

2. docs/observability.md — добавлен food_market_disk_free_bytes (Sprint 20)
   + раздел "quality-watchdog метрики" (5 метрик textfile exporter'a из
   Sprint 26: run_total, step_failure_total, endpoint_p95_ms,
   last_run_status, incidents_total). Готовые dashboards теперь содержат
   оба JSON (food-market.json + quality-watchdog.json).

3. tests/integration/07-import-export-flows.spec.ts — POST 1C-CSV import
   (semicolon-CSV cp1251) → создаются продукты с группой автоматом;
   POST /api/org/export (НЕ /api/admin/org-export) → возвращает
   {id, status}; orgB не видит export orgA. Прогон 8.2s.

4. tests/food-market.IntegrationTests/PruneQualityTestOrgsTests.cs —
   2 [Fact]'a для метода из Sprint 25. Удаляет только quality-* старше
   threshold, не трогает реальные org. Требует Testcontainers.

5. .forgejo/workflows/regression.yml — добавлен шаг integration suite
   после flows+visual. Telegram: "35 flows + 60 visual + 8 integration".

Soak-real (4h @ 50 RPS) запущен в setsid-detach session, продолжается.
Итоговые числа добавлю в sprint28-progress.md после завершения.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 03:26:39 +05:00

124 lines
5.1 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>Sprint 28: тест для <see cref="HousekeepingJobs.PruneQualityTestOrgsAsync"/>
/// (введён в Sprint 25). Требует реальный PostgreSQL — метод использует
/// information_schema + DO $$ блоки, которых нет в SQLite.
///
/// Проверяет:
/// - Только org'и с именем <c>quality-%</c> старше N часов удаляются.
/// - Свежие <c>quality-*</c> не трогаются.
/// - Не-quality org'и (включая <c>Test-*</c>, реальные имена) не трогаются.
/// - FK-loop ретрая работает (нет foreign_key_violation при множественных
/// зависимостях employees ↔ employee_roles).
/// </summary>
[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<string, string?>
{
["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<AppDbContext>();
// Сетап: 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<HousekeepingJobs>.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<AppDbContext>();
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<AppDbContext>();
// Только свежая 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<HousekeepingJobs>.Instance);
var deleted = await jobs.PruneQualityTestOrgsAsync();
deleted.Should().Be(0);
using var scope2 = _factory.Services.CreateScope();
var db2 = scope2.ServiceProvider.GetRequiredService<AppDbContext>();
var stillExists = await db2.Organizations
.IgnoreQueryFilters()
.AnyAsync(o => o.Id == fresh.Id);
stillExists.Should().BeTrue();
}
}