Перевести FakeTenantRepository на NSubstitute
ci / build-test (push) Canceled after 0s

Хелпер Support/TestTenantRepository: фиксированный список + журнал запросов
страниц (PageRequests), неожиданные методы бросают NotSupportedException.
Потребители (7 файлов) перетипизированы на .Repository, фейк удалён,
тесты 1340 зелёные.
This commit is contained in:
Rustam Khalimov
2026-09-12 21:44:58 +03:00
parent 668c5b416d
commit 1bee0003ca
8 changed files with 89 additions and 40 deletions
@@ -90,7 +90,7 @@ public sealed class DiscoveryWorkerSchedulerTests
// Собирает планировщик на реальных сервисах модуля Discovery и тенант-фейках (без таймера). // Собирает планировщик на реальных сервисах модуля Discovery и тенант-фейках (без таймера).
private static Context CreateContext() private static Context CreateContext()
{ {
var tenants = new FakeTenantRepository(Tenant(TenantA), Tenant(TenantB)); var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
var tenantContext = new TenantContext(); var tenantContext = new TenantContext();
var storeA = new FakeDiscoveryStore(); var storeA = new FakeDiscoveryStore();
var storeB = new FakeDiscoveryStore(); var storeB = new FakeDiscoveryStore();
@@ -103,7 +103,7 @@ public sealed class DiscoveryWorkerSchedulerTests
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddSingleton<ITenantContext>(tenantContext); services.AddSingleton<ITenantContext>(tenantContext);
services.AddSingleton<ITenantRepository>(tenants); services.AddSingleton<ITenantRepository>(tenants.Repository);
services.AddSingleton<IMlClient>(new FakeMlClient()); services.AddSingleton<IMlClient>(new FakeMlClient());
services.AddSingleton<IAiTools>(_ => TestAiTools.New()); services.AddSingleton<IAiTools>(_ => TestAiTools.New());
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant // Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
@@ -44,7 +44,7 @@ public sealed class MlOutboxFlushSchedulerTests
var tenantContext = new TenantContext(); var tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
port, port,
new FakeTenantRepository(Tenant(TenantA)), new TestTenantRepository(Tenant(TenantA)),
tenantContext, tenantContext,
new Dictionary<Guid, FakeMlLearningStore> { [TenantA] = store }); new Dictionary<Guid, FakeMlLearningStore> { [TenantA] = store });
MlOutboxFlushScheduler scheduler = CreateScheduler(provider); MlOutboxFlushScheduler scheduler = CreateScheduler(provider);
@@ -70,7 +70,7 @@ public sealed class MlOutboxFlushSchedulerTests
var tenantContext = new TenantContext(); var tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
port, port,
new FakeTenantRepository(Tenant(TenantA)), new TestTenantRepository(Tenant(TenantA)),
tenantContext, tenantContext,
new Dictionary<Guid, FakeMlLearningStore> { [TenantA] = store }); new Dictionary<Guid, FakeMlLearningStore> { [TenantA] = store });
MlOutboxFlushScheduler scheduler = CreateScheduler(provider); MlOutboxFlushScheduler scheduler = CreateScheduler(provider);
@@ -98,7 +98,7 @@ public sealed class MlOutboxFlushSchedulerTests
var tenantContext = new TenantContext(); var tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
port, port,
new FakeTenantRepository(Tenant(TenantA), Tenant(TenantB)), new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)),
tenantContext, tenantContext,
new Dictionary<Guid, FakeMlLearningStore> { [TenantA] = storeA, [TenantB] = storeB }); new Dictionary<Guid, FakeMlLearningStore> { [TenantA] = storeA, [TenantB] = storeB });
MlOutboxFlushScheduler scheduler = CreateScheduler(provider); MlOutboxFlushScheduler scheduler = CreateScheduler(provider);
@@ -124,7 +124,7 @@ public sealed class MlOutboxFlushSchedulerTests
var tenantContext = new TenantContext(); var tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
port, port,
new FakeTenantRepository(Tenant(TenantA)), new TestTenantRepository(Tenant(TenantA)),
tenantContext, tenantContext,
new Dictionary<Guid, FakeMlLearningStore> { [TenantA] = store }); new Dictionary<Guid, FakeMlLearningStore> { [TenantA] = store });
MlOutboxFlushScheduler scheduler = CreateScheduler(provider); MlOutboxFlushScheduler scheduler = CreateScheduler(provider);
@@ -147,13 +147,13 @@ public sealed class MlOutboxFlushSchedulerTests
// Возвращает: Провайдер с зарегистрированными сервисами теста. // Возвращает: Провайдер с зарегистрированными сервисами теста.
private static ServiceProvider BuildProvider( private static ServiceProvider BuildProvider(
int port, int port,
ITenantRepository tenants, TestTenantRepository tenants,
TenantContext tenantContext, TenantContext tenantContext,
Dictionary<Guid, FakeMlLearningStore> storesByTenant) Dictionary<Guid, FakeMlLearningStore> storesByTenant)
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddSingleton<ITenantContext>(tenantContext); services.AddSingleton<ITenantContext>(tenantContext);
services.AddSingleton(tenants); services.AddSingleton(tenants.Repository);
// Транспорт ml-service создаётся сразу (как в AddDealIntegrations при UseLocal=false): env-токен уже // Транспорт ml-service создаётся сразу (как в AddDealIntegrations при UseLocal=false): env-токен уже
// выставлен харнессом MlGrpcTestHost, endpoint — эфемерный порт хоста-фейка. // выставлен харнессом MlGrpcTestHost, endpoint — эфемерный порт хоста-фейка.
services.AddSingleton(new MlGrpcConnection(new MlServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" })); services.AddSingleton(new MlGrpcConnection(new MlServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" }));
@@ -1,4 +1,5 @@
using Deal.Api.Observability; using Deal.Api.Observability;
using Deal.Tests.Unit.Support;
using Deal.Contracts.Integrations.Abstractions; using Deal.Contracts.Integrations.Abstractions;
using Deal.Infrastructure.Data; using Deal.Infrastructure.Data;
using Deal.Modules.Kanban.Application.Abstractions; using Deal.Modules.Kanban.Application.Abstractions;
@@ -145,12 +146,12 @@ public sealed class RuntimeDepthsCollectorTests
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddSingleton<ITenantContext, TenantContext>(); services.AddSingleton<ITenantContext, TenantContext>();
services.AddSingleton<ITenantRepository>(new FakeTenantRepository( services.AddSingleton<ITenantRepository>(new TestTenantRepository(
(tenants ?? new[] (tenants ?? new[]
{ {
new TenantRecordDto(TenantA, "A", "active", DateTimeOffset.UtcNow), new TenantRecordDto(TenantA, "A", "active", DateTimeOffset.UtcNow),
new TenantRecordDto(TenantB, "B", "active", DateTimeOffset.UtcNow), new TenantRecordDto(TenantB, "B", "active", DateTimeOffset.UtcNow),
}).ToArray())); }).ToArray()).Repository);
services.AddScoped<IPipelineStore>(provider => pipelineByTenant[CurrentTenant(provider)]); services.AddScoped<IPipelineStore>(provider => pipelineByTenant[CurrentTenant(provider)]);
services.AddScoped<IMlClient>(_ => new FakeMlClient()); services.AddScoped<IMlClient>(_ => new FakeMlClient());
@@ -21,7 +21,7 @@ public sealed class TenantSchemaMigrationServiceTests
[Fact] [Fact]
public async Task MigrateAllAsync_EmptyRegistry_ReturnsEmptySummary() public async Task MigrateAllAsync_EmptyRegistry_ReturnsEmptySummary()
{ {
var service = NewService(new FakeTenantRepository(), new TestTenantProvisioner().Provisioner); var service = NewService(new TestTenantRepository().Repository, new TestTenantProvisioner().Provisioner);
TenantMigrationSummary summary = await service.MigrateAllAsync(CancellationToken.None); TenantMigrationSummary summary = await service.MigrateAllAsync(CancellationToken.None);
@@ -35,10 +35,10 @@ public sealed class TenantSchemaMigrationServiceTests
public async Task MigrateAllAsync_ProvisionsEveryTenantSchema() public async Task MigrateAllAsync_ProvisionsEveryTenantSchema()
{ {
var provisioner = new TestTenantProvisioner(); var provisioner = new TestTenantProvisioner();
var repository = new FakeTenantRepository( var repository = new TestTenantRepository(
TenantRecord(TenantA, "A"), TenantRecord(TenantA, "A"),
TenantRecord(TenantB, "B")); TenantRecord(TenantB, "B"));
var service = NewService(repository, provisioner.Provisioner); var service = NewService(repository.Repository, provisioner.Provisioner);
TenantMigrationSummary summary = await service.MigrateAllAsync(CancellationToken.None); TenantMigrationSummary summary = await service.MigrateAllAsync(CancellationToken.None);
@@ -56,10 +56,10 @@ public sealed class TenantSchemaMigrationServiceTests
{ {
string failingSchema = $"tenant_{TenantA:N}"; string failingSchema = $"tenant_{TenantA:N}";
var provisioner = new TestTenantProvisioner(failingSchema); var provisioner = new TestTenantProvisioner(failingSchema);
var repository = new FakeTenantRepository( var repository = new TestTenantRepository(
TenantRecord(TenantA, "A"), TenantRecord(TenantA, "A"),
TenantRecord(TenantB, "B")); TenantRecord(TenantB, "B"));
var service = NewService(repository, provisioner.Provisioner); var service = NewService(repository.Repository, provisioner.Provisioner);
TenantMigrationSummary summary = await service.MigrateAllAsync(CancellationToken.None); TenantMigrationSummary summary = await service.MigrateAllAsync(CancellationToken.None);
@@ -76,8 +76,8 @@ public sealed class TenantSchemaMigrationServiceTests
public async Task MigrateAllAsync_MultiplePages_ReadsEveryTenant() public async Task MigrateAllAsync_MultiplePages_ReadsEveryTenant()
{ {
var provisioner = new TestTenantProvisioner(); var provisioner = new TestTenantProvisioner();
var repository = new FakeTenantRepository(Records(5)); var repository = new TestTenantRepository(Records(5));
var service = NewService(repository, provisioner.Provisioner); var service = NewService(repository.Repository, provisioner.Provisioner);
TenantMigrationSummary summary = await service.MigrateAllAsync(4, 2, CancellationToken.None); TenantMigrationSummary summary = await service.MigrateAllAsync(4, 2, CancellationToken.None);
@@ -97,8 +97,8 @@ public sealed class TenantSchemaMigrationServiceTests
public async Task MigrateAllAsync_PageSizeOne_ReadsEveryTenant() public async Task MigrateAllAsync_PageSizeOne_ReadsEveryTenant()
{ {
var provisioner = new TestTenantProvisioner(); var provisioner = new TestTenantProvisioner();
var repository = new FakeTenantRepository(Records(4)); var repository = new TestTenantRepository(Records(4));
var service = NewService(repository, provisioner.Provisioner); var service = NewService(repository.Repository, provisioner.Provisioner);
TenantMigrationSummary summary = await service.MigrateAllAsync(2, 1, CancellationToken.None); TenantMigrationSummary summary = await service.MigrateAllAsync(2, 1, CancellationToken.None);
@@ -118,8 +118,8 @@ public sealed class TenantSchemaMigrationServiceTests
TenantRecordDto[] records = Records(3); TenantRecordDto[] records = Records(3);
string failingSchema = $"tenant_{records[0].Id:N}"; string failingSchema = $"tenant_{records[0].Id:N}";
var provisioner = new TestTenantProvisioner(failingSchema); var provisioner = new TestTenantProvisioner(failingSchema);
var repository = new FakeTenantRepository(records); var repository = new TestTenantRepository(records);
var service = NewService(repository, provisioner.Provisioner); var service = NewService(repository.Repository, provisioner.Provisioner);
TenantMigrationSummary summary = await service.MigrateAllAsync(1, 2, CancellationToken.None); TenantMigrationSummary summary = await service.MigrateAllAsync(1, 2, CancellationToken.None);
@@ -136,8 +136,8 @@ public sealed class TenantSchemaMigrationServiceTests
public async Task MigrateAllAsync_NonPositiveParameters_ClampToMinimum() public async Task MigrateAllAsync_NonPositiveParameters_ClampToMinimum()
{ {
var provisioner = new TestTenantProvisioner(); var provisioner = new TestTenantProvisioner();
var repository = new FakeTenantRepository(Records(3)); var repository = new TestTenantRepository(Records(3));
var service = NewService(repository, provisioner.Provisioner); var service = NewService(repository.Repository, provisioner.Provisioner);
TenantMigrationSummary summary = await service.MigrateAllAsync(0, 0, CancellationToken.None); TenantMigrationSummary summary = await service.MigrateAllAsync(0, 0, CancellationToken.None);
@@ -152,10 +152,10 @@ public sealed class TenantSchemaMigrationServiceTests
public async Task MigrateAllAsync_WithExplicitParallelism_ProvisionsEveryTenant() public async Task MigrateAllAsync_WithExplicitParallelism_ProvisionsEveryTenant()
{ {
var provisioner = new TestTenantProvisioner(); var provisioner = new TestTenantProvisioner();
var repository = new FakeTenantRepository( var repository = new TestTenantRepository(
TenantRecord(TenantA, "A"), TenantRecord(TenantA, "A"),
TenantRecord(TenantB, "B")); TenantRecord(TenantB, "B"));
var service = NewService(repository, provisioner.Provisioner); var service = NewService(repository.Repository, provisioner.Provisioner);
TenantMigrationSummary summary = await service.MigrateAllAsync(2, CancellationToken.None); TenantMigrationSummary summary = await service.MigrateAllAsync(2, CancellationToken.None);
@@ -31,7 +31,7 @@ public sealed class BudgetAlertSchedulerTests
limits.Preload(TenantA, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 800); limits.Preload(TenantA, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 800);
limits.Preload(TenantB, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 1000); limits.Preload(TenantB, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 1000);
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
new FakeTenantRepository(Tenant(TenantA), Tenant(TenantB)), limits); new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository, limits);
SseBroker broker = provider.GetRequiredService<SseBroker>(); SseBroker broker = provider.GetRequiredService<SseBroker>();
SseSubscription subscriptionA = broker.Subscribe(TenantA); SseSubscription subscriptionA = broker.Subscribe(TenantA);
@@ -56,7 +56,7 @@ public sealed class BudgetAlertSchedulerTests
{ {
FakeTenantLimitStore limits = new(); FakeTenantLimitStore limits = new();
limits.Preload(TenantA, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 700); limits.Preload(TenantA, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 700);
await using ServiceProvider provider = BuildProvider(new FakeTenantRepository(Tenant(TenantA)), limits); await using ServiceProvider provider = BuildProvider(new TestTenantRepository(Tenant(TenantA)).Repository, limits);
SseBroker broker = provider.GetRequiredService<SseBroker>(); SseBroker broker = provider.GetRequiredService<SseBroker>();
SseSubscription subscriptionA = broker.Subscribe(TenantA); SseSubscription subscriptionA = broker.Subscribe(TenantA);
@@ -76,7 +76,7 @@ public sealed class BudgetAlertSchedulerTests
// расход ≥ бюджета): за один проход выходят оба порога ровно по одному разу. // расход ≥ бюджета): за один проход выходят оба порога ровно по одному разу.
FakeTenantLimitStore limits = new(); FakeTenantLimitStore limits = new();
limits.Preload(TenantA, budgetTokens: 500, TenantLimitPeriods.Month, Now(), usedTokens: 700); limits.Preload(TenantA, budgetTokens: 500, TenantLimitPeriods.Month, Now(), usedTokens: 700);
await using ServiceProvider provider = BuildProvider(new FakeTenantRepository(Tenant(TenantA)), limits); await using ServiceProvider provider = BuildProvider(new TestTenantRepository(Tenant(TenantA)).Repository, limits);
SseBroker broker = provider.GetRequiredService<SseBroker>(); SseBroker broker = provider.GetRequiredService<SseBroker>();
SseSubscription subscriptionA = broker.Subscribe(TenantA); SseSubscription subscriptionA = broker.Subscribe(TenantA);
@@ -95,7 +95,7 @@ public sealed class BudgetAlertSchedulerTests
{ {
FakeTenantLimitStore limits = new(); FakeTenantLimitStore limits = new();
limits.Preload(TenantA, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 0); limits.Preload(TenantA, budgetTokens: 1000, TenantLimitPeriods.Month, Now(), usedTokens: 0);
await using ServiceProvider provider = BuildProvider(new FakeTenantRepository(Tenant(TenantA)), limits); await using ServiceProvider provider = BuildProvider(new TestTenantRepository(Tenant(TenantA)).Repository, limits);
SseBroker broker = provider.GetRequiredService<SseBroker>(); SseBroker broker = provider.GetRequiredService<SseBroker>();
SseSubscription subscriptionA = broker.Subscribe(TenantA); SseSubscription subscriptionA = broker.Subscribe(TenantA);
@@ -148,7 +148,7 @@ public sealed class PipelineWorkerSchedulerTests
// Возвращает: Планировщик, фейки очередей/канбана тенантов, каналы подписок и гейт. // Возвращает: Планировщик, фейки очередей/канбана тенантов, каналы подписок и гейт.
private static Context CreateContext(bool withThrowingQueueReadA = false) private static Context CreateContext(bool withThrowingQueueReadA = false)
{ {
var tenants = new FakeTenantRepository(Tenant(TenantA), Tenant(TenantB)); var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
var tenantContext = new TenantContext(); var tenantContext = new TenantContext();
FakePipelineStore pipelineA = withThrowingQueueReadA ? new ThrowingQueueReadPipelineStore() : new FakePipelineStore(); FakePipelineStore pipelineA = withThrowingQueueReadA ? new ThrowingQueueReadPipelineStore() : new FakePipelineStore();
var pipelineB = new FakePipelineStore(); var pipelineB = new FakePipelineStore();
@@ -165,7 +165,7 @@ public sealed class PipelineWorkerSchedulerTests
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddSingleton<ITenantContext>(tenantContext); services.AddSingleton<ITenantContext>(tenantContext);
services.AddSingleton<SseBroker>(); services.AddSingleton<SseBroker>();
services.AddSingleton<ITenantRepository>(tenants); services.AddSingleton<ITenantRepository>(tenants.Repository);
services.AddSingleton(pumpGate); services.AddSingleton(pumpGate);
services.AddSingleton<IMlClient>(new FakeMlClient()); services.AddSingleton<IMlClient>(new FakeMlClient());
services.AddSingleton<IAiClassifier>(aiClassifier.Classifier); services.AddSingleton<IAiClassifier>(aiClassifier.Classifier);
@@ -45,7 +45,7 @@ public sealed class StorageTickSchedulerTests
FakeKanjStore storeA = StoreWithExpiredInboxCard("l_a_old"); FakeKanjStore storeA = StoreWithExpiredInboxCard("l_a_old");
FakeKanjStore storeB = StoreWithExpiredInboxCard("l_b_old"); FakeKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
var settings = new FakeSettingsStore(); var settings = new FakeSettingsStore();
var tenants = new FakeTenantRepository(Tenant(TenantA), Tenant(TenantB)); var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
var tenantContext = new TenantContext(); var tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider(tenants, tenantContext, storeA, storeB, settings); await using ServiceProvider provider = BuildProvider(tenants, tenantContext, storeA, storeB, settings);
@@ -76,7 +76,7 @@ public sealed class StorageTickSchedulerTests
var settings = new FakeSettingsStore(); var settings = new FakeSettingsStore();
var tenantContext = new TenantContext(); var tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
new FakeTenantRepository(Tenant(TenantA), Tenant(TenantB)), new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)),
tenantContext, tenantContext,
storeA, storeA,
storeB, storeB,
@@ -112,7 +112,7 @@ public sealed class StorageTickSchedulerTests
[TenantB] = storeB, [TenantB] = storeB,
}; };
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
new FakeTenantRepository(Tenant(TenantA), Tenant(TenantB)), new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
tenantContext, tenantContext,
storesByTenant, storesByTenant,
settingsByTenant); settingsByTenant);
@@ -158,7 +158,7 @@ public sealed class StorageTickSchedulerTests
pipelineStoreA.SeedRejected(Rejected("r_old_a", (long)(nowMs - TimeSpan.FromDays(4).TotalMilliseconds))); pipelineStoreA.SeedRejected(Rejected("r_old_a", (long)(nowMs - TimeSpan.FromDays(4).TotalMilliseconds)));
pipelineStoreA.SeedRejected(Rejected("r_fresh", (long)(nowMs - TimeSpan.FromDays(1).TotalMilliseconds))); pipelineStoreA.SeedRejected(Rejected("r_fresh", (long)(nowMs - TimeSpan.FromDays(1).TotalMilliseconds)));
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
new FakeTenantRepository(Tenant(TenantA), Tenant(TenantB)), new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
tenantContext, tenantContext,
new Dictionary<Guid, FakeKanjStore> { [TenantA] = kanjStore, [TenantB] = new FakeKanjStore() }, new Dictionary<Guid, FakeKanjStore> { [TenantA] = kanjStore, [TenantB] = new FakeKanjStore() },
new Dictionary<Guid, ISettingsStore> { [TenantA] = settings, [TenantB] = settings }, new Dictionary<Guid, ISettingsStore> { [TenantA] = settings, [TenantB] = settings },
@@ -188,7 +188,7 @@ public sealed class StorageTickSchedulerTests
var cardStoreB = new FakeKanjStore(); var cardStoreB = new FakeKanjStore();
var tenantContext = new TenantContext(); var tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
new FakeTenantRepository(Tenant(TenantA), Tenant(TenantB)), new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
tenantContext, tenantContext,
new Dictionary<Guid, FakeKanjStore> { [TenantA] = cardStoreA, [TenantB] = cardStoreB }, new Dictionary<Guid, FakeKanjStore> { [TenantA] = cardStoreA, [TenantB] = cardStoreB },
new Dictionary<Guid, ISettingsStore> { [TenantA] = new FakeSettingsStore(), [TenantB] = new FakeSettingsStore() }, new Dictionary<Guid, ISettingsStore> { [TenantA] = new FakeSettingsStore(), [TenantB] = new FakeSettingsStore() },
@@ -223,7 +223,7 @@ public sealed class StorageTickSchedulerTests
settingsA.Preload(SettingsKeys.RemindersEnabled, "false"); settingsA.Preload(SettingsKeys.RemindersEnabled, "false");
var tenantContext = new TenantContext(); var tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
new FakeTenantRepository(Tenant(TenantA)), new TestTenantRepository(Tenant(TenantA)).Repository,
tenantContext, tenantContext,
new Dictionary<Guid, FakeKanjStore> { [TenantA] = cardStoreA }, new Dictionary<Guid, FakeKanjStore> { [TenantA] = cardStoreA },
new Dictionary<Guid, ISettingsStore> { [TenantA] = settingsA }, new Dictionary<Guid, ISettingsStore> { [TenantA] = settingsA },
@@ -249,7 +249,7 @@ public sealed class StorageTickSchedulerTests
FakeKanjStore storeB = StoreWithExpiredInboxCard("l_b_old"); FakeKanjStore storeB = StoreWithExpiredInboxCard("l_b_old");
var tenantContext = new TenantContext(); var tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider( await using ServiceProvider provider = BuildProvider(
new FakeTenantRepository(Tenant(TenantA), Tenant(TenantB)), new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)).Repository,
tenantContext, tenantContext,
new Dictionary<Guid, FakeKanjStore> { [TenantA] = cardStoreA, [TenantB] = storeB }, new Dictionary<Guid, FakeKanjStore> { [TenantA] = cardStoreA, [TenantB] = storeB },
new Dictionary<Guid, ISettingsStore> { [TenantA] = new FakeSettingsStore(), [TenantB] = new FakeSettingsStore() }, new Dictionary<Guid, ISettingsStore> { [TenantA] = new FakeSettingsStore(), [TenantB] = new FakeSettingsStore() },
@@ -279,14 +279,14 @@ public sealed class StorageTickSchedulerTests
// settings: Настройки обоих тенантов (пустые — дефолты тика). // settings: Настройки обоих тенантов (пустые — дефолты тика).
// Возвращает: Провайдер с зарегистрированными сервисами теста. // Возвращает: Провайдер с зарегистрированными сервисами теста.
private static ServiceProvider BuildProvider( private static ServiceProvider BuildProvider(
FakeTenantRepository tenants, TestTenantRepository tenants,
TenantContext tenantContext, TenantContext tenantContext,
FakeKanjStore storeA, FakeKanjStore storeA,
FakeKanjStore storeB, FakeKanjStore storeB,
FakeSettingsStore settings) FakeSettingsStore settings)
{ {
return BuildProvider( return BuildProvider(
tenants, tenants.Repository,
tenantContext, tenantContext,
new Dictionary<Guid, FakeKanjStore> { [TenantA] = storeA, [TenantB] = storeB }, new Dictionary<Guid, FakeKanjStore> { [TenantA] = storeA, [TenantB] = storeB },
new Dictionary<Guid, ISettingsStore> { [TenantA] = settings, [TenantB] = settings }); new Dictionary<Guid, ISettingsStore> { [TenantA] = settings, [TenantB] = settings });
@@ -0,0 +1,48 @@
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Models;
using NSubstitute;
namespace Deal.Tests.Unit.Support;
/// <summary>
/// Подставка <see cref="ITenantRepository"/> с фиксированным списком тенантов: сервисы получают
/// NSubstitute-подставку (<see cref="Repository"/>), тесты проверяют запросы страниц через
/// <see cref="PageRequests"/>. Неожиданные методы бросают <see cref="NotSupportedException"/>.
/// </summary>
public sealed class TestTenantRepository
{
private readonly IReadOnlyList<TenantRecordDto> _tenants;
private readonly List<(int Offset, int Limit)> _pageRequests = [];
/// <summary>
/// Подставка порта реестра тенантов (создаётся в конструкторе).
/// </summary>
public ITenantRepository Repository { get; }
/// <summary>
/// Запрошенные страницы (offset, limit) в порядке обращений.
/// </summary>
public IReadOnlyList<(int Offset, int Limit)> PageRequests => _pageRequests;
/// <summary>
/// Создаёт реестр с фиксированным списком тенантов.
/// </summary>
/// <param name="tenants">Тенанты реестра (как строки public.tenants).</param>
public TestTenantRepository(params TenantRecordDto[] tenants)
{
_tenants = tenants.ToList();
Repository = Substitute.For<ITenantRepository>();
Repository.ListAsync(Arg.Any<CancellationToken>())
.Returns(_tenants);
Repository.When(r => r.ListPageAsync(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>()))
.Do(ci => _pageRequests.Add((ci.ArgAt<int>(0), ci.ArgAt<int>(1))));
Repository.ListPageAsync(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(ci => (IReadOnlyList<TenantRecordDto>)_tenants.Skip(ci.ArgAt<int>(0)).Take(ci.ArgAt<int>(1)).ToList());
Repository.FindByIdAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
.Returns<TenantRecordDto?>(_ => throw new NotSupportedException("FindByIdAsync не используется тестами реестра"));
Repository.UpdateStatusAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
.Returns<bool>(_ => throw new NotSupportedException("UpdateStatusAsync не используется тестами реестра"));
Repository.When(r => r.CreateAsync(Arg.Any<TenantRecordDto>(), Arg.Any<CancellationToken>()))
.Do(_ => throw new NotSupportedException("CreateAsync не используется тестами реестра"));
}
}