Хелпер Support/TestDiscoveryStore сгенерирован из фейка: состояние (tasks/ candidates/blacklist/log/monitored) сохранено, 29 методов порта читают состояние на момент вызова, неожиданных методов нет. Потребители (8 файлов) перетипизированы на .Store, фейк удалён, тесты 1340 зелёные.
This commit is contained in:
@@ -35,8 +35,8 @@ public sealed class DiscoveryWorkerSchedulerTests
|
|||||||
// Контекст теста: планировщик на общих фейках + хранилища/гейты тенантов.
|
// Контекст теста: планировщик на общих фейках + хранилища/гейты тенантов.
|
||||||
private sealed record Context(
|
private sealed record Context(
|
||||||
DiscoveryWorkerScheduler Scheduler,
|
DiscoveryWorkerScheduler Scheduler,
|
||||||
FakeDiscoveryStore StoreA,
|
TestDiscoveryStore StoreA,
|
||||||
FakeDiscoveryStore StoreB,
|
TestDiscoveryStore StoreB,
|
||||||
TestDiscoveryGateway GatewayA,
|
TestDiscoveryGateway GatewayA,
|
||||||
TestDiscoveryGateway GatewayB,
|
TestDiscoveryGateway GatewayB,
|
||||||
TenantContext TenantContext,
|
TenantContext TenantContext,
|
||||||
@@ -92,8 +92,8 @@ public sealed class DiscoveryWorkerSchedulerTests
|
|||||||
{
|
{
|
||||||
var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
|
var tenants = new TestTenantRepository(Tenant(TenantA), Tenant(TenantB));
|
||||||
var tenantContext = new TenantContext();
|
var tenantContext = new TenantContext();
|
||||||
var storeA = new FakeDiscoveryStore();
|
var storeA = new TestDiscoveryStore();
|
||||||
var storeB = new FakeDiscoveryStore();
|
var storeB = new TestDiscoveryStore();
|
||||||
var settingsA = new FakeSettingsStore();
|
var settingsA = new FakeSettingsStore();
|
||||||
var settingsB = new FakeSettingsStore();
|
var settingsB = new FakeSettingsStore();
|
||||||
var gatewayA = new TestDiscoveryGateway();
|
var gatewayA = new TestDiscoveryGateway();
|
||||||
@@ -108,7 +108,7 @@ public sealed class DiscoveryWorkerSchedulerTests
|
|||||||
services.AddSingleton<IAiTools>(_ => TestAiTools.New());
|
services.AddSingleton<IAiTools>(_ => TestAiTools.New());
|
||||||
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
|
// Тенант-scoped адаптеры: фейк выбирает хранилище по ITenantContext, который цикл заполняет SetTenant
|
||||||
// (эталон PipelineWorkerSchedulerTests/ConnectionStringProvider.ForTenant).
|
// (эталон PipelineWorkerSchedulerTests/ConnectionStringProvider.ForTenant).
|
||||||
services.AddScoped<IDiscoveryStore>(provider => TenantOf(provider) == TenantA ? storeA : storeB);
|
services.AddScoped<IDiscoveryStore>(provider => TenantOf(provider) == TenantA ? storeA.Store : storeB.Store);
|
||||||
services.AddScoped<ISettingsStore>(provider => TenantOf(provider) == TenantA ? settingsA : settingsB);
|
services.AddScoped<ISettingsStore>(provider => TenantOf(provider) == TenantA ? settingsA : settingsB);
|
||||||
services.AddScoped<ITelegramGateway>(provider => TenantOf(provider) == TenantA ? gatewayA.Gateway : gatewayB.Gateway);
|
services.AddScoped<ITelegramGateway>(provider => TenantOf(provider) == TenantA ? gatewayA.Gateway : gatewayB.Gateway);
|
||||||
// Реальные сервисы модуля Discovery — как AddDiscoveryModule в Program.cs: цикл резолвит их в tenant-scope.
|
// Реальные сервисы модуля Discovery — как AddDiscoveryModule в Program.cs: цикл резолвит их в tenant-scope.
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ public sealed class DiscoveryWorkerServiceTests
|
|||||||
|
|
||||||
// Контекст теста: воркер на фейках + ссылки для проверок состояния.
|
// Контекст теста: воркер на фейках + ссылки для проверок состояния.
|
||||||
private sealed record Fixture(
|
private sealed record Fixture(
|
||||||
FakeDiscoveryStore Store,
|
TestDiscoveryStore Store,
|
||||||
FakeSettingsStore Settings,
|
FakeSettingsStore Settings,
|
||||||
TestDiscoveryGateway Gateway,
|
TestDiscoveryGateway Gateway,
|
||||||
IDiscoveryPacer Pacer,
|
IDiscoveryPacer Pacer,
|
||||||
@@ -428,22 +428,22 @@ public sealed class DiscoveryWorkerServiceTests
|
|||||||
// Создаёт воркер на общих фейках; ML/ИИ выключены — оценка идёт эвристикой по ключам.
|
// Создаёт воркер на общих фейках; ML/ИИ выключены — оценка идёт эвристикой по ключам.
|
||||||
private static Fixture Create()
|
private static Fixture Create()
|
||||||
{
|
{
|
||||||
var store = new FakeDiscoveryStore();
|
var store = new TestDiscoveryStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
settings.Preload(SettingsKeys.MlEnabled, "false");
|
settings.Preload(SettingsKeys.MlEnabled, "false");
|
||||||
settings.Preload(SettingsKeys.AiEnabled, "false");
|
settings.Preload(SettingsKeys.AiEnabled, "false");
|
||||||
var ml = new FakeMlClient();
|
var ml = new FakeMlClient();
|
||||||
var ai = TestAiTools.New();
|
var ai = TestAiTools.New();
|
||||||
var log = new DiscoveryLogService(store);
|
var log = new DiscoveryLogService(store.Store);
|
||||||
var blacklist = new DiscoveryBlacklistService(store);
|
var blacklist = new DiscoveryBlacklistService(store.Store);
|
||||||
var candidates = new DiscoveryCandidatesService(store, log, blacklist);
|
var candidates = new DiscoveryCandidatesService(store.Store, log, blacklist);
|
||||||
var planGuard = new DiscoveryPlanGuard(store, settings);
|
var planGuard = new DiscoveryPlanGuard(store.Store, settings);
|
||||||
var tasks = new DiscoveryTasksService(store, planGuard, settings);
|
var tasks = new DiscoveryTasksService(store.Store, planGuard, settings);
|
||||||
var banGuard = new DiscoveryBanGuard(store, settings);
|
var banGuard = new DiscoveryBanGuard(store.Store, settings);
|
||||||
var pacer = Substitute.For<IDiscoveryPacer>();
|
var pacer = Substitute.For<IDiscoveryPacer>();
|
||||||
var gateway = new TestDiscoveryGateway();
|
var gateway = new TestDiscoveryGateway();
|
||||||
var worker = new DiscoveryWorkerService(
|
var worker = new DiscoveryWorkerService(
|
||||||
store, tasks, candidates, log, new DiscoveryEvaluator(settings, ml, ai), banGuard, pacer, gateway.Gateway);
|
store.Store, tasks, candidates, log, new DiscoveryEvaluator(settings, ml, ai), banGuard, pacer, gateway.Gateway);
|
||||||
return new Fixture(store, settings, gateway, pacer, worker, tasks);
|
return new Fixture(store, settings, gateway, pacer, worker, tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Deal.Modules.Discovery.Application.Models;
|
using Deal.Modules.Discovery.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Discovery.Application.Services;
|
using Deal.Modules.Discovery.Application.Services;
|
||||||
using Deal.Modules.Settings.Application.Models;
|
using Deal.Modules.Settings.Application.Models;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
@@ -28,7 +29,7 @@ public sealed class DiscoveryBanGuardTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CanAutoJoin_LimitReachedToday_Blocked()
|
public async Task CanAutoJoin_LimitReachedToday_Blocked()
|
||||||
{
|
{
|
||||||
(DiscoveryBanGuard guard, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryBanGuard guard, TestDiscoveryStore store, _) = Create();
|
||||||
|
|
||||||
for (int i = 0; i < 50; i++)
|
for (int i = 0; i < 50; i++)
|
||||||
{
|
{
|
||||||
@@ -38,9 +39,9 @@ public sealed class DiscoveryBanGuardTests
|
|||||||
Assert.False(await guard.CanAutoJoinAsync(CancellationToken.None));
|
Assert.False(await guard.CanAutoJoinAsync(CancellationToken.None));
|
||||||
|
|
||||||
// Вчерашние вступления в суточную квоту не входят: 49 сегодня → ещё можно.
|
// Вчерашние вступления в суточную квоту не входят: 49 сегодня → ещё можно.
|
||||||
FakeDiscoveryStore yesterdayStore = new(() => Now);
|
TestDiscoveryStore yesterdayStore = new(() => Now);
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
var secondGuard = new DiscoveryBanGuard(yesterdayStore, settings, () => Now);
|
var secondGuard = new DiscoveryBanGuard(yesterdayStore.Store, settings, () => Now);
|
||||||
for (int i = 0; i < 49; i++)
|
for (int i = 0; i < 49; i++)
|
||||||
{
|
{
|
||||||
yesterdayStore.SeedLog(DiscoveryLogEvents.JoinAuto, TodayStart.AddMinutes(i));
|
yesterdayStore.SeedLog(DiscoveryLogEvents.JoinAuto, TodayStart.AddMinutes(i));
|
||||||
@@ -53,7 +54,7 @@ public sealed class DiscoveryBanGuardTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CountsOnlyJoinAutoEventsToday()
|
public async Task CountsOnlyJoinAutoEventsToday()
|
||||||
{
|
{
|
||||||
(DiscoveryBanGuard guard, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryBanGuard guard, TestDiscoveryStore store, _) = Create();
|
||||||
|
|
||||||
store.SeedLog(DiscoveryLogEvents.JoinAuto, TodayStart.AddHours(1));
|
store.SeedLog(DiscoveryLogEvents.JoinAuto, TodayStart.AddHours(1));
|
||||||
store.SeedLog(DiscoveryLogEvents.JoinAuto, TodayStart.AddHours(2));
|
store.SeedLog(DiscoveryLogEvents.JoinAuto, TodayStart.AddHours(2));
|
||||||
@@ -98,7 +99,7 @@ public sealed class DiscoveryBanGuardTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task CustomJoinLimit_AppliedWhenSettingStored()
|
public async Task CustomJoinLimit_AppliedWhenSettingStored()
|
||||||
{
|
{
|
||||||
(DiscoveryBanGuard guard, FakeDiscoveryStore store, FakeSettingsStore settings) = Create();
|
(DiscoveryBanGuard guard, TestDiscoveryStore store, FakeSettingsStore settings) = Create();
|
||||||
settings.Preload(SettingsKeys.DiscJoinLimit, "2");
|
settings.Preload(SettingsKeys.DiscJoinLimit, "2");
|
||||||
|
|
||||||
Assert.True(await guard.CanAutoJoinAsync(CancellationToken.None));
|
Assert.True(await guard.CanAutoJoinAsync(CancellationToken.None));
|
||||||
@@ -108,11 +109,11 @@ public sealed class DiscoveryBanGuardTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Создаёт связку гард + фейки на фиксированных «часах» (store и гард смотрят одно время).
|
// Создаёт связку гард + фейки на фиксированных «часах» (store и гард смотрят одно время).
|
||||||
private static (DiscoveryBanGuard Guard, FakeDiscoveryStore Store, FakeSettingsStore Settings) Create()
|
private static (DiscoveryBanGuard Guard, TestDiscoveryStore Store, FakeSettingsStore Settings) Create()
|
||||||
{
|
{
|
||||||
var store = new FakeDiscoveryStore(() => Now);
|
var store = new TestDiscoveryStore(() => Now);
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
var guard = new DiscoveryBanGuard(store, settings, () => Now);
|
var guard = new DiscoveryBanGuard(store.Store, settings, () => Now);
|
||||||
return (guard, store, settings);
|
return (guard, store, settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Deal.Modules.Discovery.Application.Models;
|
using Deal.Modules.Discovery.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Discovery.Application.Services;
|
using Deal.Modules.Discovery.Application.Services;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Discovery;
|
namespace Deal.Tests.Unit.Modules.Discovery;
|
||||||
@@ -11,7 +12,7 @@ public sealed class DiscoveryBlacklistServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_EmptyName_UsesDialogId()
|
public async Task Add_EmptyName_UsesDialogId()
|
||||||
{
|
{
|
||||||
(DiscoveryBlacklistService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryBlacklistService service, TestDiscoveryStore store) = Create();
|
||||||
|
|
||||||
DiscoveryBlacklistDto entry = await service.AddAsync("-1001", "", "отклонён", CancellationToken.None);
|
DiscoveryBlacklistDto entry = await service.AddAsync("-1001", "", "отклонён", CancellationToken.None);
|
||||||
|
|
||||||
@@ -24,8 +25,8 @@ public sealed class DiscoveryBlacklistServiceTests
|
|||||||
public async Task Add_OverwriteUpdatesNameAndReason_KeepsCreatedAt()
|
public async Task Add_OverwriteUpdatesNameAndReason_KeepsCreatedAt()
|
||||||
{
|
{
|
||||||
long tick = 0;
|
long tick = 0;
|
||||||
var store = new FakeDiscoveryStore(() => DateTimeOffset.FromUnixTimeMilliseconds(++tick * 1000L));
|
var store = new TestDiscoveryStore(() => DateTimeOffset.FromUnixTimeMilliseconds(++tick * 1000L));
|
||||||
var service = new DiscoveryBlacklistService(store);
|
var service = new DiscoveryBlacklistService(store.Store);
|
||||||
|
|
||||||
// Первая запись источника — CreatedAt фиксируется (t=1000).
|
// Первая запись источника — CreatedAt фиксируется (t=1000).
|
||||||
DiscoveryBlacklistDto first = await service.AddAsync("-1001", "Первое имя", "отклонено вручную", CancellationToken.None);
|
DiscoveryBlacklistDto first = await service.AddAsync("-1001", "Первое имя", "отклонено вручную", CancellationToken.None);
|
||||||
@@ -41,7 +42,7 @@ public sealed class DiscoveryBlacklistServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Remove_DeletesRow()
|
public async Task Remove_DeletesRow()
|
||||||
{
|
{
|
||||||
(DiscoveryBlacklistService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryBlacklistService service, TestDiscoveryStore store) = Create();
|
||||||
await service.AddAsync("-1001", "Канал", "причина", CancellationToken.None);
|
await service.AddAsync("-1001", "Канал", "причина", CancellationToken.None);
|
||||||
|
|
||||||
await service.RemoveAsync("-1001", CancellationToken.None);
|
await service.RemoveAsync("-1001", CancellationToken.None);
|
||||||
@@ -53,8 +54,8 @@ public sealed class DiscoveryBlacklistServiceTests
|
|||||||
public async Task List_ReturnsNewestFirst()
|
public async Task List_ReturnsNewestFirst()
|
||||||
{
|
{
|
||||||
long tick = 0;
|
long tick = 0;
|
||||||
var store = new FakeDiscoveryStore(() => DateTimeOffset.FromUnixTimeMilliseconds(++tick * 1000L));
|
var store = new TestDiscoveryStore(() => DateTimeOffset.FromUnixTimeMilliseconds(++tick * 1000L));
|
||||||
var service = new DiscoveryBlacklistService(store);
|
var service = new DiscoveryBlacklistService(store.Store);
|
||||||
|
|
||||||
await service.AddAsync("-1001", "Старый", "r1", CancellationToken.None); // t=1000
|
await service.AddAsync("-1001", "Старый", "r1", CancellationToken.None); // t=1000
|
||||||
await service.AddAsync("-1002", "Новый", "r2", CancellationToken.None); // t=2000
|
await service.AddAsync("-1002", "Новый", "r2", CancellationToken.None); // t=2000
|
||||||
@@ -65,9 +66,9 @@ public sealed class DiscoveryBlacklistServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Создаёт связку blacklist-сервиса + фейк.
|
// Создаёт связку blacklist-сервиса + фейк.
|
||||||
private static (DiscoveryBlacklistService Service, FakeDiscoveryStore Store) Create()
|
private static (DiscoveryBlacklistService Service, TestDiscoveryStore Store) Create()
|
||||||
{
|
{
|
||||||
var store = new FakeDiscoveryStore();
|
var store = new TestDiscoveryStore();
|
||||||
return (new DiscoveryBlacklistService(store), store);
|
return (new DiscoveryBlacklistService(store.Store), store);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-24
@@ -1,5 +1,6 @@
|
|||||||
using Deal.Modules.Discovery.Application.Exceptions;
|
using Deal.Modules.Discovery.Application.Exceptions;
|
||||||
using Deal.Modules.Discovery.Application.Models;
|
using Deal.Modules.Discovery.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Discovery.Application.Services;
|
using Deal.Modules.Discovery.Application.Services;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Discovery;
|
namespace Deal.Tests.Unit.Modules.Discovery;
|
||||||
@@ -12,7 +13,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_NewSource_CreatesNewCandidateAndBumpsFound()
|
public async Task Add_NewSource_CreatesNewCandidateAndBumpsFound()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
|
|
||||||
DiscoveryCandidateDto? candidate = await service.AddAsync(
|
DiscoveryCandidateDto? candidate = await service.AddAsync(
|
||||||
@@ -34,7 +35,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_EmptyNameAndKind_UsesDefaults()
|
public async Task Add_EmptyNameAndKind_UsesDefaults()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
|
|
||||||
DiscoveryCandidateDto? candidate = await service.AddAsync(
|
DiscoveryCandidateDto? candidate = await service.AddAsync(
|
||||||
@@ -49,7 +50,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_AlreadyMonitored_SkipsWithLog()
|
public async Task Add_AlreadyMonitored_SkipsWithLog()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
store.SeedMonitored("-1001");
|
store.SeedMonitored("-1001");
|
||||||
|
|
||||||
@@ -67,9 +68,9 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_Blacklisted_SkipsWithLog()
|
public async Task Add_Blacklisted_SkipsWithLog()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
await store.UpsertBlacklistAsync("-1001", "Канал", "отклонён вручную", CancellationToken.None);
|
await store.Store.UpsertBlacklistAsync("-1001", "Канал", "отклонён вручную", CancellationToken.None);
|
||||||
|
|
||||||
DiscoveryCandidateDto? candidate = await service.AddAsync(
|
DiscoveryCandidateDto? candidate = await service.AddAsync(
|
||||||
"dt_1", "-1001", "Канал", "", DiscoveryCandidateKinds.Channel, "", CancellationToken.None);
|
"dt_1", "-1001", "Канал", "", DiscoveryCandidateKinds.Channel, "", CancellationToken.None);
|
||||||
@@ -82,7 +83,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_AlreadyActiveCandidate_SkipsWithLog()
|
public async Task Add_AlreadyActiveCandidate_SkipsWithLog()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
store.SeedCandidate(Candidate("-1001", "dt_1", status: DiscoveryCandidateStatuses.Review));
|
store.SeedCandidate(Candidate("-1001", "dt_1", status: DiscoveryCandidateStatuses.Review));
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_StaleRejectedRow_IsReplacedByNewCandidate()
|
public async Task Add_StaleRejectedRow_IsReplacedByNewCandidate()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
store.SeedCandidate(Candidate("-1001", "dt_1", status: DiscoveryCandidateStatuses.Rejected));
|
store.SeedCandidate(Candidate("-1001", "dt_1", status: DiscoveryCandidateStatuses.Rejected));
|
||||||
|
|
||||||
@@ -115,7 +116,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_MissingTask_ReturnsNullWithoutLog()
|
public async Task Add_MissingTask_ReturnsNullWithoutLog()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
|
|
||||||
DiscoveryCandidateDto? candidate = await service.AddAsync(
|
DiscoveryCandidateDto? candidate = await service.AddAsync(
|
||||||
"dt_missing", "-1001", "Канал", "", DiscoveryCandidateKinds.Channel, "", CancellationToken.None);
|
"dt_missing", "-1001", "Канал", "", DiscoveryCandidateKinds.Channel, "", CancellationToken.None);
|
||||||
@@ -128,7 +129,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MarkJoined_Auto_SetsJoinedCountersAndLog()
|
public async Task MarkJoined_Auto_SetsJoinedCountersAndLog()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
store.SeedCandidate(Candidate("-1001", "dt_1"));
|
store.SeedCandidate(Candidate("-1001", "dt_1"));
|
||||||
|
|
||||||
@@ -146,7 +147,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MarkJoined_Manual_LogsJoinManual()
|
public async Task MarkJoined_Manual_LogsJoinManual()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
store.SeedCandidate(Candidate("-1001", "dt_1"));
|
store.SeedCandidate(Candidate("-1001", "dt_1"));
|
||||||
|
|
||||||
@@ -161,7 +162,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MarkJoined_AlreadyJoined_IsIdempotent()
|
public async Task MarkJoined_AlreadyJoined_IsIdempotent()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
store.SeedCandidate(Candidate("-1001", "dt_1", status: DiscoveryCandidateStatuses.Joined));
|
store.SeedCandidate(Candidate("-1001", "dt_1", status: DiscoveryCandidateStatuses.Joined));
|
||||||
|
|
||||||
@@ -186,7 +187,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MarkRejected_WritesRejectedCountersLogAndBlacklist()
|
public async Task MarkRejected_WritesRejectedCountersLogAndBlacklist()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
store.SeedCandidate(Candidate("-1001", "dt_1", name: "Канал"));
|
store.SeedCandidate(Candidate("-1001", "dt_1", name: "Канал"));
|
||||||
|
|
||||||
@@ -208,7 +209,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MarkRejected_AlreadyRejected_IsIdempotent()
|
public async Task MarkRejected_AlreadyRejected_IsIdempotent()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
store.SeedCandidate(Candidate("-1001", "dt_1", status: DiscoveryCandidateStatuses.Rejected));
|
store.SeedCandidate(Candidate("-1001", "dt_1", status: DiscoveryCandidateStatuses.Rejected));
|
||||||
|
|
||||||
@@ -224,7 +225,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task MarkRejected_JoinedCandidate_Throws()
|
public async Task MarkRejected_JoinedCandidate_Throws()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
store.SeedCandidate(Candidate("-1001", "dt_1", status: DiscoveryCandidateStatuses.Joined));
|
store.SeedCandidate(Candidate("-1001", "dt_1", status: DiscoveryCandidateStatuses.Joined));
|
||||||
|
|
||||||
@@ -250,7 +251,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task SetStatus_ToReview_WritesReviewLog()
|
public async Task SetStatus_ToReview_WritesReviewLog()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
store.SeedCandidate(Candidate("-1001", "dt_1"));
|
store.SeedCandidate(Candidate("-1001", "dt_1"));
|
||||||
|
|
||||||
@@ -267,7 +268,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task SetStatus_Joined_Throws()
|
public async Task SetStatus_Joined_Throws()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
store.SeedCandidate(Candidate("-1001", "dt_1"));
|
store.SeedCandidate(Candidate("-1001", "dt_1"));
|
||||||
|
|
||||||
@@ -282,7 +283,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Set_UpdatesEvaluationFieldsAndKeepsEmptyName()
|
public async Task Set_UpdatesEvaluationFieldsAndKeepsEmptyName()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
store.SeedCandidate(Candidate("-1001", "dt_1", name: "Канал"));
|
store.SeedCandidate(Candidate("-1001", "dt_1", name: "Канал"));
|
||||||
|
|
||||||
@@ -316,7 +317,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Set_WrongTask_ReturnsNull()
|
public async Task Set_WrongTask_ReturnsNull()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
store.SeedTask(Task("dt_2"));
|
store.SeedTask(Task("dt_2"));
|
||||||
store.SeedCandidate(Candidate("-1001", "dt_1"));
|
store.SeedCandidate(Candidate("-1001", "dt_1"));
|
||||||
@@ -330,7 +331,7 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Delete_RemovesCandidateRow()
|
public async Task Delete_RemovesCandidateRow()
|
||||||
{
|
{
|
||||||
(DiscoveryCandidatesService service, FakeDiscoveryStore store) = Create();
|
(DiscoveryCandidatesService service, TestDiscoveryStore store) = Create();
|
||||||
store.SeedCandidate(Candidate("-1001", "dt_1"));
|
store.SeedCandidate(Candidate("-1001", "dt_1"));
|
||||||
|
|
||||||
await service.DeleteAsync("-1001", CancellationToken.None);
|
await service.DeleteAsync("-1001", CancellationToken.None);
|
||||||
@@ -339,12 +340,12 @@ public sealed class DiscoveryCandidatesServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Создаёт связку кандидатного сервиса + фейк (лог/чёрный список — те же фейки).
|
// Создаёт связку кандидатного сервиса + фейк (лог/чёрный список — те же фейки).
|
||||||
private static (DiscoveryCandidatesService Service, FakeDiscoveryStore Store) Create()
|
private static (DiscoveryCandidatesService Service, TestDiscoveryStore Store) Create()
|
||||||
{
|
{
|
||||||
var store = new FakeDiscoveryStore();
|
var store = new TestDiscoveryStore();
|
||||||
var log = new DiscoveryLogService(store);
|
var log = new DiscoveryLogService(store.Store);
|
||||||
var blacklist = new DiscoveryBlacklistService(store);
|
var blacklist = new DiscoveryBlacklistService(store.Store);
|
||||||
return (new DiscoveryCandidatesService(store, log, blacklist), store);
|
return (new DiscoveryCandidatesService(store.Store, log, blacklist), store);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Задача как строка DiscTasks (running; счётчики 0).
|
// Задача как строка DiscTasks (running; счётчики 0).
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Deal.Modules.Discovery.Application.Models;
|
using Deal.Modules.Discovery.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Discovery.Application.Services;
|
using Deal.Modules.Discovery.Application.Services;
|
||||||
|
|
||||||
namespace Deal.Tests.Unit.Modules.Discovery;
|
namespace Deal.Tests.Unit.Modules.Discovery;
|
||||||
@@ -11,8 +12,8 @@ public sealed class DiscoveryLogServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Add_WritesRowWithDlPrefix()
|
public async Task Add_WritesRowWithDlPrefix()
|
||||||
{
|
{
|
||||||
var store = new FakeDiscoveryStore();
|
var store = new TestDiscoveryStore();
|
||||||
var service = new DiscoveryLogService(store);
|
var service = new DiscoveryLogService(store.Store);
|
||||||
|
|
||||||
await service.AddAsync("dt_1", DiscoveryLogEvents.Skip, "пропущен -1001: причина", CancellationToken.None);
|
await service.AddAsync("dt_1", DiscoveryLogEvents.Skip, "пропущен -1001: причина", CancellationToken.None);
|
||||||
|
|
||||||
@@ -28,8 +29,8 @@ public sealed class DiscoveryLogServiceTests
|
|||||||
public async Task TaskLog_ReturnsNewestFirst()
|
public async Task TaskLog_ReturnsNewestFirst()
|
||||||
{
|
{
|
||||||
long tick = 0;
|
long tick = 0;
|
||||||
var store = new FakeDiscoveryStore(() => DateTimeOffset.FromUnixTimeMilliseconds(++tick * 1000L));
|
var store = new TestDiscoveryStore(() => DateTimeOffset.FromUnixTimeMilliseconds(++tick * 1000L));
|
||||||
var service = new DiscoveryLogService(store);
|
var service = new DiscoveryLogService(store.Store);
|
||||||
|
|
||||||
await service.AddAsync("dt_1", DiscoveryLogEvents.Search, "первое", CancellationToken.None); // t=1000
|
await service.AddAsync("dt_1", DiscoveryLogEvents.Search, "первое", CancellationToken.None); // t=1000
|
||||||
await service.AddAsync("dt_1", DiscoveryLogEvents.JoinAuto, "второе", CancellationToken.None); // t=2000
|
await service.AddAsync("dt_1", DiscoveryLogEvents.JoinAuto, "второе", CancellationToken.None); // t=2000
|
||||||
@@ -44,8 +45,8 @@ public sealed class DiscoveryLogServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task TaskLog_EmptyTask_ReturnsEmpty()
|
public async Task TaskLog_EmptyTask_ReturnsEmpty()
|
||||||
{
|
{
|
||||||
var store = new FakeDiscoveryStore();
|
var store = new TestDiscoveryStore();
|
||||||
var service = new DiscoveryLogService(store);
|
var service = new DiscoveryLogService(store.Store);
|
||||||
|
|
||||||
IReadOnlyList<DiscoveryLogDto> items = await service.TaskLogAsync("dt_no_log", CancellationToken.None);
|
IReadOnlyList<DiscoveryLogDto> items = await service.TaskLogAsync("dt_no_log", CancellationToken.None);
|
||||||
|
|
||||||
@@ -56,8 +57,8 @@ public sealed class DiscoveryLogServiceTests
|
|||||||
public async Task TaskLog_LimitTakesNewest()
|
public async Task TaskLog_LimitTakesNewest()
|
||||||
{
|
{
|
||||||
long tick = 0;
|
long tick = 0;
|
||||||
var store = new FakeDiscoveryStore(() => DateTimeOffset.FromUnixTimeMilliseconds(++tick * 1000L));
|
var store = new TestDiscoveryStore(() => DateTimeOffset.FromUnixTimeMilliseconds(++tick * 1000L));
|
||||||
var service = new DiscoveryLogService(store);
|
var service = new DiscoveryLogService(store.Store);
|
||||||
|
|
||||||
await service.AddAsync("dt_1", DiscoveryLogEvents.Search, "первое", CancellationToken.None);
|
await service.AddAsync("dt_1", DiscoveryLogEvents.Search, "первое", CancellationToken.None);
|
||||||
await service.AddAsync("dt_1", DiscoveryLogEvents.Search, "второе", CancellationToken.None);
|
await service.AddAsync("dt_1", DiscoveryLogEvents.Search, "второе", CancellationToken.None);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Deal.Modules.Discovery.Application.Exceptions;
|
using Deal.Modules.Discovery.Application.Exceptions;
|
||||||
using Deal.Modules.Discovery.Application.Models;
|
using Deal.Modules.Discovery.Application.Models;
|
||||||
|
using Deal.Tests.Unit.Support;
|
||||||
using Deal.Modules.Discovery.Application.Services;
|
using Deal.Modules.Discovery.Application.Services;
|
||||||
using Deal.Tests.Unit.Modules.Settings;
|
using Deal.Tests.Unit.Modules.Settings;
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Create_EmptyName_ThrowsAndCreatesNothing()
|
public async Task Create_EmptyName_ThrowsAndCreatesNothing()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, _) = Create();
|
||||||
|
|
||||||
DiscoveryValidationException error = await Assert.ThrowsAsync<DiscoveryValidationException>(
|
DiscoveryValidationException error = await Assert.ThrowsAsync<DiscoveryValidationException>(
|
||||||
() => service.CreateAsync(new DiscoveryTaskDraft { Name = " " }, CancellationToken.None));
|
() => service.CreateAsync(new DiscoveryTaskDraft { Name = " " }, CancellationToken.None));
|
||||||
@@ -25,7 +26,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Create_PlanBelowOne_Throws()
|
public async Task Create_PlanBelowOne_Throws()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, _) = Create();
|
||||||
|
|
||||||
DiscoveryValidationException error = await Assert.ThrowsAsync<DiscoveryValidationException>(
|
DiscoveryValidationException error = await Assert.ThrowsAsync<DiscoveryValidationException>(
|
||||||
() => service.CreateAsync(NewDraft(planJoins: 0), CancellationToken.None));
|
() => service.CreateAsync(NewDraft(planJoins: 0), CancellationToken.None));
|
||||||
@@ -37,7 +38,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Create_PlanAboveDailyLimit_Throws()
|
public async Task Create_PlanAboveDailyLimit_Throws()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, _) = Create();
|
||||||
|
|
||||||
DiscoveryValidationException error = await Assert.ThrowsAsync<DiscoveryValidationException>(
|
DiscoveryValidationException error = await Assert.ThrowsAsync<DiscoveryValidationException>(
|
||||||
() => service.CreateAsync(NewDraft(planJoins: 51), CancellationToken.None));
|
() => service.CreateAsync(NewDraft(planJoins: 51), CancellationToken.None));
|
||||||
@@ -49,7 +50,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Create_TaskAtLimitLeavesNoBudget_Throws()
|
public async Task Create_TaskAtLimitLeavesNoBudget_Throws()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, _) = Create();
|
||||||
store.SeedTask(Task("dt_1", planJoins: 50));
|
store.SeedTask(Task("dt_1", planJoins: 50));
|
||||||
|
|
||||||
DiscoveryValidationException error = await Assert.ThrowsAsync<DiscoveryValidationException>(
|
DiscoveryValidationException error = await Assert.ThrowsAsync<DiscoveryValidationException>(
|
||||||
@@ -62,7 +63,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Create_RemainingQuota_FillsUpToLimitAndRejectsOver()
|
public async Task Create_RemainingQuota_FillsUpToLimitAndRejectsOver()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, _) = Create();
|
||||||
store.SeedTask(Task("dt_1", planJoins: 25));
|
store.SeedTask(Task("dt_1", planJoins: 25));
|
||||||
|
|
||||||
// 25 + 25 = 50 — вторая задача на 25 влезает (сумма планов ≤ discJoinLimit).
|
// 25 + 25 = 50 — вторая задача на 25 влезает (сумма планов ≤ discJoinLimit).
|
||||||
@@ -79,7 +80,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Create_NormalizesAndCreatesDraft()
|
public async Task Create_NormalizesAndCreatesDraft()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, _) = Create();
|
||||||
|
|
||||||
DiscoveryTaskDto task = await service.CreateAsync(
|
DiscoveryTaskDto task = await service.CreateAsync(
|
||||||
new DiscoveryTaskDraft
|
new DiscoveryTaskDraft
|
||||||
@@ -116,7 +117,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Create_CustomThresholdAndSample_Respected()
|
public async Task Create_CustomThresholdAndSample_Respected()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, FakeSettingsStore settings) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, FakeSettingsStore settings) = Create();
|
||||||
settings.Preload("discEvalThreshold", "60");
|
settings.Preload("discEvalThreshold", "60");
|
||||||
settings.Preload("discEvalSample", "20");
|
settings.Preload("discEvalSample", "20");
|
||||||
|
|
||||||
@@ -143,7 +144,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Patch_AppliesFieldsAndClamps()
|
public async Task Patch_AppliesFieldsAndClamps()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, _) = Create();
|
||||||
store.SeedTask(Task("dt_1", keywords: new[] { "старое" }));
|
store.SeedTask(Task("dt_1", keywords: new[] { "старое" }));
|
||||||
|
|
||||||
DiscoveryTaskDto? task = await service.PatchAsync(
|
DiscoveryTaskDto? task = await service.PatchAsync(
|
||||||
@@ -170,7 +171,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Patch_NullOnlyPatch_ReturnsCurrentWithoutWrite()
|
public async Task Patch_NullOnlyPatch_ReturnsCurrentWithoutWrite()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, _) = Create();
|
||||||
store.SeedTask(Task("dt_1", name: "Старое"));
|
store.SeedTask(Task("dt_1", name: "Старое"));
|
||||||
|
|
||||||
DiscoveryTaskDto? task = await service.PatchAsync("dt_1", new DiscoveryTaskPatch(), CancellationToken.None);
|
DiscoveryTaskDto? task = await service.PatchAsync("dt_1", new DiscoveryTaskPatch(), CancellationToken.None);
|
||||||
@@ -183,7 +184,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Patch_PlanGrowthBeyondBudget_Throws()
|
public async Task Patch_PlanGrowthBeyondBudget_Throws()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, _) = Create();
|
||||||
store.SeedTask(Task("dt_1", planJoins: 20));
|
store.SeedTask(Task("dt_1", planJoins: 20));
|
||||||
store.SeedTask(Task("dt_2", planJoins: 20));
|
store.SeedTask(Task("dt_2", planJoins: 20));
|
||||||
|
|
||||||
@@ -212,7 +213,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Start_NoKeywords_Throws()
|
public async Task Start_NoKeywords_Throws()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, _) = Create();
|
||||||
store.SeedTask(Task("dt_1", keywords: Array.Empty<string>()));
|
store.SeedTask(Task("dt_1", keywords: Array.Empty<string>()));
|
||||||
|
|
||||||
DiscoveryValidationException error = await Assert.ThrowsAsync<DiscoveryValidationException>(
|
DiscoveryValidationException error = await Assert.ThrowsAsync<DiscoveryValidationException>(
|
||||||
@@ -224,7 +225,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Start_FromPaused_KeepsSearchProgress()
|
public async Task Start_FromPaused_KeepsSearchProgress()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, _) = Create();
|
||||||
store.SeedTask(Task("dt_1", status: DiscoveryTaskStatuses.Paused, keywords: new[] { "a", "b" }, searchIdx: 1));
|
store.SeedTask(Task("dt_1", status: DiscoveryTaskStatuses.Paused, keywords: new[] { "a", "b" }, searchIdx: 1));
|
||||||
|
|
||||||
DiscoveryTaskDto? task = await service.StartAsync("dt_1", CancellationToken.None);
|
DiscoveryTaskDto? task = await service.StartAsync("dt_1", CancellationToken.None);
|
||||||
@@ -238,7 +239,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Start_FromDone_ResetsSearchProgress()
|
public async Task Start_FromDone_ResetsSearchProgress()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, _) = Create();
|
||||||
store.SeedTask(Task(
|
store.SeedTask(Task(
|
||||||
"dt_1",
|
"dt_1",
|
||||||
status: DiscoveryTaskStatuses.Done,
|
status: DiscoveryTaskStatuses.Done,
|
||||||
@@ -261,7 +262,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Pause_SetsPaused()
|
public async Task Pause_SetsPaused()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, _) = Create();
|
||||||
store.SeedTask(Task("dt_1", status: DiscoveryTaskStatuses.Running, keywords: new[] { "a" }));
|
store.SeedTask(Task("dt_1", status: DiscoveryTaskStatuses.Running, keywords: new[] { "a" }));
|
||||||
|
|
||||||
DiscoveryTaskDto? task = await service.PauseAsync("dt_1", CancellationToken.None);
|
DiscoveryTaskDto? task = await service.PauseAsync("dt_1", CancellationToken.None);
|
||||||
@@ -273,12 +274,12 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Delete_RemovesTaskCandidatesAndLog_KeepsBlacklist()
|
public async Task Delete_RemovesTaskCandidatesAndLog_KeepsBlacklist()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, _) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
store.SeedTask(Task("dt_2"));
|
store.SeedTask(Task("dt_2"));
|
||||||
store.SeedCandidate(Candidate("c_1", "dt_1"));
|
store.SeedCandidate(Candidate("c_1", "dt_1"));
|
||||||
store.SeedCandidate(Candidate("c_2", "dt_2"));
|
store.SeedCandidate(Candidate("c_2", "dt_2"));
|
||||||
await store.UpsertBlacklistAsync("c_1", "Источник", "причина", CancellationToken.None);
|
await store.Store.UpsertBlacklistAsync("c_1", "Источник", "причина", CancellationToken.None);
|
||||||
|
|
||||||
bool deleted = await service.DeleteAsync("dt_1", CancellationToken.None);
|
bool deleted = await service.DeleteAsync("dt_1", CancellationToken.None);
|
||||||
|
|
||||||
@@ -303,7 +304,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task AdvanceSearch_MovesIndexAndFinishes()
|
public async Task AdvanceSearch_MovesIndexAndFinishes()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, _) = Create();
|
||||||
store.SeedTask(Task("dt_1", keywords: new[] { "a", "b" }));
|
store.SeedTask(Task("dt_1", keywords: new[] { "a", "b" }));
|
||||||
|
|
||||||
Assert.True(await service.AdvanceSearchAsync("dt_1", CancellationToken.None));
|
Assert.True(await service.AdvanceSearchAsync("dt_1", CancellationToken.None));
|
||||||
@@ -320,7 +321,7 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task BumpCounter_IncrementsField()
|
public async Task BumpCounter_IncrementsField()
|
||||||
{
|
{
|
||||||
(DiscoveryTasksService service, FakeDiscoveryStore store, _) = Create();
|
(DiscoveryTasksService service, TestDiscoveryStore store, _) = Create();
|
||||||
store.SeedTask(Task("dt_1"));
|
store.SeedTask(Task("dt_1"));
|
||||||
|
|
||||||
Assert.True(await service.BumpCounterAsync("dt_1", DiscoveryCounterField.Evaluated, 3, CancellationToken.None));
|
Assert.True(await service.BumpCounterAsync("dt_1", DiscoveryCounterField.Evaluated, 3, CancellationToken.None));
|
||||||
@@ -329,12 +330,12 @@ public sealed class DiscoveryTasksServiceTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Создаёт связку сервис + фейки (пустые настройки → дефолты SettingsDefaults).
|
// Создаёт связку сервис + фейки (пустые настройки → дефолты SettingsDefaults).
|
||||||
private static (DiscoveryTasksService Service, FakeDiscoveryStore Store, FakeSettingsStore Settings) Create()
|
private static (DiscoveryTasksService Service, TestDiscoveryStore Store, FakeSettingsStore Settings) Create()
|
||||||
{
|
{
|
||||||
var store = new FakeDiscoveryStore();
|
var store = new TestDiscoveryStore();
|
||||||
var settings = new FakeSettingsStore();
|
var settings = new FakeSettingsStore();
|
||||||
var planGuard = new DiscoveryPlanGuard(store, settings);
|
var planGuard = new DiscoveryPlanGuard(store.Store, settings);
|
||||||
return (new DiscoveryTasksService(store, planGuard, settings), store, settings);
|
return (new DiscoveryTasksService(store.Store, planGuard, settings), store, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draft задачи с дефолтными полями (name/planJoins переопределяются).
|
// Draft задачи с дефолтными полями (name/planJoins переопределяются).
|
||||||
|
|||||||
@@ -0,0 +1,611 @@
|
|||||||
|
using Deal.Modules.Discovery.Application.Abstractions;
|
||||||
|
using Deal.Modules.Discovery.Application.Models;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
|
namespace Deal.Tests.Unit.Support;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Подставка <see cref="IDiscoveryStore"/> на списках: сервисы получают NSubstitute-подставку
|
||||||
|
/// (<see cref="Store"/>), тесты сеют данные и проверяют состояние напрямую.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TestDiscoveryStore
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Подставка порта Discovery-хранилища (создаётся в конструкторе).
|
||||||
|
/// </summary>
|
||||||
|
public IDiscoveryStore Store { get; }
|
||||||
|
|
||||||
|
private readonly List<DiscoveryTaskDto> _tasks = [];
|
||||||
|
private readonly List<DiscoveryCandidateDto> _candidates = [];
|
||||||
|
private readonly List<DiscoveryBlacklistDto> _blacklist = [];
|
||||||
|
private readonly List<DiscoveryLogDto> _log = [];
|
||||||
|
private readonly HashSet<string> _monitoredIds = new(StringComparer.Ordinal);
|
||||||
|
private readonly Func<DateTimeOffset> _clock;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Создаёт подставку с «часами» по умолчанию
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="clock">Источник текущего времени (по умолчанию <see cref="DateTimeOffset.UtcNow"/>).</param>
|
||||||
|
public TestDiscoveryStore(Func<DateTimeOffset>? clock = null)
|
||||||
|
{
|
||||||
|
Store = Substitute.For<IDiscoveryStore>();
|
||||||
|
_clock = clock ?? (() => DateTimeOffset.UtcNow);
|
||||||
|
|
||||||
|
Store.ListTasksAsync(Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => ListTasksAsync(ci.ArgAt<CancellationToken>(0)));
|
||||||
|
Store.GetTaskAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => GetTaskAsync(ci.ArgAt<string>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.CreateTaskAsync(Arg.Any<DiscoveryTaskRow>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => CreateTaskAsync(ci.ArgAt<DiscoveryTaskRow>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.PatchTaskAsync(Arg.Any<string>(), Arg.Any<DiscoveryTaskPatch>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => PatchTaskAsync(ci.ArgAt<string>(0), ci.ArgAt<DiscoveryTaskPatch>(1), ci.ArgAt<CancellationToken>(2)));
|
||||||
|
Store.DeleteTaskAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => DeleteTaskAsync(ci.ArgAt<string>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.SetTaskRunningAsync(Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => SetTaskRunningAsync(ci.ArgAt<string>(0), ci.ArgAt<bool>(1), ci.ArgAt<CancellationToken>(2)));
|
||||||
|
Store.SetTaskPausedAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => SetTaskPausedAsync(ci.ArgAt<string>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.SetTaskDoneAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => SetTaskDoneAsync(ci.ArgAt<string>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.BumpTaskCounterAsync(Arg.Any<string>(), Arg.Any<DiscoveryCounterField>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => BumpTaskCounterAsync(ci.ArgAt<string>(0), ci.ArgAt<DiscoveryCounterField>(1), ci.ArgAt<int>(2), ci.ArgAt<CancellationToken>(3)));
|
||||||
|
Store.AdvanceSearchAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => AdvanceSearchAsync(ci.ArgAt<string>(0), ci.ArgAt<int>(1), ci.ArgAt<bool>(2), ci.ArgAt<CancellationToken>(3)));
|
||||||
|
Store.SumActivePlanAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => SumActivePlanAsync(ci.ArgAt<string>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.ListCandidatesAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => ListCandidatesAsync(ci.ArgAt<string>(0), ci.ArgAt<string>(1), ci.ArgAt<CancellationToken>(2)));
|
||||||
|
Store.GetCandidateAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => GetCandidateAsync(ci.ArgAt<string>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.IsDialogMonitoredAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => IsDialogMonitoredAsync(ci.ArgAt<string>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.IsBlacklistedAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => IsBlacklistedAsync(ci.ArgAt<string>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.CreateCandidateAsync(Arg.Any<DiscoveryCandidateRow>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => CreateCandidateAsync(ci.ArgAt<DiscoveryCandidateRow>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.DeleteCandidateAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => DeleteCandidateAsync(ci.ArgAt<string>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.PatchCandidateAsync(Arg.Any<string>(), Arg.Any<DiscoveryCandidatePatch>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => PatchCandidateAsync(ci.ArgAt<string>(0), ci.ArgAt<DiscoveryCandidatePatch>(1), ci.ArgAt<CancellationToken>(2)));
|
||||||
|
Store.SetCandidateStatusAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => SetCandidateStatusAsync(ci.ArgAt<string>(0), ci.ArgAt<string>(1), ci.ArgAt<CancellationToken>(2)));
|
||||||
|
Store.SetCandidateJoinedAsync(Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => SetCandidateJoinedAsync(ci.ArgAt<string>(0), ci.ArgAt<bool>(1), ci.ArgAt<CancellationToken>(2)));
|
||||||
|
Store.IncrementJoinFailuresAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => IncrementJoinFailuresAsync(ci.ArgAt<string>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.SetCandidateRejectedAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => SetCandidateRejectedAsync(ci.ArgAt<string>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.UpsertBlacklistAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => UpsertBlacklistAsync(ci.ArgAt<string>(0), ci.ArgAt<string>(1), ci.ArgAt<string>(2), ci.ArgAt<CancellationToken>(3)));
|
||||||
|
Store.RemoveBlacklistAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => RemoveBlacklistAsync(ci.ArgAt<string>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.GetBlacklistAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => GetBlacklistAsync(ci.ArgAt<string>(0), ci.ArgAt<CancellationToken>(1)));
|
||||||
|
Store.ListBlacklistAsync(Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => ListBlacklistAsync(ci.ArgAt<CancellationToken>(0)));
|
||||||
|
Store.AddLogAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => AddLogAsync(ci.ArgAt<string>(0), ci.ArgAt<string>(1), ci.ArgAt<string>(2), ci.ArgAt<string>(3), ci.ArgAt<CancellationToken>(4)));
|
||||||
|
Store.CountLogEventAsync(Arg.Any<string>(), Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => CountLogEventAsync(ci.ArgAt<string>(0), ci.ArgAt<DateTimeOffset>(1), ci.ArgAt<CancellationToken>(2)));
|
||||||
|
Store.ListTaskLogAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => ListTaskLogAsync(ci.ArgAt<string>(0), ci.ArgAt<int>(1), ci.ArgAt<CancellationToken>(2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Задачи фейка
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<DiscoveryTaskDto> Tasks => _tasks.ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Кандидаты фейка
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<DiscoveryCandidateDto> Candidates => _candidates.ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Чёрный список фейка
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<DiscoveryBlacklistDto> Blacklist => _blacklist.ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Лог фейка в порядке записи
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<DiscoveryLogDto> Log => _log.ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Кладёт задачу напрямую
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="task">Задача как если бы была прочитана адаптером.</param>
|
||||||
|
public void SeedTask(DiscoveryTaskDto task)
|
||||||
|
{
|
||||||
|
_tasks.Add(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Кладёт кандидата напрямую
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="candidate">Кандидат как если бы был прочитан адаптером.</param>
|
||||||
|
public void SeedCandidate(DiscoveryCandidateDto candidate)
|
||||||
|
{
|
||||||
|
_candidates.Add(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Помечает источник «уже мониторится» — есть в каталоге Dialogs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dialogId">Подписанный id источника.</param>
|
||||||
|
public void SeedMonitored(string dialogId)
|
||||||
|
{
|
||||||
|
_monitoredIds.Add(dialogId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Кладёт событие лога напрямую
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="event">Событие (join_auto/…).</param>
|
||||||
|
/// <param name="createdAt">Момент события (UTC).</param>
|
||||||
|
/// <param name="taskId">Id задачи (по умолчанию — произвольный).</param>
|
||||||
|
public void SeedLog(
|
||||||
|
string @event,
|
||||||
|
DateTimeOffset createdAt,
|
||||||
|
string taskId = "dt_x")
|
||||||
|
{
|
||||||
|
_log.Add(new DiscoveryLogDto("dl_x", taskId, @event, string.Empty, createdAt.ToUnixTimeMilliseconds()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<IReadOnlyList<DiscoveryTaskDto>> ListTasksAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IReadOnlyList<DiscoveryTaskDto>>(_tasks
|
||||||
|
.OrderBy(task => task.CreatedAt)
|
||||||
|
.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<DiscoveryTaskDto?> GetTaskAsync(string taskId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return Task.FromResult(_tasks.FirstOrDefault(task => task.Id == taskId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<bool> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<bool> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<bool> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<bool> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<bool> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<bool> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<int> SumActivePlanAsync(string? excludeTaskId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
IEnumerable<DiscoveryTaskDto> 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<IReadOnlyList<DiscoveryCandidateDto>> ListCandidatesAsync( string taskId, string? status, CancellationToken ct)
|
||||||
|
{
|
||||||
|
IEnumerable<DiscoveryCandidateDto> query = _candidates.Where(candidate => candidate.TaskId == taskId);
|
||||||
|
if (status is not null)
|
||||||
|
{
|
||||||
|
query = query.Where(candidate => candidate.Status == status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<IReadOnlyList<DiscoveryCandidateDto>>(query
|
||||||
|
.OrderBy(candidate => candidate.CreatedAt)
|
||||||
|
.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<DiscoveryCandidateDto?> GetCandidateAsync(string dialogId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return Task.FromResult(_candidates.FirstOrDefault(candidate => candidate.DialogId == dialogId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<bool> IsDialogMonitoredAsync(string dialogId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return Task.FromResult(_monitoredIds.Contains(dialogId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<bool> IsBlacklistedAsync(string dialogId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return Task.FromResult(_blacklist.Any(entry => entry.DialogId == dialogId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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<string>(),
|
||||||
|
new List<DiscoveryTopicDto>(),
|
||||||
|
FitRatio: null,
|
||||||
|
DiscoveryCandidateStatuses.New,
|
||||||
|
AutoJoined: false,
|
||||||
|
JoinFailures: 0,
|
||||||
|
nowMs,
|
||||||
|
nowMs));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task DeleteCandidateAsync(string dialogId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
_candidates.RemoveAll(candidate => candidate.DialogId == dialogId);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<bool> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<bool> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<bool> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<int?> IncrementJoinFailuresAsync(string dialogId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
int index = _candidates.FindIndex(candidate => candidate.DialogId == dialogId);
|
||||||
|
if (index < 0 || _candidates[index].Status != DiscoveryCandidateStatuses.Review)
|
||||||
|
{
|
||||||
|
return Task.FromResult<int?>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
DiscoveryCandidateDto candidate = _candidates[index] with
|
||||||
|
{
|
||||||
|
JoinFailures = _candidates[index].JoinFailures + 1,
|
||||||
|
UpdatedAt = _clock().ToUnixTimeMilliseconds(),
|
||||||
|
};
|
||||||
|
_candidates[index] = candidate;
|
||||||
|
return Task.FromResult<int?>(candidate.JoinFailures);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<bool> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task RemoveBlacklistAsync(string dialogId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
_blacklist.RemoveAll(entry => entry.DialogId == dialogId);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<DiscoveryBlacklistDto?> GetBlacklistAsync(string dialogId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return Task.FromResult(_blacklist.FirstOrDefault(entry => entry.DialogId == dialogId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<IReadOnlyList<DiscoveryBlacklistDto>> ListBlacklistAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IReadOnlyList<DiscoveryBlacklistDto>>(_blacklist
|
||||||
|
.OrderByDescending(entry => entry.CreatedAt)
|
||||||
|
.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<int> CountLogEventAsync( string logEvent, DateTimeOffset sinceUtc, CancellationToken ct)
|
||||||
|
{
|
||||||
|
long sinceMs = sinceUtc.ToUnixTimeMilliseconds();
|
||||||
|
return Task.FromResult(_log.Count(entry => entry.Event == logEvent && entry.CreatedAt >= sinceMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<IReadOnlyList<DiscoveryLogDto>> ListTaskLogAsync( string taskId, int limit, CancellationToken ct)
|
||||||
|
{
|
||||||
|
IReadOnlyList<DiscoveryLogDto> entries = _log
|
||||||
|
.Where(entry => entry.TaskId == taskId)
|
||||||
|
.OrderByDescending(entry => entry.CreatedAt)
|
||||||
|
.Take(limit)
|
||||||
|
.ToList();
|
||||||
|
return Task.FromResult(entries);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user