Хелпер Support/TestTelegramStore: словари каталога/превью + NSubstitute-подставка, все методы порта читают состояние на момент вызова (семантика sync/upsert/ мониторов/бэкфилла сохранена). Потребители (5 файлов) перетипизированы на .Store, фейк удалён, тесты 1340 зелёные.
This commit is contained in:
@@ -42,7 +42,7 @@ public sealed class TgStatusServiceTests
|
||||
[Fact]
|
||||
public async Task GetAsync_GatewayUnavailable_ReturnsIdleFormWithKvAccountCountAndKeys()
|
||||
{
|
||||
(TgStatusService service, FakeTelegramStore store, FakeSettingsStore settings, TestTelegramGateway gateway, _, TestGlobalSettingsStore globalSettings) = Create();
|
||||
(TgStatusService service, TestTelegramStore store, FakeSettingsStore 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));
|
||||
@@ -68,7 +68,7 @@ public sealed class TgStatusServiceTests
|
||||
[Fact]
|
||||
public async Task GetAsync_ReadyGateway_ComposesLiveFieldsWithKvAccountMonitoredAndKeys()
|
||||
{
|
||||
(TgStatusService service, FakeTelegramStore store, FakeSettingsStore settings, TestTelegramGateway gateway, _, TestGlobalSettingsStore globalSettings) = Create();
|
||||
(TgStatusService service, TestTelegramStore store, FakeSettingsStore settings, TestTelegramGateway gateway, _, TestGlobalSettingsStore globalSettings) = Create();
|
||||
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true));
|
||||
settings.Preload(SettingsKeys.TgAccount, "\"@realuser\"");
|
||||
PreloadKeys(globalSettings, "123456", "abcdefghijklmnop");
|
||||
@@ -105,15 +105,15 @@ public sealed class TgStatusServiceTests
|
||||
// ─── Хелперы ────────────────────────────────────────────────────────────
|
||||
|
||||
// Собирает сервис на фейках (FakeTelegramGateway/Store/SettingsStore/GlobalSettingsStore + FakeSecretCipher).
|
||||
private static (TgStatusService Service, FakeTelegramStore Store, FakeSettingsStore Settings, TestTelegramGateway Gateway, FakeSecretCipher Cipher, TestGlobalSettingsStore GlobalSettings) Create()
|
||||
private static (TgStatusService Service, TestTelegramStore Store, FakeSettingsStore Settings, TestTelegramGateway Gateway, FakeSecretCipher Cipher, TestGlobalSettingsStore GlobalSettings) Create()
|
||||
{
|
||||
var store = new FakeTelegramStore();
|
||||
var store = new TestTelegramStore();
|
||||
var settings = new FakeSettingsStore();
|
||||
var gateway = new TestTelegramGateway();
|
||||
var cipher = new FakeSecretCipher();
|
||||
var globalSettings = new TestGlobalSettingsStore();
|
||||
var keys = new TelegramKeysService(globalSettings.Store, cipher);
|
||||
var dialogs = new DialogsService(store, settings, gateway.Gateway, NullLogger<DialogsService>.Instance);
|
||||
var dialogs = new DialogsService(store.Store, settings, gateway.Gateway, NullLogger<DialogsService>.Instance);
|
||||
var service = new TgStatusService(gateway.Gateway, dialogs, settings, keys);
|
||||
return (service, store, settings, gateway, cipher, globalSettings);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task Sync_NewDialogsAutoMonitorNewDefaultTrue_TurnMonitoringOn()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, _) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, _) = Create();
|
||||
|
||||
int count = await service.SyncFromTelegramAsync(Catalog(
|
||||
Entry("d_1", "Канал", "channel"),
|
||||
@@ -38,7 +38,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task Sync_AutoMonitorNewFalse_AddsNewDialogsUnmonitored()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, FakeSettingsStore settings, _) = Create();
|
||||
(DialogsService service, TestTelegramStore store, FakeSettingsStore settings, _) = Create();
|
||||
settings.Preload(SettingsKeys.AutoMonitorNew, "false");
|
||||
|
||||
int count = await service.SyncFromTelegramAsync(Catalog(Entry("d_1", "Канал", "channel")), CancellationToken.None);
|
||||
@@ -55,7 +55,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task Sync_ExistingDialog_UpdatesMetadataAndKeepsUserMonitor()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, _) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, _) = Create();
|
||||
store.Seed(Dialog("d_1", "Старое имя", "channel", Monitor: false, Backfilled: true));
|
||||
|
||||
await service.SyncFromTelegramAsync(
|
||||
@@ -76,7 +76,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task Sync_DialogLeftCatalog_IsDeleted()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, _) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, _) = Create();
|
||||
store.Seed(Dialog("d_left", "Ушёл", "channel", Monitor: true, Backfilled: true));
|
||||
|
||||
int count = await service.SyncFromTelegramAsync(Catalog(Entry("d_stay", "Остался", "group")), CancellationToken.None);
|
||||
@@ -93,7 +93,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task Sync_EmptyCatalog_ReturnsZeroAndKeepsRows()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, _) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, _) = Create();
|
||||
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true, Backfilled: true));
|
||||
|
||||
int count = await service.SyncFromTelegramAsync([], CancellationToken.None);
|
||||
@@ -110,7 +110,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task SetMonitor_EnableFirstTimeNotBackfilled_SetsFlagCallsGatewayAndNeedsBackfill()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: false, Backfilled: false));
|
||||
|
||||
TelegramMonitorToggleDto result = await service.SetMonitorAsync("d_1", enabled: true, CancellationToken.None);
|
||||
@@ -127,7 +127,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task SetMonitor_EnableAlreadyBackfilled_NoBackfillNeeded()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: false, Backfilled: true));
|
||||
|
||||
TelegramMonitorToggleDto result = await service.SetMonitorAsync("d_1", enabled: true, CancellationToken.None);
|
||||
@@ -143,7 +143,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task SetMonitor_Disable_TurnsOffAndNotifiesMirror()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true, Backfilled: true));
|
||||
|
||||
TelegramMonitorToggleDto result = await service.SetMonitorAsync("d_1", enabled: false, CancellationToken.None);
|
||||
@@ -160,7 +160,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task SetMonitor_UnknownDialog_NoOpWithoutGatewayCall()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
|
||||
TelegramMonitorToggleDto result = await service.SetMonitorAsync("missing", enabled: true, CancellationToken.None);
|
||||
|
||||
@@ -178,7 +178,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task SetMonitorAll_Enable_ReturnsCountAndNotBackfilledIdsAndNotifiesMirror()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
store.Seed(Dialog("d_a", "А", "channel", Monitor: false, Backfilled: true));
|
||||
store.Seed(Dialog("d_b", "Б", "channel", Monitor: false, Backfilled: false));
|
||||
store.Seed(Dialog("d_c", "В", "group", Monitor: false, Backfilled: false));
|
||||
@@ -197,7 +197,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task SetMonitorAll_Disable_TurnsAllOffAndEmptyBackfillList()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
store.Seed(Dialog("d_a", "А", "channel", Monitor: true, Backfilled: false));
|
||||
store.Seed(Dialog("d_b", "Б", "channel", Monitor: true, Backfilled: true));
|
||||
|
||||
@@ -215,7 +215,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task MarkBackfilled_SetsFlagOnDialog()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, _) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, _) = Create();
|
||||
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true, Backfilled: false));
|
||||
|
||||
await service.MarkBackfilledAsync("d_1", CancellationToken.None);
|
||||
@@ -230,7 +230,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task ReadRecent_OneDialogFails_ContinuesWithOthersAndMarksOnlySuccessful()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true, Backfilled: false));
|
||||
store.Seed(Dialog("d_2", "Группа", "group", Monitor: true, Backfilled: false));
|
||||
store.Seed(Dialog("d_3", "Форум", "forum", Monitor: true, Backfilled: false));
|
||||
@@ -251,7 +251,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task BackfillOne_UnknownOrAlreadyBackfilledWithoutForce_ReturnsZeroWithoutRpc()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
store.Seed(Dialog("d_known", "Канал", "channel", Monitor: true, Backfilled: true));
|
||||
store.Seed(Dialog("d_fresh", "Группа", "group", Monitor: false, Backfilled: false));
|
||||
|
||||
@@ -272,7 +272,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task BackfillOne_ForceTrue_BackfillsAlreadyBackfilledDialogAndMarksAgain()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true, Backfilled: true));
|
||||
|
||||
int processed = await service.BackfillOneAsync("d_1", force: true, CancellationToken.None);
|
||||
@@ -290,7 +290,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task ReadRecent_OnlyMonitoredDialogs_BackfillsEachMonitoredWithForce()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true, Backfilled: true));
|
||||
store.Seed(Dialog("d_2", "Группа", "group", Monitor: true, Backfilled: false));
|
||||
store.Seed(Dialog("d_off", "Выключен", "channel", Monitor: false, Backfilled: false));
|
||||
@@ -309,7 +309,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task ReadRecent_NoMonitoredDialogs_ReturnsZeroWithoutGatewayCalls()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
store.Seed(Dialog("d_off", "Выключен", "channel", Monitor: false, Backfilled: false));
|
||||
|
||||
int count = await service.ReadRecentAsync(CancellationToken.None);
|
||||
@@ -326,7 +326,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task List_ReturnsMonitoredFirstByNameOrderWithLastMessage()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, _) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, _) = Create();
|
||||
DateTimeOffset lastAt = DateTimeOffset.UtcNow.AddMinutes(-5);
|
||||
store.Seed(Dialog("d_gamma", "Гамма", "channel", Monitor: true, Backfilled: true, LastText: "привет", LastAt: lastAt));
|
||||
store.Seed(Dialog("d_alpha", "Альфа", "group", Monitor: false, Backfilled: false));
|
||||
@@ -352,7 +352,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task SavePreview_PersistsPreviewRowAndTouchesDialogLast()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, _) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, _) = Create();
|
||||
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true, Backfilled: false));
|
||||
string longText = new('т', 5000);
|
||||
DateTimeOffset msgAt = DateTimeOffset.UtcNow.AddMinutes(-1);
|
||||
@@ -375,7 +375,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task SavePreview_EmptyText_DoesNotTouchStore()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, _) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, _) = Create();
|
||||
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true, Backfilled: false));
|
||||
|
||||
await service.SavePreviewAsync("d_1", msgId: 7, " ", DateTimeOffset.UtcNow, CancellationToken.None);
|
||||
@@ -391,7 +391,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task SavePreview_NoMsgId_UpdatesLastOnly()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, _) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, _) = Create();
|
||||
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true, Backfilled: false));
|
||||
|
||||
await service.SavePreviewAsync("d_1", msgId: null, "Новое сообщение", null, CancellationToken.None);
|
||||
@@ -407,7 +407,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task SavePreview_DuplicateMessageId_KeepsFirstRow()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, _) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, _) = Create();
|
||||
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true, Backfilled: false));
|
||||
DateTimeOffset firstAt = DateTimeOffset.UtcNow.AddMinutes(-2);
|
||||
|
||||
@@ -426,7 +426,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task AddDiscoveredMonitored_NewDialog_CreatesMonitoredRowAndNotifiesMirror()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
|
||||
await service.AddDiscoveredMonitoredAsync("d_100", "Канал", "channel_handle", "channel", "#a33", CancellationToken.None);
|
||||
|
||||
@@ -447,7 +447,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task AddDiscoveredMonitored_ExistingDialog_UpdatesMetadataAndForcesMonitorOn()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
store.Seed(Dialog("d_100", "Старое имя", "group", Monitor: false, Backfilled: true));
|
||||
|
||||
await service.AddDiscoveredMonitoredAsync("d_100", "Новое имя", "new_handle", "channel", "#c21", CancellationToken.None);
|
||||
@@ -468,7 +468,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task AddDiscoveredMonitored_EmptyNameAndHue_FallBackToDialogIdAndDefaultHue()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, _) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, _) = Create();
|
||||
|
||||
await service.AddDiscoveredMonitoredAsync("d_100", " ", string.Empty, string.Empty, string.Empty, CancellationToken.None);
|
||||
|
||||
@@ -485,7 +485,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task AddDiscoveredMonitored_GatewayMirrorFails_RowStillWritten()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
(DialogsService service, TestTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
gateway.SetMonitorFailures.Add("d_100");
|
||||
|
||||
await service.AddDiscoveredMonitoredAsync("d_100", "Канал", string.Empty, "channel", "#666", CancellationToken.None);
|
||||
@@ -496,13 +496,13 @@ public sealed class DialogsServiceTests
|
||||
|
||||
// ─── Хелперы ────────────────────────────────────────────────────────────
|
||||
|
||||
// Собирает сервис на фейках (FakeTelegramStore — семантика адаптера; настройки/гейт чистые).
|
||||
private static (DialogsService Service, FakeTelegramStore Store, FakeSettingsStore Settings, TestTelegramGateway Gateway) Create()
|
||||
// Собирает сервис на фейках (TestTelegramStore — семантика адаптера; настройки/гейт чистые).
|
||||
private static (DialogsService Service, TestTelegramStore Store, FakeSettingsStore Settings, TestTelegramGateway Gateway) Create()
|
||||
{
|
||||
var store = new FakeTelegramStore();
|
||||
var store = new TestTelegramStore();
|
||||
var settings = new FakeSettingsStore();
|
||||
var gateway = new TestTelegramGateway();
|
||||
return (new DialogsService(store, settings, gateway.Gateway, NullLogger<DialogsService>.Instance), store, settings, gateway);
|
||||
return (new DialogsService(store.Store, settings, gateway.Gateway, NullLogger<DialogsService>.Instance), store, settings, gateway);
|
||||
}
|
||||
|
||||
// Строка каталога минимально (id/имя/kind + флаги/хвост каталога).
|
||||
|
||||
@@ -35,11 +35,11 @@ public sealed class TelegramIngressServiceTests
|
||||
public async Task SyncDialogs_KnownTenant_AppliesCatalogAndReturnsMonitoredIds()
|
||||
{
|
||||
var registry = TestRegistry.With(Tenant(TenantA));
|
||||
var dialogs = new FakeTelegramStore();
|
||||
var dialogs = new TestTelegramStore();
|
||||
|
||||
await RunAsync(
|
||||
registry,
|
||||
services => services.AddScoped<ITelegramStore>(_ => dialogs),
|
||||
services => services.AddScoped<ITelegramStore>(_ => dialogs.Store),
|
||||
async channel =>
|
||||
{
|
||||
var request = new SyncDialogsRequest();
|
||||
@@ -63,14 +63,14 @@ public sealed class TelegramIngressServiceTests
|
||||
var registry = TestRegistry.With(Tenant(TenantA));
|
||||
var settings = new FakeSettingsStore();
|
||||
settings.Preload(SettingsKeys.AutoMonitorNew, "false");
|
||||
var dialogs = new FakeTelegramStore();
|
||||
var dialogs = new TestTelegramStore();
|
||||
|
||||
await RunAsync(
|
||||
registry,
|
||||
services =>
|
||||
{
|
||||
services.AddScoped<ISettingsStore>(_ => settings);
|
||||
services.AddScoped<ITelegramStore>(_ => dialogs);
|
||||
services.AddScoped<ITelegramStore>(_ => dialogs.Store);
|
||||
},
|
||||
async channel =>
|
||||
{
|
||||
|
||||
@@ -100,7 +100,7 @@ internal static class TelegramIngressTestHost
|
||||
}
|
||||
|
||||
builder.Services.AddScoped<ISettingsStore>(_ => new FakeSettingsStore());
|
||||
builder.Services.AddScoped<ITelegramStore>(_ => new FakeTelegramStore());
|
||||
builder.Services.AddScoped<ITelegramStore>(_ => new TestTelegramStore().Store);
|
||||
builder.Services.AddScoped<ITelegramGateway>(_ => new TestTelegramGateway().Gateway);
|
||||
builder.Services.AddTelegramModule();
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed class TelegramSourceIngestObserverTests
|
||||
[Fact]
|
||||
public async Task OnIngestedAsync_KnownKind_SavesPreview()
|
||||
{
|
||||
(TelegramSourceIngestObserver observer, FakeTelegramStore store) = Create();
|
||||
(TelegramSourceIngestObserver observer, TestTelegramStore store) = Create();
|
||||
store.Seed(Dialog());
|
||||
DateTimeOffset receivedAt = DateTimeOffset.UtcNow.AddMinutes(-1);
|
||||
|
||||
@@ -40,7 +40,7 @@ public sealed class TelegramSourceIngestObserverTests
|
||||
[Fact]
|
||||
public async Task OnIngestedAsync_ForeignKind_DoesNotTouchStore()
|
||||
{
|
||||
(TelegramSourceIngestObserver observer, FakeTelegramStore store) = Create();
|
||||
(TelegramSourceIngestObserver observer, TestTelegramStore store) = Create();
|
||||
store.Seed(Dialog());
|
||||
|
||||
await observer.OnIngestedAsync(Item("avito", DialogId, "7", "Текст", DateTimeOffset.UtcNow), CancellationToken.None);
|
||||
@@ -56,7 +56,7 @@ public sealed class TelegramSourceIngestObserverTests
|
||||
[Fact]
|
||||
public async Task OnIngestedAsync_NoOriginRef_DoesNotTouchStore()
|
||||
{
|
||||
(TelegramSourceIngestObserver observer, FakeTelegramStore store) = Create();
|
||||
(TelegramSourceIngestObserver observer, TestTelegramStore store) = Create();
|
||||
store.Seed(Dialog());
|
||||
|
||||
await observer.OnIngestedAsync(Item("telegram", null, "7", "Текст", DateTimeOffset.UtcNow), CancellationToken.None);
|
||||
@@ -66,11 +66,11 @@ public sealed class TelegramSourceIngestObserverTests
|
||||
}
|
||||
|
||||
// Собирает наблюдателя на фейковом хранилище диалогов.
|
||||
private static (TelegramSourceIngestObserver Observer, FakeTelegramStore Store) Create()
|
||||
private static (TelegramSourceIngestObserver Observer, TestTelegramStore Store) Create()
|
||||
{
|
||||
var store = new FakeTelegramStore();
|
||||
var store = new TestTelegramStore();
|
||||
var dialogs = new DialogsService(
|
||||
store,
|
||||
store.Store,
|
||||
new FakeSettingsStore(),
|
||||
new TestTelegramGateway().Gateway,
|
||||
NullLogger<DialogsService>.Instance);
|
||||
|
||||
+68
-81
@@ -1,28 +1,70 @@
|
||||
using Deal.Contracts.Integrations.Models;
|
||||
using Deal.Modules.Telegram.Application;
|
||||
using Deal.Modules.Telegram.Application.Models;
|
||||
using Deal.Tests.Unit.Support;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Deal.Tests.Unit.Contracts;
|
||||
namespace Deal.Tests.Unit.Support;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory реализация <see cref="ITelegramStore"/> для юнит-тестов DialogsService/ингресса.
|
||||
/// Подставка <see cref="ITelegramStore"/> на словарях: сервисы получают NSubstitute-подставку
|
||||
/// (<see cref="Store"/>), тесты сеют данные через <see cref="Seed"/>/<see cref="SeedMessage"/>
|
||||
/// и читают состояние через <see cref="Dialogs"/>/<see cref="Messages"/>.
|
||||
/// </summary>
|
||||
public sealed class FakeTelegramStore : ITelegramStore
|
||||
public sealed class TestTelegramStore
|
||||
{
|
||||
private readonly Dictionary<string, FakeTelegramDialogRow> _dialogs = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, FakeTelegramMessageRow> _messages = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Строки каталога фейка
|
||||
/// Подставка порта Telegram-хранилища (создаётся в конструкторе).
|
||||
/// </summary>
|
||||
public ITelegramStore Store { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Строки каталога подставки
|
||||
/// </summary>
|
||||
public IReadOnlyList<FakeTelegramDialogRow> Dialogs => _dialogs.Values.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Строки превью фейка
|
||||
/// Строки превью подставки
|
||||
/// </summary>
|
||||
public IReadOnlyList<FakeTelegramMessageRow> Messages => _messages.Values.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт подставку с пустыми словарями.
|
||||
/// </summary>
|
||||
public TestTelegramStore()
|
||||
{
|
||||
Store = Substitute.For<ITelegramStore>();
|
||||
Store.SyncFromTelegramAsync(Arg.Any<IReadOnlyCollection<TelegramDialogEntryDto>>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => SyncFromTelegram(
|
||||
ci.ArgAt<IReadOnlyCollection<TelegramDialogEntryDto>>(0), ci.ArgAt<bool>(1)));
|
||||
Store.ListAsync(Arg.Any<CancellationToken>()).Returns(ci => ListDialogs());
|
||||
Store.ListMonitoredIdsAsync(Arg.Any<CancellationToken>()).Returns(ci => ListMonitoredIds());
|
||||
Store.When(s => s.SetMonitorAsync(Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => SetMonitor(ci.ArgAt<string>(0), ci.ArgAt<bool>(1)));
|
||||
Store.SetMonitorAllAsync(Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => SetMonitorAll(ci.ArgAt<bool>(0)));
|
||||
Store.GetBackfilledAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => GetBackfilled(ci.ArgAt<string>(0)));
|
||||
Store.ListNotBackfilledIdsAsync(Arg.Any<CancellationToken>()).Returns(ci => ListNotBackfilledIds());
|
||||
Store.When(s => s.SetBackfilledAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => SetBackfilled(ci.ArgAt<string>(0)));
|
||||
Store.When(s => s.UpsertDiscoveredMonitoredAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => UpsertDiscoveredMonitored(
|
||||
ci.ArgAt<string>(0), ci.ArgAt<string>(1), ci.ArgAt<string>(2), ci.ArgAt<string>(3), ci.ArgAt<string>(4)));
|
||||
Store.When(s => s.SavePreviewAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => SavePreview(
|
||||
ci.ArgAt<string>(0), ci.ArgAt<string>(1), ci.ArgAt<string>(2), ci.ArgAt<DateTimeOffset>(3)));
|
||||
Store.When(s => s.TouchDialogLastAsync(
|
||||
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTimeOffset>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => TouchDialogLast(ci.ArgAt<string>(0), ci.ArgAt<string>(1), ci.ArgAt<DateTimeOffset>(2)));
|
||||
Store.ListMessagesAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => ListMessages(ci.ArgAt<string>(0), ci.ArgAt<int>(1)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Кладёт строку каталога напрямую
|
||||
/// </summary>
|
||||
@@ -41,15 +83,11 @@ public sealed class FakeTelegramStore : ITelegramStore
|
||||
_messages[row.Id] = row;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> SyncFromTelegramAsync(
|
||||
IReadOnlyCollection<TelegramDialogEntryDto> entries,
|
||||
bool autoMonitorNew,
|
||||
CancellationToken ct)
|
||||
private int SyncFromTelegram(IReadOnlyCollection<TelegramDialogEntryDto> entries, bool autoMonitorNew)
|
||||
{
|
||||
if (entries.Count == 0)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
@@ -81,47 +119,36 @@ public sealed class FakeTelegramStore : ITelegramStore
|
||||
_dialogs.Remove(id);
|
||||
}
|
||||
|
||||
return Task.FromResult(entries.Count);
|
||||
return entries.Count;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<TelegramDialogDto>> ListAsync(CancellationToken ct)
|
||||
private IReadOnlyList<TelegramDialogDto> ListDialogs()
|
||||
{
|
||||
IReadOnlyList<TelegramDialogDto> items = _dialogs.Values
|
||||
return _dialogs.Values
|
||||
.OrderByDescending(row => row.Monitor)
|
||||
.ThenBy(row => row.Name)
|
||||
.Select(ToDialogDto)
|
||||
.ToList();
|
||||
return Task.FromResult(items);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyCollection<string>> ListMonitoredIdsAsync(CancellationToken ct)
|
||||
private IReadOnlyCollection<string> ListMonitoredIds()
|
||||
{
|
||||
IReadOnlyCollection<string> ids = _dialogs.Values
|
||||
return _dialogs.Values
|
||||
.Where(row => row.Monitor)
|
||||
.OrderBy(row => row.Id)
|
||||
.Select(row => row.Id)
|
||||
.ToList();
|
||||
return Task.FromResult(ids);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetMonitorAsync(
|
||||
string dialogId,
|
||||
bool enabled,
|
||||
CancellationToken ct)
|
||||
private void SetMonitor(string dialogId, bool enabled)
|
||||
{
|
||||
if (_dialogs.TryGetValue(dialogId, out FakeTelegramDialogRow? existing))
|
||||
{
|
||||
_dialogs[dialogId] = existing with { Monitor = enabled, UpdatedAt = DateTimeOffset.UtcNow };
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> SetMonitorAllAsync(bool enabled, CancellationToken ct)
|
||||
private int SetMonitorAll(bool enabled)
|
||||
{
|
||||
int count = _dialogs.Count;
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
@@ -130,50 +157,32 @@ public sealed class FakeTelegramStore : ITelegramStore
|
||||
_dialogs[id] = _dialogs[id] with { Monitor = enabled, UpdatedAt = now };
|
||||
}
|
||||
|
||||
return Task.FromResult(count);
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool?> GetBackfilledAsync(string dialogId, CancellationToken ct)
|
||||
private bool? GetBackfilled(string dialogId)
|
||||
{
|
||||
if (_dialogs.TryGetValue(dialogId, out FakeTelegramDialogRow? row))
|
||||
{
|
||||
return Task.FromResult<bool?>(row.Backfilled);
|
||||
}
|
||||
|
||||
return Task.FromResult<bool?>(null);
|
||||
return _dialogs.TryGetValue(dialogId, out FakeTelegramDialogRow? row) ? row.Backfilled : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyCollection<string>> ListNotBackfilledIdsAsync(CancellationToken ct)
|
||||
private IReadOnlyCollection<string> ListNotBackfilledIds()
|
||||
{
|
||||
IReadOnlyCollection<string> ids = _dialogs.Values
|
||||
return _dialogs.Values
|
||||
.Where(row => !row.Backfilled)
|
||||
.OrderBy(row => row.Id)
|
||||
.Select(row => row.Id)
|
||||
.ToList();
|
||||
return Task.FromResult(ids);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetBackfilledAsync(string dialogId, CancellationToken ct)
|
||||
private void SetBackfilled(string dialogId)
|
||||
{
|
||||
if (_dialogs.TryGetValue(dialogId, out FakeTelegramDialogRow? existing))
|
||||
{
|
||||
_dialogs[dialogId] = existing with { Backfilled = true };
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpsertDiscoveredMonitoredAsync(
|
||||
string dialogId,
|
||||
string name,
|
||||
string handle,
|
||||
string kind,
|
||||
string hue,
|
||||
CancellationToken ct)
|
||||
private void UpsertDiscoveredMonitored(string dialogId, string name, string handle, string kind, string hue)
|
||||
{
|
||||
if (_dialogs.TryGetValue(dialogId, out FakeTelegramDialogRow? existing))
|
||||
{
|
||||
@@ -194,54 +203,32 @@ public sealed class FakeTelegramStore : ITelegramStore
|
||||
dialogId, name, handle, kind, hue,
|
||||
Monitor: true, LastText: string.Empty, LastAt: null, Backfilled: false, UpdatedAt: DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SavePreviewAsync(
|
||||
string messageId,
|
||||
string dialogId,
|
||||
string text,
|
||||
DateTimeOffset msgAt,
|
||||
CancellationToken ct)
|
||||
private void SavePreview(string messageId, string dialogId, string text, DateTimeOffset msgAt)
|
||||
{
|
||||
if (!_messages.ContainsKey(messageId))
|
||||
{
|
||||
_messages[messageId] = new FakeTelegramMessageRow(messageId, dialogId, text, msgAt, LeadId: null);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task TouchDialogLastAsync(
|
||||
string dialogId,
|
||||
string text,
|
||||
DateTimeOffset at,
|
||||
CancellationToken ct)
|
||||
private void TouchDialogLast(string dialogId, string text, DateTimeOffset at)
|
||||
{
|
||||
if (_dialogs.TryGetValue(dialogId, out FakeTelegramDialogRow? existing))
|
||||
{
|
||||
_dialogs[dialogId] = existing with { LastText = text, LastAt = at, UpdatedAt = at };
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<TelegramMessageDto>> ListMessagesAsync(
|
||||
string dialogId,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
private IReadOnlyList<TelegramMessageDto> ListMessages(string dialogId, int limit)
|
||||
{
|
||||
IReadOnlyList<TelegramMessageDto> items = _messages.Values
|
||||
return _messages.Values
|
||||
.Where(row => row.DialogId == dialogId)
|
||||
.OrderByDescending(row => row.MsgAt)
|
||||
.Take(limit)
|
||||
.Select(row => new TelegramMessageDto(row.Id, row.Text, row.MsgAt.ToUnixTimeMilliseconds(), row.LeadId is not null))
|
||||
.ToList();
|
||||
return Task.FromResult(items);
|
||||
}
|
||||
|
||||
private static TelegramDialogDto ToDialogDto(FakeTelegramDialogRow row)
|
||||
Reference in New Issue
Block a user