Files
Deal/src/core/tests/Deal.Tests.Unit/Api/DataRetentionSchedulerTests.cs
T
Rustam Khalimov 5bfa92a4ac Сгруппировать тестовые проекты по областям
Deal.Tests.Unit, Deal.Telegram.Tests, Deal.Ai.Tests, Deal.Ml.Tests
разложены по областям (Modules/<X>, Api, Infrastructure, Contracts,
Grpc, ...), общие хелперы -> Support; namespace = папка, using
между областями добавлены итеративно по ошибкам сборки.
2026-09-11 13:28:26 +03:00

118 lines
6.4 KiB
C#
Raw 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 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);
}