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