Перевести FakeOperatorAuthStore на NSubstitute

Хелпер Support/TestOperatorAuthStore: операторы/сессии/журнал Calls, поиск и
мутации через Returns/When-Do. Потребители (14 файлов) перетипизированы на
.Store, фейк удалён, тесты 1340 зелёные.
This commit is contained in:
2026-09-13 00:47:05 +03:00
parent 79b1893b28
commit a63bff7904
15 changed files with 138 additions and 50 deletions
@@ -128,13 +128,13 @@ public sealed class OperatorBootstrapHostedServiceTests
IConfiguration configuration = new ConfigurationBuilder() IConfiguration configuration = new ConfigurationBuilder()
.AddInMemoryCollection(values) .AddInMemoryCollection(values)
.Build(); .Build();
var store = new FakeOperatorAuthStore(); var store = new TestOperatorAuthStore();
var passwordHasher = TestHashers.New(); var passwordHasher = TestHashers.New();
var logger = new ListLogger(); var logger = new ListLogger();
// Hosted-шаг резолвит scoped OperatorBootstrapService из scope (как TenantBootstrapService): // Hosted-шаг резолвит scoped OperatorBootstrapService из scope (как TenantBootstrapService):
// провайдер собирается с реальной регистрацией модуля поверх фейков. // провайдер собирается с реальной регистрацией модуля поверх фейков.
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddScoped<IOperatorAuthStore>(_ => store); services.AddScoped<IOperatorAuthStore>(_ => store.Store);
services.AddScoped<IPasswordHasher>(_ => passwordHasher); services.AddScoped<IPasswordHasher>(_ => passwordHasher);
services.AddScoped<OperatorBootstrapService>(); services.AddScoped<OperatorBootstrapService>();
// Провайдер живёт всё время теста (без using): hosted резолвит scope в StartAsync после создания. // Провайдер живёт всё время теста (без using): hosted резолвит scope в StartAsync после создания.
@@ -150,7 +150,7 @@ public sealed class OperatorBootstrapHostedServiceTests
// Контекст теста bootstrap-шага. // Контекст теста bootstrap-шага.
private sealed record Context( private sealed record Context(
OperatorBootstrapHostedService Hosted, OperatorBootstrapHostedService Hosted,
FakeOperatorAuthStore Store, TestOperatorAuthStore Store,
IPasswordHasher PasswordHasher, IPasswordHasher PasswordHasher,
ListLogger Logs); ListLogger Logs);
@@ -14,15 +14,15 @@ public sealed class OperatorAuthServiceTests
private static readonly Guid OperatorId = Guid.NewGuid(); private static readonly Guid OperatorId = Guid.NewGuid();
private readonly FakeOperatorAuthStore _store; private readonly TestOperatorAuthStore _store;
private readonly OperatorAuthService _service; private readonly OperatorAuthService _service;
public OperatorAuthServiceTests() public OperatorAuthServiceTests()
{ {
var passwordHasher = TestHashers.New(); var passwordHasher = TestHashers.New();
_store = new FakeOperatorAuthStore(); _store = new TestOperatorAuthStore();
_store.AddOperator(new StoredOperatorDto(OperatorId, OperatorLogin, "active", passwordHasher.Hash(OperatorPassword))); _store.AddOperator(new StoredOperatorDto(OperatorId, OperatorLogin, "active", passwordHasher.Hash(OperatorPassword)));
_service = new OperatorAuthService(_store, passwordHasher); _service = new OperatorAuthService(_store.Store, passwordHasher);
} }
[Fact] [Fact]
@@ -10,15 +10,15 @@ namespace Deal.Tests.Unit.Modules.Tenants;
/// </summary> /// </summary>
public sealed class OperatorBootstrapServiceTests public sealed class OperatorBootstrapServiceTests
{ {
private readonly FakeOperatorAuthStore _store; private readonly TestOperatorAuthStore _store;
private readonly IPasswordHasher _passwordHasher; private readonly IPasswordHasher _passwordHasher;
private readonly OperatorBootstrapService _service; private readonly OperatorBootstrapService _service;
public OperatorBootstrapServiceTests() public OperatorBootstrapServiceTests()
{ {
_passwordHasher = TestHashers.New(); _passwordHasher = TestHashers.New();
_store = new FakeOperatorAuthStore(); _store = new TestOperatorAuthStore();
_service = new OperatorBootstrapService(_store, _passwordHasher); _service = new OperatorBootstrapService(_store.Store, _passwordHasher);
} }
[Fact] [Fact]
@@ -35,7 +35,7 @@ public sealed class LoginAttemptEndpointHttpTests
public async Task Login_AfterFiveWrongAttempts_Returns429WithBlockedDetail() public async Task Login_AfterFiveWrongAttempts_Returns429WithBlockedDetail()
{ {
await OperatorAuthHttpHost.RunAsync( await OperatorAuthHttpHost.RunAsync(
new FakeOperatorAuthStore(), new TestOperatorAuthStore(),
NewUserStore(), NewUserStore(),
async (baseAddress, _, users, _, _, _, _) => async (baseAddress, _, users, _, _, _, _) =>
{ {
@@ -66,7 +66,7 @@ public sealed class LoginAttemptEndpointHttpTests
public async Task Login_SuccessResetsCounter_FurtherFailuresNotBlocked() public async Task Login_SuccessResetsCounter_FurtherFailuresNotBlocked()
{ {
await OperatorAuthHttpHost.RunAsync( await OperatorAuthHttpHost.RunAsync(
new FakeOperatorAuthStore(), new TestOperatorAuthStore(),
NewUserStore(), NewUserStore(),
async (baseAddress, _, users, _, _, _, _) => async (baseAddress, _, users, _, _, _, _) =>
{ {
@@ -108,7 +108,7 @@ public sealed class LoginAttemptEndpointHttpTests
using var capture = new SecurityEventCapture(); using var capture = new SecurityEventCapture();
await OperatorAuthHttpHost.RunAsync( await OperatorAuthHttpHost.RunAsync(
new FakeOperatorAuthStore(), new TestOperatorAuthStore(),
NewUserStore(), NewUserStore(),
async (baseAddress, _, _, _, _, _, _) => async (baseAddress, _, _, _, _, _, _) =>
{ {
@@ -256,10 +256,10 @@ public sealed class OperatorAnalyticsEndpointsHttpTests
new TenantRecordDto(ActiveTenant, "active-tenant", TenantStatuses.Active, DateTimeOffset.UtcNow.AddDays(-2)), new TenantRecordDto(ActiveTenant, "active-tenant", TenantStatuses.Active, DateTimeOffset.UtcNow.AddDays(-2)),
new TenantRecordDto(SuspendedTenant, "suspended-tenant", TenantStatuses.Suspended, DateTimeOffset.UtcNow.AddDays(-1))); new TenantRecordDto(SuspendedTenant, "suspended-tenant", TenantStatuses.Suspended, DateTimeOffset.UtcNow.AddDays(-1)));
private static FakeOperatorAuthStore NewOperatorStore() private static TestOperatorAuthStore NewOperatorStore()
{ {
var passwordHasher = TestHashers.New(); var passwordHasher = TestHashers.New();
var store = new FakeOperatorAuthStore(); var store = new TestOperatorAuthStore();
store.AddOperator(new StoredOperatorDto( store.AddOperator(new StoredOperatorDto(
Guid.NewGuid(), OperatorLogin, "active", passwordHasher.Hash(OperatorPassword))); Guid.NewGuid(), OperatorLogin, "active", passwordHasher.Hash(OperatorPassword)));
return store; return store;
@@ -279,10 +279,10 @@ public sealed class OperatorAuditEndpointsHttpTests
// ─── Хелперы ───────────────────────────────────────────────────────── // ─── Хелперы ─────────────────────────────────────────────────────────
// Фейк-хранилище оператора с активным operator/operator. // Фейк-хранилище оператора с активным operator/operator.
private static FakeOperatorAuthStore NewOperatorStore() private static TestOperatorAuthStore NewOperatorStore()
{ {
var passwordHasher = TestHashers.New(); var passwordHasher = TestHashers.New();
var store = new FakeOperatorAuthStore(); var store = new TestOperatorAuthStore();
store.AddOperator(new StoredOperatorDto( store.AddOperator(new StoredOperatorDto(
Guid.NewGuid(), Guid.NewGuid(),
OperatorLogin, OperatorLogin,
@@ -131,7 +131,7 @@ public sealed class OperatorAuthEndpointsHttpTests
[Fact] [Fact]
public async Task OperatorWithNonActiveStatus_LoginSucceeds_ButMeReturns401() public async Task OperatorWithNonActiveStatus_LoginSucceeds_ButMeReturns401()
{ {
FakeOperatorAuthStore operatorStore = NewOperatorStore(active: false); TestOperatorAuthStore operatorStore = NewOperatorStore(active: false);
await OperatorAuthHttpHost.RunAsync( await OperatorAuthHttpHost.RunAsync(
operatorStore, operatorStore,
@@ -189,10 +189,10 @@ public sealed class OperatorAuthEndpointsHttpTests
} }
// Фейк-хранилище оператора с активным (или нет) оператором operator/operator. // Фейк-хранилище оператора с активным (или нет) оператором operator/operator.
private static FakeOperatorAuthStore NewOperatorStore(bool active = true) private static TestOperatorAuthStore NewOperatorStore(bool active = true)
{ {
var passwordHasher = TestHashers.New(); var passwordHasher = TestHashers.New();
var store = new FakeOperatorAuthStore(); var store = new TestOperatorAuthStore();
store.AddOperator(new StoredOperatorDto( store.AddOperator(new StoredOperatorDto(
Guid.NewGuid(), Guid.NewGuid(),
OperatorLogin, OperatorLogin,
@@ -40,9 +40,9 @@ internal static class OperatorAuthHttpHost
/// <param name="scenario">Сценарий: получает base-адрес и хранилища для проверок/досидирования.</param> /// <param name="scenario">Сценарий: получает base-адрес и хранилища для проверок/досидирования.</param>
/// <param name="auditStore">Фейк-хранилище аудита (IAuditLogStore); null — создаётся пустое внутри.</param> /// <param name="auditStore">Фейк-хранилище аудита (IAuditLogStore); null — создаётся пустое внутри.</param>
public static async Task RunAsync( public static async Task RunAsync(
FakeOperatorAuthStore operatorStore, TestOperatorAuthStore operatorStore,
FakeAuthStore userStore, FakeAuthStore userStore,
Func<string, FakeOperatorAuthStore, FakeAuthStore, Task> scenario, Func<string, TestOperatorAuthStore, FakeAuthStore, Task> scenario,
TestAuditLogStore? auditStore = null) => TestAuditLogStore? auditStore = null) =>
await RunCoreAsync( await RunCoreAsync(
operatorStore, operatorStore,
@@ -62,9 +62,9 @@ internal static class OperatorAuthHttpHost
/// <param name="auditStore">Фейк-хранилище аудита (IAuditLogStore); null — создаётся пустое внутри.</param> /// <param name="auditStore">Фейк-хранилище аудита (IAuditLogStore); null — создаётся пустое внутри.</param>
/// <param name="inviteStore">Фейк-хранилище приглашений (IInviteStore); null — создаётся пустое внутри.</param> /// <param name="inviteStore">Фейк-хранилище приглашений (IInviteStore); null — создаётся пустое внутри.</param>
public static async Task RunAsync( public static async Task RunAsync(
FakeOperatorAuthStore operatorStore, TestOperatorAuthStore operatorStore,
FakeAuthStore userStore, FakeAuthStore userStore,
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestInviteStore, TestAuditLogStore, Task> scenario, Func<string, TestOperatorAuthStore, FakeAuthStore, TestInviteStore, TestAuditLogStore, Task> scenario,
TestAuditLogStore? auditStore = null, TestAuditLogStore? auditStore = null,
TestInviteStore? inviteStore = null) => TestInviteStore? inviteStore = null) =>
await RunCoreAsync( await RunCoreAsync(
@@ -86,9 +86,9 @@ internal static class OperatorAuthHttpHost
/// <param name="inviteStore">Фейк-хранилище приглашений (IInviteStore); null — создаётся пустое внутри.</param> /// <param name="inviteStore">Фейк-хранилище приглашений (IInviteStore); null — создаётся пустое внутри.</param>
/// <param name="tenantStore">Фейк-хранилище реестра тенантов (ITenantRepository); null — создаётся пустое внутри.</param> /// <param name="tenantStore">Фейк-хранилище реестра тенантов (ITenantRepository); null — создаётся пустое внутри.</param>
public static async Task RunAsync( public static async Task RunAsync(
FakeOperatorAuthStore operatorStore, TestOperatorAuthStore operatorStore,
FakeAuthStore userStore, FakeAuthStore userStore,
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, Task> scenario, Func<string, TestOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, Task> scenario,
TestAuditLogStore? auditStore = null, TestAuditLogStore? auditStore = null,
TestInviteStore? inviteStore = null, TestInviteStore? inviteStore = null,
TestTenantStore? tenantStore = null) => TestTenantStore? tenantStore = null) =>
@@ -113,9 +113,9 @@ internal static class OperatorAuthHttpHost
/// <param name="limitStore">Фейк-хранилище лимитов (ITenantLimitStore); null — создаётся пустое внутри.</param> /// <param name="limitStore">Фейк-хранилище лимитов (ITenantLimitStore); null — создаётся пустое внутри.</param>
/// <param name="rateLimitOptions">Опции rate limiting: null — выключенные по умолчанию (LoginAttemptGuard no-op, существующие сценарии не режутся); Enabled-опции — сценарии защиты входа.</param> /// <param name="rateLimitOptions">Опции rate limiting: null — выключенные по умолчанию (LoginAttemptGuard no-op, существующие сценарии не режутся); Enabled-опции — сценарии защиты входа.</param>
public static async Task RunAsync( public static async Task RunAsync(
FakeOperatorAuthStore operatorStore, TestOperatorAuthStore operatorStore,
FakeAuthStore userStore, FakeAuthStore userStore,
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, FakeTenantLimitStore, Task> scenario, Func<string, TestOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, FakeTenantLimitStore, Task> scenario,
TestAuditLogStore? auditStore = null, TestAuditLogStore? auditStore = null,
TestInviteStore? inviteStore = null, TestInviteStore? inviteStore = null,
TestTenantStore? tenantStore = null, TestTenantStore? tenantStore = null,
@@ -142,9 +142,9 @@ internal static class OperatorAuthHttpHost
/// <param name="auditStore">Фейк-хранилище аудита (IAuditLogStore); null — создаётся пустое внутри.</param> /// <param name="auditStore">Фейк-хранилище аудита (IAuditLogStore); null — создаётся пустое внутри.</param>
/// <param name="globalSettingsStore">Фейк глобального KV-хранилища (IGlobalSettingsStore); null — создаётся пустое.</param> /// <param name="globalSettingsStore">Фейк глобального KV-хранилища (IGlobalSettingsStore); null — создаётся пустое.</param>
public static async Task RunWithGlobalSettingsAsync( public static async Task RunWithGlobalSettingsAsync(
FakeOperatorAuthStore operatorStore, TestOperatorAuthStore operatorStore,
FakeAuthStore userStore, FakeAuthStore userStore,
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestGlobalSettingsStore, TestAuditLogStore, Task> scenario, Func<string, TestOperatorAuthStore, FakeAuthStore, TestGlobalSettingsStore, TestAuditLogStore, Task> scenario,
TestAuditLogStore? auditStore = null, TestAuditLogStore? auditStore = null,
TestGlobalSettingsStore? globalSettingsStore = null) => TestGlobalSettingsStore? globalSettingsStore = null) =>
await RunCoreAsync( await RunCoreAsync(
@@ -159,13 +159,13 @@ internal static class OperatorAuthHttpHost
// Поднимает in-process Kestrel-хост и прогоняет сценарий (см. публичные перегрузки). // Поднимает in-process Kestrel-хост и прогоняет сценарий (см. публичные перегрузки).
private static async Task RunCoreAsync( private static async Task RunCoreAsync(
FakeOperatorAuthStore operatorStore, TestOperatorAuthStore operatorStore,
FakeAuthStore userStore, FakeAuthStore userStore,
TestAuditLogStore? auditStore, TestAuditLogStore? auditStore,
TestInviteStore? inviteStore, TestInviteStore? inviteStore,
TestTenantStore? tenantStore, TestTenantStore? tenantStore,
FakeTenantLimitStore? limitStore, FakeTenantLimitStore? limitStore,
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, FakeTenantLimitStore, TestGlobalSettingsStore, Task> scenario, Func<string, TestOperatorAuthStore, 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)
@@ -187,7 +187,7 @@ internal static class OperatorAuthHttpHost
builder.Services.AddSingleton(TestHashers.New()); builder.Services.AddSingleton(TestHashers.New());
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.Store);
builder.Services.AddSingleton<IAuditLogStore>(effectiveAuditStore.Store); 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);
@@ -92,14 +92,14 @@ public sealed class OperatorHealthEndpointsHttpTests
} }
// Прогоняет сценарий на хосте с активным оператором operator/operator (реестр/лимиты пустые). // Прогоняет сценарий на хосте с активным оператором operator/operator (реестр/лимиты пустые).
private static Task RunAsync(Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, FakeTenantLimitStore, Task> scenario) => private static Task RunAsync(Func<string, TestOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, FakeTenantLimitStore, Task> scenario) =>
OperatorAuthHttpHost.RunAsync(NewOperatorStore(), new FakeAuthStore(), scenario); OperatorAuthHttpHost.RunAsync(NewOperatorStore(), new FakeAuthStore(), scenario);
// Фейк-хранилище оператора с активным оператором operator/operator. // Фейк-хранилище оператора с активным оператором operator/operator.
private static FakeOperatorAuthStore NewOperatorStore() private static TestOperatorAuthStore NewOperatorStore()
{ {
var passwordHasher = TestHashers.New(); var passwordHasher = TestHashers.New();
var store = new FakeOperatorAuthStore(); var store = new TestOperatorAuthStore();
store.AddOperator(new StoredOperatorDto( store.AddOperator(new StoredOperatorDto(
Guid.NewGuid(), Guid.NewGuid(),
OperatorLogin, OperatorLogin,
@@ -262,10 +262,10 @@ public sealed class OperatorInvitesEndpointsHttpTests
} }
// Фейк-хранилище оператора с активным оператором operator/operator. // Фейк-хранилище оператора с активным оператором operator/operator.
private static FakeOperatorAuthStore NewOperatorStore() private static TestOperatorAuthStore NewOperatorStore()
{ {
var passwordHasher = TestHashers.New(); var passwordHasher = TestHashers.New();
var store = new FakeOperatorAuthStore(); var store = new TestOperatorAuthStore();
store.AddOperator(new StoredOperatorDto( store.AddOperator(new StoredOperatorDto(
Guid.NewGuid(), Guid.NewGuid(),
OperatorLogin, OperatorLogin,
@@ -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, TestAuditLogStore, FakeTenantLimitStore, Task> scenario, TestAuditLogStore? auditStore = null) private static Task RunAsync(Func<string, TestOperatorAuthStore, 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 не срабатывает).
@@ -326,10 +326,10 @@ public sealed class OperatorLimitsEndpointsHttpTests
} }
// Фейк-хранилище оператора с активным оператором operator/operator. // Фейк-хранилище оператора с активным оператором operator/operator.
private static FakeOperatorAuthStore NewOperatorStore() private static TestOperatorAuthStore NewOperatorStore()
{ {
var passwordHasher = TestHashers.New(); var passwordHasher = TestHashers.New();
var store = new FakeOperatorAuthStore(); var store = new TestOperatorAuthStore();
store.AddOperator(new StoredOperatorDto( store.AddOperator(new StoredOperatorDto(
Guid.NewGuid(), Guid.NewGuid(),
OperatorLogin, OperatorLogin,
@@ -72,14 +72,14 @@ public sealed class OperatorMaintenanceEndpointsHttpTests
} }
// Прогоняет сценарий на хосте с активным оператором operator/operator и заданным реестром тенантов. // Прогоняет сценарий на хосте с активным оператором operator/operator и заданным реестром тенантов.
private static Task RunAsync(TestTenantStore tenantStore, Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestInviteStore, TestAuditLogStore, Task> scenario) => private static Task RunAsync(TestTenantStore tenantStore, Func<string, TestOperatorAuthStore, 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.
private static FakeOperatorAuthStore NewOperatorStore() private static TestOperatorAuthStore NewOperatorStore()
{ {
var passwordHasher = TestHashers.New(); var passwordHasher = TestHashers.New();
var store = new FakeOperatorAuthStore(); var store = new TestOperatorAuthStore();
store.AddOperator(new StoredOperatorDto( store.AddOperator(new StoredOperatorDto(
Guid.NewGuid(), Guid.NewGuid(),
OperatorLogin, OperatorLogin,
@@ -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, TestAuditLogStore, Task> scenario, TestAuditLogStore? auditStore = null) => private static Task RunAsync(Func<string, TestOperatorAuthStore, FakeAuthStore, TestGlobalSettingsStore, TestAuditLogStore, Task> scenario, TestAuditLogStore? auditStore = null) =>
OperatorAuthHttpHost.RunWithGlobalSettingsAsync( OperatorAuthHttpHost.RunWithGlobalSettingsAsync(
NewOperatorStore(), NewOperatorStore(),
new FakeAuthStore(), new FakeAuthStore(),
@@ -273,10 +273,10 @@ public sealed class OperatorSettingsEndpointsHttpTests
} }
// Фейк-хранилище оператора с активным оператором operator/operator. // Фейк-хранилище оператора с активным оператором operator/operator.
private static FakeOperatorAuthStore NewOperatorStore() private static TestOperatorAuthStore NewOperatorStore()
{ {
var passwordHasher = TestHashers.New(); var passwordHasher = TestHashers.New();
var store = new FakeOperatorAuthStore(); var store = new TestOperatorAuthStore();
store.AddOperator(new StoredOperatorDto( store.AddOperator(new StoredOperatorDto(
Guid.NewGuid(), Guid.NewGuid(),
OperatorLogin, OperatorLogin,
@@ -486,7 +486,7 @@ public sealed class OperatorTenantsEndpointsHttpTests
// Прогоняет сценарий на хосте с одним активным тенантом и одним пользователем в нём. // Прогоняет сценарий на хосте с одним активным тенантом и одним пользователем в нём.
private static Task RunAsync( private static Task RunAsync(
Func<string, FakeOperatorAuthStore, FakeAuthStore, TestTenantStore, TestAuditLogStore, Task> scenario, Func<string, TestOperatorAuthStore, FakeAuthStore, TestTenantStore, TestAuditLogStore, Task> scenario,
TestAuditLogStore? auditStore = null, TestAuditLogStore? auditStore = null,
TestTenantStore? tenantStore = null) TestTenantStore? tenantStore = null)
{ {
@@ -504,7 +504,7 @@ public sealed class OperatorTenantsEndpointsHttpTests
// Проверяет запись tenant_status_changed с заданным статусом и актором-оператором. // Проверяет запись tenant_status_changed с заданным статусом и актором-оператором.
private static void AssertStatusChangedAudit( private static void AssertStatusChangedAudit(
TestAuditLogStore auditStore, TestAuditLogStore auditStore,
FakeOperatorAuthStore operatorStore, TestOperatorAuthStore operatorStore,
string status) string status)
{ {
// В ленте может быть несколько tenant_status_changed этого тенанта (suspend → unsuspend) — ищем по статусу. // В ленте может быть несколько tenant_status_changed этого тенанта (suspend → unsuspend) — ищем по статусу.
@@ -569,10 +569,10 @@ public sealed class OperatorTenantsEndpointsHttpTests
} }
// Фейк-хранилище оператора с активным оператором operator/operator. // Фейк-хранилище оператора с активным оператором operator/operator.
private static FakeOperatorAuthStore NewOperatorStore() private static TestOperatorAuthStore NewOperatorStore()
{ {
var passwordHasher = TestHashers.New(); var passwordHasher = TestHashers.New();
var store = new FakeOperatorAuthStore(); var store = new TestOperatorAuthStore();
store.AddOperator(new StoredOperatorDto( store.AddOperator(new StoredOperatorDto(
Guid.NewGuid(), Guid.NewGuid(),
OperatorLogin, OperatorLogin,
@@ -0,0 +1,88 @@
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Models;
using NSubstitute;
namespace Deal.Tests.Unit.Support;
/// <summary>
/// Подставка <see cref="IOperatorAuthStore"/> на списках: сервисы получают NSubstitute-подставку
/// (<see cref="Store"/>), тесты сеют/проверяют операторов и сессии через
/// <see cref="AddOperator"/>, <see cref="AddSession"/>, <see cref="Operators"/>, <see cref="Sessions"/>,
/// журнал мутирующих вызовов — <see cref="Calls"/>.
/// </summary>
public sealed class TestOperatorAuthStore
{
private readonly List<StoredOperatorDto> _operators = [];
private readonly List<OperatorSessionDto> _sessions = [];
private readonly List<string> _calls = [];
/// <summary>
/// Подставка порта операторской аутентификации (создаётся в конструкторе).
/// </summary>
public IOperatorAuthStore Store { get; }
/// <summary>
/// Операторы хранилища.
/// </summary>
public IReadOnlyList<StoredOperatorDto> Operators => _operators;
/// <summary>
/// Сессии операторов хранилища.
/// </summary>
public IReadOnlyList<OperatorSessionDto> Sessions => _sessions;
/// <summary>
/// Журнал мутирующих вызовов в порядке их совершения.
/// </summary>
public IReadOnlyList<string> Calls => _calls;
/// <summary>
/// Создаёт подставку с пустыми списками.
/// </summary>
public TestOperatorAuthStore()
{
Store = Substitute.For<IOperatorAuthStore>();
Store.FindByLoginAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(ci => _operators.SingleOrDefault(o => o.Login == ci.ArgAt<string>(0)));
Store.FindSessionByTokenHashAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns(ci => _sessions.SingleOrDefault(s => s.TokenHash == ci.ArgAt<string>(0)));
Store.When(s => s.CreateAsync(Arg.Any<StoredOperatorDto>(), Arg.Any<CancellationToken>()))
.Do(ci =>
{
StoredOperatorDto operatorRecord = ci.Arg<StoredOperatorDto>();
_operators.Add(operatorRecord);
_calls.Add($"create-operator:{operatorRecord.Id}");
});
Store.When(s => s.CreateSessionAsync(Arg.Any<OperatorSessionDto>(), Arg.Any<CancellationToken>()))
.Do(ci =>
{
OperatorSessionDto session = ci.Arg<OperatorSessionDto>();
_sessions.Add(session);
_calls.Add($"create-session:{session.OperatorId}");
});
Store.When(s => s.DeleteSessionAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()))
.Do(ci =>
{
_sessions.RemoveAll(s => s.TokenHash == ci.ArgAt<string>(0));
_calls.Add($"delete-session:{ci.ArgAt<string>(0)}");
});
Store.When(s => s.DeleteExpiredSessionsAsync(Arg.Any<CancellationToken>()))
.Do(_ =>
{
_sessions.RemoveAll(s => s.ExpiresAt <= DateTimeOffset.UtcNow);
_calls.Add("delete-expired");
});
}
/// <summary>
/// Добавляет оператора
/// </summary>
/// <param name="operatorRecord">Оператор.</param>
public void AddOperator(StoredOperatorDto operatorRecord) => _operators.Add(operatorRecord);
/// <summary>
/// Добавляет сессию
/// </summary>
/// <param name="session">Сессия.</param>
public void AddSession(OperatorSessionDto session) => _sessions.Add(session);
}