diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml
index d170780..bcb91ed 100644
--- a/.gitea/workflows/ci.yml
+++ b/.gitea/workflows/ci.yml
@@ -1,10 +1,27 @@
name: ci
+# Прогоняем CI в двух случаях:
+# 1) МР: создан, переоткрыт или в ветку МР запушены новые коммиты (synchronize).
+# 2) Влитие в main (пуш в main — в т.ч. мерж МР).
+# Обычные коммиты в ветки без открытого МР CI не запускают.
on:
push:
+ branches:
+ - main
pull_request:
+ types:
+ - opened
+ - synchronize
+ - reopened
workflow_dispatch:
+# Новый прогон того же рефа (та же ветка МР / тот же main) отменяет незавершённый предыдущий:
+# коммит-А стартовал, через пару минут коммит-Б — прогон А отменяется, если ещё не закончил.
+# Завершённые (успех/фейл) прогоны не трогаются.
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
build-test:
runs-on: ubuntu-latest
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs
index ef39851..9ed88c7 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs
@@ -20,7 +20,7 @@ public sealed class DiscoveryBanGuard
///
/// Хранилище Discovery (счётчик DiscLog за сутки).
/// KV-настройки тенанта (discJoinLimit/discFloodDay/discPaused).
- /// Источник текущего времени (UTC; тесты передают фиксированные «часы», эталон FakeDiscoveryStore). По умолчанию — .
+ /// Источник текущего времени (UTC). По умолчанию — .
public DiscoveryBanGuard(
IDiscoveryStore store,
ISettingsStore settings,
diff --git a/src/core/tests/Deal.Tests.Unit/Api/DataRetentionSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Api/DataRetentionSchedulerTests.cs
index 9ba33ff..60b1d53 100644
--- a/src/core/tests/Deal.Tests.Unit/Api/DataRetentionSchedulerTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Api/DataRetentionSchedulerTests.cs
@@ -24,11 +24,11 @@ public sealed class DataRetentionSchedulerTests
public async Task RunCycle_PurgesAgedAuditResetsExpiredLimitsAndDeletesExpiredCounters()
{
DateTimeOffset now = DateTimeOffset.UtcNow;
- var audit = new FakeAuditLogStore();
- await audit.AppendAsync(AuditRecord(AuditEvents.OperatorLoginOk, now.AddDays(-(RetentionDays + 20))), CancellationToken.None);
- await audit.AppendAsync(AuditRecord(AuditEvents.TenantLoginOk, now.AddDays(-1)), CancellationToken.None);
+ var audit = new TestAuditLogStore();
+ await audit.Store.AppendAsync(AuditRecord(AuditEvents.OperatorLoginOk, now.AddDays(-(RetentionDays + 20))), CancellationToken.None);
+ await audit.Store.AppendAsync(AuditRecord(AuditEvents.TenantLoginOk, now.AddDays(-1)), CancellationToken.None);
- var limits = new FakeTenantLimitStore();
+ var limits = new TestTenantLimitStore();
// Период месяца начат два месяца назад → истёк; строка с накоплениями должна обнулиться.
limits.Preload(TenantId, budgetTokens: 1000, TenantLimitPeriods.Month, now.AddMonths(-2), usedTokens: 700, warned80: true);
@@ -63,9 +63,9 @@ public sealed class DataRetentionSchedulerTests
public async Task RunCycle_IsIdempotent()
{
DateTimeOffset now = DateTimeOffset.UtcNow;
- var audit = new FakeAuditLogStore();
- await audit.AppendAsync(AuditRecord(AuditEvents.OperatorLoginOk, now.AddDays(-(RetentionDays + 1))), CancellationToken.None);
- var limits = new FakeTenantLimitStore();
+ var audit = new TestAuditLogStore();
+ await audit.Store.AppendAsync(AuditRecord(AuditEvents.OperatorLoginOk, now.AddDays(-(RetentionDays + 1))), CancellationToken.None);
+ var limits = new TestTenantLimitStore();
limits.Preload(TenantId, budgetTokens: 1000, TenantLimitPeriods.Month, now.AddMonths(-2), usedTokens: 500);
await using ServiceProvider provider = BuildProvider(audit, limits, new TestRateLimitCounterStore());
DataRetentionScheduler scheduler = new(
@@ -86,13 +86,13 @@ public sealed class DataRetentionSchedulerTests
// counters: Хелпер счётчиков (окна посеяны сценарием).
// Возвращает: Провайдер с сервисами цикла.
private static ServiceProvider BuildProvider(
- FakeAuditLogStore audit,
- FakeTenantLimitStore limits,
+ TestAuditLogStore audit,
+ TestTenantLimitStore limits,
TestRateLimitCounterStore counters)
{
var services = new ServiceCollection();
- services.AddScoped(_ => audit);
- services.AddScoped(_ => limits);
+ services.AddScoped(_ => audit.Store);
+ services.AddScoped(_ => limits.Store);
services.AddScoped(_ => counters.Store);
return services.BuildServiceProvider();
}
diff --git a/src/core/tests/Deal.Tests.Unit/Api/DiscoveryWorkerSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Api/DiscoveryWorkerSchedulerTests.cs
index e954560..8e12fd4 100644
--- a/src/core/tests/Deal.Tests.Unit/Api/DiscoveryWorkerSchedulerTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Api/DiscoveryWorkerSchedulerTests.cs
@@ -94,8 +94,8 @@ public sealed class DiscoveryWorkerSchedulerTests
var tenantContext = new TenantContext();
var storeA = new TestDiscoveryStore();
var storeB = new TestDiscoveryStore();
- var settingsA = new FakeSettingsStore();
- var settingsB = new FakeSettingsStore();
+ var settingsA = new TestSettingsStore();
+ var settingsB = new TestSettingsStore();
var gatewayA = new TestDiscoveryGateway();
var gatewayB = new TestDiscoveryGateway();
PreloadDiscoveryEvalOff(settingsA);
@@ -104,12 +104,12 @@ public sealed class DiscoveryWorkerSchedulerTests
var services = new ServiceCollection();
services.AddSingleton(tenantContext);
services.AddSingleton(tenants.Repository);
- services.AddSingleton(new FakeMlClient());
+ services.AddSingleton(new TestMlClient().Client);
services.AddSingleton(_ => TestAiTools.New());
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
// (эталон PipelineWorkerSchedulerTests/ConnectionStringProvider.ForTenant).
services.AddScoped(provider => TenantOf(provider) == TenantA ? storeA.Store : storeB.Store);
- services.AddScoped(provider => TenantOf(provider) == TenantA ? settingsA : settingsB);
+ services.AddScoped(provider => TenantOf(provider) == TenantA ? settingsA.Store : settingsB.Store);
services.AddScoped(provider => TenantOf(provider) == TenantA ? gatewayA.Gateway : gatewayB.Gateway);
// Реальные сервисы модуля Discovery — как AddDiscoveryModule в Program.cs: цикл резолвит их в tenant-scope.
services.AddDiscoveryModule();
@@ -124,7 +124,7 @@ public sealed class DiscoveryWorkerSchedulerTests
}
// ML/ИИ выключены в настройках тенанта (оценка содержания — эвристика по ключам).
- private static void PreloadDiscoveryEvalOff(FakeSettingsStore settings)
+ private static void PreloadDiscoveryEvalOff(TestSettingsStore settings)
{
settings.Preload(SettingsKeys.MlEnabled, "false");
settings.Preload(SettingsKeys.AiEnabled, "false");
diff --git a/src/core/tests/Deal.Tests.Unit/Api/IngressRateLimitInterceptorTests.cs b/src/core/tests/Deal.Tests.Unit/Api/IngressRateLimitInterceptorTests.cs
index da58789..7928bdb 100644
--- a/src/core/tests/Deal.Tests.Unit/Api/IngressRateLimitInterceptorTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Api/IngressRateLimitInterceptorTests.cs
@@ -49,7 +49,7 @@ public sealed class IngressRateLimitInterceptorTests
await RunAsync(
registry,
- services => services.AddScoped(_ => new FakePipelineStore()),
+ services => services.AddScoped(_ => new TestPipelineStore().Store),
async channel =>
{
PushSourceReply first = await PushAsync(channel, TenantA);
@@ -77,7 +77,7 @@ public sealed class IngressRateLimitInterceptorTests
await RunAsync(
registry,
- services => services.AddScoped(_ => new FakePipelineStore()),
+ services => services.AddScoped(_ => new TestPipelineStore().Store),
async channel =>
{
// Окно ингресса — 1/мин: единственный разрешённый вызов исчерпывает лимит.
diff --git a/src/core/tests/Deal.Tests.Unit/Api/MlOutboxFlushSchedulerTests.cs b/src/core/tests/Deal.Tests.Unit/Api/MlOutboxFlushSchedulerTests.cs
index 3986016..ef50d41 100644
--- a/src/core/tests/Deal.Tests.Unit/Api/MlOutboxFlushSchedulerTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Api/MlOutboxFlushSchedulerTests.cs
@@ -39,14 +39,14 @@ public sealed class MlOutboxFlushSchedulerTests
{
await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) =>
{
- var store = new FakeMlLearningStore();
+ var store = new TestMlLearningStore();
SeedRows(store, count: 25, prefix: "a");
var tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider(
port,
new TestTenantRepository(Tenant(TenantA)),
tenantContext,
- new Dictionary { [TenantA] = store });
+ new Dictionary { [TenantA] = store });
MlOutboxFlushScheduler scheduler = CreateScheduler(provider);
await scheduler.RunCycleAsync(CancellationToken.None);
@@ -54,7 +54,7 @@ public sealed class MlOutboxFlushSchedulerTests
// 25 строк → 3 батча по 10/10/5 (ml_client.flush_outbox: chunk=10), строки удалены после успеха.
Assert.Equal(3, service.TrainCalls);
Assert.Equal(new[] { 10, 10, 5 }, service.TrainBatches.Select(batch => batch.Items.Count).ToArray());
- Assert.Equal(0, await store.CountOutboxAsync(CancellationToken.None));
+ Assert.Equal(0, await store.Store.CountOutboxAsync(CancellationToken.None));
Assert.False(tenantContext.HasTenant); // контекст AsyncLocal не переживает проход
});
}
@@ -65,23 +65,23 @@ public sealed class MlOutboxFlushSchedulerTests
await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) =>
{
service.TrainUnavailable = true;
- var store = new FakeMlLearningStore();
+ var store = new TestMlLearningStore();
SeedRows(store, count: 5, prefix: "a");
var tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider(
port,
new TestTenantRepository(Tenant(TenantA)),
tenantContext,
- new Dictionary { [TenantA] = store });
+ new Dictionary { [TenantA] = store });
MlOutboxFlushScheduler scheduler = CreateScheduler(provider);
await scheduler.RunCycleAsync(CancellationToken.None);
- Assert.Equal(5, await store.CountOutboxAsync(CancellationToken.None)); // строки остались
+ Assert.Equal(5, await store.Store.CountOutboxAsync(CancellationToken.None)); // строки остались
// Следующий цикл — повторная попытка (ретрай на каждом тике; строки снова не удалены).
await scheduler.RunCycleAsync(CancellationToken.None);
Assert.Equal(2, service.TrainCalls);
- Assert.Equal(5, await store.CountOutboxAsync(CancellationToken.None));
+ Assert.Equal(5, await store.Store.CountOutboxAsync(CancellationToken.None));
Assert.False(tenantContext.HasTenant);
});
}
@@ -91,16 +91,16 @@ public sealed class MlOutboxFlushSchedulerTests
{
await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) =>
{
- var storeA = new FakeMlLearningStore();
+ var storeA = new TestMlLearningStore();
SeedRows(storeA, count: 12, prefix: "a");
- var storeB = new FakeMlLearningStore();
+ var storeB = new TestMlLearningStore();
SeedRows(storeB, count: 3, prefix: "b");
var tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider(
port,
new TestTenantRepository(Tenant(TenantA), Tenant(TenantB)),
tenantContext,
- new Dictionary { [TenantA] = storeA, [TenantB] = storeB });
+ new Dictionary { [TenantA] = storeA, [TenantB] = storeB });
MlOutboxFlushScheduler scheduler = CreateScheduler(provider);
await scheduler.RunCycleAsync(CancellationToken.None);
@@ -108,8 +108,8 @@ public sealed class MlOutboxFlushSchedulerTests
// Каждый тенант выгрузил свою очередь в собственном scope (A: 10+2, B: 3) — строки удалены.
Assert.Equal(3, service.TrainCalls);
Assert.Equal(new[] { 10, 2, 3 }, service.TrainBatches.Select(batch => batch.Items.Count).ToArray());
- Assert.Equal(0, await storeA.CountOutboxAsync(CancellationToken.None));
- Assert.Equal(0, await storeB.CountOutboxAsync(CancellationToken.None));
+ Assert.Equal(0, await storeA.Store.CountOutboxAsync(CancellationToken.None));
+ Assert.Equal(0, await storeB.Store.CountOutboxAsync(CancellationToken.None));
Assert.False(tenantContext.HasTenant);
});
}
@@ -119,20 +119,20 @@ public sealed class MlOutboxFlushSchedulerTests
{
await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) =>
{
- var store = new FakeMlLearningStore();
+ var store = new TestMlLearningStore();
SeedRows(store, count: 105, prefix: "a");
var tenantContext = new TenantContext();
await using ServiceProvider provider = BuildProvider(
port,
new TestTenantRepository(Tenant(TenantA)),
tenantContext,
- new Dictionary { [TenantA] = store });
+ new Dictionary { [TenantA] = store });
MlOutboxFlushScheduler scheduler = CreateScheduler(provider);
await scheduler.RunCycleAsync(CancellationToken.None);
Assert.Equal(10, service.TrainCalls);
- Assert.Equal(5, await store.CountOutboxAsync(CancellationToken.None));
+ Assert.Equal(5, await store.Store.CountOutboxAsync(CancellationToken.None));
});
}
@@ -149,7 +149,7 @@ public sealed class MlOutboxFlushSchedulerTests
int port,
TestTenantRepository tenants,
TenantContext tenantContext,
- Dictionary storesByTenant)
+ Dictionary storesByTenant)
{
var services = new ServiceCollection();
services.AddSingleton(tenantContext);
@@ -162,9 +162,9 @@ public sealed class MlOutboxFlushSchedulerTests
services.AddScoped(provider => provider.GetRequiredService());
services.AddScoped(provider => provider.GetRequiredService());
// Tenant-scoped адаптеры: выбирают фейк по тому же ITenantContext, который планировщик заполняет SetTenant.
- services.AddScoped(provider => storesByTenant[TenantOf(provider)]);
- services.AddScoped(_ => new FakeSettingsStore());
- services.AddScoped(_ => new FakeTenantLimitStore());
+ services.AddScoped(provider => storesByTenant[TenantOf(provider)].Store);
+ services.AddScoped(_ => new TestSettingsStore().Store);
+ services.AddScoped(_ => new TestTenantLimitStore().Store);
services.AddScoped(_ => new TokenUsageEventService(new TestTokenUsageEventStore().Store));
services.AddScoped();
services.AddLogging();
@@ -196,7 +196,7 @@ public sealed class MlOutboxFlushSchedulerTests
// count: Число строк.
// prefix: Префикс id (различает тенантов теста).
private static void SeedRows(
- FakeMlLearningStore store,
+ TestMlLearningStore store,
int count,
string prefix)
{
diff --git a/src/core/tests/Deal.Tests.Unit/Api/OperatorBootstrapHostedServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Api/OperatorBootstrapHostedServiceTests.cs
index 0be3a42..ff6bb2b 100644
--- a/src/core/tests/Deal.Tests.Unit/Api/OperatorBootstrapHostedServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Api/OperatorBootstrapHostedServiceTests.cs
@@ -128,13 +128,13 @@ public sealed class OperatorBootstrapHostedServiceTests
IConfiguration configuration = new ConfigurationBuilder()
.AddInMemoryCollection(values)
.Build();
- var store = new FakeOperatorAuthStore();
+ var store = new TestOperatorAuthStore();
var passwordHasher = TestHashers.New();
var logger = new ListLogger();
// Hosted-шаг резолвит scoped OperatorBootstrapService из scope (как TenantBootstrapService):
// провайдер собирается с реальной регистрацией модуля поверх фейков.
var services = new ServiceCollection();
- services.AddScoped(_ => store);
+ services.AddScoped(_ => store.Store);
services.AddScoped(_ => passwordHasher);
services.AddScoped();
// Провайдер живёт всё время теста (без using): hosted резолвит scope в StartAsync после создания.
@@ -150,7 +150,7 @@ public sealed class OperatorBootstrapHostedServiceTests
// Контекст теста bootstrap-шага.
private sealed record Context(
OperatorBootstrapHostedService Hosted,
- FakeOperatorAuthStore Store,
+ TestOperatorAuthStore Store,
IPasswordHasher PasswordHasher,
ListLogger Logs);
diff --git a/src/core/tests/Deal.Tests.Unit/Api/RuntimeDepthsCollectorTests.cs b/src/core/tests/Deal.Tests.Unit/Api/RuntimeDepthsCollectorTests.cs
index 6a46433..e4c14ac 100644
--- a/src/core/tests/Deal.Tests.Unit/Api/RuntimeDepthsCollectorTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Api/RuntimeDepthsCollectorTests.cs
@@ -29,19 +29,19 @@ public sealed class RuntimeDepthsCollectorTests
[Fact]
public async Task CollectAsync_SumsQueueAndOutboxAcrossTenants()
{
- var pipelineByTenant = new Dictionary
+ var pipelineByTenant = new Dictionary
{
- [TenantA.ToString("N")] = new FakePipelineStore(),
- [TenantB.ToString("N")] = new FakePipelineStore(),
+ [TenantA.ToString("N")] = new TestPipelineStore(),
+ [TenantB.ToString("N")] = new TestPipelineStore(),
};
pipelineByTenant[TenantA.ToString("N")].SeedQueue(QueueItem("p_a1"));
pipelineByTenant[TenantA.ToString("N")].SeedQueue(QueueItem("p_a2"));
pipelineByTenant[TenantB.ToString("N")].SeedQueue(QueueItem("p_b1"));
- var outboxByTenant = new Dictionary
+ var outboxByTenant = new Dictionary
{
- [TenantA.ToString("N")] = new FakeMlLearningStore(),
- [TenantB.ToString("N")] = new FakeMlLearningStore(),
+ [TenantA.ToString("N")] = new TestMlLearningStore(),
+ [TenantB.ToString("N")] = new TestMlLearningStore(),
};
outboxByTenant[TenantA.ToString("N")].SeedOutbox("mle_1", "текст", "spam");
@@ -59,7 +59,7 @@ public sealed class RuntimeDepthsCollectorTests
[Fact]
public async Task CollectAsync_CollectsBudgetRatiosPerTenant()
{
- var limits = new FakeTenantLimitStore();
+ var limits = new TestTenantLimitStore();
DateTimeOffset periodStart = DateTimeOffset.UtcNow;
limits.Preload(TenantA, 100, TenantLimitPeriods.Month, periodStart, 80);
limits.Preload(TenantB, 0, TenantLimitPeriods.Month, periodStart, 55);
@@ -69,7 +69,7 @@ public sealed class RuntimeDepthsCollectorTests
PipelineStores(TenantA, TenantB, TenantC),
OutboxStores(TenantA, TenantB, TenantC),
TenantRecords(TenantA, TenantB, TenantC),
- limits);
+ limits.Store);
RuntimeDepthsDto depths = await collector.CollectAsync(CancellationToken.None);
@@ -83,7 +83,7 @@ public sealed class RuntimeDepthsCollectorTests
[Fact]
public async Task CollectAsync_BudgetReadFailure_SkipsFailedTenant()
{
- var limits = new FakeTenantLimitStore();
+ var limits = new TestTenantLimitStore();
limits.FailStateReads.Add(TenantA);
limits.Preload(TenantB, 100, TenantLimitPeriods.Month, DateTimeOffset.UtcNow, 80);
@@ -91,7 +91,7 @@ public sealed class RuntimeDepthsCollectorTests
PipelineStores(TenantA, TenantB),
OutboxStores(TenantA, TenantB),
TenantRecords(TenantA, TenantB),
- limits);
+ limits.Store);
RuntimeDepthsDto depths = await collector.CollectAsync(CancellationToken.None);
@@ -105,8 +105,8 @@ public sealed class RuntimeDepthsCollectorTests
public async Task CollectAsync_NoTenants_ReturnsZeros()
{
RuntimeDepthsCollector collector = Build(
- new Dictionary(),
- new Dictionary(),
+ new Dictionary(),
+ new Dictionary(),
tenants: Array.Empty());
RuntimeDepthsDto depths = await collector.CollectAsync(CancellationToken.None);
@@ -123,11 +123,11 @@ public sealed class RuntimeDepthsCollectorTests
Status = PipelineQueueStatuses.New,
};
- private static Dictionary PipelineStores(params Guid[] tenants)
- => tenants.ToDictionary(tenant => tenant.ToString("N"), _ => new FakePipelineStore());
+ private static Dictionary PipelineStores(params Guid[] tenants)
+ => tenants.ToDictionary(tenant => tenant.ToString("N"), _ => new TestPipelineStore());
- private static Dictionary OutboxStores(params Guid[] tenants)
- => tenants.ToDictionary(tenant => tenant.ToString("N"), _ => new FakeMlLearningStore());
+ private static Dictionary OutboxStores(params Guid[] tenants)
+ => tenants.ToDictionary(tenant => tenant.ToString("N"), _ => new TestMlLearningStore());
private static TenantRecordDto[] TenantRecords(params Guid[] tenants)
=> tenants
@@ -139,8 +139,8 @@ public sealed class RuntimeDepthsCollectorTests
// Собирает коллектор поверх tenant-scoped фейков (как реальные адаптеры по ITenantContext).
private static RuntimeDepthsCollector Build(
- IReadOnlyDictionary pipelineByTenant,
- IReadOnlyDictionary outboxByTenant,
+ IReadOnlyDictionary pipelineByTenant,
+ IReadOnlyDictionary outboxByTenant,
IReadOnlyList? tenants = null,
ITenantLimitStore? limitStore = null)
{
@@ -153,12 +153,12 @@ public sealed class RuntimeDepthsCollectorTests
new TenantRecordDto(TenantB, "B", "active", DateTimeOffset.UtcNow),
}).ToArray()).Repository);
- services.AddScoped(provider => pipelineByTenant[CurrentTenant(provider)]);
- services.AddScoped(_ => new FakeMlClient());
+ services.AddScoped(provider => pipelineByTenant[CurrentTenant(provider)].Store);
+ services.AddScoped(_ => new TestMlClient().Client);
services.AddScoped();
services.AddScoped();
- services.AddScoped(provider => outboxByTenant[CurrentTenant(provider)]);
- services.AddSingleton(limitStore ?? new FakeTenantLimitStore());
+ services.AddScoped(provider => outboxByTenant[CurrentTenant(provider)].Store);
+ services.AddSingleton(limitStore ?? new TestTenantLimitStore().Store);
ServiceProvider provider = services.BuildServiceProvider();
return new RuntimeDepthsCollector(
diff --git a/src/core/tests/Deal.Tests.Unit/Api/TgStatusServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Api/TgStatusServiceTests.cs
index a8ad167..cc45382 100644
--- a/src/core/tests/Deal.Tests.Unit/Api/TgStatusServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Api/TgStatusServiceTests.cs
@@ -43,7 +43,7 @@ public sealed class TgStatusServiceTests
[Fact]
public async Task GetAsync_GatewayUnavailable_ReturnsIdleFormWithKvAccountCountAndKeys()
{
- (TgStatusService service, TestTelegramStore store, FakeSettingsStore settings, TestTelegramGateway gateway, _, TestGlobalSettingsStore globalSettings) = Create();
+ (TgStatusService service, TestTelegramStore store, TestSettingsStore settings, TestTelegramGateway gateway, _, TestGlobalSettingsStore globalSettings) = Create();
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true));
store.Seed(Dialog("d_2", "Группа", "group", Monitor: true));
store.Seed(Dialog("d_off", "Выключен", "channel", Monitor: false));
@@ -69,7 +69,7 @@ public sealed class TgStatusServiceTests
[Fact]
public async Task GetAsync_ReadyGateway_ComposesLiveFieldsWithKvAccountMonitoredAndKeys()
{
- (TgStatusService service, TestTelegramStore store, FakeSettingsStore settings, TestTelegramGateway gateway, _, TestGlobalSettingsStore globalSettings) = Create();
+ (TgStatusService service, TestTelegramStore store, TestSettingsStore settings, TestTelegramGateway gateway, _, TestGlobalSettingsStore globalSettings) = Create();
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true));
settings.Preload(SettingsKeys.TgAccount, "\"@realuser\"");
PreloadKeys(globalSettings, "123456", "abcdefghijklmnop");
@@ -106,16 +106,16 @@ public sealed class TgStatusServiceTests
// ─── Хелперы ────────────────────────────────────────────────────────────
// Собирает сервис на фейках (FakeTelegramGateway/Store/SettingsStore/GlobalSettingsStore + ISecretCipher).
- private static (TgStatusService Service, TestTelegramStore Store, FakeSettingsStore Settings, TestTelegramGateway Gateway, ISecretCipher Cipher, TestGlobalSettingsStore GlobalSettings) Create()
+ private static (TgStatusService Service, TestTelegramStore Store, TestSettingsStore Settings, TestTelegramGateway Gateway, ISecretCipher Cipher, TestGlobalSettingsStore GlobalSettings) Create()
{
var store = new TestTelegramStore();
- var settings = new FakeSettingsStore();
+ var settings = new TestSettingsStore();
var gateway = new TestTelegramGateway();
var cipher = TestCiphers.New();
var globalSettings = new TestGlobalSettingsStore();
var keys = new TelegramKeysService(globalSettings.Store, cipher);
- var dialogs = new DialogsService(store.Store, settings, gateway.Gateway, NullLogger.Instance);
- var service = new TgStatusService(gateway.Gateway, dialogs, settings, keys);
+ var dialogs = new DialogsService(store.Store, settings.Store, gateway.Gateway, NullLogger.Instance);
+ var service = new TgStatusService(gateway.Gateway, dialogs, settings.Store, keys);
return (service, store, settings, gateway, cipher, globalSettings);
}
diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiClassifierTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiClassifierTests.cs
index 5956995..ff1307c 100644
--- a/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiClassifierTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiClassifierTests.cs
@@ -18,7 +18,7 @@ namespace Deal.Tests.Unit.Contracts;
///
public sealed class BudgetedAiClassifierTests
{
- // Id тенанта сценариев строкой (формат N) — Guid ключа строк лимита FakeTenantLimitStore.
+ // Id тенанта сценариев строкой (формат N) — Guid ключа строк лимита TestTenantLimitStore.
private const string TenantIdValue = "0123456789abcdef0123456789abcdef";
// Guid того же тенанта — ключ строки лимита.
@@ -128,7 +128,7 @@ public sealed class BudgetedAiClassifierTests
Context ctx = Create(
limits => limits.Preload(
TenantGuid, budgetTokens: 0, TenantLimitPeriods.Month, PeriodStart, usedTokens: 0));
- var standaloneLocal = new LocalAiClassifier(new LocalFieldsParser(ctx.Settings));
+ var standaloneLocal = new LocalAiClassifier(new LocalFieldsParser(ctx.Settings.Store));
const string text = "Ищем Python-разработчика на проект в команду\nБюджет: до 2к$";
AiParsedCardDto result = await ctx.Decorator.ClassifyAsync(text, CancellationToken.None);
@@ -152,24 +152,24 @@ public sealed class BudgetedAiClassifierTests
private sealed record Context(
IAiClassifier Decorator,
TestAiClassifier Paid,
- FakeSettingsStore Settings);
+ TestSettingsStore Settings);
// Собирает контекст: платный фейк + реальный Local-классификатор + фейк лимитов и tenant-контекст
// (зеркало регистрации AddDealIntegrations при UseLocal=false).
// configure: Настройка строки лимита сценария (null — строки нет, ленивый дефолт-бюджет).
// Возвращает: Контекст теста.
- private static Context Create(Action? configure = null)
+ private static Context Create(Action? configure = null)
{
- var settings = new FakeSettingsStore();
- var limits = new FakeTenantLimitStore();
+ var settings = new TestSettingsStore();
+ var limits = new TestTenantLimitStore();
configure?.Invoke(limits);
ITenantContext tenantContext = new TenantContext();
tenantContext.SetTenant(new TenantId(TenantIdValue));
var paid = new TestAiClassifier();
IAiClassifier decorator = new BudgetedAiClassifier(
paid.Classifier,
- new LocalAiClassifier(new LocalFieldsParser(settings)),
- limits,
+ new LocalAiClassifier(new LocalFieldsParser(settings.Store)),
+ limits.Store,
tenantContext,
NullLogger.Instance);
return new Context(decorator, paid, settings);
diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiToolsTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiToolsTests.cs
index daac8df..5a5b6dd 100644
--- a/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiToolsTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Contracts/BudgetedAiToolsTests.cs
@@ -18,7 +18,7 @@ namespace Deal.Tests.Unit.Contracts;
///
public sealed class BudgetedAiToolsTests
{
- // Id тенанта сценариев строкой (формат N) — Guid ключа строк лимита FakeTenantLimitStore.
+ // Id тенанта сценариев строкой (формат N) — Guid ключа строк лимита TestTenantLimitStore.
private const string TenantIdValue = "0123456789abcdef0123456789abcdef";
// Guid того же тенанта — ключ строки лимита.
@@ -139,16 +139,16 @@ public sealed class BudgetedAiToolsTests
// AddDealIntegrations при UseLocal=false).
// configure: Настройка строки лимита сценария (null — строки нет, ленивый дефолт-бюджет).
// Возвращает: Контекст теста.
- private static Context Create(Action? configure = null)
+ private static Context Create(Action? configure = null)
{
- var limits = new FakeTenantLimitStore();
+ var limits = new TestTenantLimitStore();
configure?.Invoke(limits);
ITenantContext tenantContext = new TenantContext();
tenantContext.SetTenant(new TenantId(TenantIdValue));
var paid = TestAiTools.New();
IAiTools decorator = new BudgetedAiTools(
paid,
- limits,
+ limits.Store,
tenantContext,
NullLogger.Instance);
return new Context(decorator, paid);
diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/CardComposerTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/CardComposerTests.cs
index 5967b56..006b11c 100644
--- a/src/core/tests/Deal.Tests.Unit/Contracts/CardComposerTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Contracts/CardComposerTests.cs
@@ -6,7 +6,6 @@ using Deal.Modules.Pipeline.Application.Models;
using Deal.Modules.Pipeline.Application.Services;
using Deal.Modules.Settings.Application.Models;
using Deal.Modules.Settings.Application.Services;
-using Deal.Tests.Unit.Modules.Cards;
using Deal.Tests.Unit.Modules.Settings;
using Deal.Tests.Unit.Support;
@@ -109,7 +108,7 @@ public sealed class CardComposerTests
[Fact]
public async Task BuildAsync_BoardWithoutRules_PlacesCardIntoBoardWithEmptyMatchHits()
{
- (CardComposer composer, FakeKanjStore store, _) = Create();
+ (CardComposer composer, TestKanjStore store, _) = Create();
store.SeedBoard(new ContainerDto { Id = "b_py" });
CardSnapshot snapshot = await composer.BuildAsync(
@@ -125,7 +124,7 @@ public sealed class CardComposerTests
[Fact]
public async Task BuildAsync_BoardWithNonMatchingRules_FallsBackToInbox()
{
- (CardComposer composer, FakeKanjStore store, _) = Create();
+ (CardComposer composer, TestKanjStore store, _) = Create();
store.SeedBoard(BoardWithRules("b_wpf", new[] { "wpf" }));
CardSnapshot snapshot = await composer.BuildAsync(
@@ -141,7 +140,7 @@ public sealed class CardComposerTests
[Fact]
public async Task BuildAsync_BoardWithMatchingRules_PlacesCardAndComputesMatchHits()
{
- (CardComposer composer, FakeKanjStore store, _) = Create();
+ (CardComposer composer, TestKanjStore store, _) = Create();
store.SeedBoard(BoardWithRules("b_py", new[] { "python" }));
CardSnapshot snapshot = await composer.BuildAsync(
@@ -193,7 +192,7 @@ public sealed class CardComposerTests
[Fact]
public async Task BuildAsync_ConversionDisabled_LeavesConvFieldsEmpty()
{
- (CardComposer composer, _, FakeSettingsStore settings) = Create();
+ (CardComposer composer, _, TestSettingsStore settings) = Create();
settings.Preload(SettingsKeys.ConversionOn, "false"); // budget_to_target L348–349
CardSnapshot snapshot = await composer.BuildAsync(
@@ -232,11 +231,11 @@ public sealed class CardComposerTests
// Создаёт контекст теста: пустые хранилища (дефолты: конверсия включена, курсы — мок).
// Возвращает: Кортеж (композитор, канбан-хранилище, KV-настройки).
- private static (CardComposer Composer, FakeKanjStore Store, FakeSettingsStore Settings) Create()
+ private static (CardComposer Composer, TestKanjStore Store, TestSettingsStore Settings) Create()
{
- var store = new FakeKanjStore();
- var settings = new FakeSettingsStore();
- return (new CardComposer(store, settings), store, settings);
+ var store = new TestKanjStore();
+ var settings = new TestSettingsStore();
+ return (new CardComposer(store.Store, settings.Store), store, settings);
}
// Разбор карточки со значениями по умолчанию (сценарий теста перекрывает нужные поля).
diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs
index 8ae48b4..717e77e 100644
--- a/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Contracts/CardsServiceTests.cs
@@ -3,7 +3,6 @@ using Deal.Tests.Unit.Support;
using Deal.Modules.Cards.Application.Sources;
using Deal.Modules.Kanban.Application.Models;
using Deal.Modules.Kanban.Application.Services;
-using Deal.Tests.Unit.Modules.Cards;
using Deal.Tests.Unit.Modules.Settings;
namespace Deal.Tests.Unit.Contracts;
@@ -17,7 +16,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task ListCards_NoCol_ReturnsAllOrderedByReceivedAtDesc()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedCard(Card("l_1", "inbox", receivedAtMs: 100));
store.SeedCard(Card("l_2", "b_x", receivedAtMs: 300));
@@ -30,7 +29,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task ListCards_ByColumn_ReturnsOnlyColumnCards()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedCard(Card("l_1", "inbox"));
store.SeedCard(Card("l_2", KanbanColumns.Trash));
@@ -43,7 +42,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task GetCard_Existing_ReturnsCard()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedCard(Card("l_1", "inbox", title: "Middle Python"));
CardDto? card = await service.GetCardAsync("l_1", CancellationToken.None);
@@ -66,7 +65,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Move_ToBoardWithRules_UpdatesColumnAndComputesMatchHits()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Нужен Middle Python-разработчик на бота, 1600$."));
@@ -84,7 +83,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Move_ToBoard_WritesJournalAndPush()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Нужен Python-разработчик"));
@@ -105,7 +104,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Move_ToInbox_WritesJournalWithoutPush()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
store.SeedCard(Card("l_1", "b_py", sourceMsg: "Нужен Python-разработчик", prevCol: "inbox"));
@@ -124,7 +123,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Move_ToUnknownBoard_Returns400Text()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Нужен Python-разработчик"));
CardResultDto result = await service.MoveDashboardCardAsync("l_1", "b_ghost", CancellationToken.None);
@@ -148,7 +147,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Move_CardMissing_ReturnsNullLead()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedBoard(Board("b_py"));
CardResultDto result = await service.MoveDashboardCardAsync("l_ghost", "b_py", CancellationToken.None);
@@ -162,7 +161,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Move_SameColumn_NoOpWithoutJournalAndPush()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
store.SeedCard(Card("l_1", "b_py", sourceMsg: "Нужен Python-разработчик", isNew: true));
@@ -179,7 +178,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Move_ToBoardWithoutRules_MatchHitsEmptyButPushSent()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedBoard(Board("b_free")); // правил нет
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Любой текст"));
@@ -195,7 +194,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Move_FromBoardToBoard_PrevColIsOldBoardAndPushNewLabel()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedBoard(Board("b_java", Rules(stack: new[] { "java" })));
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
store.SeedCard(Card("l_1", "b_java", sourceMsg: "Нужен Java-разработчик", prevCol: "inbox"));
@@ -214,7 +213,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Move_EmptySourceText_UsesTitleForMatchHitsAndPush()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
store.SeedCard(Card("l_1", "inbox", title: "Middle Python-разработчик", sourceMsg: " "));
@@ -228,7 +227,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Move_EmptySourceAndTitle_JournalWithoutPush()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedBoard(Board("b_py"));
store.SeedCard(Card("l_1", "inbox"));
@@ -241,7 +240,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Move_ToBoardWithGradeAndBudget_HitsIncludeGradeWordAndBudget()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedBoard(Board("b_mid", Rules(
grade: new[] { "middle" },
budget: new BudgetRangeDto(1000, 3000, "USD"))));
@@ -263,7 +262,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Move_FromArchive_Returns400AndWritesNothing()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedBoard(Board("b_py"));
store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая вакансия", prevCol: "b_py"));
@@ -279,7 +278,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Move_FromTrash_Returns400AndWritesNothing()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedBoard(Board("b_py"));
store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Спам-текст", prevCol: "b_py"));
@@ -296,7 +295,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Trash_FromInbox_MovesToTrashWithJournalAndSpamPush()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Предлагаю услуги продвижения", isNew: true));
CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None);
@@ -318,7 +317,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Trash_AlreadyInTrash_NoOpWithoutJournalAndPush()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Текст"));
CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None);
@@ -331,7 +330,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Trash_FromArchive_WritesJournalWithoutPush()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая карточка", prevCol: "b_py"));
CardDto? result = await service.TrashCardAsync("l_1", CancellationToken.None);
@@ -346,7 +345,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Trash_CardMissing_ReturnsNull()
{
- (CardsService service, _, _, FakeMlClient ml) = Create();
+ (CardsService service, _, _, TestMlClient ml) = Create();
CardDto? result = await service.TrashCardAsync("l_ghost", CancellationToken.None);
@@ -358,7 +357,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Restore_FromTrash_ToPrevColBoard_UnlearnsSpam()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Нужен Python-разработчик", prevCol: "b_py"));
@@ -383,7 +382,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Restore_FromArchive_NoPush()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedBoard(Board("b_py"));
store.SeedCard(Card("l_1", KanbanColumns.Archive, sourceMsg: "Старая карточка", prevCol: "b_py"));
@@ -399,7 +398,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Restore_FromTrash_PrevColDeletedBoard_FallsBackToInbox()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Текст", prevCol: "b_gone"));
string? back = await service.RestoreCardAsync("l_1", CancellationToken.None);
@@ -414,7 +413,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Restore_FromTrash_PrevColInbox_ReturnsInbox()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedCard(Card("l_1", KanbanColumns.Trash, sourceMsg: "Текст", prevCol: "inbox"));
string? back = await service.RestoreCardAsync("l_1", CancellationToken.None);
@@ -439,7 +438,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task DeleteForever_RemovesCardAndComments_KeepsJournal()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedCard(Card("l_1", KanbanColumns.Trash,
sourceMsg: "Нужен Python-разработчик",
comments: [new CardCommentDto("cm_1", "Вы", "Перезвонить", "5 мин")]));
@@ -454,7 +453,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task DeleteForever_JournalRowsSurviveCardDeletion()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedBoard(Board("b_py", Rules(stack: new[] { "python" })));
store.SeedCard(Card("l_1", "inbox", sourceMsg: "Нужен Python-разработчик"));
@@ -480,7 +479,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task ClearCol_Trash_ReturnsClearedCount()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedCard(Card("l_1", KanbanColumns.Trash));
store.SeedCard(Card("l_2", KanbanColumns.Trash));
store.SeedCard(Card("l_3", KanbanColumns.Archive));
@@ -507,7 +506,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task ClearCol_Board_Returns400Text()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedBoard(Board("b_py"));
store.SeedCard(Card("l_1", "b_py"));
@@ -531,7 +530,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task MarkSeen_ById_OnlyThatCard()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedCard(Card("l_1", "inbox", isNew: true));
store.SeedCard(Card("l_2", "inbox", isNew: true));
@@ -544,7 +543,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task MarkSeen_ByCol_OnlyColumnCards()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedCard(Card("l_1", "inbox", isNew: true));
store.SeedCard(Card("l_2", "inbox", isNew: true));
store.SeedCard(Card("l_3", "b_py", isNew: true));
@@ -558,7 +557,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task MarkSeen_All_WithoutParameters()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedCard(Card("l_1", "inbox", isNew: true));
store.SeedCard(Card("l_2", KanbanColumns.Trash, isNew: true));
@@ -571,7 +570,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task AddComment_EmptyOrWhitespaceText_Returns400Text()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedCard(Card("l_1", "inbox"));
AddCommentResultDto empty = await service.AddCommentAsync("l_1", " ", CancellationToken.None);
@@ -585,7 +584,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task AddComment_Valid_AppendsTrimmedCommentWithJournal()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedCard(Card("l_1", "inbox"));
AddCommentResultDto result = await service.AddCommentAsync("l_1", " Перезвонить завтра ", CancellationToken.None);
@@ -618,7 +617,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Counts_ColumnsNewAndMlStats()
{
- (CardsService service, FakeKanjStore store, _, FakeMlClient ml) = Create();
+ (CardsService service, TestKanjStore store, _, TestMlClient ml) = Create();
store.SeedCard(Card("l_1", "inbox", isNew: true));
store.SeedCard(Card("l_2", "inbox"));
store.SeedCard(Card("l_3", "b_py"));
@@ -652,7 +651,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Search_QueryShorterThanTwoChars_ReturnsEmptyAndDoesNotCallStore()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedCard(Card("l_1", "inbox", title: "Python-разработчик"));
IReadOnlyList one = await service.SearchCardsAsync("p", CancellationToken.None);
@@ -666,7 +665,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Search_DelegatesToStoreWithQueryAndLimit()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedCard(Card("l_1", "inbox", title: "Middle Python-разработчик", receivedAtMs: 300));
IReadOnlyList result = await service.SearchCardsAsync(" pYtHoN ", CancellationToken.None);
@@ -681,7 +680,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Search_FindsByTitleSummaryContactSourceText_OrderedByReceivedAtDesc()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
store.SeedCard(Card("l_title", "inbox", title: "Middle Python-разработчик", receivedAtMs: 300));
store.SeedCard(Card("l_summary", "b_py", summary: "проект на python", receivedAtMs: 200));
store.SeedCard(Card("l_contact", "inbox", contact: "@python_dev", receivedAtMs: 100));
@@ -696,7 +695,7 @@ public sealed class CardsServiceTests
[Fact]
public async Task Search_LimitIsTwelve()
{
- (CardsService service, FakeKanjStore store, _, _) = Create();
+ (CardsService service, TestKanjStore store, _, _) = Create();
for (int i = 0; i < 15; i++)
{
store.SeedCard(Card($"l_{i:00}", "inbox", title: $"python {i}", receivedAtMs: i));
@@ -711,12 +710,12 @@ public sealed class CardsServiceTests
// ─── Хелперы ──────────────────────────────────────────────────────────
- private static (CardsService Service, FakeKanjStore Store, FakeSettingsStore Settings, FakeMlClient Ml) Create()
+ private static (CardsService Service, TestKanjStore Store, TestSettingsStore Settings, TestMlClient Ml) Create()
{
- var store = new FakeKanjStore();
- var settings = new FakeSettingsStore();
- var ml = new FakeMlClient();
- return (new CardsService(store, settings, ml, new TestFileStorage().Storage), store, settings, ml);
+ var store = new TestKanjStore();
+ var settings = new TestSettingsStore();
+ var ml = new TestMlClient();
+ return (new CardsService(store.Store, settings.Store, ml.Client, new TestFileStorage().Storage), store, settings, ml);
}
// Доска с правилами (ContainerRulesDto) либо без них.
diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/DialogsServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/DialogsServiceTests.cs
index c32a759..02c7e24 100644
--- a/src/core/tests/Deal.Tests.Unit/Contracts/DialogsServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Contracts/DialogsServiceTests.cs
@@ -38,7 +38,7 @@ public sealed class DialogsServiceTests
[Fact]
public async Task Sync_AutoMonitorNewFalse_AddsNewDialogsUnmonitored()
{
- (DialogsService service, TestTelegramStore store, FakeSettingsStore settings, _) = Create();
+ (DialogsService service, TestTelegramStore store, TestSettingsStore settings, _) = Create();
settings.Preload(SettingsKeys.AutoMonitorNew, "false");
int count = await service.SyncFromTelegramAsync(Catalog(Entry("d_1", "Канал", "channel")), CancellationToken.None);
@@ -497,12 +497,12 @@ public sealed class DialogsServiceTests
// ─── Хелперы ────────────────────────────────────────────────────────────
// Собирает сервис на фейках (TestTelegramStore — семантика адаптера; настройки/гейт чистые).
- private static (DialogsService Service, TestTelegramStore Store, FakeSettingsStore Settings, TestTelegramGateway Gateway) Create()
+ private static (DialogsService Service, TestTelegramStore Store, TestSettingsStore Settings, TestTelegramGateway Gateway) Create()
{
var store = new TestTelegramStore();
- var settings = new FakeSettingsStore();
+ var settings = new TestSettingsStore();
var gateway = new TestTelegramGateway();
- return (new DialogsService(store.Store, settings, gateway.Gateway, NullLogger.Instance), store, settings, gateway);
+ return (new DialogsService(store.Store, settings.Store, gateway.Gateway, NullLogger.Instance), store, settings, gateway);
}
// Строка каталога минимально (id/имя/kind + флаги/хвост каталога).
diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryEvaluatorTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryEvaluatorTests.cs
index 3520c6e..2dd7620 100644
--- a/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryEvaluatorTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryEvaluatorTests.cs
@@ -30,7 +30,7 @@ public sealed class DiscoveryEvaluatorTests
[Fact]
public async Task EvaluateMessage_MlSpam_NotFit()
{
- (DiscoveryEvaluator evaluator, _, FakeMlClient ml, _) = Create(aiEnabled: false, mlEnabled: true);
+ (DiscoveryEvaluator evaluator, _, TestMlClient ml, _) = Create(aiEnabled: false, mlEnabled: true);
ml.Predict = new MlPredictResultDto(
Take: true, Label: "spam", Scores: new Dictionary(), Hits: 3, Ready: true, Margin: 0.9,
Terms: Array.Empty(), Type: null);
@@ -46,7 +46,7 @@ public sealed class DiscoveryEvaluatorTests
[Fact]
public async Task EvaluateMessage_AiFits_ReturnsAiVerdict()
{
- (DiscoveryEvaluator evaluator, _, FakeMlClient ml, IAiTools ai) = Create(aiEnabled: true, mlEnabled: false);
+ (DiscoveryEvaluator evaluator, _, TestMlClient ml, IAiTools ai) = Create(aiEnabled: true, mlEnabled: false);
ai.ReturnsFit(new AiEvaluateFitResultDto(Fit: true, "тема совпадает"));
DiscoveryMessageFit fit = await evaluator.EvaluateMessageAsync(
@@ -168,14 +168,14 @@ public sealed class DiscoveryEvaluatorTests
=> new(id, text, DateMs: 1_700_000_000_000 + id, topic, TopicTitle: null);
// Создаёт оценщик на фейках: флаги веток выставляются сценарием.
- private static (DiscoveryEvaluator Evaluator, FakeSettingsStore Settings, FakeMlClient Ml, IAiTools Ai) Create(bool aiEnabled, bool mlEnabled)
+ private static (DiscoveryEvaluator Evaluator, TestSettingsStore Settings, TestMlClient Ml, IAiTools Ai) Create(bool aiEnabled, bool mlEnabled)
{
- var settings = new FakeSettingsStore();
+ var settings = new TestSettingsStore();
settings.Preload(SettingsKeys.AiEnabled, aiEnabled ? "true" : "false");
settings.Preload(SettingsKeys.MlEnabled, mlEnabled ? "true" : "false");
- var ml = new FakeMlClient();
+ var ml = new TestMlClient();
var ai = TestAiTools.New();
- return (new DiscoveryEvaluator(settings, ml, ai), settings, ml, ai);
+ return (new DiscoveryEvaluator(settings.Store, ml.Client, ai), settings, ml, ai);
}
// Задача как строка DiscTasks (адаптер-форма; нужны только keywords/description/threshold).
diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs
index 698a23a..fea2453 100644
--- a/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Contracts/DiscoveryWorkerServiceTests.cs
@@ -419,7 +419,7 @@ public sealed class DiscoveryWorkerServiceTests
// Контекст теста: воркер на фейках + ссылки для проверок состояния.
private sealed record Fixture(
TestDiscoveryStore Store,
- FakeSettingsStore Settings,
+ TestSettingsStore Settings,
TestDiscoveryGateway Gateway,
IDiscoveryPacer Pacer,
DiscoveryWorkerService Worker,
@@ -429,21 +429,21 @@ public sealed class DiscoveryWorkerServiceTests
private static Fixture Create()
{
var store = new TestDiscoveryStore();
- var settings = new FakeSettingsStore();
+ var settings = new TestSettingsStore();
settings.Preload(SettingsKeys.MlEnabled, "false");
settings.Preload(SettingsKeys.AiEnabled, "false");
- var ml = new FakeMlClient();
+ var ml = new TestMlClient();
var ai = TestAiTools.New();
var log = new DiscoveryLogService(store.Store);
var blacklist = new DiscoveryBlacklistService(store.Store);
var candidates = new DiscoveryCandidatesService(store.Store, log, blacklist);
- var planGuard = new DiscoveryPlanGuard(store.Store, settings);
- var tasks = new DiscoveryTasksService(store.Store, planGuard, settings);
- var banGuard = new DiscoveryBanGuard(store.Store, settings);
+ var planGuard = new DiscoveryPlanGuard(store.Store, settings.Store);
+ var tasks = new DiscoveryTasksService(store.Store, planGuard, settings.Store);
+ var banGuard = new DiscoveryBanGuard(store.Store, settings.Store);
var pacer = Substitute.For();
var gateway = new TestDiscoveryGateway();
var worker = new DiscoveryWorkerService(
- store.Store, tasks, candidates, log, new DiscoveryEvaluator(settings, ml, ai), banGuard, pacer, gateway.Gateway);
+ store.Store, tasks, candidates, log, new DiscoveryEvaluator(settings.Store, ml.Client, ai), banGuard, pacer, gateway.Gateway);
return new Fixture(store, settings, gateway, pacer, worker, tasks);
}
diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/FakeFileStorage.cs b/src/core/tests/Deal.Tests.Unit/Contracts/FakeFileStorage.cs
deleted file mode 100644
index 8824bee..0000000
--- a/src/core/tests/Deal.Tests.Unit/Contracts/FakeFileStorage.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using Deal.Contracts.Integrations.Abstractions;
-using Deal.Contracts.Integrations.Models;
-
-namespace Deal.Tests.Unit.Contracts;
-
-///
-/// In-memory реализация для unit-тестов файлов карточки
-///
-public sealed class FakeFileStorage : IFileStorage
-{
- private readonly Dictionary _objects = new(StringComparer.Ordinal);
- private readonly Dictionary _contentTypes = new(StringComparer.Ordinal);
- private readonly List _deletedKeys = [];
-
- ///
- /// objectKey всех объектов, сохранённых на данный момент
- ///
- public IReadOnlyList StoredObjectKeys => _objects.Keys.ToList();
-
- ///
- /// objectKey всех удалений в порядке вызовов DeleteAsync
- ///
- public IReadOnlyList DeletedKeys => _deletedKeys.ToList();
-
- ///
- /// Содержимое сохранённого объекта по ключу либо null — объекта нет
- ///
- /// Ключ объекта (opaque).
- public byte[]? ContentOf(string objectKey)
- {
- return _objects.TryGetValue(objectKey, out byte[]? content) ? content : null;
- }
-
- ///
- public Task PutAsync(
- string objectKey,
- Stream content,
- string contentType,
- CancellationToken ct)
- {
- ArgumentNullException.ThrowIfNull(content);
-
- using MemoryStream buffer = new();
- if (content.CanSeek && content.Position != 0)
- {
- content.Position = 0; // контракт порта: Put читает всё содержимое с позиции 0 (Ruling T6)
- }
-
- content.CopyTo(buffer);
- _objects[objectKey] = buffer.ToArray();
- _contentTypes[objectKey] = contentType;
- return Task.FromResult(objectKey);
- }
-
- ///
- public Task GetAsync(string objectKey, CancellationToken ct)
- {
- if (!_objects.TryGetValue(objectKey, out byte[]? content))
- {
- return Task.FromResult(null);
- }
-
- return Task.FromResult(new MemoryStream(content));
- }
-
- ///
- public Task StatAsync(string objectKey, CancellationToken ct)
- {
- if (!_objects.TryGetValue(objectKey, out byte[]? content))
- {
- return Task.FromResult(null);
- }
-
- string contentType = _contentTypes.TryGetValue(objectKey, out string? stored) ? stored : string.Empty;
- return Task.FromResult(new FileMeta(objectKey, content.Length, contentType));
- }
-
- ///
- public Task DeleteAsync(string objectKey, CancellationToken ct)
- {
- _deletedKeys.Add(objectKey);
- _objects.Remove(objectKey);
- return Task.CompletedTask;
- }
-}
diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/GrpcAiToolsTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcAiToolsTests.cs
index 5a637a8..437011a 100644
--- a/src/core/tests/Deal.Tests.Unit/Contracts/GrpcAiToolsTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcAiToolsTests.cs
@@ -39,9 +39,9 @@ public sealed class GrpcAiToolsTests
reply.Keywords.Add("python разработка");
reply.Keywords.Add("telegram каналы");
service.GenerateKeywordsReply = reply;
- FakeSettingsStore settings = new();
+ TestSettingsStore settings = new();
ISecretCipher cipher = TestCiphers.New();
- FakeTenantLimitStore limits = new();
+ TestTenantLimitStore limits = new();
IAiTools tools = CreateTools(port, settings, cipher, limits);
AiGenerateKeywordsResultDto result = await tools.GenerateKeywordsAsync("Бэкенд-разработка на Python", CancellationToken.None);
@@ -66,7 +66,7 @@ public sealed class GrpcAiToolsTests
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
{
service.KeywordsUnavailable = true;
- IAiTools tools = CreateTools(port, new FakeSettingsStore(), TestCiphers.New());
+ IAiTools tools = CreateTools(port, new TestSettingsStore(), TestCiphers.New());
AiGenerateKeywordsResultDto result = await tools.GenerateKeywordsAsync("описание", CancellationToken.None);
@@ -88,8 +88,8 @@ public sealed class GrpcAiToolsTests
Reason = "другая сфера",
Usage = new Usage { Prompt = 200, Completion = 10, Total = 210 },
};
- FakeTenantLimitStore limits = new();
- IAiTools tools = CreateTools(port, new FakeSettingsStore(), TestCiphers.New(), limits);
+ TestTenantLimitStore limits = new();
+ IAiTools tools = CreateTools(port, new TestSettingsStore(), TestCiphers.New(), limits);
AiEvaluateFitResultDto result = await tools.EvaluateFitAsync(
"Ищу дизайнера для лендинга", "Разработка сайтов на Python", new[] { "python", "бэкенд" }, CancellationToken.None);
@@ -113,7 +113,7 @@ public sealed class GrpcAiToolsTests
await AiGrpcTestHost.RunAsync(AiGrpcTestHost.DefaultToken, new RecordingAiService(), async (port, service) =>
{
service.FitUnavailable = true;
- IAiTools tools = CreateTools(port, new FakeSettingsStore(), TestCiphers.New());
+ IAiTools tools = CreateTools(port, new TestSettingsStore(), TestCiphers.New());
await Assert.ThrowsAsync(
() => tools.EvaluateFitAsync("текст", "описание", new[] { "ключ" }, CancellationToken.None));
@@ -128,18 +128,19 @@ public sealed class GrpcAiToolsTests
// Возвращает: Экземпляр GrpcAiTools.
private static GrpcAiTools CreateTools(
int port,
- FakeSettingsStore settings,
+ TestSettingsStore settings,
ISecretCipher cipher,
- FakeTenantLimitStore? limits = null)
+ TestTenantLimitStore? limits = null)
{
+ limits ??= new TestTenantLimitStore();
ITenantContext tenantContext = new TenantContext();
tenantContext.SetTenant(new TenantId(TenantIdValue));
var connection = new AiGrpcConnection(new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" });
return new GrpcAiTools(
tenantContext,
connection,
- new AiProviderConfigBuilder(settings, cipher),
- new TokenUsageRecorder(settings, limits ?? new FakeTenantLimitStore(), tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
+ new AiProviderConfigBuilder(settings.Store, cipher),
+ new TokenUsageRecorder(settings.Store, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
NullLogger.Instance);
}
}
diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/GrpcMlClientTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcMlClientTests.cs
index 5a43988..484de59 100644
--- a/src/core/tests/Deal.Tests.Unit/Contracts/GrpcMlClientTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Contracts/GrpcMlClientTests.cs
@@ -110,10 +110,10 @@ public sealed class GrpcMlClientTests
Eval = new ModelEval { Count = 5, Correct = 4, Accuracy = 0.8 },
};
- var settings = new FakeSettingsStore();
+ var settings = new TestSettingsStore();
settings.Preload(SettingsKeys.MlDecisions, "7");
settings.Preload(SettingsKeys.AiDecisions, "3");
- var learning = new FakeMlLearningStore { LearningCount = 9 };
+ var learning = new TestMlLearningStore { LearningCount = 9 };
IMlClient client = CreateClient(port, settings, learning);
MlStatusResponseDto status = await client.StatusAsync(CancellationToken.None);
@@ -195,7 +195,7 @@ public sealed class GrpcMlClientTests
{
await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) =>
{
- var learning = new FakeMlLearningStore();
+ var learning = new TestMlLearningStore();
learning.SeedOutbox("mle_1", "текст 1", "b_a", 1.0);
learning.SeedOutbox("mle_2", "текст 2", "spam", 1.0);
IMlClient client = CreateClient(port, learning: learning);
@@ -206,7 +206,7 @@ public sealed class GrpcMlClientTests
Assert.True(reset.Ok);
Assert.Null(reset.Error);
- Assert.Equal(0, await learning.CountOutboxAsync(CancellationToken.None)); // очередь очищена
+ Assert.Equal(0, await learning.Store.CountOutboxAsync(CancellationToken.None)); // очередь очищена
Assert.Equal(1, service.ResetCalls);
_ = await client.StatusAsync(CancellationToken.None);
@@ -220,7 +220,7 @@ public sealed class GrpcMlClientTests
await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) =>
{
service.ResetReply = new ResetReply { Ok = false, Error = "не удалось пересоздать файл модели" };
- var learning = new FakeMlLearningStore();
+ var learning = new TestMlLearningStore();
learning.SeedOutbox("mle_1", "текст", "b_a", 1.0);
IMlClient client = CreateClient(port, learning: learning);
@@ -228,7 +228,7 @@ public sealed class GrpcMlClientTests
Assert.False(reset.Ok);
Assert.Equal("не удалось пересоздать файл модели", reset.Error);
- Assert.Equal(1, await learning.CountOutboxAsync(CancellationToken.None));
+ Assert.Equal(1, await learning.Store.CountOutboxAsync(CancellationToken.None));
});
}
@@ -238,7 +238,7 @@ public sealed class GrpcMlClientTests
await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) =>
{
service.ResetUnavailable = true;
- var learning = new FakeMlLearningStore();
+ var learning = new TestMlLearningStore();
learning.SeedOutbox("mle_1", "текст", "spam", 1.0);
IMlClient client = CreateClient(port, learning: learning);
@@ -246,7 +246,7 @@ public sealed class GrpcMlClientTests
Assert.False(reset.Ok);
Assert.Equal("ML-сервис недоступен", reset.Error);
- Assert.Equal(1, await learning.CountOutboxAsync(CancellationToken.None));
+ Assert.Equal(1, await learning.Store.CountOutboxAsync(CancellationToken.None));
});
}
@@ -256,7 +256,7 @@ public sealed class GrpcMlClientTests
{
await MlGrpcTestHost.RunAsync(MlGrpcTestHost.DefaultToken, new RecordingMlService(), async (port, service) =>
{
- var learning = new FakeMlLearningStore();
+ var learning = new TestMlLearningStore();
IMlClient client = CreateClient(port, learning: learning);
await client.PushAsync(" нужен python ", "b_junior", 1.0, CancellationToken.None);
@@ -309,22 +309,24 @@ public sealed class GrpcMlClientTests
// Возвращает: Экземпляр GrpcMlClient в tenant-контексте теста.
private static GrpcMlClient CreateClient(
int port,
- FakeSettingsStore? settings = null,
- FakeMlLearningStore? learning = null,
+ TestSettingsStore? settings = null,
+ TestMlLearningStore? learning = null,
MlStatusCache? cache = null)
{
+ settings ??= new TestSettingsStore();
+ learning ??= new TestMlLearningStore();
ITenantContext tenantContext = new TenantContext();
tenantContext.SetTenant(new TenantId(TenantIdValue));
var options = new MlServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" };
return new GrpcMlClient(
tenantContext,
- settings ?? new FakeSettingsStore(),
- learning ?? new FakeMlLearningStore(),
+ settings.Store,
+ learning.Store,
new MlGrpcConnection(options),
cache ?? new MlStatusCache(),
new TokenUsageRecorder(
- settings ?? new FakeSettingsStore(),
- new FakeTenantLimitStore(),
+ settings.Store,
+ new TestTenantLimitStore().Store,
tenantContext,
new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
NullLogger.Instance);
diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs
index ad859a0..e30468d 100644
--- a/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Contracts/IntegrationsDiTests.cs
@@ -13,7 +13,6 @@ using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Services;
using Deal.SharedKernel.Tenants.Abstractions;
using Deal.SharedKernel.Tenants.Models;
-using Deal.Tests.Unit.Modules.Cards;
using Deal.Tests.Unit.Modules.Kanban;
using Deal.Tests.Unit.Modules.Settings;
using Deal.Tests.Unit.Modules.Tenants;
@@ -144,11 +143,11 @@ public sealed class IntegrationsDiTests
ITenantContext tenantContext = new TenantContext();
tenantContext.SetTenant(new TenantId(Guid.NewGuid().ToString("N")));
services.AddSingleton(tenantContext);
- services.AddScoped(_ => new FakeSettingsStore());
+ services.AddScoped(_ => new TestSettingsStore().Store);
services.AddScoped(_ => TestCiphers.New());
- services.AddScoped(_ => new FakeKanjStore());
- services.AddScoped(_ => new FakeMlLearningStore());
- services.AddScoped(_ => new FakeTenantLimitStore());
+ services.AddScoped(_ => new TestKanjStore().Store);
+ services.AddScoped(_ => new TestMlLearningStore().Store);
+ services.AddScoped(_ => new TestTenantLimitStore().Store);
services.AddScoped(_ => new TokenUsageEventService(new TestTokenUsageEventStore().Store));
services.AddScoped();
services.AddScoped();
diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/LocalAiClassifierTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/LocalAiClassifierTests.cs
index 8568af3..7d1e58e 100644
--- a/src/core/tests/Deal.Tests.Unit/Contracts/LocalAiClassifierTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Contracts/LocalAiClassifierTests.cs
@@ -2,6 +2,7 @@ using Deal.Contracts.Integrations.Models;
using Deal.Infrastructure.Integrations.Services;
using Deal.Modules.Pipeline.Application.Parse;
using Deal.Modules.Settings.Application.Models;
+using Deal.Tests.Unit.Support;
using Deal.Tests.Unit.Modules.Settings;
namespace Deal.Tests.Unit.Contracts;
@@ -13,10 +14,10 @@ public sealed class LocalAiClassifierTests
{
// Создаёт контекст теста: пустое KV-хранилище (дефолты маркеров) + адаптер.
// Возвращает: Кортеж (KV-настройки, классификатор).
- private static (FakeSettingsStore Settings, LocalAiClassifier Classifier) CreateContext()
+ private static (TestSettingsStore Settings, LocalAiClassifier Classifier) CreateContext()
{
- var settings = new FakeSettingsStore();
- var classifier = new LocalAiClassifier(new LocalFieldsParser(settings));
+ var settings = new TestSettingsStore();
+ var classifier = new LocalAiClassifier(new LocalFieldsParser(settings.Store));
return (settings, classifier);
}
@@ -140,7 +141,7 @@ public sealed class LocalAiClassifierTests
[Fact]
public async Task Classify_CustomHireMarkersOverride_AreUsed()
{
- (FakeSettingsStore settings, LocalAiClassifier classifier) = CreateContext();
+ (TestSettingsStore settings, LocalAiClassifier classifier) = CreateContext();
// Дефолтные маркеры заменены: «вакансия» больше не маркер найма, свой — маркер.
settings.Preload(SettingsKeys.HireMarkers, "[\"нанимаем джунов\"]");
diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/PipelineCardWriterTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineCardWriterTests.cs
index e3b5a4e..cc4d3a5 100644
--- a/src/core/tests/Deal.Tests.Unit/Contracts/PipelineCardWriterTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineCardWriterTests.cs
@@ -2,7 +2,6 @@ using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application.Models;
using Deal.Modules.Pipeline.Application.Models;
using Deal.Modules.Pipeline.Application.Services;
-using Deal.Tests.Unit.Modules.Cards;
using Deal.Tests.Unit.Modules.Kanban;
using Deal.Tests.Unit.Modules.Settings;
using Deal.Tests.Unit.Support;
@@ -18,9 +17,9 @@ public sealed class PipelineCardWriterTests
[Fact]
public async Task CreateCard_CreatesInboxCardAndLinksDedupClaim()
{
- (PipelineCardWriter writer, FakeKanjStore store, FakePipelineStore pipelineStore) = Create();
+ (PipelineCardWriter writer, TestKanjStore store, TestPipelineStore pipelineStore) = Create();
const string hash = "abcdef0123456789abcdef0123456789abcdef01";
- await pipelineStore.ClaimAsync(hash, CancellationToken.None); // заявка воркера (LeadId=null, Ruling 8)
+ await pipelineStore.Store.ClaimAsync(hash, CancellationToken.None); // заявка воркера (LeadId=null, Ruling 8)
AiParsedCardDto parsed = Parsed(
title: "Python-разработчик на бота",
budget: new AiBudgetDto(2000, 2000, "USD"),
@@ -38,16 +37,16 @@ public sealed class PipelineCardWriterTests
Assert.Equal(new CardBudgetDto(2000, 2000, "USD"), card.Budget);
Assert.Equal(card.Id, pipelineStore.DedupLeadId(hash));
- Assert.True(await pipelineStore.ExistsAsync(hash, CancellationToken.None));
+ Assert.True(await pipelineStore.Store.ExistsAsync(hash, CancellationToken.None));
}
[Fact]
public async Task CreateCard_AssignedBoardAccepted_WritesCardIntoBoardColumn()
{
- (PipelineCardWriter writer, FakeKanjStore store, FakePipelineStore pipelineStore) = Create();
+ (PipelineCardWriter writer, TestKanjStore store, TestPipelineStore pipelineStore) = Create();
store.SeedBoard(new ContainerDto { Id = "b_py" });
const string hash = "abcdef0123456789abcdef0123456789abcdef01";
- await pipelineStore.ClaimAsync(hash, CancellationToken.None);
+ await pipelineStore.Store.ClaimAsync(hash, CancellationToken.None);
CardDto card = await writer.CreateCardAsync(
Parsed(title: "Python-разработчик", board: "b_py"),
@@ -66,10 +65,10 @@ public sealed class PipelineCardWriterTests
[Fact]
public async Task CreateCard_AddCardFails_ThrowsAndDoesNotLinkDedup()
{
- (PipelineCardWriter writer, FakeKanjStore store, FakePipelineStore pipelineStore) = Create();
+ (PipelineCardWriter writer, TestKanjStore store, TestPipelineStore pipelineStore) = Create();
store.FailAddCard = true; // сбой адаптера записи (например, недоступна БД тенанта)
const string hash = "abcdef0123456789abcdef0123456789abcdef01";
- await pipelineStore.ClaimAsync(hash, CancellationToken.None);
+ await pipelineStore.Store.ClaimAsync(hash, CancellationToken.None);
await Assert.ThrowsAsync(
() => writer.CreateCardAsync(Parsed(title: "Python-разработчик"), Message("Текст"), hash, CancellationToken.None));
@@ -77,19 +76,19 @@ public sealed class PipelineCardWriterTests
// Карточка не создана → связывать нечего: заявка дедупа осталась несвязанной (LeadId=null).
Assert.Empty(store.CardDtos);
Assert.Null(pipelineStore.DedupLeadId(hash));
- Assert.True(await pipelineStore.ExistsAsync(hash, CancellationToken.None)); // заявка на месте
+ Assert.True(await pipelineStore.Store.ExistsAsync(hash, CancellationToken.None)); // заявка на месте
}
// ─── Хелперы ──────────────────────────────────────────────────────────────────────────
- // Создаёт контекст теста: фейки канбана, пайплайна и композитор поверх них.
+ // Создаёт контекст теста: подставки канбана, пайплайна и композитор поверх них.
// Возвращает: Кортеж (обёртка, канбан-хранилище, хранилище пайплайна).
- private static (PipelineCardWriter Writer, FakeKanjStore Store, FakePipelineStore PipelineStore) Create()
+ private static (PipelineCardWriter Writer, TestKanjStore Store, TestPipelineStore PipelineStore) Create()
{
- var store = new FakeKanjStore();
- var pipelineStore = new FakePipelineStore();
- var composer = new CardComposer(store, new FakeSettingsStore());
- return (new PipelineCardWriter(store, pipelineStore, composer), store, pipelineStore);
+ var store = new TestKanjStore();
+ var pipelineStore = new TestPipelineStore();
+ var composer = new CardComposer(store.Store, new TestSettingsStore().Store);
+ return (new PipelineCardWriter(store.Store, pipelineStore.Store, composer), store, pipelineStore);
}
// Разбор карточки со значениями по умолчанию (сценарий теста перекрывает нужные поля).
diff --git a/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs
index 1f34802..4cea455 100644
--- a/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Contracts/PipelineWorkerGrpcAiTests.cs
@@ -15,7 +15,6 @@ using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Services;
using Deal.SharedKernel.Tenants.Models;
using Deal.Tests.Unit.Grpc;
-using Deal.Tests.Unit.Modules.Cards;
using Deal.Tests.Unit.Modules.Kanban;
using Deal.Tests.Unit.Modules.Settings;
using Deal.Tests.Unit.Modules.Tenants;
@@ -34,7 +33,7 @@ public sealed class PipelineWorkerGrpcAiTests
// Id тенанта сценария строкой (формат N) — metadata tenant-id вызовов ai-service.
private const string TenantIdValue = "abcdefabcdefabcdefabcdefabcdefab";
- // Guid того же тенанта — ключ строки лимита в FakeTenantLimitStore (списание usage).
+ // Guid того же тенанта — ключ строки лимита в TestTenantLimitStore (списание usage).
private static readonly Guid TenantGuid = Guid.Parse(TenantIdValue);
[Fact]
@@ -127,7 +126,7 @@ public sealed class PipelineWorkerGrpcAiTests
Usage = new Usage { Prompt = 2000, Completion = 400, Total = 2400 },
};
- var limits = new FakeTenantLimitStore();
+ var limits = new TestTenantLimitStore();
limits.Preload(TenantGuid, budgetTokens: 1000, TenantLimitPeriods.Month, DateTimeOffset.UtcNow, usedTokens: 1000);
Context ctx = CreateContext(port, limits, budgeted: true);
ctx.PipelineStore.SeedQueue(QueueRowFiltered("p_1", "Нужен middle Python-разработчик в команду, оплата 2000$ в месяц"));
@@ -154,49 +153,49 @@ public sealed class PipelineWorkerGrpcAiTests
private sealed record Context(
PipelineWorkerService Worker,
- FakePipelineStore PipelineStore,
- FakeKanjStore KanjStore,
- FakeSettingsStore Settings,
- FakeTenantLimitStore Limits);
+ TestPipelineStore PipelineStore,
+ TestKanjStore KanjStore,
+ TestSettingsStore Settings,
+ TestTenantLimitStore Limits);
private static Context CreateContext(
int port,
- FakeTenantLimitStore? limits = null,
+ TestTenantLimitStore? limits = null,
bool budgeted = false)
{
- var settings = new FakeSettingsStore();
- var pipelineStore = new FakePipelineStore();
- var kanjStore = new FakeKanjStore();
- var mlClient = new FakeMlClient { Predict = NotReadyPrediction() };
- var rules = new IncomingRules(settings);
- var fieldsParser = new LocalFieldsParser(settings);
+ var settings = new TestSettingsStore();
+ var pipelineStore = new TestPipelineStore();
+ var kanjStore = new TestKanjStore();
+ var mlClient = new TestMlClient { Predict = NotReadyPrediction() };
+ var rules = new IncomingRules(settings.Store);
+ var fieldsParser = new LocalFieldsParser(settings.Store);
ITenantContext tenantContext = new TenantContext();
tenantContext.SetTenant(new TenantId(TenantIdValue));
var connection = new AiGrpcConnection(
new AiServiceOptions { UseLocal = false, Endpoint = $"http://127.0.0.1:{port}" });
- limits ??= new FakeTenantLimitStore();
+ limits ??= new TestTenantLimitStore();
var grpcClassifier = new GrpcAiClassifier(
tenantContext,
connection,
- new AiProviderConfigBuilder(settings, TestCiphers.New()),
- new AiClassifyContextBuilder(settings, kanjStore),
- new TokenUsageRecorder(settings, limits, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
+ new AiProviderConfigBuilder(settings.Store, TestCiphers.New()),
+ new AiClassifyContextBuilder(settings.Store, kanjStore.Store),
+ new TokenUsageRecorder(settings.Store, limits.Store, tenantContext, new TokenUsageEventService(new TestTokenUsageEventStore().Store)),
NullLogger.Instance);
IAiClassifier aiClassifier = budgeted
? new BudgetedAiClassifier(
grpcClassifier,
new LocalAiClassifier(fieldsParser),
- limits,
+ limits.Store,
tenantContext,
NullLogger.Instance)
: grpcClassifier;
- var processing = new PipelineProcessingService(pipelineStore, mlClient, new PipelineIngestService(pipelineStore));
- var composer = new CardComposer(kanjStore, settings);
- var writer = new PipelineCardWriter(kanjStore, pipelineStore, composer);
+ var processing = new PipelineProcessingService(pipelineStore.Store, mlClient.Client, new PipelineIngestService(pipelineStore.Store));
+ var composer = new CardComposer(kanjStore.Store, settings.Store);
+ var writer = new PipelineCardWriter(kanjStore.Store, pipelineStore.Store, composer);
var worker = new PipelineWorkerService(
- pipelineStore, settings, rules, kanjStore, mlClient, aiClassifier, processing, writer, fieldsParser);
+ pipelineStore.Store, settings.Store, rules, kanjStore.Store, mlClient.Client, aiClassifier, processing, writer, fieldsParser);
return new Context(worker, pipelineStore, kanjStore, settings, limits);
}
diff --git a/src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs b/src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs
index a44505c..8bcd7bd 100644
--- a/src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Infrastructure/CardMoverTests.cs
@@ -6,7 +6,6 @@ using Deal.Modules.Kanban.Application.Models;
using Deal.Tests.Unit.Support;
using Deal.Modules.Kanban.Application.Services;
using Deal.Tests.Unit.Contracts;
-using Deal.Tests.Unit.Modules.Cards;
using Deal.Tests.Unit.Modules.Settings;
namespace Deal.Tests.Unit.Infrastructure;
@@ -21,7 +20,7 @@ public sealed class CardMoverTests
[Fact]
public async Task Move_ToStage_RoutesToStageMoveAndResetsReminder()
{
- (CardMover mover, FakeKanjStore store) = Create();
+ (CardMover mover, TestKanjStore store) = Create();
store.SeedCard(new CardDto
{
Id = "c_1",
@@ -44,7 +43,7 @@ public sealed class CardMoverTests
[Fact]
public async Task Move_ToBoard_RoutesToDashboard()
{
- (CardMover mover, FakeKanjStore store) = Create();
+ (CardMover mover, TestKanjStore store) = Create();
store.SeedBoard(new ContainerDto { Id = "b_py", Name = "Python" });
store.SeedCard(new CardDto { Id = "c_1", Col = KanbanColumns.Inbox, Content = new SourceContent { Text = "Нужен Python" } });
@@ -59,7 +58,7 @@ public sealed class CardMoverTests
[Fact]
public async Task Move_UnknownTarget_ReturnsDashboardInvalidTargetError()
{
- (CardMover mover, FakeKanjStore store) = Create();
+ (CardMover mover, TestKanjStore store) = Create();
store.SeedCard(new CardDto { Id = "c_1", Col = KanbanColumns.Inbox, Content = new SourceContent { Text = "Текст" } });
CardMoveResultDto result = await mover.MoveAsync("c_1", "b_ghost", UserMove, CancellationToken.None);
@@ -79,10 +78,10 @@ public sealed class CardMoverTests
Assert.False(result.Exists);
}
- private static (CardMover Mover, FakeKanjStore Store) Create()
+ private static (CardMover Mover, TestKanjStore Store) Create()
{
- var store = new FakeKanjStore();
- var cardsService = new CardsService(store, new FakeSettingsStore(), new FakeMlClient(), new TestFileStorage().Storage);
+ var store = new TestKanjStore();
+ var cardsService = new CardsService(store.Store, new TestSettingsStore().Store, new TestMlClient().Client, new TestFileStorage().Storage);
return (new CardMover(cardsService), store);
}
}
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryBanGuardTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryBanGuardTests.cs
index 75547bc..abf53b9 100644
--- a/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryBanGuardTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryBanGuardTests.cs
@@ -40,8 +40,8 @@ public sealed class DiscoveryBanGuardTests
// Вчерашние вступления в суточную квоту не входят: 49 сегодня → ещё можно.
TestDiscoveryStore yesterdayStore = new(() => Now);
- var settings = new FakeSettingsStore();
- var secondGuard = new DiscoveryBanGuard(yesterdayStore.Store, settings, () => Now);
+ var settings = new TestSettingsStore();
+ var secondGuard = new DiscoveryBanGuard(yesterdayStore.Store, settings.Store, () => Now);
for (int i = 0; i < 49; i++)
{
yesterdayStore.SeedLog(DiscoveryLogEvents.JoinAuto, TodayStart.AddMinutes(i));
@@ -67,7 +67,7 @@ public sealed class DiscoveryBanGuardTests
[Fact]
public async Task NoteFlood_BlocksAutoJoinsUntilEndOfDay()
{
- (DiscoveryBanGuard guard, _, FakeSettingsStore settings) = Create();
+ (DiscoveryBanGuard guard, _, TestSettingsStore settings) = Create();
await guard.NoteFloodAsync(CancellationToken.None);
@@ -79,7 +79,7 @@ public sealed class DiscoveryBanGuardTests
[Fact]
public async Task FloodYesterday_DoesNotBlockToday()
{
- (DiscoveryBanGuard guard, _, FakeSettingsStore settings) = Create();
+ (DiscoveryBanGuard guard, _, TestSettingsStore settings) = Create();
settings.Preload(SettingsKeys.DiscFloodDay, TodayStart.AddDays(-1).ToUnixTimeMilliseconds().ToString());
Assert.False(await guard.FloodTodayAsync(CancellationToken.None));
@@ -89,7 +89,7 @@ public sealed class DiscoveryBanGuardTests
[Fact]
public async Task GlobalPaused_BlocksAutoJoins()
{
- (DiscoveryBanGuard guard, _, FakeSettingsStore settings) = Create();
+ (DiscoveryBanGuard guard, _, TestSettingsStore settings) = Create();
settings.Preload(SettingsKeys.DiscPaused, "true");
Assert.True(await guard.GlobalPausedAsync(CancellationToken.None));
@@ -99,7 +99,7 @@ public sealed class DiscoveryBanGuardTests
[Fact]
public async Task CustomJoinLimit_AppliedWhenSettingStored()
{
- (DiscoveryBanGuard guard, TestDiscoveryStore store, FakeSettingsStore settings) = Create();
+ (DiscoveryBanGuard guard, TestDiscoveryStore store, TestSettingsStore settings) = Create();
settings.Preload(SettingsKeys.DiscJoinLimit, "2");
Assert.True(await guard.CanAutoJoinAsync(CancellationToken.None));
@@ -109,11 +109,11 @@ public sealed class DiscoveryBanGuardTests
}
// Создаёт связку гард + фейки на фиксированных «часах» (store и гард смотрят одно время).
- private static (DiscoveryBanGuard Guard, TestDiscoveryStore Store, FakeSettingsStore Settings) Create()
+ private static (DiscoveryBanGuard Guard, TestDiscoveryStore Store, TestSettingsStore Settings) Create()
{
var store = new TestDiscoveryStore(() => Now);
- var settings = new FakeSettingsStore();
- var guard = new DiscoveryBanGuard(store.Store, settings, () => Now);
+ var settings = new TestSettingsStore();
+ var guard = new DiscoveryBanGuard(store.Store, settings.Store, () => Now);
return (guard, store, settings);
}
}
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryTasksServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryTasksServiceTests.cs
index f4232ef..d00bd44 100644
--- a/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryTasksServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/DiscoveryTasksServiceTests.cs
@@ -117,7 +117,7 @@ public sealed class DiscoveryTasksServiceTests
[Fact]
public async Task Create_CustomThresholdAndSample_Respected()
{
- (DiscoveryTasksService service, TestDiscoveryStore store, FakeSettingsStore settings) = Create();
+ (DiscoveryTasksService service, TestDiscoveryStore store, TestSettingsStore settings) = Create();
settings.Preload("discEvalThreshold", "60");
settings.Preload("discEvalSample", "20");
@@ -330,12 +330,12 @@ public sealed class DiscoveryTasksServiceTests
}
// Создаёт связку сервис + фейки (пустые настройки → дефолты SettingsDefaults).
- private static (DiscoveryTasksService Service, TestDiscoveryStore Store, FakeSettingsStore Settings) Create()
+ private static (DiscoveryTasksService Service, TestDiscoveryStore Store, TestSettingsStore Settings) Create()
{
var store = new TestDiscoveryStore();
- var settings = new FakeSettingsStore();
- var planGuard = new DiscoveryPlanGuard(store.Store, settings);
- return (new DiscoveryTasksService(store.Store, planGuard, settings), store, settings);
+ var settings = new TestSettingsStore();
+ var planGuard = new DiscoveryPlanGuard(store.Store, settings.Store);
+ return (new DiscoveryTasksService(store.Store, planGuard, settings.Store), store, settings);
}
// Draft задачи с дефолтными полями (name/planJoins переопределяются).
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Discovery/FakeDiscoveryStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Discovery/FakeDiscoveryStore.cs
deleted file mode 100644
index f5662ef..0000000
--- a/src/core/tests/Deal.Tests.Unit/Modules/Discovery/FakeDiscoveryStore.cs
+++ /dev/null
@@ -1,614 +0,0 @@
-using Deal.Modules.Discovery.Application.Abstractions;
-using Deal.Modules.Discovery.Application.Models;
-
-namespace Deal.Tests.Unit.Modules.Discovery;
-
-///
-/// In-memory реализация для unit-тестов Discovery.
-///
-public sealed class FakeDiscoveryStore : IDiscoveryStore
-{
- private readonly List _tasks = [];
- private readonly List _candidates = [];
- private readonly List _blacklist = [];
- private readonly List _log = [];
- private readonly HashSet _monitoredIds = new(StringComparer.Ordinal);
- private readonly Func _clock;
-
- ///
- /// Создаёт фейк с «часами» по умолчанию
- ///
- /// Источник текущего времени (по умолчанию ).
- public FakeDiscoveryStore(Func? clock = null)
- {
- _clock = clock ?? (() => DateTimeOffset.UtcNow);
- }
-
- ///
- /// Задачи фейка
- ///
- public IReadOnlyList Tasks => _tasks.ToList();
-
- ///
- /// Кандидаты фейка
- ///
- public IReadOnlyList Candidates => _candidates.ToList();
-
- ///
- /// Чёрный список фейка
- ///
- public IReadOnlyList Blacklist => _blacklist.ToList();
-
- ///
- /// Лог фейка в порядке записи
- ///
- public IReadOnlyList Log => _log.ToList();
-
- ///
- /// Кладёт задачу напрямую
- ///
- /// Задача как если бы была прочитана адаптером.
- public void SeedTask(DiscoveryTaskDto task)
- {
- _tasks.Add(task);
- }
-
- ///
- /// Кладёт кандидата напрямую
- ///
- /// Кандидат как если бы был прочитан адаптером.
- public void SeedCandidate(DiscoveryCandidateDto candidate)
- {
- _candidates.Add(candidate);
- }
-
- ///
- /// Помечает источник «уже мониторится» — есть в каталоге Dialogs.
- ///
- /// Подписанный id источника.
- public void SeedMonitored(string dialogId)
- {
- _monitoredIds.Add(dialogId);
- }
-
- ///
- /// Кладёт событие лога напрямую
- ///
- /// Событие (join_auto/…).
- /// Момент события (UTC).
- /// Id задачи (по умолчанию — произвольный).
- public void SeedLog(
- string @event,
- DateTimeOffset createdAt,
- string taskId = "dt_x")
- {
- _log.Add(new DiscoveryLogDto("dl_x", taskId, @event, string.Empty, createdAt.ToUnixTimeMilliseconds()));
- }
-
- ///
- public Task> ListTasksAsync(CancellationToken ct)
- {
- return Task.FromResult>(_tasks
- .OrderBy(task => task.CreatedAt)
- .ToList());
- }
-
- ///
- public Task GetTaskAsync(string taskId, CancellationToken ct)
- {
- return Task.FromResult(_tasks.FirstOrDefault(task => task.Id == taskId));
- }
-
- ///
- public Task CreateTaskAsync(DiscoveryTaskRow row, CancellationToken ct)
- {
- long nowMs = _clock().ToUnixTimeMilliseconds();
- _tasks.Add(new DiscoveryTaskDto(
- row.Id,
- row.Name,
- row.Description,
- row.Keywords.ToList(),
- row.MinSubscribers,
- row.Lang,
- row.Threshold,
- row.SampleSize,
- row.PlanJoins,
- row.AutoJoin,
- DiscoveryTaskStatuses.Draft,
- SearchIdx: 0,
- SearchDone: false,
- Found: 0,
- Evaluated: 0,
- Joined: 0,
- Rejected: 0,
- nowMs,
- nowMs));
- return Task.CompletedTask;
- }
-
- ///
- public Task PatchTaskAsync(
- string taskId,
- DiscoveryTaskPatch patch,
- CancellationToken ct)
- {
- int index = _tasks.FindIndex(task => task.Id == taskId);
- if (index < 0)
- {
- return Task.FromResult(false);
- }
-
- DiscoveryTaskDto task = _tasks[index];
- if (patch.Name is not null)
- {
- task = task with { Name = patch.Name };
- }
-
- if (patch.Description is not null)
- {
- task = task with { Description = patch.Description };
- }
-
- if (patch.Keywords is not null)
- {
- task = task with { Keywords = patch.Keywords.ToList() };
- }
-
- if (patch.MinSubscribers is int minSubscribers)
- {
- task = task with { MinSubscribers = minSubscribers };
- }
-
- if (patch.Lang is not null)
- {
- task = task with { Lang = patch.Lang };
- }
-
- if (patch.Threshold is int threshold)
- {
- task = task with { Threshold = threshold };
- }
-
- if (patch.SampleSize is int sampleSize)
- {
- task = task with { SampleSize = sampleSize };
- }
-
- if (patch.PlanJoins is int planJoins)
- {
- task = task with { PlanJoins = planJoins };
- }
-
- if (patch.AutoJoin is bool autoJoin)
- {
- task = task with { AutoJoin = autoJoin };
- }
-
- _tasks[index] = task with { UpdatedAt = _clock().ToUnixTimeMilliseconds() };
- return Task.FromResult(true);
- }
-
- ///
- public async Task DeleteTaskAsync(string taskId, CancellationToken ct)
- {
- if (!_tasks.Any(task => task.Id == taskId))
- {
- return false;
- }
-
- _tasks.RemoveAll(task => task.Id == taskId);
- _candidates.RemoveAll(candidate => candidate.TaskId == taskId);
- _log.RemoveAll(entry => entry.TaskId == taskId);
- return true;
- }
-
- ///
- public Task SetTaskRunningAsync(
- string taskId,
- bool resetProgress,
- CancellationToken ct)
- {
- int index = _tasks.FindIndex(task => task.Id == taskId);
- if (index < 0)
- {
- return Task.FromResult(false);
- }
-
- DiscoveryTaskDto task = _tasks[index] with { Status = DiscoveryTaskStatuses.Running };
- if (resetProgress)
- {
- task = task with
- {
- SearchIdx = 0,
- SearchDone = false,
- Found = 0,
- Evaluated = 0,
- Joined = 0,
- Rejected = 0,
- };
- }
-
- _tasks[index] = task with { UpdatedAt = _clock().ToUnixTimeMilliseconds() };
- return Task.FromResult(true);
- }
-
- ///
- public Task SetTaskPausedAsync(string taskId, CancellationToken ct)
- {
- int index = _tasks.FindIndex(task => task.Id == taskId);
- if (index < 0)
- {
- return Task.FromResult(false);
- }
-
- _tasks[index] = _tasks[index] with
- {
- Status = DiscoveryTaskStatuses.Paused,
- UpdatedAt = _clock().ToUnixTimeMilliseconds(),
- };
- return Task.FromResult(true);
- }
-
- ///
- public Task SetTaskDoneAsync(string taskId, CancellationToken ct)
- {
- int index = _tasks.FindIndex(task => task.Id == taskId);
- if (index < 0)
- {
- return Task.FromResult(false);
- }
-
- _tasks[index] = _tasks[index] with
- {
- Status = DiscoveryTaskStatuses.Done,
- UpdatedAt = _clock().ToUnixTimeMilliseconds(),
- };
- return Task.FromResult(true);
- }
-
- ///
- public Task BumpTaskCounterAsync(
- string taskId,
- DiscoveryCounterField field,
- int n,
- CancellationToken ct)
- {
- int index = _tasks.FindIndex(task => task.Id == taskId);
- if (index < 0 || n <= 0)
- {
- return Task.FromResult(index >= 0);
- }
-
- DiscoveryTaskDto task = field switch
- {
- DiscoveryCounterField.Found => _tasks[index] with { Found = _tasks[index].Found + n },
- DiscoveryCounterField.Evaluated => _tasks[index] with { Evaluated = _tasks[index].Evaluated + n },
- DiscoveryCounterField.Joined => _tasks[index] with { Joined = _tasks[index].Joined + n },
- DiscoveryCounterField.Rejected => _tasks[index] with { Rejected = _tasks[index].Rejected + n },
- _ => throw new ArgumentOutOfRangeException(nameof(field), field, "Неизвестный счётчик задачи"),
- };
- _tasks[index] = task with { UpdatedAt = _clock().ToUnixTimeMilliseconds() };
- return Task.FromResult(true);
- }
-
- ///
- public Task AdvanceSearchAsync(
- string taskId,
- int nextIndex,
- bool searchDone,
- CancellationToken ct)
- {
- int index = _tasks.FindIndex(task => task.Id == taskId);
- if (index < 0)
- {
- return Task.FromResult(false);
- }
-
- _tasks[index] = _tasks[index] with
- {
- SearchIdx = nextIndex,
- SearchDone = searchDone,
- UpdatedAt = _clock().ToUnixTimeMilliseconds(),
- };
- return Task.FromResult(true);
- }
-
- ///
- public Task SumActivePlanAsync(string? excludeTaskId, CancellationToken ct)
- {
- IEnumerable active = _tasks
- .Where(task => !DiscoveryTaskStatuses.IsFinished(task.Status));
- if (excludeTaskId is not null)
- {
- active = active.Where(task => task.Id != excludeTaskId);
- }
-
- return Task.FromResult(active.Sum(task => task.PlanJoins));
- }
-
- ///
- public Task> ListCandidatesAsync(
- string taskId,
- string? status,
- CancellationToken ct)
- {
- IEnumerable query = _candidates.Where(candidate => candidate.TaskId == taskId);
- if (status is not null)
- {
- query = query.Where(candidate => candidate.Status == status);
- }
-
- return Task.FromResult>(query
- .OrderBy(candidate => candidate.CreatedAt)
- .ToList());
- }
-
- ///
- public Task GetCandidateAsync(string dialogId, CancellationToken ct)
- {
- return Task.FromResult(_candidates.FirstOrDefault(candidate => candidate.DialogId == dialogId));
- }
-
- ///
- public Task IsDialogMonitoredAsync(string dialogId, CancellationToken ct)
- {
- return Task.FromResult(_monitoredIds.Contains(dialogId));
- }
-
- ///
- public Task IsBlacklistedAsync(string dialogId, CancellationToken ct)
- {
- return Task.FromResult(_blacklist.Any(entry => entry.DialogId == dialogId));
- }
-
- ///
- public Task CreateCandidateAsync(DiscoveryCandidateRow row, CancellationToken ct)
- {
- long nowMs = _clock().ToUnixTimeMilliseconds();
- _candidates.Add(new DiscoveryCandidateDto(
- row.DialogId,
- row.TaskId,
- row.Name,
- row.Username,
- row.Kind,
- row.Hue,
- Participants: null,
- LangRu: null,
- new List(),
- new List(),
- FitRatio: null,
- DiscoveryCandidateStatuses.New,
- AutoJoined: false,
- JoinFailures: 0,
- nowMs,
- nowMs));
- return Task.CompletedTask;
- }
-
- ///
- public Task DeleteCandidateAsync(string dialogId, CancellationToken ct)
- {
- _candidates.RemoveAll(candidate => candidate.DialogId == dialogId);
- return Task.CompletedTask;
- }
-
- ///
- public Task PatchCandidateAsync(
- string dialogId,
- DiscoveryCandidatePatch patch,
- CancellationToken ct)
- {
- int index = _candidates.FindIndex(candidate => candidate.DialogId == dialogId);
- if (index < 0)
- {
- return Task.FromResult(false);
- }
-
- DiscoveryCandidateDto candidate = _candidates[index];
- if (patch.Name is not null)
- {
- candidate = candidate with { Name = patch.Name };
- }
-
- if (patch.Username is not null)
- {
- candidate = candidate with { Username = patch.Username };
- }
-
- if (patch.Kind is not null)
- {
- candidate = candidate with { Kind = patch.Kind };
- }
-
- if (patch.Hue is not null)
- {
- candidate = candidate with { Hue = patch.Hue };
- }
-
- if (patch.Participants is int participants)
- {
- candidate = candidate with { Participants = participants };
- }
-
- if (patch.LangRu is bool langRu)
- {
- candidate = candidate with { LangRu = langRu };
- }
-
- if (patch.Marks is not null)
- {
- candidate = candidate with { Marks = patch.Marks.ToList() };
- }
-
- if (patch.Topics is not null)
- {
- candidate = candidate with { Topics = patch.Topics.ToList() };
- }
-
- if (patch.FitRatio is double fitRatio)
- {
- candidate = candidate with { FitRatio = fitRatio };
- }
-
- if (patch.AutoJoined is bool autoJoined)
- {
- candidate = candidate with { AutoJoined = autoJoined };
- }
-
- _candidates[index] = candidate with { UpdatedAt = _clock().ToUnixTimeMilliseconds() };
- return Task.FromResult(true);
- }
-
- ///
- public Task SetCandidateStatusAsync(
- string dialogId,
- string status,
- CancellationToken ct)
- {
- int index = _candidates.FindIndex(candidate => candidate.DialogId == dialogId);
- if (index < 0)
- {
- return Task.FromResult(false);
- }
-
- _candidates[index] = _candidates[index] with
- {
- Status = status,
- UpdatedAt = _clock().ToUnixTimeMilliseconds(),
- };
- return Task.FromResult(true);
- }
-
- ///
- public Task SetCandidateJoinedAsync(
- string dialogId,
- bool autoJoined,
- CancellationToken ct)
- {
- int index = _candidates.FindIndex(candidate => candidate.DialogId == dialogId);
- if (index < 0)
- {
- return Task.FromResult(false);
- }
-
- _candidates[index] = _candidates[index] with
- {
- Status = DiscoveryCandidateStatuses.Joined,
- AutoJoined = autoJoined,
- UpdatedAt = _clock().ToUnixTimeMilliseconds(),
- };
- return Task.FromResult(true);
- }
-
- ///
- public Task IncrementJoinFailuresAsync(string dialogId, CancellationToken ct)
- {
- int index = _candidates.FindIndex(candidate => candidate.DialogId == dialogId);
- if (index < 0 || _candidates[index].Status != DiscoveryCandidateStatuses.Review)
- {
- return Task.FromResult(null);
- }
-
- DiscoveryCandidateDto candidate = _candidates[index] with
- {
- JoinFailures = _candidates[index].JoinFailures + 1,
- UpdatedAt = _clock().ToUnixTimeMilliseconds(),
- };
- _candidates[index] = candidate;
- return Task.FromResult(candidate.JoinFailures);
- }
-
- ///
- public Task SetCandidateRejectedAsync(string dialogId, CancellationToken ct)
- {
- int index = _candidates.FindIndex(candidate => candidate.DialogId == dialogId);
- if (index < 0)
- {
- return Task.FromResult(false);
- }
-
- _candidates[index] = _candidates[index] with
- {
- Status = DiscoveryCandidateStatuses.Rejected,
- UpdatedAt = _clock().ToUnixTimeMilliseconds(),
- };
- return Task.FromResult(true);
- }
-
- ///
- public Task UpsertBlacklistAsync(
- string dialogId,
- string name,
- string reason,
- CancellationToken ct)
- {
- DiscoveryBlacklistDto? existing = _blacklist.FirstOrDefault(entry => entry.DialogId == dialogId);
- if (existing is null)
- {
- _blacklist.Add(new DiscoveryBlacklistDto(dialogId, name, reason, _clock().ToUnixTimeMilliseconds()));
- }
- else
- {
- _blacklist[_blacklist.IndexOf(existing)] = existing with { Name = name, Reason = reason };
- }
-
- return Task.CompletedTask;
- }
-
- ///
- public Task RemoveBlacklistAsync(string dialogId, CancellationToken ct)
- {
- _blacklist.RemoveAll(entry => entry.DialogId == dialogId);
- return Task.CompletedTask;
- }
-
- ///
- public Task GetBlacklistAsync(string dialogId, CancellationToken ct)
- {
- return Task.FromResult(_blacklist.FirstOrDefault(entry => entry.DialogId == dialogId));
- }
-
- ///
- public Task> ListBlacklistAsync(CancellationToken ct)
- {
- return Task.FromResult>(_blacklist
- .OrderByDescending(entry => entry.CreatedAt)
- .ToList());
- }
-
- ///
- public Task AddLogAsync(
- string logId,
- string taskId,
- string logEvent,
- string text,
- CancellationToken ct)
- {
- _log.Add(new DiscoveryLogDto(logId, taskId, logEvent, text, _clock().ToUnixTimeMilliseconds()));
- return Task.CompletedTask;
- }
-
- ///
- public Task CountLogEventAsync(
- string logEvent,
- DateTimeOffset sinceUtc,
- CancellationToken ct)
- {
- long sinceMs = sinceUtc.ToUnixTimeMilliseconds();
- return Task.FromResult(_log.Count(entry => entry.Event == logEvent && entry.CreatedAt >= sinceMs));
- }
-
- ///
- public Task> ListTaskLogAsync(
- string taskId,
- int limit,
- CancellationToken ct)
- {
- IReadOnlyList entries = _log
- .Where(entry => entry.TaskId == taskId)
- .OrderByDescending(entry => entry.CreatedAt)
- .Take(limit)
- .ToList();
- return Task.FromResult(entries);
- }
-}
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/AiClassifyContextBuilderTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/AiClassifyContextBuilderTests.cs
index 0e0628e..829deca 100644
--- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/AiClassifyContextBuilderTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/AiClassifyContextBuilderTests.cs
@@ -2,8 +2,8 @@ using Deal.Modules.Cards.Application.Sources;
using Deal.Modules.Kanban.Application.Models;
using Deal.Modules.Pipeline.Application.Services;
using Deal.Modules.Settings.Application.Models;
+using Deal.Tests.Unit.Support;
using Deal.Modules.Settings.Application.Services;
-using Deal.Tests.Unit.Modules.Cards;
using Deal.Tests.Unit.Modules.Settings;
namespace Deal.Tests.Unit.Modules.Kanban;
@@ -24,11 +24,11 @@ public sealed class AiClassifyContextBuilderTests
[Fact]
public async Task BuildFilterPrompt_FillsDomainAndKeywords()
{
- FakeSettingsStore settings = Context(out FakeKanjStore kanj);
+ TestSettingsStore settings = Context(out TestKanjStore kanj);
settings.Preload(SettingsKeys.AiFilterPrompt, Json("Страж. Сфера: {domain}. Маркеры: {keywords}."));
settings.Preload(SettingsKeys.DomainDescription, Json(Domain));
settings.Preload(SettingsKeys.DomainKeywords, Json(Keywords));
- var builder = new AiClassifyContextBuilder(settings, kanj);
+ var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store);
string prompt = await builder.BuildFilterPromptAsync(CancellationToken.None);
@@ -38,9 +38,9 @@ public sealed class AiClassifyContextBuilderTests
[Fact]
public async Task BuildFilterPrompt_NoDomain_UsesFallbackTextAndHint()
{
- FakeSettingsStore settings = Context(out FakeKanjStore kanj);
+ TestSettingsStore settings = Context(out TestKanjStore kanj);
settings.Preload(SettingsKeys.AiFilterPrompt, Json("[{domain}] {keywords}"));
- var builder = new AiClassifyContextBuilder(settings, kanj);
+ var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store);
string prompt = await builder.BuildFilterPromptAsync(CancellationToken.None);
@@ -52,12 +52,12 @@ public sealed class AiClassifyContextBuilderTests
[Fact]
public async Task BuildClassifySystemPrompt_AppendsCardPromptWhenSet()
{
- FakeSettingsStore settings = Context(out FakeKanjStore kanj);
+ TestSettingsStore settings = Context(out TestKanjStore kanj);
settings.Preload(SettingsKeys.AiPrompt, Json("Классифицируй {domain}."));
settings.Preload(SettingsKeys.CardPrompt, Json("Верни блок «О заявке» {keywords}."));
settings.Preload(SettingsKeys.DomainDescription, Json(Domain));
settings.Preload(SettingsKeys.DomainKeywords, Json(Keywords));
- var builder = new AiClassifyContextBuilder(settings, kanj);
+ var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store);
string prompt = await builder.BuildClassifySystemPromptAsync(CancellationToken.None);
@@ -69,8 +69,8 @@ public sealed class AiClassifyContextBuilderTests
[Fact]
public async Task BuildClassifyUserContext_EmptyBoards_ShowsNoBoardsPhrase()
{
- FakeSettingsStore settings = Context(out FakeKanjStore kanj);
- var builder = new AiClassifyContextBuilder(settings, kanj);
+ TestSettingsStore settings = Context(out TestKanjStore kanj);
+ var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store);
string context = await builder.BuildClassifyUserContextAsync("Нужен разработчик", CancellationToken.None);
@@ -82,7 +82,7 @@ public sealed class AiClassifyContextBuilderTests
[Fact]
public async Task BuildClassifyUserContext_ListsBoardsWithKeywordsAndRules()
{
- FakeSettingsStore settings = Context(out FakeKanjStore kanj);
+ TestSettingsStore settings = Context(out TestKanjStore kanj);
kanj.SeedBoard(new ContainerDto
{
Id = "b_py",
@@ -113,7 +113,7 @@ public sealed class AiClassifyContextBuilderTests
Order = 1,
});
kanj.SeedBoard(new ContainerDto { Id = "b_sug", Name = "Предложение", Suggested = true, Order = 2 });
- var builder = new AiClassifyContextBuilder(settings, kanj);
+ var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store);
string context = await builder.BuildClassifyUserContextAsync("Ищу Python-разработчика", CancellationToken.None);
@@ -128,10 +128,10 @@ public sealed class AiClassifyContextBuilderTests
[Fact]
public async Task BuildClassifyUserContext_IncludesMarkupExamplesNewestFirst()
{
- FakeSettingsStore settings = Context(out FakeKanjStore kanj);
+ TestSettingsStore settings = Context(out TestKanjStore kanj);
SeedCardWithMove(kanj, "l_1", "b_py", "Пример: middle python (старый)");
SeedCardWithMove(kanj, "l_2", "b_py", "Пример: senior go (свежий)");
- var builder = new AiClassifyContextBuilder(settings, kanj);
+ var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store);
string context = await builder.BuildClassifyUserContextAsync("Ищу разработчика", CancellationToken.None);
@@ -144,8 +144,8 @@ public sealed class AiClassifyContextBuilderTests
[Fact]
public async Task BuildClassifyUserContext_LongMessage_TruncatesTo5000CodePoints()
{
- FakeSettingsStore settings = Context(out FakeKanjStore kanj);
- var builder = new AiClassifyContextBuilder(settings, kanj);
+ TestSettingsStore settings = Context(out TestKanjStore kanj);
+ var builder = new AiClassifyContextBuilder(settings.Store, kanj.Store);
string text = new string('д', 6000);
string context = await builder.BuildClassifyUserContextAsync(text, CancellationToken.None);
@@ -158,10 +158,10 @@ public sealed class AiClassifyContextBuilderTests
// Создаёт контекст: пустое KV + пустой канбан-фейк.
// kanj: Канбан-фейк (доски/журнал примеров).
// Возвращает: KV-хранилище тенанта.
- private static FakeSettingsStore Context(out FakeKanjStore kanj)
+ private static TestSettingsStore Context(out TestKanjStore kanj)
{
- kanj = new FakeKanjStore();
- return new FakeSettingsStore();
+ kanj = new TestKanjStore();
+ return new TestSettingsStore();
}
// Кладёт карточку с исходным текстом и запись журнала move на неё (источник few-shot-примера).
@@ -170,12 +170,12 @@ public sealed class AiClassifyContextBuilderTests
// board: Колонка-назначение (доска).
// text: Исходный текст карточки (source_msg).
private static void SeedCardWithMove(
- FakeKanjStore kanj,
+ TestKanjStore kanj,
string cardId,
string board,
string text)
{
kanj.SeedCard(new CardDto { Id = cardId, Col = board, Content = new SourceContent { Text = text } });
- kanj.AddMoveAsync(new CardMoveDto("lm_" + cardId, cardId, "move", "inbox", board), CancellationToken.None).GetAwaiter().GetResult();
+ kanj.Store.AddMoveAsync(new CardMoveDto("lm_" + cardId, cardId, "move", "inbox", board), CancellationToken.None).GetAwaiter().GetResult();
}
}
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs
index af636eb..ea7bd08 100644
--- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/CardsServiceRemindersTests.cs
@@ -3,7 +3,6 @@ using Deal.Tests.Unit.Support;
using Deal.Modules.Kanban.Application.Services;
using Deal.Modules.Settings.Application.Models;
using Deal.Tests.Unit.Contracts;
-using Deal.Tests.Unit.Modules.Cards;
using Deal.Tests.Unit.Modules.Settings;
namespace Deal.Tests.Unit.Modules.Kanban;
@@ -19,7 +18,7 @@ public sealed class CardsServiceRemindersTests
[Fact]
public async Task Set_RemindersDisabled_Returns400TextAndWritesNothing()
{
- (CardsService service, FakeKanjStore store, _) = Create(remindersEnabled: false);
+ (CardsService service, TestKanjStore store, _) = Create(remindersEnabled: false);
store.SeedCard(Card("c_1", stage: "hold", title: "Отложенный бот"));
CardResultDto result = await service.SetReminderAsync("c_1", NowMs() + DayMs, CancellationToken.None);
@@ -32,7 +31,7 @@ public sealed class CardsServiceRemindersTests
[Fact]
public async Task Set_Enabled_SetsReminderAndReturnsCard()
{
- (CardsService service, FakeKanjStore store, _) = Create();
+ (CardsService service, TestKanjStore store, _) = Create();
store.SeedCard(Card("c_1", stage: "hold", title: "Отложенный бот", updatedAtMs: 1));
long atMs = NowMs() + DayMs;
@@ -61,7 +60,7 @@ public sealed class CardsServiceRemindersTests
[Fact]
public async Task Set_StageNotHold_Allowed()
{
- (CardsService service, FakeKanjStore store, _) = Create();
+ (CardsService service, TestKanjStore store, _) = Create();
store.SeedCard(Card("c_1", stage: "work", title: "В работе"));
long atMs = NowMs() + DayMs;
@@ -76,7 +75,7 @@ public sealed class CardsServiceRemindersTests
[Fact]
public async Task Clear_WithReminder_ClearsItAndReturnsTrue()
{
- (CardsService service, FakeKanjStore store, _) = Create();
+ (CardsService service, TestKanjStore store, _) = Create();
store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() - 1) });
bool cleared = await service.ClearReminderAsync("c_1", CancellationToken.None);
@@ -98,7 +97,7 @@ public sealed class CardsServiceRemindersTests
[Fact]
public async Task Clear_RemindersDisabled_StillClears()
{
- (CardsService service, FakeKanjStore store, _) = Create(remindersEnabled: false);
+ (CardsService service, TestKanjStore store, _) = Create(remindersEnabled: false);
store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() + DayMs) });
bool cleared = await service.ClearReminderAsync("c_1", CancellationToken.None);
@@ -111,7 +110,7 @@ public sealed class CardsServiceRemindersTests
[Fact]
public async Task Snooze_MovesReminderToNowPlus24h()
{
- (CardsService service, FakeKanjStore store, _) = Create();
+ (CardsService service, TestKanjStore store, _) = Create();
store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() - 1) });
long beforeMs = NowMs();
@@ -136,7 +135,7 @@ public sealed class CardsServiceRemindersTests
[Fact]
public async Task Snooze_RemindersDisabled_StillSnoozes()
{
- (CardsService service, FakeKanjStore store, _) = Create(remindersEnabled: false);
+ (CardsService service, TestKanjStore store, _) = Create(remindersEnabled: false);
store.SeedCard(Card("c_1", stage: "hold") with { Reminder = new CardReminderDto(NowMs() - 1) });
bool snoozed = await service.SnoozeReminderAsync("c_1", CancellationToken.None);
@@ -149,7 +148,7 @@ public sealed class CardsServiceRemindersTests
[Fact]
public async Task CheckDue_Disabled_ClearsExpiredAndReturnsEmpty()
{
- (CardsService service, FakeKanjStore store, _) = Create(remindersEnabled: false);
+ (CardsService service, TestKanjStore store, _) = Create(remindersEnabled: false);
long pastMs = NowMs() - 1;
store.SeedCard(Card("c_hold_past", stage: "hold") with { Reminder = new CardReminderDto(pastMs) });
store.SeedCard(Card("c_work_past", stage: "work") with { Reminder = new CardReminderDto(pastMs) });
@@ -167,7 +166,7 @@ public sealed class CardsServiceRemindersTests
[Fact]
public async Task CheckDue_Enabled_ReturnsDueAndMarksFired()
{
- (CardsService service, FakeKanjStore store, _) = Create();
+ (CardsService service, TestKanjStore store, _) = Create();
store.SeedCard(Card("c_d1", stage: "hold", title: "Ранний") with { Reminder = new CardReminderDto(NowMs() - 2 * DayMs) });
store.SeedCard(Card("c_d2", stage: "hold", title: "Поздний") with { Reminder = new CardReminderDto(NowMs() - 1) });
store.SeedCard(Card("c_future", stage: "hold", title: "Будущий") with { Reminder = new CardReminderDto(NowMs() + DayMs) });
@@ -181,21 +180,21 @@ public sealed class CardsServiceRemindersTests
due.Select(item => (item.Id, item.Title)));
Assert.All(due, item => Assert.Equal("hold", item.ContainerId));
// «Выстрелившие» помечены fired: повторная выборка due пуста (признак держит строка БД).
- Assert.Empty(await store.ListDueRemindersAsync(DateTimeOffset.UtcNow, CancellationToken.None));
+ Assert.Empty(await store.Store.ListDueRemindersAsync(DateTimeOffset.UtcNow, CancellationToken.None));
}
// ─── Хелперы ──────────────────────────────────────────────────────────
- private static (CardsService Service, FakeKanjStore Store, FakeSettingsStore Settings) Create(bool remindersEnabled = true)
+ private static (CardsService Service, TestKanjStore Store, TestSettingsStore Settings) Create(bool remindersEnabled = true)
{
- var store = new FakeKanjStore();
- var settings = new FakeSettingsStore();
+ var store = new TestKanjStore();
+ var settings = new TestSettingsStore();
if (!remindersEnabled)
{
settings.Preload(SettingsKeys.RemindersEnabled, "false");
}
- return (new CardsService(store, settings, new FakeMlClient(), new TestFileStorage().Storage), store, settings);
+ return (new CardsService(store.Store, settings.Store, new TestMlClient().Client, new TestFileStorage().Storage), store, settings);
}
// Текущее время в epoch-мс (UTC) — для посева напоминаний в прошлом/будущем.
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakeMlLearningStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakeMlLearningStore.cs
deleted file mode 100644
index 43fc9f0..0000000
--- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakeMlLearningStore.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-using Deal.Modules.Kanban.Application.Abstractions;
-using Deal.Modules.Kanban.Application.Models;
-
-namespace Deal.Tests.Unit.Modules.Kanban;
-
-///
-/// In-memory реализация для unit-тестов ML-интеграции.
-///
-public sealed class FakeMlLearningStore : IMlLearningStore
-{
- private readonly List<(string Id, string Text, string Label, double Delta)> _rows = [];
-
- ///
- /// Число записей журнала обучения
- ///
- public int LearningCount { get; set; }
-
- ///
- /// Строки очереди
- ///
- public IReadOnlyList<(string Id, string Text, string Label, double Delta)> AddedRows => _rows.ToList();
-
- ///
- public Task CountLearningAsync(CancellationToken ct) => Task.FromResult(LearningCount);
-
- ///
- public Task CountOutboxAsync(CancellationToken ct) => Task.FromResult(_rows.Count);
-
- ///
- public Task AddOutboxAsync(
- string id,
- string text,
- string label,
- double delta,
- CancellationToken ct)
- {
- _rows.Add((id, text, label, delta));
- return Task.CompletedTask;
- }
-
- ///
- /// Пишет строку очереди напрямую
- ///
- /// Id строки (mle_...).
- /// Текст обучающего примера.
- /// Метка обучения.
- /// Вес сигнала.
- public void SeedOutbox(
- string id,
- string text,
- string label,
- double delta = 1.0)
- {
- _rows.Add((id, text, label, delta));
- }
-
- ///
- public Task ClearOutboxAsync(CancellationToken ct)
- {
- _rows.Clear();
- return Task.CompletedTask;
- }
-
- ///
- public Task> TakeOutboxBatchAsync(int limit, CancellationToken ct)
- {
- return Task.FromResult>(
- _rows.Take(limit)
- .Select(row => new MlOutboxEntryDto(row.Id, row.Text, row.Label, row.Delta))
- .ToList());
- }
-
- ///
- public Task DeleteOutboxAsync(IReadOnlyCollection ids, CancellationToken ct)
- {
- _rows.RemoveAll(row => ids.Contains(row.Id));
- return Task.CompletedTask;
- }
-}
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakePipelineStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakePipelineStore.cs
deleted file mode 100644
index 742b6f3..0000000
--- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/FakePipelineStore.cs
+++ /dev/null
@@ -1,315 +0,0 @@
-using Deal.Modules.Cards.Application.Sources;
-using Deal.Modules.Kanban.Application.Models;
-using Deal.Modules.Pipeline.Application.Abstractions;
-using Deal.Modules.Pipeline.Application.Models;
-
-namespace Deal.Tests.Unit.Modules.Kanban;
-
-///
-/// In-memory реализация для unit-тестов PipelineIngestService/PipelineProcessingService.
-///
-public class FakePipelineStore : IPipelineStore
-{
- private readonly List _queue = [];
- private readonly List _rejected = [];
- private readonly Dictionary _dedup = new(StringComparer.Ordinal);
-
- ///
- /// Строки очереди фейка
- ///
- public IReadOnlyList Queue => _queue.ToList();
-
- ///
- /// Записи отсева фейка
- ///
- public IReadOnlyList Rejected => _rejected.ToList();
-
- ///
- /// Кладёт строку очереди напрямую
- ///
- /// Строка как если бы была сохранена в БД.
- public void SeedQueue(QueueItemDto item)
- {
- _queue.Add(item);
- }
-
- ///
- /// Кладёт запись отсева напрямую
- ///
- /// Запись как если бы была сохранена в БД.
- public void SeedRejected(RejectedItemDto item)
- {
- _rejected.Add(item);
- }
-
- // ── Очередь (QueueItems) ───────────────────────────────────────────────
-
- ///
- public Task ExistsDuplicateAsync(
- SourceRef source,
- CancellationToken ct)
- {
- string key = source.DedupeKey();
- bool found = _queue.Any(item => item.Source.DedupeKey() == key);
- return Task.FromResult(found);
- }
-
- ///
- public Task AddAsync(QueueItemDto item, CancellationToken ct)
- {
- _queue.Add(item);
- return Task.CompletedTask;
- }
-
- ///
- public virtual Task> ListAsync(
- string? status,
- int limit,
- CancellationToken ct)
- {
- IReadOnlyList items = _queue
- .Where(item => status is null || item.Status == status)
- .OrderBy(item => item.QueuedAtMs)
- .Take(limit)
- .ToList();
- return Task.FromResult(items);
- }
-
- ///
- public Task CountByStatusAsync(string status, CancellationToken ct)
- {
- return Task.FromResult(_queue.Count(item => item.Status == status));
- }
-
- ///
- public Task SetStatusAsync(
- string id,
- string status,
- CancellationToken ct)
- {
- int index = _queue.FindIndex(item => item.Id == id);
- if (index >= 0)
- {
- _queue[index] = _queue[index] with { Status = status };
- }
-
- return Task.CompletedTask;
- }
-
- ///
- public Task RemoveAsync(string id, CancellationToken ct)
- {
- _queue.RemoveAll(item => item.Id == id);
- return Task.CompletedTask;
- }
-
- // ── Отсев (RejectedItems) ──────────────────────────────────────────────
-
- ///
- public Task UpsertAsync(RejectRecord record, CancellationToken ct)
- {
- if (string.IsNullOrWhiteSpace(record.Text))
- {
- return Task.CompletedTask;
- }
-
- string id = record.DeterministicId ?? PrefixId.New(PipelineIdPrefixes.Rejected);
- long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
-
- int index = _rejected.FindIndex(item => item.Id == id);
- RejectedItemDto row = ToItem(record, id, nowMs);
- if (index >= 0)
- {
- RejectedItemDto existing = _rejected[index];
- row = row with
- {
- Returned = existing.Returned,
- ReturnedAtMs = existing.ReturnedAtMs,
- ReturnReason = existing.ReturnReason,
- };
- _rejected[index] = row;
- }
- else
- {
- _rejected.Add(row);
- }
-
- return Task.CompletedTask;
- }
-
- ///
- public Task> ListPageAsync(
- int offset,
- int limit,
- CancellationToken ct)
- {
- IReadOnlyList page = _rejected
- .OrderByDescending(item => item.RejectedAtMs)
- .Skip(offset)
- .Take(limit)
- .ToList();
- return Task.FromResult(page);
- }
-
- ///
- public Task> SearchAsync(
- string q,
- int limitFts,
- int limitLike,
- CancellationToken ct)
- {
- string query = q.Trim();
- IReadOnlyList rows = _rejected
- .Where(item => LikeField(item.Text, query)
- || LikeField(item.Reason, query)
- || LikeField(item.Kw, query)
- || LikeField(item.Source.DisplayName ?? string.Empty, query))
- .OrderByDescending(item => item.RejectedAtMs)
- .Take(limitLike)
- .ToList();
- return Task.FromResult(rows);
- }
-
- ///
- public Task CountAsync(CancellationToken ct)
- {
- return Task.FromResult(_rejected.Count);
- }
-
- ///
- public Task GetAsync(string id, CancellationToken ct)
- {
- return Task.FromResult(_rejected.FirstOrDefault(item => item.Id == id));
- }
-
- ///
- public Task DeleteAsync(string id, CancellationToken ct)
- {
- _rejected.RemoveAll(item => item.Id == id);
- return Task.CompletedTask;
- }
-
- ///
- public Task ClearAsync(CancellationToken ct)
- {
- int cleared = _rejected.Count;
- _rejected.Clear();
- return Task.FromResult(cleared);
- }
-
- ///
- public Task PurgeExpiredAsync(DateTimeOffset olderThan, CancellationToken ct)
- {
- long cutMs = olderThan.ToUnixTimeMilliseconds();
- int removed = _rejected.RemoveAll(item => item.RejectedAtMs < cutMs);
- return Task.FromResult(removed);
- }
-
- ///
- public Task MarkReturnedAsync(
- string id,
- string reason,
- DateTimeOffset returnedAt,
- CancellationToken ct)
- {
- int index = _rejected.FindIndex(item => item.Id == id);
- if (index >= 0)
- {
- _rejected[index] = _rejected[index] with
- {
- Returned = true,
- ReturnedAtMs = returnedAt.ToUnixTimeMilliseconds(),
- ReturnReason = reason,
- };
- }
-
- return Task.CompletedTask;
- }
-
- // ── Дедуп (DedupEntries) ───────────────────────────────────────────────
-
- ///
- /// Id карточки, связанной с хэшем дедупа
- ///
- /// SHA1-hex нормализованного текста.
- public string? DedupLeadId(string hash)
- {
- return _dedup.TryGetValue(hash, out string? leadId) ? leadId : null;
- }
-
- ///
- public virtual Task ExistsAsync(string hash, CancellationToken ct)
- {
- return Task.FromResult(_dedup.ContainsKey(hash));
- }
-
- ///
- public virtual Task ClaimAsync(string hash, CancellationToken ct)
- {
- // Атомарность claim'а (как PipelineStore.ClaimAsync, ON CONFLICT DO NOTHING): true — заявка занята
- // этим вызовом, false — хэш уже заявлен (TryAdd) другим проходом pump.
- return Task.FromResult(_dedup.TryAdd(hash, null));
- }
-
- ///
- public virtual Task DeleteClaimAsync(string hash, CancellationToken ct)
- {
- if (_dedup.TryGetValue(hash, out string? leadId) && leadId is null)
- {
- _dedup.Remove(hash);
- }
-
- return Task.CompletedTask;
- }
-
- ///
- public Task LinkAsync(
- string hash,
- string cardId,
- CancellationToken ct)
- {
- _dedup[hash] = cardId;
- return Task.CompletedTask;
- }
-
- ///
- public Task DeleteByCardAsync(string cardId, CancellationToken ct)
- {
- foreach (string hash in _dedup.Where(pair => pair.Value == cardId).Select(pair => pair.Key).ToList())
- {
- _dedup.Remove(hash);
- }
-
- return Task.CompletedTask;
- }
-
- // ── Маппинг (как адаптер: подписи — словари PipelineRejectConstants) ────
-
- // Подстрока q в lower(поле) — LIKE-половина адаптера (поле колонки SQL приводится к нижнему регистру).
- // field: Поле записи отсева (text/reason/kw/ch_name).
- // query: Поисковый запрос, нормализованный сервисом (trim+lowercase).
- // Возвращает: True — поле содержит запрос без учёта регистра поля.
- private static bool LikeField(string field, string query)
- {
- return field.ToLowerInvariant().Contains(query, StringComparison.Ordinal);
- }
-
- private static RejectedItemDto ToItem(
- RejectRecord record,
- string id,
- long rejectedAtMs) => new()
- {
- Id = id,
- Source = record.Source,
- Content = record.Content,
- Text = record.Text,
- Stage = record.Stage,
- StageLabel = PipelineRejectConstants.StageLabel(record.Stage),
- Reason = record.Reason,
- Kw = record.Kw,
- DecidedBy = record.DecidedBy,
- DecidedByLabel = PipelineRejectConstants.SourceLabel(record.DecidedBy),
- MsgAtMs = record.MsgAtMs,
- RejectedAtMs = rejectedAtMs,
- };
-}
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs
index 8d12a2c..9a2e827 100644
--- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/MlReviewServiceTests.cs
@@ -5,7 +5,6 @@ using Deal.Modules.Kanban.Application.Services;
using Deal.Modules.Pipeline.Application.Models;
using Deal.Modules.Pipeline.Application.Services;
using Deal.Tests.Unit.Contracts;
-using Deal.Tests.Unit.Modules.Cards;
using Deal.Tests.Unit.Modules.Settings;
using Deal.Tests.Unit.Support;
@@ -19,15 +18,15 @@ public sealed class MlReviewServiceTests
private const string Dialog = "d_1";
private static MlReviewService Create(
- FakePipelineStore pipeline,
- FakeKanjStore kanj,
- FakeMlClient ml,
+ TestPipelineStore pipeline,
+ TestKanjStore kanj,
+ TestMlClient ml,
out CardsService cards)
{
- var settings = new FakeSettingsStore();
- cards = new CardsService(kanj, settings, ml, new TestFileStorage().Storage);
- var processing = new PipelineProcessingService(pipeline, ml, new PipelineIngestService(pipeline));
- return new MlReviewService(pipeline, kanj, cards, processing, ml);
+ var settings = new TestSettingsStore();
+ cards = new CardsService(kanj.Store, settings.Store, ml.Client, new TestFileStorage().Storage);
+ var processing = new PipelineProcessingService(pipeline.Store, ml.Client, new PipelineIngestService(pipeline.Store));
+ return new MlReviewService(pipeline.Store, kanj.Store, cards, processing, ml.Client);
}
private static QueueItemDto QueueRow(
@@ -73,12 +72,12 @@ public sealed class MlReviewServiceTests
[Fact]
public async Task Candidates_MergesQueueRejectedAndCardsWithVerdicts()
{
- var pipeline = new FakePipelineStore();
+ var pipeline = new TestPipelineStore();
pipeline.SeedQueue(QueueRow(101, "из очереди"));
pipeline.SeedRejected(RejectedRow(102, "из отсева"));
- var kanj = new FakeKanjStore();
+ var kanj = new TestKanjStore();
kanj.SeedCard(Card(103, "из карточки"));
- MlReviewService service = Create(pipeline, kanj, new FakeMlClient(), out _);
+ MlReviewService service = Create(pipeline, kanj, new TestMlClient(), out _);
IReadOnlyList items = await service.CandidatesAsync(Dialog, 10, CancellationToken.None);
@@ -94,10 +93,10 @@ public sealed class MlReviewServiceTests
[Fact]
public async Task Candidates_FiltersByDialog()
{
- var pipeline = new FakePipelineStore();
+ var pipeline = new TestPipelineStore();
pipeline.SeedQueue(QueueRow(101, "нужный", Dialog));
pipeline.SeedQueue(QueueRow(201, "другой", "d_2"));
- MlReviewService service = Create(pipeline, new FakeKanjStore(), new FakeMlClient(), out _);
+ MlReviewService service = Create(pipeline, new TestKanjStore(), new TestMlClient(), out _);
IReadOnlyList items = await service.CandidatesAsync(Dialog, 10, CancellationToken.None);
@@ -108,10 +107,10 @@ public sealed class MlReviewServiceTests
[Fact]
public async Task Candidates_NoDialog_ReturnsAllSources()
{
- var pipeline = new FakePipelineStore();
+ var pipeline = new TestPipelineStore();
pipeline.SeedQueue(QueueRow(101, "a", "d_1"));
pipeline.SeedQueue(QueueRow(201, "b", "d_2"));
- MlReviewService service = Create(pipeline, new FakeKanjStore(), new FakeMlClient(), out _);
+ MlReviewService service = Create(pipeline, new TestKanjStore(), new TestMlClient(), out _);
IReadOnlyList items = await service.CandidatesAsync(null, 10, CancellationToken.None);
@@ -121,13 +120,13 @@ public sealed class MlReviewServiceTests
[Fact]
public async Task Candidates_ClampsLimitToMax()
{
- var pipeline = new FakePipelineStore();
+ var pipeline = new TestPipelineStore();
for (int i = 0; i < 5; i++)
{
pipeline.SeedQueue(QueueRow(100 + i, $"текст {i}"));
}
- MlReviewService service = Create(pipeline, new FakeKanjStore(), new FakeMlClient(), out _);
+ MlReviewService service = Create(pipeline, new TestKanjStore(), new TestMlClient(), out _);
IReadOnlyList items = await service.CandidatesAsync(Dialog, 2, CancellationToken.None);
@@ -137,10 +136,10 @@ public sealed class MlReviewServiceTests
[Fact]
public async Task Apply_Skip_DoesNotLearn()
{
- var pipeline = new FakePipelineStore();
+ var pipeline = new TestPipelineStore();
pipeline.SeedQueue(QueueRow(101, "текст"));
- var ml = new FakeMlClient();
- MlReviewService service = Create(pipeline, new FakeKanjStore(), ml, out _);
+ var ml = new TestMlClient();
+ MlReviewService service = Create(pipeline, new TestKanjStore(), ml, out _);
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, MlReviewService.ActionSkip, CancellationToken.None);
@@ -154,10 +153,10 @@ public sealed class MlReviewServiceTests
[Fact]
public async Task Apply_Spam_WithCard_TrashesAndLearns()
{
- var kanj = new FakeKanjStore();
+ var kanj = new TestKanjStore();
kanj.SeedCard(Card(101, "спамный текст"));
- var ml = new FakeMlClient();
- MlReviewService service = Create(new FakePipelineStore(), kanj, ml, out _);
+ var ml = new TestMlClient();
+ MlReviewService service = Create(new TestPipelineStore(), kanj, ml, out _);
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, MlReviewService.ActionSpam, CancellationToken.None);
@@ -172,10 +171,10 @@ public sealed class MlReviewServiceTests
[Fact]
public async Task Apply_Spam_QueuedMessage_RejectsAndRemovesFromQueue()
{
- var pipeline = new FakePipelineStore();
+ var pipeline = new TestPipelineStore();
pipeline.SeedQueue(QueueRow(101, "рекламный текст"));
- var ml = new FakeMlClient();
- MlReviewService service = Create(pipeline, new FakeKanjStore(), ml, out _);
+ var ml = new TestMlClient();
+ MlReviewService service = Create(pipeline, new TestKanjStore(), ml, out _);
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, MlReviewService.ActionSpam, CancellationToken.None);
@@ -192,7 +191,7 @@ public sealed class MlReviewServiceTests
[Fact]
public async Task Apply_Board_MovesCardAndLearns()
{
- var kanj = new FakeKanjStore();
+ var kanj = new TestKanjStore();
kanj.SeedBoard(new ContainerDto
{
Id = "b_py",
@@ -201,8 +200,8 @@ public sealed class MlReviewServiceTests
Kind = ContainerKinds.Board,
});
kanj.SeedCard(Card(101, "python разработчик"));
- var ml = new FakeMlClient();
- MlReviewService service = Create(new FakePipelineStore(), kanj, ml, out _);
+ var ml = new TestMlClient();
+ MlReviewService service = Create(new TestPipelineStore(), kanj, ml, out _);
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, "board:b_py", CancellationToken.None);
@@ -216,9 +215,9 @@ public sealed class MlReviewServiceTests
[Fact]
public async Task Apply_UnknownBoard_ReturnsError()
{
- var kanj = new FakeKanjStore();
+ var kanj = new TestKanjStore();
kanj.SeedCard(Card(101, "текст"));
- MlReviewService service = Create(new FakePipelineStore(), kanj, new FakeMlClient(), out _);
+ MlReviewService service = Create(new TestPipelineStore(), kanj, new TestMlClient(), out _);
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, "board:missing", CancellationToken.None);
@@ -230,9 +229,9 @@ public sealed class MlReviewServiceTests
[Fact]
public async Task Apply_UnknownAction_ReturnsError()
{
- var pipeline = new FakePipelineStore();
+ var pipeline = new TestPipelineStore();
pipeline.SeedQueue(QueueRow(101, "текст"));
- MlReviewService service = Create(pipeline, new FakeKanjStore(), new FakeMlClient(), out _);
+ MlReviewService service = Create(pipeline, new TestKanjStore(), new TestMlClient(), out _);
MlApplyResult? result = await service.ApplyAsync(Dialog, 101, "что-то", CancellationToken.None);
@@ -244,7 +243,7 @@ public sealed class MlReviewServiceTests
[Fact]
public async Task Apply_MessageNotFound_ReturnsNull()
{
- MlReviewService service = Create(new FakePipelineStore(), new FakeKanjStore(), new FakeMlClient(), out _);
+ MlReviewService service = Create(new TestPipelineStore(), new TestKanjStore(), new TestMlClient(), out _);
MlApplyResult? result = await service.ApplyAsync(Dialog, 999, MlReviewService.ActionSpam, CancellationToken.None);
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/StorageTickServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/StorageTickServiceTests.cs
index 47ef4a8..69fd13d 100644
--- a/src/core/tests/Deal.Tests.Unit/Modules/Kanban/StorageTickServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Modules/Kanban/StorageTickServiceTests.cs
@@ -1,7 +1,7 @@
using Deal.Modules.Kanban.Application.Models;
using Deal.Modules.Kanban.Application.Services;
using Deal.Modules.Settings.Application.Models;
-using Deal.Tests.Unit.Modules.Cards;
+using Deal.Tests.Unit.Support;
using Deal.Tests.Unit.Modules.Settings;
namespace Deal.Tests.Unit.Modules.Kanban;
@@ -15,7 +15,7 @@ public sealed class StorageTickServiceTests
[Fact]
public async Task Tick_DefaultSettings_ArchivesExpiredBoardAndInboxCards()
{
- (StorageTickService service, FakeKanjStore store, _) = Create();
+ (StorageTickService service, TestKanjStore store, _) = Create();
store.SeedBoard(Board("b_1"));
store.SeedCard(Card("l_board_old", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(16))));
store.SeedCard(Card("l_board_young", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(13)))); // < 14 дн. — не кандидат
@@ -47,7 +47,7 @@ public sealed class StorageTickServiceTests
[Fact]
public async Task Tick_AutoArchiveSettingFalse_KeepsExpiredCardsInPlace()
{
- (StorageTickService service, FakeKanjStore store, FakeSettingsStore settings) = Create();
+ (StorageTickService service, TestKanjStore store, TestSettingsStore settings) = Create();
settings.Preload(SettingsKeys.AutoArchive, "false");
store.SeedBoard(Board("b_1"));
store.SeedCard(Card("l_old", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(30))));
@@ -62,7 +62,7 @@ public sealed class StorageTickServiceTests
[Fact]
public async Task Tick_ArchiveAfterDays30_ArchivesOnlyCardsOlderThanConfiguredMonth()
{
- (StorageTickService service, FakeKanjStore store, FakeSettingsStore settings) = Create();
+ (StorageTickService service, TestKanjStore store, TestSettingsStore settings) = Create();
settings.Preload(SettingsKeys.ArchiveAfterDays, "30");
store.SeedBoard(Board("b_1"));
store.SeedCard(Card("l_older_month", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(32))));
@@ -78,7 +78,7 @@ public sealed class StorageTickServiceTests
[Fact]
public async Task Tick_ArchiveAfterDays1_ArchivesTwoDayOldCard()
{
- (StorageTickService service, FakeKanjStore store, FakeSettingsStore settings) = Create();
+ (StorageTickService service, TestKanjStore store, TestSettingsStore settings) = Create();
settings.Preload(SettingsKeys.ArchiveAfterDays, "1");
store.SeedCard(Card("l_inbox_old", KanbanColumns.Inbox, ReceivedAtMsAgo(TimeSpan.FromDays(2))));
@@ -91,7 +91,7 @@ public sealed class StorageTickServiceTests
[Fact]
public async Task Tick_SecondRun_DoesNotReArchiveNorPurgeJustArchivedCards()
{
- (StorageTickService service, FakeKanjStore store, _) = Create();
+ (StorageTickService service, TestKanjStore store, _) = Create();
store.SeedBoard(Board("b_1"));
store.SeedCard(Card("l_old", "b_1", ReceivedAtMsAgo(TimeSpan.FromDays(20))));
@@ -109,7 +109,7 @@ public sealed class StorageTickServiceTests
[Fact]
public async Task Tick_PurgesOnlyArchiveCardsWithArchivedAtOlderThanArchiveClearDays()
{
- (StorageTickService service, FakeKanjStore store, _) = Create();
+ (StorageTickService service, TestKanjStore store, _) = Create();
DateTimeOffset now = DateTimeOffset.UtcNow;
store.SeedCard(Card("l_expired", KanbanColumns.Archive, ReceivedAtMsAgo(TimeSpan.FromDays(30))));
store.SetArchivedAt("l_expired", now.AddDays(-100)); // старше 90 дн.
@@ -129,7 +129,7 @@ public sealed class StorageTickServiceTests
[Fact]
public async Task Tick_ArchiveClearDaysFromSettings_ControlsArchivePurgeBoundary()
{
- (StorageTickService service, FakeKanjStore store, FakeSettingsStore settings) = Create();
+ (StorageTickService service, TestKanjStore store, TestSettingsStore settings) = Create();
settings.Preload(SettingsKeys.ArchiveClearDays, "30");
DateTimeOffset now = DateTimeOffset.UtcNow;
store.SeedCard(Card("l_expired", KanbanColumns.Archive, ReceivedAtMsAgo(TimeSpan.FromDays(10))));
@@ -148,7 +148,7 @@ public sealed class StorageTickServiceTests
[Fact]
public async Task Tick_PurgesOnlyTrashCardsWithReceivedAtOlderThanTrashClearDays()
{
- (StorageTickService service, FakeKanjStore store, _) = Create();
+ (StorageTickService service, TestKanjStore store, _) = Create();
store.SeedCard(Card("l_old", KanbanColumns.Trash, ReceivedAtMsAgo(TimeSpan.FromDays(10)))); // старше 7 дн.
store.SeedCard(Card("l_fresh", KanbanColumns.Trash, ReceivedAtMsAgo(TimeSpan.FromHours(6))));
@@ -165,7 +165,7 @@ public sealed class StorageTickServiceTests
[Fact]
public async Task Tick_TrashClearDaysFromSettings_ControlsTrashPurgeBoundary()
{
- (StorageTickService service, FakeKanjStore store, FakeSettingsStore settings) = Create();
+ (StorageTickService service, TestKanjStore store, TestSettingsStore settings) = Create();
settings.Preload(SettingsKeys.TrashClearDays, "3");
store.SeedCard(Card("l_old", KanbanColumns.Trash, ReceivedAtMsAgo(TimeSpan.FromDays(5)))); // старше 3 дн.
store.SeedCard(Card("l_fresh", KanbanColumns.Trash, ReceivedAtMsAgo(TimeSpan.FromDays(1))));
@@ -194,11 +194,11 @@ public sealed class StorageTickServiceTests
// ─── Хелперы ────────────────────────────────────────────────────────────
- private static (StorageTickService Service, FakeKanjStore Store, FakeSettingsStore Settings) Create()
+ private static (StorageTickService Service, TestKanjStore Store, TestSettingsStore Settings) Create()
{
- var store = new FakeKanjStore();
- var settings = new FakeSettingsStore();
- return (new StorageTickService(store, settings), store, settings);
+ var store = new TestKanjStore();
+ var settings = new TestSettingsStore();
+ return (new StorageTickService(store.Store, settings.Store), store, settings);
}
// Доска-колонка минимально (id/имя достаточно для списка кандидатов фейка).
@@ -215,13 +215,13 @@ public sealed class StorageTickServiceTests
private static long ReceivedAtMsAgo(TimeSpan age) => DateTimeOffset.UtcNow.Subtract(age).ToUnixTimeMilliseconds();
// Колонка карточки в фейке после тика.
- private static string ColOf(FakeKanjStore store, string cardId) =>
+ private static string ColOf(TestKanjStore store, string cardId) =>
store.CardDtos.Single(card => card.Id == cardId).Col;
// Флаг is_new карточки после тика.
- private static bool IsNewOf(FakeKanjStore store, string cardId) =>
+ private static bool IsNewOf(TestKanjStore store, string cardId) =>
store.CardDtos.Single(card => card.Id == cardId).IsNew;
- private static IReadOnlyList MatchHitsOf(FakeKanjStore store, string cardId) =>
+ private static IReadOnlyList MatchHitsOf(TestKanjStore store, string cardId) =>
store.CardDtos.Single(card => card.Id == cardId).MatchHits;
}
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineIngestServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineIngestServiceTests.cs
index 3262c9d..fc224d5 100644
--- a/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineIngestServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineIngestServiceTests.cs
@@ -1,7 +1,6 @@
using Deal.Modules.Cards.Application.Sources;
using Deal.Modules.Pipeline.Application.Models;
using Deal.Modules.Pipeline.Application.Services;
-using Deal.Tests.Unit.Modules.Kanban;
using Deal.Tests.Unit.Support;
namespace Deal.Tests.Unit.Modules.Pipeline;
@@ -16,8 +15,8 @@ public sealed class PipelineIngestServiceTests
[Fact]
public async Task EnqueueAsync_TrimsTextAndWritesNewRowWithPId()
{
- FakePipelineStore store = new();
- PipelineIngestService service = new(store);
+ TestPipelineStore store = new();
+ PipelineIngestService service = new(store.Store);
PipelineIngestResultDto result = await service.EnqueueAsync(
Message(
@@ -48,8 +47,8 @@ public sealed class PipelineIngestServiceTests
[Fact]
public async Task EnqueueAsync_MsgAtAbsent_ZeroMsgAtAndQueuedNow()
{
- FakePipelineStore store = new();
- PipelineIngestService service = new(store);
+ TestPipelineStore store = new();
+ PipelineIngestService service = new(store.Store);
await service.EnqueueAsync(
Message("Сообщение без времени", receivedAt: default(DateTimeOffset)),
@@ -63,8 +62,8 @@ public sealed class PipelineIngestServiceTests
[Fact]
public async Task EnqueueAsync_ForceFlag_PropagatedToQueueRow()
{
- FakePipelineStore store = new();
- PipelineIngestService service = new(store);
+ TestPipelineStore store = new();
+ PipelineIngestService service = new(store.Store);
await service.EnqueueAsync(Message("Возврат из отсева", force: true), CancellationToken.None);
@@ -76,8 +75,8 @@ public sealed class PipelineIngestServiceTests
[Fact]
public async Task EnqueueAsync_EmptyOrWhitespaceText_Noop()
{
- FakePipelineStore store = new();
- PipelineIngestService service = new(store);
+ TestPipelineStore store = new();
+ PipelineIngestService service = new(store.Store);
PipelineIngestResultDto empty = await service.EnqueueAsync(Message(text: string.Empty), CancellationToken.None);
PipelineIngestResultDto spaces = await service.EnqueueAsync(Message(text: " "), CancellationToken.None);
@@ -93,8 +92,8 @@ public sealed class PipelineIngestServiceTests
[Fact]
public async Task EnqueueAsync_SameSourceTwice_SecondIsDuplicate()
{
- FakePipelineStore store = new();
- PipelineIngestService service = new(store);
+ TestPipelineStore store = new();
+ PipelineIngestService service = new(store.Store);
QueuedMessage first = Message("Сообщение", origin: "d_1", externalId: "7");
QueuedMessage second = Message("Сообщение снова", origin: "d_1", externalId: "7");
@@ -111,8 +110,8 @@ public sealed class PipelineIngestServiceTests
[Fact]
public async Task EnqueueAsync_SameSourceDifferentExternalIds_BothAccepted()
{
- FakePipelineStore store = new();
- PipelineIngestService service = new(store);
+ TestPipelineStore store = new();
+ PipelineIngestService service = new(store.Store);
await service.EnqueueAsync(Message("Первое", origin: "d_1", externalId: "1"), CancellationToken.None);
PipelineIngestResultDto second = await service.EnqueueAsync(Message("Второе", origin: "d_1", externalId: "2"), CancellationToken.None);
@@ -124,8 +123,8 @@ public sealed class PipelineIngestServiceTests
[Fact]
public async Task EnqueueAsync_SameOriginWithoutExternalId_SecondIsDuplicate()
{
- FakePipelineStore store = new();
- PipelineIngestService service = new(store);
+ TestPipelineStore store = new();
+ PipelineIngestService service = new(store.Store);
PipelineIngestResultDto first = await service.EnqueueAsync(
Message("Первое", origin: "d_1", externalId: null), CancellationToken.None);
@@ -142,8 +141,8 @@ public sealed class PipelineIngestServiceTests
[Fact]
public async Task EnqueueAsync_LongText_TruncatedTo6000CodePoints()
{
- FakePipelineStore store = new();
- PipelineIngestService service = new(store);
+ TestPipelineStore store = new();
+ PipelineIngestService service = new(store.Store);
string longText = new string('а', 7_000);
await service.EnqueueAsync(Message(longText), CancellationToken.None);
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs
index a0d8c0a..6ea34d0 100644
--- a/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Modules/Pipeline/PipelineProcessingServiceTests.cs
@@ -3,7 +3,6 @@ using Deal.Modules.Cards.Application.Sources;
using Deal.Modules.Pipeline.Application.Models;
using Deal.Modules.Pipeline.Application.Services;
using Deal.Tests.Unit.Contracts;
-using Deal.Tests.Unit.Modules.Kanban;
using Deal.Tests.Unit.Support;
namespace Deal.Tests.Unit.Modules.Pipeline;
@@ -17,7 +16,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task RejectAsync_WhitespaceText_Noop()
{
- (PipelineProcessingService service, FakePipelineStore store, _) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, _) = Create();
await service.RejectAsync(new RejectRecord { Text = " ", DecidedBy = "stop", Stage = "length" }, CancellationToken.None);
@@ -27,7 +26,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task RejectAsync_TruncatesFieldsDefaultsHueAndUsesDeterministicId()
{
- (PipelineProcessingService service, FakePipelineStore store, _) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, _) = Create();
await service.RejectAsync(new RejectRecord
{
@@ -53,7 +52,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task RejectAsync_SameDialogAndMsgId_UpsertsSingleRow()
{
- (PipelineProcessingService service, FakePipelineStore store, _) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, _) = Create();
await service.RejectAsync(Reject("d_1", 7, stage: "length", reason: "короткое"), CancellationToken.None);
await service.RejectAsync(Reject("d_1", 7, stage: "stop", reason: "стоп-фраза «реклама»"), CancellationToken.None);
@@ -68,7 +67,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task QueueCountsAsync_CountsNewAndFiltered()
{
- (PipelineProcessingService service, FakePipelineStore store, _) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, _) = Create();
store.SeedQueue(QueueItem("p_1", PipelineQueueStatuses.New, queuedAtMs: 1));
store.SeedQueue(QueueItem("p_2", PipelineQueueStatuses.New, queuedAtMs: 2));
store.SeedQueue(QueueItem("p_3", PipelineQueueStatuses.Filtered, queuedAtMs: 3));
@@ -83,7 +82,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task StatsAsync_ReturnsQueueCountsAndRejectedCount()
{
- (PipelineProcessingService service, FakePipelineStore store, _) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, _) = Create();
store.SeedQueue(QueueItem("p_1", PipelineQueueStatuses.New, queuedAtMs: 1));
store.SeedRejected(Rejected("r_1"));
store.SeedRejected(Rejected("r_2"));
@@ -99,7 +98,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task ListQueueAsync_OrdersByQueuedAtAndClampsLimitToMinimumOne()
{
- (PipelineProcessingService service, FakePipelineStore store, _) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, _) = Create();
store.SeedQueue(QueueItem("p_1", PipelineQueueStatuses.New, queuedAtMs: 30));
store.SeedQueue(QueueItem("p_2", PipelineQueueStatuses.Filtered, queuedAtMs: 10));
store.SeedQueue(QueueItem("p_3", PipelineQueueStatuses.New, queuedAtMs: 20));
@@ -116,7 +115,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task ListRejectedAsync_NoQuery_NewestFirstWithOffsetAndLimit()
{
- (PipelineProcessingService service, FakePipelineStore store, _) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, _) = Create();
store.SeedRejected(Rejected("r_1", rejectedAtMs: 1_000));
store.SeedRejected(Rejected("r_2", rejectedAtMs: 2_000));
store.SeedRejected(Rejected("r_3", rejectedAtMs: 3_000));
@@ -132,7 +131,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task ListRejectedAsync_QueryMatchesTextReasonKwAndSourceName()
{
- (PipelineProcessingService service, FakePipelineStore store, _) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, _) = Create();
store.SeedRejected(Rejected("r_text", text: "Middle Python разработчик", rejectedAtMs: 1_000));
store.SeedRejected(Rejected("r_reason", reason: "стоп-фраза «реклама»", rejectedAtMs: 2_000));
store.SeedRejected(Rejected("r_kw", kw: "спам", rejectedAtMs: 3_000));
@@ -152,7 +151,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task ListRejectedAsync_QueryWithOffsetPagesOverCandidatesAndEchoesTotal()
{
- (PipelineProcessingService service, FakePipelineStore store, _) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, _) = Create();
store.SeedRejected(Rejected("r_1", text: "middle a", rejectedAtMs: 1_000));
store.SeedRejected(Rejected("r_2", text: "middle b", rejectedAtMs: 2_000));
store.SeedRejected(Rejected("r_3", text: "middle c", rejectedAtMs: 3_000));
@@ -168,7 +167,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task ReturnAsync_RecordNotFound_ReturnsNull()
{
- (PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, TestMlClient ml) = Create();
RejectReturnResultDto? result = await service.ReturnAsync("r_missing", string.Empty, CancellationToken.None);
@@ -180,7 +179,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task ReturnAsync_AlreadyReturned_Returns400()
{
- (PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, TestMlClient ml) = Create();
store.SeedRejected(Rejected("r_1", returned: true, returnReason: "было"));
RejectReturnResultDto? result = await service.ReturnAsync("r_1", "ещё раз", CancellationToken.None);
@@ -195,7 +194,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task ReturnAsync_DuplicateSource_Returns400()
{
- (PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, TestMlClient ml) = Create();
store.SeedRejected(Rejected("r_1", decidedBy: "dup", stage: "dup"));
RejectReturnResultDto? result = await service.ReturnAsync("r_1", string.Empty, CancellationToken.None);
@@ -209,7 +208,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task ReturnAsync_EmptyText_Returns400()
{
- (PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, TestMlClient ml) = Create();
store.SeedRejected(Rejected("r_1", text: " "));
RejectReturnResultDto? result = await service.ReturnAsync("r_1", string.Empty, CancellationToken.None);
@@ -223,7 +222,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task ReturnAsync_SpamStage_UnlearnsSpamMarksRecordAndEnqueuesForceRow()
{
- (PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, TestMlClient ml) = Create();
store.SeedRejected(Rejected(
"r_1",
text: " Продвижение в каналах ",
@@ -270,7 +269,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task ReturnAsync_NonSpamStage_NoLearningPushButEnqueuesForceRow()
{
- (PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, TestMlClient ml) = Create();
store.SeedRejected(Rejected(
"r_1",
text: "Короткое сообщение",
@@ -296,7 +295,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task ReturnAsync_LongReason_TruncatedTo500CodePointsOnRecord()
{
- (PipelineProcessingService service, FakePipelineStore store, _) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, _) = Create();
store.SeedRejected(Rejected("r_1", text: "Текст", stage: "stale", decidedBy: "stale", origin: "d_1"));
await service.ReturnAsync("r_1", new string('п', 700), CancellationToken.None);
@@ -309,7 +308,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task ReturnAsync_NoSourceInfo_EnqueuesDirectlyWithDefaultHue()
{
- (PipelineProcessingService service, FakePipelineStore store, FakeMlClient ml) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, TestMlClient ml) = Create();
store.SeedRejected(Rejected(
"r_old",
text: "Старая запись без ссылки на исходное",
@@ -340,7 +339,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task DeleteAsync_RemovesSingleRecord()
{
- (PipelineProcessingService service, FakePipelineStore store, _) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, _) = Create();
store.SeedRejected(Rejected("r_1"));
store.SeedRejected(Rejected("r_2"));
@@ -352,7 +351,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task ClearAsync_ReturnsCountAndEmptiesRejects()
{
- (PipelineProcessingService service, FakePipelineStore store, _) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, _) = Create();
store.SeedRejected(Rejected("r_1"));
store.SeedRejected(Rejected("r_2"));
store.SeedRejected(Rejected("r_3"));
@@ -368,7 +367,7 @@ public sealed class PipelineProcessingServiceTests
[Fact]
public async Task PurgeExpiredAsync_RemovesOnlyRowsOlderThanRetention()
{
- (PipelineProcessingService service, FakePipelineStore store, _) = Create();
+ (PipelineProcessingService service, TestPipelineStore store, _) = Create();
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
store.SeedRejected(Rejected("r_old", rejectedAtMs: (long)(nowMs - TimeSpan.FromDays(4).TotalMilliseconds)));
store.SeedRejected(Rejected("r_fresh", rejectedAtMs: (long)(nowMs - TimeSpan.FromDays(1).TotalMilliseconds)));
@@ -381,12 +380,12 @@ public sealed class PipelineProcessingServiceTests
// ─── Помощники ──────────────────────────────────────────────────────────
- // Сервис на общих фейках: очередь/отсев и ML-клиент одного сценария.
- private static (PipelineProcessingService Service, FakePipelineStore Store, FakeMlClient Ml) Create()
+ // Сервис на общих подставках: очередь/отсев и ML-клиент одного сценария.
+ private static (PipelineProcessingService Service, TestPipelineStore Store, TestMlClient Ml) Create()
{
- FakePipelineStore store = new();
- FakeMlClient ml = new();
- PipelineProcessingService service = new(store, ml, new PipelineIngestService(store));
+ TestPipelineStore store = new();
+ TestMlClient ml = new();
+ PipelineProcessingService service = new(store.Store, ml.Client, new PipelineIngestService(store.Store));
return (service, store, ml);
}
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeSettingsStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeSettingsStore.cs
deleted file mode 100644
index f3fad0e..0000000
--- a/src/core/tests/Deal.Tests.Unit/Modules/Settings/FakeSettingsStore.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-using Deal.Modules.Settings.Application.Abstractions;
-using Deal.Modules.Settings.Application.Models;
-
-namespace Deal.Tests.Unit.Modules.Settings;
-
-///
-/// In-memory реализация для юнит-тестов SettingsService.
-///
-public sealed class FakeSettingsStore : ISettingsStore
-{
- private readonly Dictionary _rows = new(StringComparer.Ordinal);
-
- public IReadOnlyCollection Keys => _rows.Keys.ToList();
-
- ///
- /// Кладёт готовую строку
- ///
- /// Ключ настройки.
- /// Значение, сериализованное в JSON.
- public void Preload(string key, string valueJson)
- {
- _rows[key] = new SettingValue(key, valueJson, DateTimeOffset.UtcNow);
- }
-
- ///
- /// Возвращает JSON сохранённого значения или null, если ключа нет.
- ///
- /// Ключ настройки.
- /// JSON-строка значения или null.
- public string? GetStoredJson(string key)
- {
- return _rows.TryGetValue(key, out SettingValue? row) ? row.ValueJson : null;
- }
-
- ///
- public Task GetAsync(string key, CancellationToken ct)
- {
- return Task.FromResult(_rows.TryGetValue(key, out SettingValue? row) ? row : null);
- }
-
- ///
- public Task> GetAllAsync(CancellationToken ct)
- {
- return Task.FromResult>(_rows.Values.ToList());
- }
-
- ///
- public Task SetAsync(
- string key,
- string valueJson,
- CancellationToken ct)
- {
- _rows[key] = new SettingValue(key, valueJson, DateTimeOffset.UtcNow);
- return Task.CompletedTask;
- }
-
- ///
- public Task RemoveAsync(string key, CancellationToken ct)
- {
- _rows.Remove(key);
- return Task.CompletedTask;
- }
-}
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuditServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuditServiceTests.cs
index f50aad6..2d01bf0 100644
--- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuditServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuditServiceTests.cs
@@ -1,6 +1,7 @@
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Services;
+using Deal.Tests.Unit.Support;
namespace Deal.Tests.Unit.Modules.Tenants;
@@ -14,8 +15,8 @@ public sealed class AuditServiceTests
[Fact]
public async Task AppendAsync_SetsAtToUtcNow_AndSavesAllFields()
{
- var store = new FakeAuditLogStore();
- var service = new AuditService(store);
+ var store = new TestAuditLogStore();
+ var service = new AuditService(store.Store);
var actorId = Guid.NewGuid();
var tenantId = Guid.NewGuid();
var before = DateTimeOffset.UtcNow;
@@ -43,11 +44,11 @@ public sealed class AuditServiceTests
[Fact]
public async Task QueryAsync_ReturnsFilteredRecordsNewestFirst()
{
- var store = new FakeAuditLogStore();
+ var store = new TestAuditLogStore();
await AppendWithAtAsync(store, AuditEvents.OperatorLoginOk, AuditActorTypes.Operator, tenantId: null, DateTimeOffset.UtcNow.AddMinutes(-2));
await AppendWithAtAsync(store, AuditEvents.TenantLoginFailed, AuditActorTypes.Tenant, tenantId: null, DateTimeOffset.UtcNow.AddMinutes(-1));
await AppendWithAtAsync(store, AuditEvents.TenantLoginOk, AuditActorTypes.Tenant, tenantId: null, DateTimeOffset.UtcNow);
- var service = new AuditService(store);
+ var service = new AuditService(store.Store);
var all = await service.QueryAsync(EmptyFilter(limit: 100), CancellationToken.None);
Assert.Equal(3, all.Count);
@@ -71,7 +72,7 @@ public sealed class AuditServiceTests
[Fact]
public async Task QueryAsync_AppliesAtRangeAndTenantFilters()
{
- var store = new FakeAuditLogStore();
+ var store = new TestAuditLogStore();
var tenantId = Guid.NewGuid();
var outsideTenantId = Guid.NewGuid();
var from = DateTimeOffset.UtcNow.AddMinutes(-10);
@@ -79,7 +80,7 @@ public sealed class AuditServiceTests
await AppendWithAtAsync(store, AuditEvents.TenantLoginOk, AuditActorTypes.Tenant, tenantId, from.AddMinutes(-1)); // вне окна
await AppendWithAtAsync(store, AuditEvents.TenantLoginOk, AuditActorTypes.Tenant, tenantId, from);
await AppendWithAtAsync(store, AuditEvents.TenantLoginOk, AuditActorTypes.Tenant, outsideTenantId, to);
- var service = new AuditService(store);
+ var service = new AuditService(store.Store);
var byRange = await service.QueryAsync(
new AuditQueryDto(AuditEvents.TenantLoginOk, null, null, from, to, 100), CancellationToken.None);
@@ -94,13 +95,13 @@ public sealed class AuditServiceTests
[Fact]
public async Task CountAsync_CountsAllRowsMatchingFilterRegardlessOfLimit()
{
- var store = new FakeAuditLogStore();
+ var store = new TestAuditLogStore();
for (int i = 0; i < 7; i++)
{
await AppendWithAtAsync(store, AuditEvents.OperatorLoginOk, AuditActorTypes.Operator, tenantId: null, DateTimeOffset.UtcNow);
}
- var service = new AuditService(store);
+ var service = new AuditService(store.Store);
var filter = new AuditQueryDto(AuditEvents.OperatorLoginOk, null, null, null, null, Limit: 2);
Assert.Equal(7, await service.CountAsync(filter, CancellationToken.None));
@@ -163,13 +164,13 @@ public sealed class AuditServiceTests
// Добавляет запись в фейк с явным At/актором (минуя сервис — для проверок сортировки/фильтров).
private static async Task AppendWithAtAsync(
- FakeAuditLogStore store,
+ TestAuditLogStore store,
string eventType,
string actorType,
Guid? tenantId,
DateTimeOffset at)
{
- await store.AppendAsync(new AuditRecordDto(
+ await store.Store.AppendAsync(new AuditRecordDto(
eventType,
actorType,
ActorId: null,
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuthServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuthServiceTests.cs
index c2b4783..c652716 100644
--- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuthServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuthServiceTests.cs
@@ -5,7 +5,7 @@ using Deal.Tests.Unit.Support;
namespace Deal.Tests.Unit.Modules.Tenants;
///
-/// Юнит-тесты AuthService на fake-хранилище и детерминированном «хэшере».
+/// Юнит-тесты AuthService на подставке хранилища и детерминированном «хэшере».
///
public sealed class AuthServiceTests
{
@@ -17,17 +17,17 @@ public sealed class AuthServiceTests
private static readonly Guid UserId = Guid.NewGuid();
private static readonly Guid UserTenantId = Guid.NewGuid();
- private readonly FakeAuthStore _store;
+ private readonly TestAuthStore _store;
private readonly TestTenantStore _tenantStore;
private readonly AuthService _service;
public AuthServiceTests()
{
var passwordHasher = TestHashers.New();
- _store = new FakeAuthStore();
+ _store = new TestAuthStore();
_tenantStore = new TestTenantStore(new TenantRecordDto(UserTenantId, "Тестовый тенант", TenantStatuses.Active, DateTimeOffset.UtcNow));
_store.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword)));
- _service = new AuthService(_store, passwordHasher, _tenantStore.Repository);
+ _service = new AuthService(_store.Store, passwordHasher, _tenantStore.Repository);
}
[Fact]
@@ -133,7 +133,7 @@ public sealed class AuthServiceTests
var user = await _service.ResolveSessionAsync(rawToken, CancellationToken.None);
Assert.Null(user);
- // Очистка «протухших» сессий вызвана и удалила сессию из fake-хранилища.
+ // Очистка «протухших» сессий вызвана и удалила сессию из хранилища.
Assert.Contains("delete-expired", _store.Calls);
Assert.Empty(_store.Sessions);
}
@@ -204,10 +204,10 @@ public sealed class AuthServiceTests
public async Task LoginAsync_WhenTenantSuspended_ReturnsSuspendedErrorWithoutSession()
{
var passwordHasher = TestHashers.New();
- var suspendedStore = new FakeAuthStore();
+ var suspendedStore = new TestAuthStore();
suspendedStore.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword)));
var suspendedTenantStore = new TestTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
- var suspendedService = new AuthService(suspendedStore, passwordHasher, suspendedTenantStore.Repository);
+ var suspendedService = new AuthService(suspendedStore.Store, passwordHasher, suspendedTenantStore.Repository);
var result = await suspendedService.LoginAsync(UserLogin, UserPassword, CancellationToken.None);
@@ -225,10 +225,10 @@ public sealed class AuthServiceTests
// Порядок проверок AuthService: учётные данные раньше статуса тенанта — неверный пароль не раскрывает
// приостановку (тот же ответ, что для активного тенанта: Login/Token/Error пусты).
var passwordHasher = TestHashers.New();
- var suspendedStore = new FakeAuthStore();
+ var suspendedStore = new TestAuthStore();
suspendedStore.AddUser(new StoredUserDto(UserId, UserLogin, UserTenantId, "active", passwordHasher.Hash(UserPassword)));
var suspendedTenantStore = new TestTenantStore(new TenantRecordDto(UserTenantId, "Приостановленный", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
- var suspendedService = new AuthService(suspendedStore, passwordHasher, suspendedTenantStore.Repository);
+ var suspendedService = new AuthService(suspendedStore.Store, passwordHasher, suspendedTenantStore.Repository);
var result = await suspendedService.LoginAsync(UserLogin, "wrong-password", CancellationToken.None);
@@ -266,12 +266,12 @@ public sealed class AuthServiceTests
public async Task ImpersonateAsync_WithoutLogin_TakesFirstTenantUser()
{
var passwordHasher = TestHashers.New();
- var multiUserStore = new FakeAuthStore();
+ var multiUserStore = new TestAuthStore();
var firstUserId = Guid.NewGuid();
var secondUserId = Guid.NewGuid();
multiUserStore.AddUser(new StoredUserDto(firstUserId, "first@example.com", UserTenantId, "active", passwordHasher.Hash("p1")));
multiUserStore.AddUser(new StoredUserDto(secondUserId, "second@example.com", UserTenantId, "active", passwordHasher.Hash("p2")));
- var multiUserService = new AuthService(multiUserStore, passwordHasher, _tenantStore.Repository);
+ var multiUserService = new AuthService(multiUserStore.Store, passwordHasher, _tenantStore.Repository);
// login не задан — берётся первый пользователь тенанта (порядок хранилища = CreatedAt в EF-адаптере).
var result = await multiUserService.ImpersonateAsync(UserTenantId, targetLogin: null, Guid.NewGuid(), CancellationToken.None);
@@ -294,9 +294,9 @@ public sealed class AuthServiceTests
// Пользователь другого тенанта с тем же логином тоже не резолвится (не раскрываем существование логина).
var passwordHasher = TestHashers.New();
- var crossStore = new FakeAuthStore();
+ var crossStore = new TestAuthStore();
crossStore.AddUser(new StoredUserDto(Guid.NewGuid(), "cross@example.com", otherTenantId, "active", passwordHasher.Hash("p")));
- var crossService = new AuthService(crossStore, passwordHasher, _tenantStore.Repository);
+ var crossService = new AuthService(crossStore.Store, passwordHasher, _tenantStore.Repository);
var cross = await crossService.ImpersonateAsync(UserTenantId, "cross@example.com", Guid.NewGuid(), CancellationToken.None);
Assert.False(cross.Ok);
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeAuditLogStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeAuditLogStore.cs
deleted file mode 100644
index 28ce2be..0000000
--- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeAuditLogStore.cs
+++ /dev/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;
-
-///
-/// In-memory реализация для юнит/HTTP-тестов аудита.
-///
-public sealed class FakeAuditLogStore : IAuditLogStore
-{
- private readonly List _records = [];
-
- ///
- /// Записи хранилища в порядке добавления
- ///
- public IReadOnlyList Records => _records;
-
- ///
- public Task AppendAsync(AuditRecordDto record, CancellationToken ct)
- {
- _records.Add(record with { Id = _records.Count + 1 });
- return Task.CompletedTask;
- }
-
- ///
- public Task> QueryAsync(AuditQueryDto filter, CancellationToken ct)
- {
- IReadOnlyList 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);
- }
-
- ///
- public Task CountAsync(AuditQueryDto filter, CancellationToken ct) =>
- Task.FromResult(ApplyFilters(filter).Count());
-
- ///
- public Task PurgeOlderThanAsync(DateTimeOffset cutoff, CancellationToken ct)
- {
- int removed = _records.RemoveAll(record => record.At < cutoff);
- return Task.FromResult(removed);
- }
-
- private IEnumerable 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));
-}
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeAuthStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeAuthStore.cs
deleted file mode 100644
index 518f7b6..0000000
--- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeAuthStore.cs
+++ /dev/null
@@ -1,123 +0,0 @@
-using Deal.Modules.Tenants.Application.Abstractions;
-using Deal.Modules.Tenants.Application.Models;
-
-namespace Deal.Tests.Unit.Modules.Tenants;
-
-///
-/// In-memory реализация для юнит-тестов AuthService.
-///
-public sealed class FakeAuthStore : IAuthStore
-{
- private readonly List _users = [];
- private readonly List _sessions = [];
- private readonly List _calls = [];
-
- ///
- /// Пользователи хранилища.
- ///
- public IReadOnlyList Users => _users;
-
- ///
- /// Сессии хранилища.
- ///
- public IReadOnlyList Sessions => _sessions;
-
- ///
- /// Журнал мутирующих вызовов в порядке их совершения.
- ///
- public IReadOnlyList Calls => _calls;
-
- ///
- /// Добавляет пользователя
- ///
- /// Пользователь.
- public void AddUser(StoredUserDto user) => _users.Add(user);
-
- ///
- /// Добавляет сессию
- ///
- /// Сессия.
- public void AddSession(SessionDto session) => _sessions.Add(session);
-
- ///
- public Task FindUserByLoginAsync(string login, CancellationToken ct) =>
- Task.FromResult(_users.SingleOrDefault(u => u.Login == login));
-
- ///
- public Task FindSessionByTokenHashAsync(string tokenHash, CancellationToken ct) =>
- Task.FromResult(_sessions.SingleOrDefault(s => s.TokenHash == tokenHash));
-
- ///
- public Task FindUserByIdAsync(Guid userId, CancellationToken ct)
- {
- var user = _users.SingleOrDefault(u => u.Id == userId);
- return Task.FromResult(
- user is null
- ? null
- : new UserIdentityDto(user.Id, user.Login, user.TenantId, user.Status));
- }
-
- ///
- public Task> ListUsersByTenantIdAsync(Guid tenantId, CancellationToken ct) =>
- Task.FromResult>(_users
- .Where(u => u.TenantId == tenantId)
- .Select(u => new UserIdentityDto(u.Id, u.Login, u.TenantId, u.Status))
- .ToList());
-
- ///
- public Task CreateUserAsync(StoredUserDto user, CancellationToken ct)
- {
- _users.Add(user);
- _calls.Add($"create-user:{user.Id}");
- return Task.CompletedTask;
- }
-
- ///
- public Task CreateSessionAsync(SessionDto session, CancellationToken ct)
- {
- _sessions.Add(session);
- _calls.Add($"create-session:{session.UserId}");
- return Task.CompletedTask;
- }
-
- ///
- public Task DeleteSessionAsync(string tokenHash, CancellationToken ct)
- {
- _sessions.RemoveAll(s => s.TokenHash == tokenHash);
- _calls.Add($"delete-session:{tokenHash}");
- return Task.CompletedTask;
- }
-
- ///
- public Task DeleteSessionsByUserIdAsync(Guid userId, CancellationToken ct)
- {
- _sessions.RemoveAll(s => s.UserId == userId);
- _calls.Add($"delete-user-sessions:{userId}");
- return Task.CompletedTask;
- }
-
- ///
- public Task UpdatePasswordHashAsync(
- Guid userId,
- string passwordHash,
- CancellationToken ct)
- {
- int index = _users.FindIndex(u => u.Id == userId);
- if (index >= 0)
- {
- var user = _users[index];
- _users[index] = user with { PasswordHash = passwordHash };
- }
-
- _calls.Add($"update-password-hash:{userId}");
- return Task.CompletedTask;
- }
-
- ///
- public Task DeleteExpiredSessionsAsync(CancellationToken ct)
- {
- _sessions.RemoveAll(s => s.ExpiresAt <= DateTimeOffset.UtcNow);
- _calls.Add("delete-expired");
- return Task.CompletedTask;
- }
-}
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeOperatorAuthStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeOperatorAuthStore.cs
deleted file mode 100644
index 91b350d..0000000
--- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeOperatorAuthStore.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-using Deal.Modules.Tenants.Application.Abstractions;
-using Deal.Modules.Tenants.Application.Models;
-
-namespace Deal.Tests.Unit.Modules.Tenants;
-
-///
-/// In-memory реализация для юнит-тестов OperatorAuthService.
-///
-public sealed class FakeOperatorAuthStore : IOperatorAuthStore
-{
- private readonly List _operators = [];
- private readonly List _sessions = [];
- private readonly List _calls = [];
-
- ///
- /// Операторы хранилища.
- ///
- public IReadOnlyList Operators => _operators;
-
- ///
- /// Сессии операторов хранилища.
- ///
- public IReadOnlyList Sessions => _sessions;
-
- ///
- /// Журнал мутирующих вызовов в порядке их совершения.
- ///
- public IReadOnlyList Calls => _calls;
-
- ///
- /// Добавляет оператора
- ///
- /// Оператор.
- public void AddOperator(StoredOperatorDto operatorRecord) => _operators.Add(operatorRecord);
-
- ///
- /// Добавляет сессию
- ///
- /// Сессия.
- public void AddSession(OperatorSessionDto session) => _sessions.Add(session);
-
- ///
- public Task FindByLoginAsync(string login, CancellationToken ct) =>
- Task.FromResult(_operators.SingleOrDefault(o => o.Login == login));
-
- ///
- public Task FindSessionByTokenHashAsync(string tokenHash, CancellationToken ct) =>
- Task.FromResult(_sessions.SingleOrDefault(s => s.TokenHash == tokenHash));
-
- ///
- public Task CreateAsync(StoredOperatorDto operatorRecord, CancellationToken ct)
- {
- _operators.Add(operatorRecord);
- _calls.Add($"create-operator:{operatorRecord.Id}");
- return Task.CompletedTask;
- }
-
- ///
- public Task CreateSessionAsync(OperatorSessionDto session, CancellationToken ct)
- {
- _sessions.Add(session);
- _calls.Add($"create-session:{session.OperatorId}");
- return Task.CompletedTask;
- }
-
- ///
- public Task DeleteSessionAsync(string tokenHash, CancellationToken ct)
- {
- _sessions.RemoveAll(s => s.TokenHash == tokenHash);
- _calls.Add($"delete-session:{tokenHash}");
- return Task.CompletedTask;
- }
-
- ///
- public Task DeleteExpiredSessionsAsync(CancellationToken ct)
- {
- _sessions.RemoveAll(s => s.ExpiresAt <= DateTimeOffset.UtcNow);
- _calls.Add("delete-expired");
- return Task.CompletedTask;
- }
-}
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantRepository.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantRepository.cs
deleted file mode 100644
index 86a4192..0000000
--- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantRepository.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using Deal.Modules.Tenants.Application.Abstractions;
-using Deal.Modules.Tenants.Application.Models;
-
-namespace Deal.Tests.Unit.Modules.Tenants;
-
-///
-/// In-memory реализация для unit-тестов
-///
-public sealed class FakeTenantRepository : ITenantRepository
-{
- private readonly IReadOnlyList _tenants;
- private readonly List<(int Offset, int Limit)> _pageRequests = [];
-
- ///
- /// Создаёт реестр с фиксированным списком тенантов.
- ///
- /// Тенанты реестра (как строки public.tenants).
- public FakeTenantRepository(params TenantRecordDto[] tenants)
- {
- _tenants = tenants.ToList();
- }
-
- ///
- /// Запрошенные страницы (offset, limit) в порядке обращений.
- ///
- public IReadOnlyList<(int Offset, int Limit)> PageRequests => _pageRequests;
-
- ///
- public Task> ListAsync(CancellationToken ct) =>
- Task.FromResult>(_tenants.ToList());
-
- ///
- public Task> ListPageAsync(
- int offset,
- int limit,
- CancellationToken ct)
- {
- _pageRequests.Add((offset, limit));
- IReadOnlyList page = _tenants.Skip(offset).Take(limit).ToList();
- return Task.FromResult(page);
- }
-
- ///
- public Task FindByIdAsync(Guid id, CancellationToken ct) =>
- throw new NotSupportedException("FindByIdAsync не используется тестами реестра");
-
- ///
- public Task UpdateStatusAsync(
- Guid id,
- string status,
- CancellationToken ct) =>
- throw new NotSupportedException("UpdateStatusAsync не используется тестами реестра");
-
- ///
- public Task CreateAsync(TenantRecordDto tenant, CancellationToken ct) =>
- throw new NotSupportedException("CreateAsync не используется тестами реестра");
-}
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantStore.cs
deleted file mode 100644
index 24e12b6..0000000
--- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantStore.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-using Deal.Modules.Tenants.Application.Abstractions;
-using Deal.Modules.Tenants.Application.Models;
-
-namespace Deal.Tests.Unit.Modules.Tenants;
-
-///
-/// In-memory реализация с поддержкой создания тенантов.
-///
-public sealed class FakeTenantStore : ITenantRepository
-{
- private readonly List _tenants;
-
- ///
- /// Создаёт хранилище, опционально с предзаполненным реестром тенантов.
- ///
- /// Тенанты, которые уже есть в реестре (как строки public.tenants).
- public FakeTenantStore(params TenantRecordDto[] seed)
- {
- _tenants = seed.ToList();
- }
-
- ///
- /// Записи реестра в порядке добавления.
- ///
- public IReadOnlyList Tenants => _tenants;
-
- ///
- public Task FindByIdAsync(Guid id, CancellationToken ct) =>
- Task.FromResult(_tenants.FirstOrDefault(tenant => tenant.Id == id));
-
- ///
- public Task CreateAsync(TenantRecordDto tenant, CancellationToken ct)
- {
- _tenants.Add(tenant);
- return Task.CompletedTask;
- }
-
- ///
- public Task> ListAsync(CancellationToken ct) =>
- Task.FromResult>(_tenants.ToList());
-
- ///
- public Task> ListPageAsync(
- int offset,
- int limit,
- CancellationToken ct)
- {
- IReadOnlyList page = _tenants.Skip(offset).Take(limit).ToList();
- return Task.FromResult(page);
- }
-
- ///
- public Task UpdateStatusAsync(
- Guid id,
- string status,
- CancellationToken ct)
- {
- int index = _tenants.FindIndex(tenant => tenant.Id == id);
- if (index < 0)
- {
- return Task.FromResult(false);
- }
-
- var current = _tenants[index];
- _tenants[index] = current with { Status = status };
- return Task.FromResult(true);
- }
-}
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTokenUsageEventStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTokenUsageEventStore.cs
deleted file mode 100644
index 6394a0b..0000000
--- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTokenUsageEventStore.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using Deal.Modules.Tenants.Application.Abstractions;
-using Deal.Modules.Tenants.Application.Models;
-
-namespace Deal.Tests.Unit.Modules.Tenants;
-
-///
-/// In-memory реализация для юнит/HTTP-тестов.
-///
-public sealed class FakeTokenUsageEventStore : ITokenUsageEventStore
-{
- private readonly List _records = [];
-
- ///
- /// Записи хранилища в порядке добавления.
- ///
- public IReadOnlyList Records => _records;
-
- ///
- public Task AppendAsync(TokenUsageEventDto record, CancellationToken ct)
- {
- _records.Add(record);
- return Task.CompletedTask;
- }
-
- ///
- public Task> AggregateAsync(TokenUsageEventQueryDto query, CancellationToken ct)
- {
- IEnumerable filtered = _records.Where(record =>
- (query.TenantId is null || record.TenantId == query.TenantId) &&
- (string.IsNullOrWhiteSpace(query.Provider) || record.Provider == query.Provider) &&
- (string.IsNullOrWhiteSpace(query.Model) || record.Model == query.Model) &&
- (string.IsNullOrWhiteSpace(query.Kind) || record.Kind == query.Kind) &&
- (query.From is null || record.At >= query.From.Value) &&
- (query.To is null || record.At <= query.To.Value));
-
- IReadOnlyList result = query.GroupBy switch
- {
- TokenUsageGroupBys.Day => GroupByDay(filtered),
- TokenUsageGroupBys.Tenant => GroupByString(filtered, record => record.TenantId.ToString("D")),
- TokenUsageGroupBys.Provider => GroupByString(filtered, record => record.Provider),
- TokenUsageGroupBys.Model => GroupByString(filtered, record => record.Model),
- _ => throw new ArgumentException($"Неизвестная группировка: '{query.GroupBy}'.", nameof(query)),
- };
- return Task.FromResult(result);
- }
-
- private static IReadOnlyList GroupByDay(IEnumerable source) =>
- source
- .GroupBy(record => record.At.UtcDateTime.Date)
- .OrderBy(group => group.Key)
- .Select(group => ToAggregate(group.Key.ToString("yyyy-MM-dd"), group))
- .ToList();
-
- private static IReadOnlyList GroupByString(IEnumerable source, Func keySelector) =>
- source
- .GroupBy(keySelector)
- .Select(group => ToAggregate(group.Key, group))
- .OrderByDescending(row => row.TotalTokens)
- .ToList();
-
- private static TokenUsageAggregateDto ToAggregate(string key, IEnumerable group)
- {
- List rows = group.ToList();
- return new TokenUsageAggregateDto(
- key,
- rows.Sum(row => row.PromptTokens),
- rows.Sum(row => row.CompletionTokens),
- rows.Sum(row => row.TotalTokens),
- rows.Count);
- }
-}
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs
index 2b4bc8e..6ac3d1b 100644
--- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/JoinFlowTests.cs
@@ -29,7 +29,7 @@ public sealed class JoinFlowTests
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
var provisioner = new TestTenantProvisioner();
var tenantStore = new TestTenantStore();
- var authStore = new FakeAuthStore();
+ var authStore = new TestAuthStore();
var passwordHasher = TestHashers.New();
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, passwordHasher);
@@ -70,7 +70,7 @@ public sealed class JoinFlowTests
var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
var tenantStore = new TestTenantStore();
- var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, new FakeAuthStore(), TestHashers.New());
+ var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, new TestAuthStore(), TestHashers.New());
var result = await service.ActivateAsync(Code, Email, name: " ", Password, CancellationToken.None);
@@ -87,7 +87,7 @@ public sealed class JoinFlowTests
var provisioner = new TestTenantProvisioner();
// Активный тенант уже существует (создан оператором ранее) — join присоединяется к нему.
var tenantStore = new TestTenantStore(new TenantRecordDto(tenantId, "Existing", TenantStatuses.Active, DateTimeOffset.UtcNow));
- var authStore = new FakeAuthStore();
+ var authStore = new TestAuthStore();
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
@@ -108,7 +108,7 @@ public sealed class JoinFlowTests
var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId));
var tenantStore = new TestTenantStore(); // целевого тенанта в реестре нет — «битый» инвайт
- var authStore = new FakeAuthStore();
+ var authStore = new TestAuthStore();
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
@@ -126,7 +126,7 @@ public sealed class JoinFlowTests
var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: tenantId));
var tenantStore = new TestTenantStore(new TenantRecordDto(tenantId, "Suspended", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
- var authStore = new FakeAuthStore();
+ var authStore = new TestAuthStore();
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
@@ -142,7 +142,7 @@ public sealed class JoinFlowTests
{
var inviteStore = new TestInviteStore();
var tenantStore = new TestTenantStore();
- var authStore = new FakeAuthStore();
+ var authStore = new TestAuthStore();
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
var result = await service.ActivateAsync(WrongCode, Email, name: null, Password, CancellationToken.None);
@@ -159,7 +159,7 @@ public sealed class JoinFlowTests
var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, expiresAt: DateTimeOffset.UtcNow.AddHours(-1)));
var tenantStore = new TestTenantStore();
- var authStore = new FakeAuthStore();
+ var authStore = new TestAuthStore();
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
@@ -176,7 +176,7 @@ public sealed class JoinFlowTests
{
var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Activated));
- var service = NewService(inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), TestHashers.New());
+ var service = NewService(inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new TestAuthStore(), TestHashers.New());
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
@@ -189,7 +189,7 @@ public sealed class JoinFlowTests
{
var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Revoked));
- var service = NewService(inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new FakeAuthStore(), TestHashers.New());
+ var service = NewService(inviteStore, new TestTenantStore(), new TestTenantProvisioner().Provisioner, new TestAuthStore(), TestHashers.New());
var result = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
@@ -203,7 +203,7 @@ public sealed class JoinFlowTests
var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
var tenantStore = new TestTenantStore();
- var authStore = new FakeAuthStore();
+ var authStore = new TestAuthStore();
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
var result = await service.ActivateAsync(Code, OtherEmail, name: null, Password, CancellationToken.None);
@@ -221,7 +221,7 @@ public sealed class JoinFlowTests
var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
var tenantStore = new TestTenantStore();
- var authStore = new FakeAuthStore();
+ var authStore = new TestAuthStore();
authStore.AddUser(new StoredUserDto(Guid.NewGuid(), Email, TenantId: Guid.NewGuid(), Status: StatusActive, "hash"));
var provisioner = new TestTenantProvisioner();
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
@@ -248,7 +248,7 @@ public sealed class JoinFlowTests
var inviteStore = new TestInviteStore();
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
var tenantStore = new TestTenantStore();
- var authStore = new FakeAuthStore();
+ var authStore = new TestAuthStore();
var service = NewService(inviteStore, tenantStore, new TestTenantProvisioner().Provisioner, authStore, TestHashers.New());
var result = await service.ActivateAsync(Code, Email, name: null, password, CancellationToken.None);
@@ -267,7 +267,7 @@ public sealed class JoinFlowTests
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null));
var provisioner = new TestTenantProvisioner();
var tenantStore = new TestTenantStore();
- var authStore = new FakeAuthStore();
+ var authStore = new TestAuthStore();
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
JoinResultDto first = await service.ActivateAsync(Code, Email, name: null, Password, CancellationToken.None);
@@ -288,7 +288,7 @@ public sealed class JoinFlowTests
// Конкурент (отзыв) «успел» до нашего CAS — приглашение уже отозвано, активация не выполнится.
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Revoked));
var tenantStore = new TestTenantStore();
- var authStore = new FakeAuthStore();
+ var authStore = new TestAuthStore();
var provisioner = new TestTenantProvisioner();
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
@@ -311,7 +311,7 @@ public sealed class JoinFlowTests
// Конкурентная активация «успела» до нашего CAS — приглашение уже активировано.
inviteStore.AddInvite(NewInvite(Code, Email, tenantId: null, status: InviteStatuses.Activated));
var tenantStore = new TestTenantStore();
- var authStore = new FakeAuthStore();
+ var authStore = new TestAuthStore();
var provisioner = new TestTenantProvisioner();
var service = NewService(inviteStore, tenantStore, provisioner.Provisioner, authStore, TestHashers.New());
@@ -353,17 +353,17 @@ public sealed class JoinFlowTests
// ─── Хелперы ─────────────────────────────────────────────────────────
- // Собирает JoinService на общих фейк-хранилищах (все состояния — свежие).
+ // Собирает JoinService на общих подставках хранилищ (все состояния — свежие).
private static JoinService NewService(
TestInviteStore inviteStore,
TestTenantStore tenantStore,
ITenantProvisioner provisioner,
- FakeAuthStore authStore,
+ TestAuthStore authStore,
IPasswordHasher passwordHasher) =>
new(
new InvitesService(inviteStore.Store),
new TenantService(tenantStore.Repository, provisioner),
- authStore,
+ authStore.Store,
passwordHasher,
tenantStore.Repository);
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorAuthServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorAuthServiceTests.cs
index 4603707..75de423 100644
--- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorAuthServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorAuthServiceTests.cs
@@ -14,15 +14,15 @@ public sealed class OperatorAuthServiceTests
private static readonly Guid OperatorId = Guid.NewGuid();
- private readonly FakeOperatorAuthStore _store;
+ private readonly TestOperatorAuthStore _store;
private readonly OperatorAuthService _service;
public OperatorAuthServiceTests()
{
var passwordHasher = TestHashers.New();
- _store = new FakeOperatorAuthStore();
+ _store = new TestOperatorAuthStore();
_store.AddOperator(new StoredOperatorDto(OperatorId, OperatorLogin, "active", passwordHasher.Hash(OperatorPassword)));
- _service = new OperatorAuthService(_store, passwordHasher);
+ _service = new OperatorAuthService(_store.Store, passwordHasher);
}
[Fact]
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorBootstrapServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorBootstrapServiceTests.cs
index c44ed26..02b5a94 100644
--- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorBootstrapServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/OperatorBootstrapServiceTests.cs
@@ -10,15 +10,15 @@ namespace Deal.Tests.Unit.Modules.Tenants;
///
public sealed class OperatorBootstrapServiceTests
{
- private readonly FakeOperatorAuthStore _store;
+ private readonly TestOperatorAuthStore _store;
private readonly IPasswordHasher _passwordHasher;
private readonly OperatorBootstrapService _service;
public OperatorBootstrapServiceTests()
{
_passwordHasher = TestHashers.New();
- _store = new FakeOperatorAuthStore();
- _service = new OperatorBootstrapService(_store, _passwordHasher);
+ _store = new TestOperatorAuthStore();
+ _service = new OperatorBootstrapService(_store.Store, _passwordHasher);
}
[Fact]
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/SuspiciousActivityServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/SuspiciousActivityServiceTests.cs
index 1e2498b..c54c787 100644
--- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/SuspiciousActivityServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/SuspiciousActivityServiceTests.cs
@@ -1,5 +1,6 @@
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Services;
+using Deal.Tests.Unit.Support;
namespace Deal.Tests.Unit.Modules.Tenants;
@@ -14,7 +15,7 @@ public sealed class SuspiciousActivityServiceTests
[Fact]
public async Task AnalyzeAsync_NoRecords_ReturnsEmpty()
{
- SuspiciousActivityService service = Create(new FakeAuditLogStore());
+ SuspiciousActivityService service = Create(new TestAuditLogStore());
SuspiciousActivityDto report = await service.AnalyzeAsync(null, null, CancellationToken.None);
@@ -26,7 +27,7 @@ public sealed class SuspiciousActivityServiceTests
[Fact]
public async Task AnalyzeAsync_FailedLoginsPerIp_TriggersAtThreshold()
{
- var store = new FakeAuditLogStore();
+ var store = new TestAuditLogStore();
for (int i = 0; i < SuspiciousActivityService.FailedLoginsPerIpThreshold; i++)
{
SeedFailed(store, ip: "10.0.0.1", login: "user", minutesAgo: i);
@@ -46,7 +47,7 @@ public sealed class SuspiciousActivityServiceTests
[Fact]
public async Task AnalyzeAsync_FailedLoginsPerIp_DoubleThreshold_IsHigh()
{
- var store = new FakeAuditLogStore();
+ var store = new TestAuditLogStore();
int count = SuspiciousActivityService.FailedLoginsPerIpThreshold * 2;
for (int i = 0; i < count; i++)
{
@@ -66,7 +67,7 @@ public sealed class SuspiciousActivityServiceTests
[Fact]
public async Task AnalyzeAsync_FailedLoginsPerLogin_Triggers()
{
- var store = new FakeAuditLogStore();
+ var store = new TestAuditLogStore();
for (int i = 0; i < SuspiciousActivityService.FailedLoginsPerLoginThreshold; i++)
{
SeedFailed(store, ip: $"10.0.0.{i + 1}", login: "target", minutesAgo: i);
@@ -84,11 +85,11 @@ public sealed class SuspiciousActivityServiceTests
[Fact]
public async Task AnalyzeAsync_ManyIpsPerActor_Triggers()
{
- var store = new FakeAuditLogStore();
+ var store = new TestAuditLogStore();
Guid actor = Guid.NewGuid();
for (int i = 0; i < SuspiciousActivityService.DistinctIpsPerActorThreshold; i++)
{
- await store.AppendAsync(
+ await store.Store.AppendAsync(
new AuditRecordDto(
AuditEvents.TenantLoginOk,
AuditActorTypes.Tenant,
@@ -113,10 +114,10 @@ public sealed class SuspiciousActivityServiceTests
[Fact]
public async Task AnalyzeAsync_AuthFailuresPerTenant_Triggers()
{
- var store = new FakeAuditLogStore();
+ var store = new TestAuditLogStore();
for (int i = 0; i < SuspiciousActivityService.AuthFailuresPerTenantThreshold; i++)
{
- await store.AppendAsync(
+ await store.Store.AppendAsync(
new AuditRecordDto(
AuditEvents.TenantLoginFailed,
AuditActorTypes.Tenant,
@@ -140,7 +141,7 @@ public sealed class SuspiciousActivityServiceTests
[Fact]
public async Task AnalyzeAsync_DistinctLoginsPerIp_TriggersAtThreshold()
{
- var store = new FakeAuditLogStore();
+ var store = new TestAuditLogStore();
for (int i = 0; i < SuspiciousActivityService.DistinctLoginsPerIpThreshold; i++)
{
SeedFailed(store, ip: "10.3.0.1", login: $"user{i}", minutesAgo: i);
@@ -160,7 +161,7 @@ public sealed class SuspiciousActivityServiceTests
[Fact]
public async Task AnalyzeAsync_DistinctLoginsPerIp_DoubleThreshold_IsHigh()
{
- var store = new FakeAuditLogStore();
+ var store = new TestAuditLogStore();
int count = SuspiciousActivityService.DistinctLoginsPerIpThreshold * 2;
for (int i = 0; i < count; i++)
{
@@ -180,7 +181,7 @@ public sealed class SuspiciousActivityServiceTests
[Fact]
public async Task AnalyzeAsync_DistinctLoginsPerIp_BelowThreshold_NoFinding()
{
- var store = new FakeAuditLogStore();
+ var store = new TestAuditLogStore();
for (int i = 0; i < SuspiciousActivityService.DistinctLoginsPerIpThreshold - 1; i++)
{
SeedFailed(store, ip: "10.3.0.3", login: $"user{i}", minutesAgo: i);
@@ -195,7 +196,7 @@ public sealed class SuspiciousActivityServiceTests
[Fact]
public async Task AnalyzeAsync_DistinctLoginsPerIp_SameLoginRepeated_NoFinding()
{
- var store = new FakeAuditLogStore();
+ var store = new TestAuditLogStore();
for (int i = 0; i < SuspiciousActivityService.DistinctLoginsPerIpThreshold + 1; i++)
{
SeedFailed(store, ip: "10.3.0.4", login: "repeated", minutesAgo: i);
@@ -210,7 +211,7 @@ public sealed class SuspiciousActivityServiceTests
[Fact]
public async Task AnalyzeAsync_DistinctLoginsPerIp_SuccessfulLoginsIgnored()
{
- var store = new FakeAuditLogStore();
+ var store = new TestAuditLogStore();
for (int i = 0; i < SuspiciousActivityService.DistinctLoginsPerIpThreshold; i++)
{
SeedSuccess(store, ip: "10.3.0.5", login: $"user{i}", minutesAgo: i);
@@ -225,7 +226,7 @@ public sealed class SuspiciousActivityServiceTests
[Fact]
public async Task AnalyzeAsync_RecordsOutsideWindow_AreIgnored()
{
- var store = new FakeAuditLogStore();
+ var store = new TestAuditLogStore();
for (int i = 0; i < 50; i++)
{
SeedFailed(store, ip: "10.0.0.1", login: "user", minutesAgo: 60 * 30); // 30 часов назад — вне суток
@@ -239,12 +240,12 @@ public sealed class SuspiciousActivityServiceTests
// Пишет запись «неудачный вход» с заданным временем.
private static void SeedFailed(
- FakeAuditLogStore store,
+ TestAuditLogStore store,
string ip,
string login,
int minutesAgo)
{
- store.AppendAsync(
+ store.Store.AppendAsync(
new AuditRecordDto(
AuditEvents.TenantLoginFailed,
AuditActorTypes.Tenant,
@@ -258,12 +259,12 @@ public sealed class SuspiciousActivityServiceTests
// Пишет запись «успешный вход» с заданным временем.
private static void SeedSuccess(
- FakeAuditLogStore store,
+ TestAuditLogStore store,
string ip,
string login,
int minutesAgo)
{
- store.AppendAsync(
+ store.Store.AppendAsync(
new AuditRecordDto(
AuditEvents.TenantLoginOk,
AuditActorTypes.Tenant,
@@ -275,5 +276,5 @@ public sealed class SuspiciousActivityServiceTests
CancellationToken.None);
}
- private static SuspiciousActivityService Create(FakeAuditLogStore store) => new(store, () => Now);
+ private static SuspiciousActivityService Create(TestAuditLogStore store) => new(store.Store, () => Now);
}
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs
index b56cfb3..e0cb343 100644
--- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs
+++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TenantAdminServiceTests.cs
@@ -18,7 +18,7 @@ public sealed class TenantAdminServiceTests
{
var tenantStore = new TestTenantStore();
var provisioner = new TestTenantProvisioner();
- var service = NewService(tenantStore, new FakeAuthStore(), provisioner);
+ var service = NewService(tenantStore, new TestAuthStore(), provisioner);
TenantCreateResultDto result = await service.CreateAsync(" Новый тенант ", email: null, CancellationToken.None);
@@ -42,7 +42,7 @@ public sealed class TenantAdminServiceTests
public async Task CreateAsync_WithEmail_CreatesOwnerWithOneTimePassword()
{
var tenantStore = new TestTenantStore();
- var authStore = new FakeAuthStore();
+ var authStore = new TestAuthStore();
var hasher = TestHashers.New();
var service = NewService(tenantStore, authStore, new TestTenantProvisioner(), hasher);
@@ -66,7 +66,7 @@ public sealed class TenantAdminServiceTests
public async Task CreateAsync_WithTakenEmail_ReturnsEmailTakenWithoutTenant()
{
var tenantStore = new TestTenantStore();
- var authStore = new FakeAuthStore();
+ var authStore = new TestAuthStore();
authStore.AddUser(NewUser(Guid.NewGuid(), "owner@example.com"));
var service = NewService(tenantStore, authStore);
@@ -82,7 +82,7 @@ public sealed class TenantAdminServiceTests
public async Task CreateAsync_WithInvalidEmail_ReturnsInvalidEmail()
{
var tenantStore = new TestTenantStore();
- var service = NewService(tenantStore, new FakeAuthStore());
+ var service = NewService(tenantStore, new TestAuthStore());
TenantCreateResultDto result = await service.CreateAsync("Тенант", "not-an-email", CancellationToken.None);
@@ -95,7 +95,7 @@ public sealed class TenantAdminServiceTests
public async Task CreateAsync_WithEmptyName_ReturnsNameRequired()
{
var tenantStore = new TestTenantStore();
- var service = NewService(tenantStore, new FakeAuthStore());
+ var service = NewService(tenantStore, new TestAuthStore());
TenantCreateResultDto result = await service.CreateAsync(" ", email: null, CancellationToken.None);
@@ -110,7 +110,7 @@ public sealed class TenantAdminServiceTests
var tenantStore = new TestTenantStore(
new TenantRecordDto(FirstTenantId, "Первый", TenantStatuses.Active, DateTimeOffset.UtcNow.AddDays(-2)),
new TenantRecordDto(SecondTenantId, "Второй", TenantStatuses.Suspended, DateTimeOffset.UtcNow.AddDays(-1)));
- var authStore = new FakeAuthStore();
+ var authStore = new TestAuthStore();
authStore.AddUser(NewUser(FirstTenantId, "a@example.com"));
authStore.AddUser(NewUser(FirstTenantId, "b@example.com"));
authStore.AddUser(NewUser(Guid.NewGuid(), "other@example.com"));
@@ -132,7 +132,7 @@ public sealed class TenantAdminServiceTests
public async Task GetAsync_ForExistingTenant_ReturnsDetailWithUsers()
{
var tenantStore = new TestTenantStore(new TenantRecordDto(FirstTenantId, "Первый", TenantStatuses.Active, DateTimeOffset.UtcNow));
- var authStore = new FakeAuthStore();
+ var authStore = new TestAuthStore();
authStore.AddUser(NewUser(FirstTenantId, "a@example.com"));
authStore.AddUser(NewUser(Guid.NewGuid(), "other@example.com"));
var service = NewService(tenantStore, authStore);
@@ -150,7 +150,7 @@ public sealed class TenantAdminServiceTests
[Fact]
public async Task GetAsync_ForUnknownTenant_ReturnsNull()
{
- var service = NewService(new TestTenantStore(), new FakeAuthStore());
+ var service = NewService(new TestTenantStore(), new TestAuthStore());
Assert.Null(await service.GetAsync(Guid.NewGuid(), CancellationToken.None));
}
@@ -159,7 +159,7 @@ public sealed class TenantAdminServiceTests
public async Task ChangeStatusAsync_Suspend_UpdatesTenantAndReportsChanged()
{
var tenantStore = new TestTenantStore(new TenantRecordDto(FirstTenantId, "Первый", TenantStatuses.Active, DateTimeOffset.UtcNow));
- var service = NewService(tenantStore, new FakeAuthStore());
+ var service = NewService(tenantStore, new TestAuthStore());
TenantStatusChangeResultDto result = await service.ChangeStatusAsync(FirstTenantId, TenantStatuses.Suspended, CancellationToken.None);
@@ -174,7 +174,7 @@ public sealed class TenantAdminServiceTests
public async Task ChangeStatusAsync_WhenAlreadyInStatus_ReturnsOkWithoutChange()
{
var tenantStore = new TestTenantStore(new TenantRecordDto(FirstTenantId, "Первый", TenantStatuses.Suspended, DateTimeOffset.UtcNow));
- var service = NewService(tenantStore, new FakeAuthStore());
+ var service = NewService(tenantStore, new TestAuthStore());
// Повторный suspend приостановленного — Ok без изменения (аудит не дублируется, идемпотентность).
TenantStatusChangeResultDto result = await service.ChangeStatusAsync(FirstTenantId, TenantStatuses.Suspended, CancellationToken.None);
@@ -187,7 +187,7 @@ public sealed class TenantAdminServiceTests
[Fact]
public async Task ChangeStatusAsync_ForUnknownTenant_ReturnsNotFound()
{
- var service = NewService(new TestTenantStore(), new FakeAuthStore());
+ var service = NewService(new TestTenantStore(), new TestAuthStore());
TenantStatusChangeResultDto result = await service.ChangeStatusAsync(Guid.NewGuid(), TenantStatuses.Suspended, CancellationToken.None);
@@ -196,15 +196,15 @@ public sealed class TenantAdminServiceTests
Assert.Null(result.Tenant);
}
- // Сервис на фейк-хранилищах (владелец-пользователи создаются через FakeAuthStore и хэшер-подставку).
+ // Сервис на подставках хранилищ (владелец-пользователи создаются через TestAuthStore и хэшер-подставку).
private static TenantAdminService NewService(
TestTenantStore tenantStore,
- FakeAuthStore authStore,
+ TestAuthStore authStore,
TestTenantProvisioner? provisioner = null,
IPasswordHasher? hasher = null)
{
var tenantService = new TenantService(tenantStore.Repository, provisioner?.Provisioner ?? new TestTenantProvisioner().Provisioner);
- return new TenantAdminService(tenantStore.Repository, authStore, tenantService, hasher ?? TestHashers.New());
+ return new TenantAdminService(tenantStore.Repository, authStore.Store, tenantService, hasher ?? TestHashers.New());
}
// Пользователь тенанта (активный, без хэша — для чтения).
diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantLimitStore.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TestTenantLimitStore.cs
similarity index 72%
rename from src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantLimitStore.cs
rename to src/core/tests/Deal.Tests.Unit/Modules/Tenants/TestTenantLimitStore.cs
index cb1cb2c..ff224e1 100644
--- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/FakeTenantLimitStore.cs
+++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/TestTenantLimitStore.cs
@@ -1,13 +1,15 @@
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.Modules.Tenants.Application.Models;
using Deal.Modules.Tenants.Application.Services;
+using NSubstitute;
namespace Deal.Tests.Unit.Modules.Tenants;
///
-/// In-memory реализация для юнит-тестов recorder'а и бюджетного гейта/алертов
+/// Подставка для юнит-тестов recorder'а и бюджетного гейта/алертов:
+/// сервисы получают NSubstitute-подставку (), тесты управляют сценарием через свойства
///
-public sealed class FakeTenantLimitStore : ITenantLimitStore
+public sealed class TestTenantLimitStore
{
private sealed class Row
{
@@ -28,26 +30,46 @@ public sealed class FakeTenantLimitStore : ITenantLimitStore
private readonly Dictionary _rows = new();
private readonly TokenBudgetService _budgetService = new();
+
+ ///
+ /// Подставка порта лимитов (создаётся в конструкторе).
+ ///
+ public ITenantLimitStore Store { get; }
private readonly Func _utcNow;
///
- /// Создаёт фейк с системными часами и статусом тенанта active.
+ /// Создаёт подставку с системными часами и статусом тенанта active.
///
- public FakeTenantLimitStore()
+ public TestTenantLimitStore()
: this(() => DateTimeOffset.UtcNow, TenantStatuses.Active)
{
}
///
- /// Создаёт фейк с заданными часами и статусом тенанта
+ /// Создаёт подставку с заданными часами и статусом тенанта
///
/// Источник текущего времени (UTC).
/// Статус тенанта для всех строк (константа ).
- public FakeTenantLimitStore(Func utcNow, string tenantStatus = TenantStatuses.Active)
+ public TestTenantLimitStore(Func