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
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>
124 lines
5.1 KiB
C#
124 lines
5.1 KiB
C#
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();
|
||
}
|
||
}
|