Deal.Tests.Unit, Deal.Telegram.Tests, Deal.Ai.Tests, Deal.Ml.Tests разложены по областям (Modules/<X>, Api, Infrastructure, Contracts, Grpc, ...), общие хелперы -> Support; namespace = папка, using между областями добавлены итеративно по ошибкам сборки.
118 lines
6.4 KiB
C#
118 lines
6.4 KiB
C#
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);
|
||
}
|