Перевести FakeAuditLogStore на NSubstitute
Хелпер Support/TestAuditLogStore: журнал + фильтры Query/Count, PurgeOlderThan, Id присваивается при добавлении. Потребители (13 файлов) перетипизированы на .Store, фейк удалён, тесты 1340 зелёные.
This commit is contained in:
@@ -24,9 +24,9 @@ public sealed class DataRetentionSchedulerTests
|
||||
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 audit = new TestAuditLogStore();
|
||||
await audit.Store.AppendAsync(AuditRecord(AuditEvents.OperatorLoginOk, now.AddDays(-(RetentionDays + 20))), CancellationToken.None);
|
||||
await audit.Store.AppendAsync(AuditRecord(AuditEvents.TenantLoginOk, now.AddDays(-1)), CancellationToken.None);
|
||||
|
||||
var limits = new FakeTenantLimitStore();
|
||||
// Период месяца начат два месяца назад → истёк; строка с накоплениями должна обнулиться.
|
||||
@@ -63,8 +63,8 @@ public sealed class DataRetentionSchedulerTests
|
||||
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 audit = new TestAuditLogStore();
|
||||
await audit.Store.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 TestRateLimitCounterStore());
|
||||
@@ -86,12 +86,12 @@ public sealed class DataRetentionSchedulerTests
|
||||
// counters: Хелпер счётчиков (окна посеяны сценарием).
|
||||
// Возвращает: Провайдер с сервисами цикла.
|
||||
private static ServiceProvider BuildProvider(
|
||||
FakeAuditLogStore audit,
|
||||
TestAuditLogStore audit,
|
||||
FakeTenantLimitStore limits,
|
||||
TestRateLimitCounterStore counters)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddScoped<IAuditLogStore>(_ => audit);
|
||||
services.AddScoped<IAuditLogStore>(_ => audit.Store);
|
||||
services.AddScoped<ITenantLimitStore>(_ => limits);
|
||||
services.AddScoped<IRateLimitCounterStore>(_ => counters.Store);
|
||||
return services.BuildServiceProvider();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Deal.Modules.Tenants.Application.Abstractions;
|
||||
using Deal.Modules.Tenants.Application.Models;
|
||||
using Deal.Modules.Tenants.Application.Services;
|
||||
using Deal.Tests.Unit.Support;
|
||||
|
||||
namespace Deal.Tests.Unit.Modules.Tenants;
|
||||
|
||||
@@ -14,8 +15,8 @@ public sealed class AuditServiceTests
|
||||
[Fact]
|
||||
public async Task AppendAsync_SetsAtToUtcNow_AndSavesAllFields()
|
||||
{
|
||||
var store = new FakeAuditLogStore();
|
||||
var service = new AuditService(store);
|
||||
var store = new TestAuditLogStore();
|
||||
var service = new AuditService(store.Store);
|
||||
var actorId = Guid.NewGuid();
|
||||
var tenantId = Guid.NewGuid();
|
||||
var before = DateTimeOffset.UtcNow;
|
||||
@@ -43,11 +44,11 @@ public sealed class AuditServiceTests
|
||||
[Fact]
|
||||
public async Task QueryAsync_ReturnsFilteredRecordsNewestFirst()
|
||||
{
|
||||
var store = new FakeAuditLogStore();
|
||||
var store = new TestAuditLogStore();
|
||||
await AppendWithAtAsync(store, AuditEvents.OperatorLoginOk, AuditActorTypes.Operator, tenantId: null, DateTimeOffset.UtcNow.AddMinutes(-2));
|
||||
await AppendWithAtAsync(store, AuditEvents.TenantLoginFailed, AuditActorTypes.Tenant, tenantId: null, DateTimeOffset.UtcNow.AddMinutes(-1));
|
||||
await AppendWithAtAsync(store, AuditEvents.TenantLoginOk, AuditActorTypes.Tenant, tenantId: null, DateTimeOffset.UtcNow);
|
||||
var service = new AuditService(store);
|
||||
var service = new AuditService(store.Store);
|
||||
|
||||
var all = await service.QueryAsync(EmptyFilter(limit: 100), CancellationToken.None);
|
||||
Assert.Equal(3, all.Count);
|
||||
@@ -71,7 +72,7 @@ public sealed class AuditServiceTests
|
||||
[Fact]
|
||||
public async Task QueryAsync_AppliesAtRangeAndTenantFilters()
|
||||
{
|
||||
var store = new FakeAuditLogStore();
|
||||
var store = new TestAuditLogStore();
|
||||
var tenantId = Guid.NewGuid();
|
||||
var outsideTenantId = Guid.NewGuid();
|
||||
var from = DateTimeOffset.UtcNow.AddMinutes(-10);
|
||||
@@ -79,7 +80,7 @@ public sealed class AuditServiceTests
|
||||
await AppendWithAtAsync(store, AuditEvents.TenantLoginOk, AuditActorTypes.Tenant, tenantId, from.AddMinutes(-1)); // вне окна
|
||||
await AppendWithAtAsync(store, AuditEvents.TenantLoginOk, AuditActorTypes.Tenant, tenantId, from);
|
||||
await AppendWithAtAsync(store, AuditEvents.TenantLoginOk, AuditActorTypes.Tenant, outsideTenantId, to);
|
||||
var service = new AuditService(store);
|
||||
var service = new AuditService(store.Store);
|
||||
|
||||
var byRange = await service.QueryAsync(
|
||||
new AuditQueryDto(AuditEvents.TenantLoginOk, null, null, from, to, 100), CancellationToken.None);
|
||||
@@ -94,13 +95,13 @@ public sealed class AuditServiceTests
|
||||
[Fact]
|
||||
public async Task CountAsync_CountsAllRowsMatchingFilterRegardlessOfLimit()
|
||||
{
|
||||
var store = new FakeAuditLogStore();
|
||||
var store = new TestAuditLogStore();
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
await AppendWithAtAsync(store, AuditEvents.OperatorLoginOk, AuditActorTypes.Operator, tenantId: null, DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
var service = new AuditService(store);
|
||||
var service = new AuditService(store.Store);
|
||||
var filter = new AuditQueryDto(AuditEvents.OperatorLoginOk, null, null, null, null, Limit: 2);
|
||||
|
||||
Assert.Equal(7, await service.CountAsync(filter, CancellationToken.None));
|
||||
@@ -163,13 +164,13 @@ public sealed class AuditServiceTests
|
||||
|
||||
// Добавляет запись в фейк с явным At/актором (минуя сервис — для проверок сортировки/фильтров).
|
||||
private static async Task AppendWithAtAsync(
|
||||
FakeAuditLogStore store,
|
||||
TestAuditLogStore store,
|
||||
string eventType,
|
||||
string actorType,
|
||||
Guid? tenantId,
|
||||
DateTimeOffset at)
|
||||
{
|
||||
await store.AppendAsync(new AuditRecordDto(
|
||||
await store.Store.AppendAsync(new AuditRecordDto(
|
||||
eventType,
|
||||
actorType,
|
||||
ActorId: null,
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
using Deal.Modules.Tenants.Application.Abstractions;
|
||||
using Deal.Modules.Tenants.Application.Models;
|
||||
using Deal.Modules.Tenants.Application.Services;
|
||||
|
||||
namespace Deal.Tests.Unit.Modules.Tenants;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory реализация <see cref="IAuditLogStore"/> для юнит/HTTP-тестов аудита.
|
||||
/// </summary>
|
||||
public sealed class FakeAuditLogStore : IAuditLogStore
|
||||
{
|
||||
private readonly List<AuditRecordDto> _records = [];
|
||||
|
||||
/// <summary>
|
||||
/// Записи хранилища в порядке добавления
|
||||
/// </summary>
|
||||
public IReadOnlyList<AuditRecordDto> Records => _records;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AppendAsync(AuditRecordDto record, CancellationToken ct)
|
||||
{
|
||||
_records.Add(record with { Id = _records.Count + 1 });
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<AuditRecordDto>> QueryAsync(AuditQueryDto filter, CancellationToken ct)
|
||||
{
|
||||
IReadOnlyList<AuditRecordDto> result = ApplyFilters(filter)
|
||||
.OrderByDescending(r => r.At)
|
||||
.Skip(Math.Max(0, filter.Offset))
|
||||
.Take(Math.Max(1, Math.Min(AuditService.MaxQueryLimit, filter.Limit)))
|
||||
.ToList();
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> CountAsync(AuditQueryDto filter, CancellationToken ct) =>
|
||||
Task.FromResult(ApplyFilters(filter).Count());
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> PurgeOlderThanAsync(DateTimeOffset cutoff, CancellationToken ct)
|
||||
{
|
||||
int removed = _records.RemoveAll(record => record.At < cutoff);
|
||||
return Task.FromResult(removed);
|
||||
}
|
||||
|
||||
private IEnumerable<AuditRecordDto> ApplyFilters(AuditQueryDto filter) =>
|
||||
_records.Where(r =>
|
||||
(filter.EventType is null || r.EventType == filter.EventType) &&
|
||||
(filter.ActorType is null || r.ActorType == filter.ActorType) &&
|
||||
(filter.TenantId is null || r.TenantId == filter.TenantId) &&
|
||||
(filter.ActorId is null || r.ActorId == filter.ActorId) &&
|
||||
(filter.From is null || r.At >= filter.From.Value) &&
|
||||
(filter.To is null || r.At <= filter.To.Value));
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Deal.Modules.Tenants.Application.Models;
|
||||
using Deal.Modules.Tenants.Application.Services;
|
||||
using Deal.Tests.Unit.Support;
|
||||
|
||||
namespace Deal.Tests.Unit.Modules.Tenants;
|
||||
|
||||
@@ -14,7 +15,7 @@ public sealed class SuspiciousActivityServiceTests
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_NoRecords_ReturnsEmpty()
|
||||
{
|
||||
SuspiciousActivityService service = Create(new FakeAuditLogStore());
|
||||
SuspiciousActivityService service = Create(new TestAuditLogStore());
|
||||
|
||||
SuspiciousActivityDto report = await service.AnalyzeAsync(null, null, CancellationToken.None);
|
||||
|
||||
@@ -26,7 +27,7 @@ public sealed class SuspiciousActivityServiceTests
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_FailedLoginsPerIp_TriggersAtThreshold()
|
||||
{
|
||||
var store = new FakeAuditLogStore();
|
||||
var store = new TestAuditLogStore();
|
||||
for (int i = 0; i < SuspiciousActivityService.FailedLoginsPerIpThreshold; i++)
|
||||
{
|
||||
SeedFailed(store, ip: "10.0.0.1", login: "user", minutesAgo: i);
|
||||
@@ -46,7 +47,7 @@ public sealed class SuspiciousActivityServiceTests
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_FailedLoginsPerIp_DoubleThreshold_IsHigh()
|
||||
{
|
||||
var store = new FakeAuditLogStore();
|
||||
var store = new TestAuditLogStore();
|
||||
int count = SuspiciousActivityService.FailedLoginsPerIpThreshold * 2;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
@@ -66,7 +67,7 @@ public sealed class SuspiciousActivityServiceTests
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_FailedLoginsPerLogin_Triggers()
|
||||
{
|
||||
var store = new FakeAuditLogStore();
|
||||
var store = new TestAuditLogStore();
|
||||
for (int i = 0; i < SuspiciousActivityService.FailedLoginsPerLoginThreshold; i++)
|
||||
{
|
||||
SeedFailed(store, ip: $"10.0.0.{i + 1}", login: "target", minutesAgo: i);
|
||||
@@ -84,11 +85,11 @@ public sealed class SuspiciousActivityServiceTests
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_ManyIpsPerActor_Triggers()
|
||||
{
|
||||
var store = new FakeAuditLogStore();
|
||||
var store = new TestAuditLogStore();
|
||||
Guid actor = Guid.NewGuid();
|
||||
for (int i = 0; i < SuspiciousActivityService.DistinctIpsPerActorThreshold; i++)
|
||||
{
|
||||
await store.AppendAsync(
|
||||
await store.Store.AppendAsync(
|
||||
new AuditRecordDto(
|
||||
AuditEvents.TenantLoginOk,
|
||||
AuditActorTypes.Tenant,
|
||||
@@ -113,10 +114,10 @@ public sealed class SuspiciousActivityServiceTests
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_AuthFailuresPerTenant_Triggers()
|
||||
{
|
||||
var store = new FakeAuditLogStore();
|
||||
var store = new TestAuditLogStore();
|
||||
for (int i = 0; i < SuspiciousActivityService.AuthFailuresPerTenantThreshold; i++)
|
||||
{
|
||||
await store.AppendAsync(
|
||||
await store.Store.AppendAsync(
|
||||
new AuditRecordDto(
|
||||
AuditEvents.TenantLoginFailed,
|
||||
AuditActorTypes.Tenant,
|
||||
@@ -140,7 +141,7 @@ public sealed class SuspiciousActivityServiceTests
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DistinctLoginsPerIp_TriggersAtThreshold()
|
||||
{
|
||||
var store = new FakeAuditLogStore();
|
||||
var store = new TestAuditLogStore();
|
||||
for (int i = 0; i < SuspiciousActivityService.DistinctLoginsPerIpThreshold; i++)
|
||||
{
|
||||
SeedFailed(store, ip: "10.3.0.1", login: $"user{i}", minutesAgo: i);
|
||||
@@ -160,7 +161,7 @@ public sealed class SuspiciousActivityServiceTests
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DistinctLoginsPerIp_DoubleThreshold_IsHigh()
|
||||
{
|
||||
var store = new FakeAuditLogStore();
|
||||
var store = new TestAuditLogStore();
|
||||
int count = SuspiciousActivityService.DistinctLoginsPerIpThreshold * 2;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
@@ -180,7 +181,7 @@ public sealed class SuspiciousActivityServiceTests
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DistinctLoginsPerIp_BelowThreshold_NoFinding()
|
||||
{
|
||||
var store = new FakeAuditLogStore();
|
||||
var store = new TestAuditLogStore();
|
||||
for (int i = 0; i < SuspiciousActivityService.DistinctLoginsPerIpThreshold - 1; i++)
|
||||
{
|
||||
SeedFailed(store, ip: "10.3.0.3", login: $"user{i}", minutesAgo: i);
|
||||
@@ -195,7 +196,7 @@ public sealed class SuspiciousActivityServiceTests
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DistinctLoginsPerIp_SameLoginRepeated_NoFinding()
|
||||
{
|
||||
var store = new FakeAuditLogStore();
|
||||
var store = new TestAuditLogStore();
|
||||
for (int i = 0; i < SuspiciousActivityService.DistinctLoginsPerIpThreshold + 1; i++)
|
||||
{
|
||||
SeedFailed(store, ip: "10.3.0.4", login: "repeated", minutesAgo: i);
|
||||
@@ -210,7 +211,7 @@ public sealed class SuspiciousActivityServiceTests
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_DistinctLoginsPerIp_SuccessfulLoginsIgnored()
|
||||
{
|
||||
var store = new FakeAuditLogStore();
|
||||
var store = new TestAuditLogStore();
|
||||
for (int i = 0; i < SuspiciousActivityService.DistinctLoginsPerIpThreshold; i++)
|
||||
{
|
||||
SeedSuccess(store, ip: "10.3.0.5", login: $"user{i}", minutesAgo: i);
|
||||
@@ -225,7 +226,7 @@ public sealed class SuspiciousActivityServiceTests
|
||||
[Fact]
|
||||
public async Task AnalyzeAsync_RecordsOutsideWindow_AreIgnored()
|
||||
{
|
||||
var store = new FakeAuditLogStore();
|
||||
var store = new TestAuditLogStore();
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
SeedFailed(store, ip: "10.0.0.1", login: "user", minutesAgo: 60 * 30); // 30 часов назад — вне суток
|
||||
@@ -239,12 +240,12 @@ public sealed class SuspiciousActivityServiceTests
|
||||
|
||||
// Пишет запись «неудачный вход» с заданным временем.
|
||||
private static void SeedFailed(
|
||||
FakeAuditLogStore store,
|
||||
TestAuditLogStore store,
|
||||
string ip,
|
||||
string login,
|
||||
int minutesAgo)
|
||||
{
|
||||
store.AppendAsync(
|
||||
store.Store.AppendAsync(
|
||||
new AuditRecordDto(
|
||||
AuditEvents.TenantLoginFailed,
|
||||
AuditActorTypes.Tenant,
|
||||
@@ -258,12 +259,12 @@ public sealed class SuspiciousActivityServiceTests
|
||||
|
||||
// Пишет запись «успешный вход» с заданным временем.
|
||||
private static void SeedSuccess(
|
||||
FakeAuditLogStore store,
|
||||
TestAuditLogStore store,
|
||||
string ip,
|
||||
string login,
|
||||
int minutesAgo)
|
||||
{
|
||||
store.AppendAsync(
|
||||
store.Store.AppendAsync(
|
||||
new AuditRecordDto(
|
||||
AuditEvents.TenantLoginOk,
|
||||
AuditActorTypes.Tenant,
|
||||
@@ -275,5 +276,5 @@ public sealed class SuspiciousActivityServiceTests
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
private static SuspiciousActivityService Create(FakeAuditLogStore store) => new(store, () => Now);
|
||||
private static SuspiciousActivityService Create(TestAuditLogStore store) => new(store.Store, () => Now);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public sealed class JoinEndpointHttpTests
|
||||
var authStore = new FakeAuthStore();
|
||||
var tenantStore = new TestTenantStore();
|
||||
var provisioner = new TestTenantProvisioner();
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
|
||||
await RunAsync(
|
||||
inviteStore, tenantStore, provisioner.Provisioner, authStore, auditStore,
|
||||
@@ -83,7 +83,7 @@ public sealed class JoinEndpointHttpTests
|
||||
var authStore = new FakeAuthStore();
|
||||
|
||||
await RunAsync(
|
||||
inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new FakeAuditLogStore(),
|
||||
inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new TestAuditLogStore(),
|
||||
async (baseAddress, client) =>
|
||||
{
|
||||
using (HttpResponseMessage first = await PostJsonAsync(
|
||||
@@ -111,7 +111,7 @@ public sealed class JoinEndpointHttpTests
|
||||
var authStore = new FakeAuthStore();
|
||||
|
||||
await RunAsync(
|
||||
inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new FakeAuditLogStore(),
|
||||
inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new TestAuditLogStore(),
|
||||
async (baseAddress, client) =>
|
||||
{
|
||||
using HttpResponseMessage response = await PostJsonAsync(
|
||||
@@ -132,7 +132,7 @@ public sealed class JoinEndpointHttpTests
|
||||
inviteStore.AddInvite(NewInvite(Email, status: InviteStatuses.Revoked));
|
||||
|
||||
await RunAsync(
|
||||
inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new FakeAuditLogStore(),
|
||||
inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new TestAuditLogStore(),
|
||||
async (baseAddress, client) =>
|
||||
{
|
||||
using HttpResponseMessage response = await PostJsonAsync(
|
||||
@@ -150,7 +150,7 @@ public sealed class JoinEndpointHttpTests
|
||||
inviteStore.AddInvite(NewInvite(Email, expiresAt: DateTimeOffset.UtcNow.AddHours(-1)));
|
||||
|
||||
await RunAsync(
|
||||
inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new FakeAuditLogStore(),
|
||||
inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new TestAuditLogStore(),
|
||||
async (baseAddress, client) =>
|
||||
{
|
||||
using HttpResponseMessage response = await PostJsonAsync(
|
||||
@@ -165,7 +165,7 @@ public sealed class JoinEndpointHttpTests
|
||||
public async Task Join_WithUnknownCode_Returns400NotFound()
|
||||
{
|
||||
await RunAsync(
|
||||
new TestInviteStore(), new TestTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new FakeAuditLogStore(),
|
||||
new TestInviteStore(), new TestTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new TestAuditLogStore(),
|
||||
async (baseAddress, client) =>
|
||||
{
|
||||
using HttpResponseMessage response = await PostJsonAsync(
|
||||
@@ -183,7 +183,7 @@ public sealed class JoinEndpointHttpTests
|
||||
inviteStore.AddInvite(NewInvite(Email));
|
||||
|
||||
await RunAsync(
|
||||
inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new FakeAuditLogStore(),
|
||||
inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), new TestAuditLogStore(),
|
||||
async (baseAddress, client) =>
|
||||
{
|
||||
using HttpResponseMessage response = await PostJsonAsync(
|
||||
@@ -205,7 +205,7 @@ public sealed class JoinEndpointHttpTests
|
||||
authStore.AddUser(new StoredUserDto(Guid.NewGuid(), Email, TenantId: Guid.NewGuid(), Status: "active", "hash"));
|
||||
|
||||
await RunAsync(
|
||||
inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new FakeAuditLogStore(),
|
||||
inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, authStore, new TestAuditLogStore(),
|
||||
async (baseAddress, client) =>
|
||||
{
|
||||
using HttpResponseMessage response = await PostJsonAsync(
|
||||
@@ -231,7 +231,7 @@ public sealed class JoinEndpointHttpTests
|
||||
var authStore = new FakeAuthStore();
|
||||
|
||||
await RunAsync(
|
||||
inviteStore, tenantStore, provisioner.Provisioner, authStore, new FakeAuditLogStore(),
|
||||
inviteStore, tenantStore, provisioner.Provisioner, authStore, new TestAuditLogStore(),
|
||||
async (baseAddress, client) =>
|
||||
{
|
||||
using HttpResponseMessage response = await PostJsonAsync(
|
||||
@@ -253,7 +253,7 @@ public sealed class JoinEndpointHttpTests
|
||||
TestTenantStore tenantStore,
|
||||
ITenantProvisioner provisioner,
|
||||
FakeAuthStore authStore,
|
||||
FakeAuditLogStore auditStore,
|
||||
TestAuditLogStore auditStore,
|
||||
Func<string, HttpClient, Task> scenario)
|
||||
{
|
||||
int port = TestPort.Allocate();
|
||||
@@ -265,7 +265,7 @@ public sealed class JoinEndpointHttpTests
|
||||
// последняя регистрация (зеркало OperatorAuthHttpHost).
|
||||
builder.Services.AddSingleton(TestHashers.New());
|
||||
builder.Services.AddSingleton<IAuthStore>(authStore);
|
||||
builder.Services.AddSingleton<IAuditLogStore>(auditStore);
|
||||
builder.Services.AddSingleton<IAuditLogStore>(auditStore.Store);
|
||||
builder.Services.AddSingleton<IInviteStore>(inviteStore.Store);
|
||||
builder.Services.AddSingleton<ITenantRepository>(tenantStore.Repository);
|
||||
builder.Services.AddSingleton<ITenantProvisioner>(provisioner);
|
||||
|
||||
@@ -43,7 +43,7 @@ public sealed class OperatorAnalyticsEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task Overview_ReturnsTenantsTokensEventsAndLoginCounters()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
await SeedAuditAsync(auditStore, AuditEvents.TenantLoginOk, AuditActorTypes.Tenant, ActiveTenant);
|
||||
await SeedAuditAsync(auditStore, AuditEvents.TenantLogout, AuditActorTypes.Tenant, ActiveTenant);
|
||||
await SeedAuditAsync(auditStore, AuditEvents.TenantLoginFailed, AuditActorTypes.Tenant, ActiveTenant);
|
||||
@@ -83,10 +83,10 @@ public sealed class OperatorAnalyticsEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task Suspicious_ReturnsFindingsForFailedLoginBurst()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
for (int i = 0; i < SuspiciousActivityService.FailedLoginsPerIpThreshold; i++)
|
||||
{
|
||||
await auditStore.AppendAsync(
|
||||
await auditStore.Store.AppendAsync(
|
||||
new AuditRecordDto(
|
||||
AuditEvents.TenantLoginFailed,
|
||||
AuditActorTypes.Tenant,
|
||||
@@ -179,7 +179,7 @@ public sealed class OperatorAnalyticsEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task Activity_FiltersByActorIdAndPaginates()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
Guid actor = Guid.NewGuid();
|
||||
await SeedAuditAsync(auditStore, AuditEvents.CardCreated, AuditActorTypes.Tenant, ActiveTenant, actor, minutesAgo: 30);
|
||||
await SeedAuditAsync(auditStore, AuditEvents.CardMoved, AuditActorTypes.Tenant, ActiveTenant, actor, minutesAgo: 20);
|
||||
@@ -222,7 +222,7 @@ public sealed class OperatorAnalyticsEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task Audit_FiltersByActorIdAndOffset()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
Guid actor = Guid.NewGuid();
|
||||
await SeedAuditAsync(auditStore, AuditEvents.CardCreated, AuditActorTypes.Tenant, ActiveTenant, actor, minutesAgo: 30);
|
||||
await SeedAuditAsync(auditStore, AuditEvents.CardMoved, AuditActorTypes.Tenant, ActiveTenant, actor, minutesAgo: 20);
|
||||
@@ -275,13 +275,13 @@ public sealed class OperatorAnalyticsEndpointsHttpTests
|
||||
}
|
||||
|
||||
private static Task SeedAuditAsync(
|
||||
FakeAuditLogStore store,
|
||||
TestAuditLogStore store,
|
||||
string eventType,
|
||||
string actorType,
|
||||
Guid? tenantId,
|
||||
Guid? actorId = null,
|
||||
int minutesAgo = 1) =>
|
||||
store.AppendAsync(
|
||||
store.Store.AppendAsync(
|
||||
new AuditRecordDto(
|
||||
eventType,
|
||||
actorType,
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed class OperatorAuditEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task OperatorLoginSuccess_WritesOperatorLoginOkAuditRecord()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
|
||||
await OperatorAuthHttpHost.RunAsync(
|
||||
NewOperatorStore(),
|
||||
@@ -65,7 +65,7 @@ public sealed class OperatorAuditEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task OperatorLoginFailure_WritesOperatorLoginFailedAuditRecord()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
|
||||
await OperatorAuthHttpHost.RunAsync(
|
||||
NewOperatorStore(),
|
||||
@@ -91,7 +91,7 @@ public sealed class OperatorAuditEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task TenantLoginSuccessAndFailure_WriteTenantAuditRecords()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
|
||||
await OperatorAuthHttpHost.RunAsync(
|
||||
NewOperatorStore(),
|
||||
@@ -137,9 +137,9 @@ public sealed class OperatorAuditEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task Audit_ReturnsItemsNewestFirstWithTotal()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
// Историческая запись со старым At — гарантированно последняя в выборке (проверка At DESC).
|
||||
await auditStore.AppendAsync(new AuditRecordDto(
|
||||
await auditStore.Store.AppendAsync(new AuditRecordDto(
|
||||
AuditEvents.TenantCreated,
|
||||
AuditActorTypes.Operator,
|
||||
ActorId: Guid.NewGuid(),
|
||||
@@ -177,7 +177,7 @@ public sealed class OperatorAuditEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task Audit_FiltersByEventTypeAndActorType()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
|
||||
await OperatorAuthHttpHost.RunAsync(
|
||||
NewOperatorStore(),
|
||||
@@ -218,7 +218,7 @@ public sealed class OperatorAuditEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task TenantLogout_WritesTenantLogoutAuditRecord()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
|
||||
await OperatorAuthHttpHost.RunAsync(
|
||||
NewOperatorStore(),
|
||||
@@ -251,7 +251,7 @@ public sealed class OperatorAuditEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task OperatorLogout_WritesOperatorLogoutAuditRecord()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
|
||||
await OperatorAuthHttpHost.RunAsync(
|
||||
NewOperatorStore(),
|
||||
|
||||
@@ -43,7 +43,7 @@ internal static class OperatorAuthHttpHost
|
||||
FakeOperatorAuthStore operatorStore,
|
||||
FakeAuthStore userStore,
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, Task> scenario,
|
||||
FakeAuditLogStore? auditStore = null) =>
|
||||
TestAuditLogStore? auditStore = null) =>
|
||||
await RunCoreAsync(
|
||||
operatorStore,
|
||||
userStore,
|
||||
@@ -64,8 +64,8 @@ internal static class OperatorAuthHttpHost
|
||||
public static async Task RunAsync(
|
||||
FakeOperatorAuthStore operatorStore,
|
||||
FakeAuthStore userStore,
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestInviteStore, FakeAuditLogStore, Task> scenario,
|
||||
FakeAuditLogStore? auditStore = null,
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestInviteStore, TestAuditLogStore, Task> scenario,
|
||||
TestAuditLogStore? auditStore = null,
|
||||
TestInviteStore? inviteStore = null) =>
|
||||
await RunCoreAsync(
|
||||
operatorStore,
|
||||
@@ -88,8 +88,8 @@ internal static class OperatorAuthHttpHost
|
||||
public static async Task RunAsync(
|
||||
FakeOperatorAuthStore operatorStore,
|
||||
FakeAuthStore userStore,
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, FakeAuditLogStore, Task> scenario,
|
||||
FakeAuditLogStore? auditStore = null,
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, Task> scenario,
|
||||
TestAuditLogStore? auditStore = null,
|
||||
TestInviteStore? inviteStore = null,
|
||||
TestTenantStore? tenantStore = null) =>
|
||||
await RunCoreAsync(
|
||||
@@ -115,8 +115,8 @@ internal static class OperatorAuthHttpHost
|
||||
public static async Task RunAsync(
|
||||
FakeOperatorAuthStore operatorStore,
|
||||
FakeAuthStore userStore,
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, FakeAuditLogStore, FakeTenantLimitStore, Task> scenario,
|
||||
FakeAuditLogStore? auditStore = null,
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, FakeTenantLimitStore, Task> scenario,
|
||||
TestAuditLogStore? auditStore = null,
|
||||
TestInviteStore? inviteStore = null,
|
||||
TestTenantStore? tenantStore = null,
|
||||
FakeTenantLimitStore? limitStore = null,
|
||||
@@ -144,8 +144,8 @@ internal static class OperatorAuthHttpHost
|
||||
public static async Task RunWithGlobalSettingsAsync(
|
||||
FakeOperatorAuthStore operatorStore,
|
||||
FakeAuthStore userStore,
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestGlobalSettingsStore, FakeAuditLogStore, Task> scenario,
|
||||
FakeAuditLogStore? auditStore = null,
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestGlobalSettingsStore, TestAuditLogStore, Task> scenario,
|
||||
TestAuditLogStore? auditStore = null,
|
||||
TestGlobalSettingsStore? globalSettingsStore = null) =>
|
||||
await RunCoreAsync(
|
||||
operatorStore,
|
||||
@@ -161,16 +161,16 @@ internal static class OperatorAuthHttpHost
|
||||
private static async Task RunCoreAsync(
|
||||
FakeOperatorAuthStore operatorStore,
|
||||
FakeAuthStore userStore,
|
||||
FakeAuditLogStore? auditStore,
|
||||
TestAuditLogStore? auditStore,
|
||||
TestInviteStore? inviteStore,
|
||||
TestTenantStore? tenantStore,
|
||||
FakeTenantLimitStore? limitStore,
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, FakeAuditLogStore, FakeTenantLimitStore, TestGlobalSettingsStore, Task> scenario,
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, FakeTenantLimitStore, TestGlobalSettingsStore, Task> scenario,
|
||||
RateLimitOptions? rateLimitOptions = null,
|
||||
TestTokenUsageEventStore? tokenUsageStore = null,
|
||||
TestGlobalSettingsStore? globalSettingsStore = null)
|
||||
{
|
||||
FakeAuditLogStore effectiveAuditStore = auditStore ?? new FakeAuditLogStore();
|
||||
TestAuditLogStore effectiveAuditStore = auditStore ?? new TestAuditLogStore();
|
||||
TestInviteStore effectiveInviteStore = inviteStore ?? new TestInviteStore();
|
||||
TestTenantStore effectiveTenantStore = tenantStore ?? new TestTenantStore();
|
||||
FakeTenantLimitStore effectiveLimitStore = limitStore ?? new FakeTenantLimitStore();
|
||||
@@ -188,7 +188,7 @@ internal static class OperatorAuthHttpHost
|
||||
builder.Services.AddSingleton<ITenantContext, TenantContext>();
|
||||
builder.Services.AddSingleton<IAuthStore>(userStore);
|
||||
builder.Services.AddSingleton<IOperatorAuthStore>(operatorStore);
|
||||
builder.Services.AddSingleton<IAuditLogStore>(effectiveAuditStore);
|
||||
builder.Services.AddSingleton<IAuditLogStore>(effectiveAuditStore.Store);
|
||||
builder.Services.AddSingleton<IInviteStore>(effectiveInviteStore.Store);
|
||||
builder.Services.AddSingleton<ITenantRepository>(effectiveTenantStore.Repository);
|
||||
builder.Services.AddSingleton<ITenantLimitStore>(effectiveLimitStore);
|
||||
|
||||
@@ -92,7 +92,7 @@ public sealed class OperatorHealthEndpointsHttpTests
|
||||
}
|
||||
|
||||
// Прогоняет сценарий на хосте с активным оператором operator/operator (реестр/лимиты пустые).
|
||||
private static Task RunAsync(Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, FakeAuditLogStore, FakeTenantLimitStore, Task> scenario) =>
|
||||
private static Task RunAsync(Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, FakeTenantLimitStore, Task> scenario) =>
|
||||
OperatorAuthHttpHost.RunAsync(NewOperatorStore(), new FakeAuthStore(), scenario);
|
||||
|
||||
// Фейк-хранилище оператора с активным оператором operator/operator.
|
||||
|
||||
@@ -70,7 +70,7 @@ public sealed class OperatorInvitesEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task CreateThenListThenRevoke_FullOperatorFlow_WorksAndWritesAudit()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
|
||||
await OperatorAuthHttpHost.RunAsync(
|
||||
NewOperatorStore(),
|
||||
|
||||
@@ -120,7 +120,7 @@ public sealed class OperatorLimitsEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task PatchLimit_ResetsFlags_UpdatesBudgetAndWritesAudit()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
const long newBudget = 20_000_000;
|
||||
|
||||
await RunAsync(
|
||||
@@ -165,7 +165,7 @@ public sealed class OperatorLimitsEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task PatchLimit_OnlyPeriod_KeepsBudgetAndWritesAudit()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
|
||||
await RunAsync(
|
||||
async (baseAddress, operatorStore, _, _, _, _, _) =>
|
||||
@@ -195,7 +195,7 @@ public sealed class OperatorLimitsEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task PatchLimit_WithSameValues_IsIdempotent_WithoutNewAudit()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
const long budget = 25_000_000;
|
||||
|
||||
await RunAsync(
|
||||
@@ -284,7 +284,7 @@ public sealed class OperatorLimitsEndpointsHttpTests
|
||||
$"{baseAddress}/api/operator/tenants/{tenantId}/limit";
|
||||
|
||||
// Прогоняет сценарий на хосте с двумя активными тенантами и предзаполненными строками лимитов.
|
||||
private static Task RunAsync(Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, FakeAuditLogStore, FakeTenantLimitStore, Task> scenario, FakeAuditLogStore? auditStore = null)
|
||||
private static Task RunAsync(Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, FakeTenantLimitStore, Task> scenario, TestAuditLogStore? auditStore = null)
|
||||
{
|
||||
// Первый тенант: расход 80% месячного бюджета (8 000 000 / 10 000 000) + флаг Warned80;
|
||||
// второй: небольшой бюджет без расхода. Дата начала периода — текущая (reset не срабатывает).
|
||||
|
||||
@@ -72,7 +72,7 @@ public sealed class OperatorMaintenanceEndpointsHttpTests
|
||||
}
|
||||
|
||||
// Прогоняет сценарий на хосте с активным оператором operator/operator и заданным реестром тенантов.
|
||||
private static Task RunAsync(TestTenantStore tenantStore, Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, FakeAuditLogStore, Task> scenario) =>
|
||||
private static Task RunAsync(TestTenantStore tenantStore, Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, Task> scenario) =>
|
||||
OperatorAuthHttpHost.RunAsync(NewOperatorStore(), new FakeAuthStore(), scenario, tenantStore: tenantStore);
|
||||
|
||||
// Фейк-хранилище оператора с активным оператором operator/operator.
|
||||
|
||||
@@ -61,7 +61,7 @@ public sealed class OperatorSettingsEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task PutTelegramKeys_SavesEncryptedKeysMasksResponseAndWritesAudit()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
|
||||
await RunAsync(
|
||||
async (baseAddress, operatorStore, _, globalSettings, audit) =>
|
||||
@@ -257,7 +257,7 @@ public sealed class OperatorSettingsEndpointsHttpTests
|
||||
$"{baseAddress}/api/operator/settings/telegram-keys";
|
||||
|
||||
// Прогоняет сценарий на хосте с фейком глобального хранилища и аудита.
|
||||
private static Task RunAsync(Func<string, FakeOperatorAuthStore, FakeAuthStore, TestGlobalSettingsStore, FakeAuditLogStore, Task> scenario, FakeAuditLogStore? auditStore = null) =>
|
||||
private static Task RunAsync(Func<string, FakeOperatorAuthStore, FakeAuthStore, TestGlobalSettingsStore, TestAuditLogStore, Task> scenario, TestAuditLogStore? auditStore = null) =>
|
||||
OperatorAuthHttpHost.RunWithGlobalSettingsAsync(
|
||||
NewOperatorStore(),
|
||||
new FakeAuthStore(),
|
||||
|
||||
@@ -97,7 +97,7 @@ public sealed class OperatorTenantsEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task Create_ReturnsCreatedTenant_AndWritesTenantCreatedAudit()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
|
||||
await RunAsync(
|
||||
async (baseAddress, operatorStore, _, tenantStore, _) =>
|
||||
@@ -138,7 +138,7 @@ public sealed class OperatorTenantsEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task Create_WithOwnerEmail_ReturnsOneTimePasswordAndCreatesOwner()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
const string ownerEmail = "owner@created.com";
|
||||
|
||||
await RunAsync(
|
||||
@@ -237,7 +237,7 @@ public sealed class OperatorTenantsEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task Suspend_BlocksTenantLogin_ThenUnsuspend_RestoresLogin()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
|
||||
await RunAsync(
|
||||
async (baseAddress, operatorStore, userStore, tenantStore, _) =>
|
||||
@@ -300,7 +300,7 @@ public sealed class OperatorTenantsEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task Suspend_AlreadySuspendedTenant_IsOkWithoutDuplicateAudit()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
var tenantStore = new TestTenantStore(
|
||||
new TenantRecordDto(TenantId, TenantName, TenantStatuses.Suspended, DateTimeOffset.UtcNow));
|
||||
|
||||
@@ -342,7 +342,7 @@ public sealed class OperatorTenantsEndpointsHttpTests
|
||||
[Fact]
|
||||
public async Task Impersonate_ReturnsSessionToken_WorksAsDealSession_AndLogoutWritesStoppedAudit()
|
||||
{
|
||||
var auditStore = new FakeAuditLogStore();
|
||||
var auditStore = new TestAuditLogStore();
|
||||
|
||||
await RunAsync(
|
||||
async (baseAddress, operatorStore, userStore, _, _) =>
|
||||
@@ -486,8 +486,8 @@ public sealed class OperatorTenantsEndpointsHttpTests
|
||||
|
||||
// Прогоняет сценарий на хосте с одним активным тенантом и одним пользователем в нём.
|
||||
private static Task RunAsync(
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, FakeAuditLogStore, Task> scenario,
|
||||
FakeAuditLogStore? auditStore = null,
|
||||
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestAuditLogStore, Task> scenario,
|
||||
TestAuditLogStore? auditStore = null,
|
||||
TestTenantStore? tenantStore = null)
|
||||
{
|
||||
// По умолчанию в реестре — активный целевой тенант (пользователь в нём — NewUserStore).
|
||||
@@ -503,7 +503,7 @@ public sealed class OperatorTenantsEndpointsHttpTests
|
||||
|
||||
// Проверяет запись tenant_status_changed с заданным статусом и актором-оператором.
|
||||
private static void AssertStatusChangedAudit(
|
||||
FakeAuditLogStore auditStore,
|
||||
TestAuditLogStore auditStore,
|
||||
FakeOperatorAuthStore operatorStore,
|
||||
string status)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
using Deal.Modules.Tenants.Application.Abstractions;
|
||||
using Deal.Modules.Tenants.Application.Models;
|
||||
using Deal.Modules.Tenants.Application.Services;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Deal.Tests.Unit.Support;
|
||||
|
||||
/// <summary>
|
||||
/// Подставка <see cref="IAuditLogStore"/> на списках: сервисы получают NSubstitute-подставку
|
||||
/// (<see cref="Store"/>), тесты проверяют записи через <see cref="Records"/>.
|
||||
/// </summary>
|
||||
public sealed class TestAuditLogStore
|
||||
{
|
||||
private readonly List<AuditRecordDto> _records = [];
|
||||
|
||||
/// <summary>
|
||||
/// Подставка порта аудита (создаётся в конструкторе).
|
||||
/// </summary>
|
||||
public IAuditLogStore Store { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Записи хранилища в порядке добавления.
|
||||
/// </summary>
|
||||
public IReadOnlyList<AuditRecordDto> Records => _records;
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт подставку с пустым журналом.
|
||||
/// </summary>
|
||||
public TestAuditLogStore()
|
||||
{
|
||||
Store = Substitute.For<IAuditLogStore>();
|
||||
Store.When(s => s.AppendAsync(Arg.Any<AuditRecordDto>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => _records.Add(ci.Arg<AuditRecordDto>() with { Id = _records.Count + 1 }));
|
||||
Store.QueryAsync(Arg.Any<AuditQueryDto>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => Query(ci.Arg<AuditQueryDto>()));
|
||||
Store.CountAsync(Arg.Any<AuditQueryDto>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => ApplyFilters(ci.Arg<AuditQueryDto>()).Count());
|
||||
Store.PurgeOlderThanAsync(Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => _records.RemoveAll(record => record.At < ci.ArgAt<DateTimeOffset>(0)));
|
||||
}
|
||||
|
||||
private IReadOnlyList<AuditRecordDto> Query(AuditQueryDto filter)
|
||||
{
|
||||
return ApplyFilters(filter)
|
||||
.OrderByDescending(r => r.At)
|
||||
.Skip(Math.Max(0, filter.Offset))
|
||||
.Take(Math.Max(1, Math.Min(AuditService.MaxQueryLimit, filter.Limit)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private IEnumerable<AuditRecordDto> ApplyFilters(AuditQueryDto filter) =>
|
||||
_records.Where(r =>
|
||||
(filter.EventType is null || r.EventType == filter.EventType) &&
|
||||
(filter.ActorType is null || r.ActorType == filter.ActorType) &&
|
||||
(filter.TenantId is null || r.TenantId == filter.TenantId) &&
|
||||
(filter.ActorId is null || r.ActorId == filter.ActorId) &&
|
||||
(filter.From is null || r.At >= filter.From.Value) &&
|
||||
(filter.To is null || r.At <= filter.To.Value));
|
||||
}
|
||||
Reference in New Issue
Block a user