Сгруппировать тестовые проекты по областям

Deal.Tests.Unit, Deal.Telegram.Tests, Deal.Ai.Tests, Deal.Ml.Tests
разложены по областям (Modules/<X>, Api, Infrastructure, Contracts,
Grpc, ...), общие хелперы -> Support; namespace = папка, using
между областями добавлены итеративно по ошибкам сборки.
This commit is contained in:
Rustam Khalimov
2026-09-11 13:28:26 +03:00
parent e3a2692507
commit 5bfa92a4ac
212 changed files with 1982 additions and 1686 deletions
@@ -0,0 +1,117 @@
using Deal.Api.Configuration;
using Deal.Api.Hosting;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Extensions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Registrars;
using Deal.Modules.Tenants.Application.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Deal.Tests.Unit.Support;
using Deal.Tests.Unit.Modules.Tenants;
namespace Deal.Tests.Unit.Api;
/// <summary>
/// Тесты логики прохода <see cref="DataRetentionScheduler"/> (этап 12, пакет B): авто-очистка
/// audit_log по retention, сброс накопительных полей tenant_limits прошедших периодов и уборка
/// завершившихся окон распределённых счётчиков. Тайминги цикла не тестируются — итерация через публичный
/// <see cref="DataRetentionScheduler.RunCycleAsync"/>.
/// </summary>
/// <remarks>
/// Скоупы/DI — реальный ServiceCollection с фейками хранилищ (<see cref="FakeAuditLogStore"/>,
/// <see cref="FakeTenantLimitStore"/>, <see cref="FakeRateLimitCounterStore"/>), зеркалящими семантику
/// EF-адаптеров. Срок хранения аудита — <see cref="DataRetentionOptions.AuditRetentionDays"/>.
/// </remarks>
public sealed class DataRetentionSchedulerTests
{
// Тенант сценария (строка лимита).
private static readonly Guid TenantId = Guid.NewGuid();
// Срок хранения аудита сценария (дней).
private const int RetentionDays = 180;
[Fact]
public async Task RunCycle_PurgesAgedAuditResetsExpiredLimitsAndDeletesExpiredCounters()
{
DateTimeOffset now = DateTimeOffset.UtcNow;
var audit = new FakeAuditLogStore();
await audit.AppendAsync(AuditRecord(AuditEvents.OperatorLoginOk, now.AddDays(-(RetentionDays + 20))), CancellationToken.None);
await audit.AppendAsync(AuditRecord(AuditEvents.TenantLoginOk, now.AddDays(-1)), CancellationToken.None);
var limits = new FakeTenantLimitStore();
// Период месяца начат два месяца назад → истёк; строка с накоплениями должна обнулиться.
limits.Preload(TenantId, budgetTokens: 1000, TenantLimitPeriods.Month, now.AddMonths(-2), usedTokens: 700, warned80: true);
var counters = new FakeRateLimitCounterStore();
await counters.IncrementAsync("expired", now.AddHours(-2), now.AddHours(-2).AddMinutes(1), 1, CancellationToken.None);
await counters.IncrementAsync("active", now, now.AddMinutes(1), 1, CancellationToken.None);
await using ServiceProvider provider = BuildProvider(audit, limits, counters);
DataRetentionScheduler scheduler = new(
provider.GetRequiredService<IServiceScopeFactory>(),
new DataRetentionOptions { Enabled = true, AuditRetentionDays = RetentionDays },
NullLogger<DataRetentionScheduler>.Instance);
await scheduler.RunCycleAsync(CancellationToken.None);
// Аудит: старая запись удалена, свежая осталась.
var remainingAudit = Assert.Single(audit.Records);
Assert.Equal(AuditEvents.TenantLoginOk, remainingAudit.EventType);
// Лимиты: накопления прошедшего периода сброшены.
Assert.Equal(0, limits.UsedTokens(TenantId));
// Счётчики: завершившееся окно удалено, активное осталось.
Assert.Equal(0, await counters.GetCountAsync("expired", now.AddHours(-2), CancellationToken.None));
Assert.Equal(1, await counters.GetCountAsync("active", now, CancellationToken.None));
}
/// <summary>
/// Повторный проход на тех же данных — идемпотентен (чистить больше нечего).
/// </summary>
[Fact]
public async Task RunCycle_IsIdempotent()
{
DateTimeOffset now = DateTimeOffset.UtcNow;
var audit = new FakeAuditLogStore();
await audit.AppendAsync(AuditRecord(AuditEvents.OperatorLoginOk, now.AddDays(-(RetentionDays + 1))), CancellationToken.None);
var limits = new FakeTenantLimitStore();
limits.Preload(TenantId, budgetTokens: 1000, TenantLimitPeriods.Month, now.AddMonths(-2), usedTokens: 500);
await using ServiceProvider provider = BuildProvider(audit, limits, new FakeRateLimitCounterStore());
DataRetentionScheduler scheduler = new(
provider.GetRequiredService<IServiceScopeFactory>(),
new DataRetentionOptions { AuditRetentionDays = RetentionDays },
NullLogger<DataRetentionScheduler>.Instance);
await scheduler.RunCycleAsync(CancellationToken.None);
await scheduler.RunCycleAsync(CancellationToken.None);
Assert.Empty(audit.Records);
Assert.Equal(0, limits.UsedTokens(TenantId));
}
// Строит DI-провайдер теста: три фейк-хранилища в scope прохода.
// audit: Фейк-хранилище аудита (записи посеяны сценарием).
// limits: Фейк-хранилище лимитов (строки посеяны сценарием).
// counters: Фейк-хранилище счётчиков (окна посеяны сценарием).
// Возвращает: Провайдер с сервисами цикла.
private static ServiceProvider BuildProvider(
FakeAuditLogStore audit,
FakeTenantLimitStore limits,
FakeRateLimitCounterStore counters)
{
var services = new ServiceCollection();
services.AddScoped<IAuditLogStore>(_ => audit);
services.AddScoped<ITenantLimitStore>(_ => limits);
services.AddScoped<IRateLimitCounterStore>(_ => counters);
return services.BuildServiceProvider();
}
// Запись аудита сценария.
// eventType: Тип события.
// at: Момент события (UTC).
// Возвращает: DTO записи аудита.
private static AuditRecordDto AuditRecord(string eventType, DateTimeOffset at) =>
new(eventType, AuditActorTypes.Operator, ActorId: null, TenantId: null, Ip: "127.0.0.1", DetailJson: null, at);
}