Перевести FakeTelegramGateway и FakeDiscoveryGateway на NSubstitute
ci / build-test (push) Canceled after 0s
ci / build-test (push) Canceled after 0s
Хелперы Support/TestTelegramGateway (журналы SetMonitor/Backfill/ReadSource, сценарии Status/Catalog/сбоев по диалогам) и Support/TestDiscoveryGateway (поиск/инфо/чтения/Join + NotFoundException на неожиданных методах). Потребители (7 файлов) перетипизированы на .Gateway, фейк-классы удалены, тесты 1340 зелёные.
This commit is contained in:
@@ -37,8 +37,8 @@ public sealed class DiscoveryWorkerSchedulerTests
|
||||
DiscoveryWorkerScheduler Scheduler,
|
||||
FakeDiscoveryStore StoreA,
|
||||
FakeDiscoveryStore StoreB,
|
||||
FakeDiscoveryGateway GatewayA,
|
||||
FakeDiscoveryGateway GatewayB,
|
||||
TestDiscoveryGateway GatewayA,
|
||||
TestDiscoveryGateway GatewayB,
|
||||
TenantContext TenantContext,
|
||||
ListLogger Logs);
|
||||
|
||||
@@ -96,8 +96,8 @@ public sealed class DiscoveryWorkerSchedulerTests
|
||||
var storeB = new FakeDiscoveryStore();
|
||||
var settingsA = new FakeSettingsStore();
|
||||
var settingsB = new FakeSettingsStore();
|
||||
var gatewayA = new FakeDiscoveryGateway();
|
||||
var gatewayB = new FakeDiscoveryGateway();
|
||||
var gatewayA = new TestDiscoveryGateway();
|
||||
var gatewayB = new TestDiscoveryGateway();
|
||||
PreloadDiscoveryEvalOff(settingsA);
|
||||
PreloadDiscoveryEvalOff(settingsB);
|
||||
|
||||
@@ -110,7 +110,7 @@ public sealed class DiscoveryWorkerSchedulerTests
|
||||
// (эталон PipelineWorkerSchedulerTests/ConnectionStringProvider.ForTenant).
|
||||
services.AddScoped<IDiscoveryStore>(provider => TenantOf(provider) == TenantA ? storeA : storeB);
|
||||
services.AddScoped<ISettingsStore>(provider => TenantOf(provider) == TenantA ? settingsA : settingsB);
|
||||
services.AddScoped<ITelegramGateway>(provider => TenantOf(provider) == TenantA ? gatewayA : gatewayB);
|
||||
services.AddScoped<ITelegramGateway>(provider => TenantOf(provider) == TenantA ? gatewayA.Gateway : gatewayB.Gateway);
|
||||
// Реальные сервисы модуля Discovery — как AddDiscoveryModule в Program.cs: цикл резолвит их в tenant-scope.
|
||||
services.AddDiscoveryModule();
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ public sealed class TgStatusServiceTests
|
||||
[Fact]
|
||||
public async Task GetAsync_GatewayUnavailable_ReturnsIdleFormWithKvAccountCountAndKeys()
|
||||
{
|
||||
(TgStatusService service, FakeTelegramStore store, FakeSettingsStore settings, FakeTelegramGateway gateway, _, TestGlobalSettingsStore globalSettings) = Create();
|
||||
(TgStatusService service, FakeTelegramStore 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, FakeTelegramGateway gateway, _, TestGlobalSettingsStore globalSettings) = Create();
|
||||
(TgStatusService service, FakeTelegramStore 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");
|
||||
@@ -91,7 +91,7 @@ public sealed class TgStatusServiceTests
|
||||
[Fact]
|
||||
public async Task GetAsync_QrPhase_ReturnsQrUrlFromGateway()
|
||||
{
|
||||
(TgStatusService service, _, _, FakeTelegramGateway gateway, _, _) = Create();
|
||||
(TgStatusService service, _, _, TestTelegramGateway gateway, _, _) = Create();
|
||||
gateway.Status = new TelegramAccountStatusDto(
|
||||
Phase: "qr", Connected: false, Listener: false, Account: string.Empty, Error: null, QrUrl: "https://t.me/qr/test");
|
||||
|
||||
@@ -105,16 +105,16 @@ public sealed class TgStatusServiceTests
|
||||
// ─── Хелперы ────────────────────────────────────────────────────────────
|
||||
|
||||
// Собирает сервис на фейках (FakeTelegramGateway/Store/SettingsStore/GlobalSettingsStore + FakeSecretCipher).
|
||||
private static (TgStatusService Service, FakeTelegramStore Store, FakeSettingsStore Settings, FakeTelegramGateway Gateway, FakeSecretCipher Cipher, TestGlobalSettingsStore GlobalSettings) Create()
|
||||
private static (TgStatusService Service, FakeTelegramStore Store, FakeSettingsStore Settings, TestTelegramGateway Gateway, FakeSecretCipher Cipher, TestGlobalSettingsStore GlobalSettings) Create()
|
||||
{
|
||||
var store = new FakeTelegramStore();
|
||||
var settings = new FakeSettingsStore();
|
||||
var gateway = new FakeTelegramGateway();
|
||||
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, NullLogger<DialogsService>.Instance);
|
||||
var service = new TgStatusService(gateway, dialogs, settings, keys);
|
||||
var dialogs = new DialogsService(store, settings, gateway.Gateway, NullLogger<DialogsService>.Instance);
|
||||
var service = new TgStatusService(gateway.Gateway, dialogs, settings, keys);
|
||||
return (service, store, settings, gateway, cipher, globalSettings);
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task SetMonitor_EnableFirstTimeNotBackfilled_SetsFlagCallsGatewayAndNeedsBackfill()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, FakeTelegramGateway gateway) = Create();
|
||||
(DialogsService service, FakeTelegramStore 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, _, FakeTelegramGateway gateway) = Create();
|
||||
(DialogsService service, FakeTelegramStore 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, _, FakeTelegramGateway gateway) = Create();
|
||||
(DialogsService service, FakeTelegramStore 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, _, FakeTelegramGateway gateway) = Create();
|
||||
(DialogsService service, FakeTelegramStore 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, _, FakeTelegramGateway gateway) = Create();
|
||||
(DialogsService service, FakeTelegramStore 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, _, FakeTelegramGateway gateway) = Create();
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
store.Seed(Dialog("d_a", "А", "channel", Monitor: true, Backfilled: false));
|
||||
store.Seed(Dialog("d_b", "Б", "channel", Monitor: true, Backfilled: true));
|
||||
|
||||
@@ -230,7 +230,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task ReadRecent_OneDialogFails_ContinuesWithOthersAndMarksOnlySuccessful()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, FakeTelegramGateway gateway) = Create();
|
||||
(DialogsService service, FakeTelegramStore 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, _, FakeTelegramGateway gateway) = Create();
|
||||
(DialogsService service, FakeTelegramStore 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, _, FakeTelegramGateway gateway) = Create();
|
||||
(DialogsService service, FakeTelegramStore 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, _, FakeTelegramGateway gateway) = Create();
|
||||
(DialogsService service, FakeTelegramStore 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, _, FakeTelegramGateway gateway) = Create();
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
store.Seed(Dialog("d_off", "Выключен", "channel", Monitor: false, Backfilled: false));
|
||||
|
||||
int count = await service.ReadRecentAsync(CancellationToken.None);
|
||||
@@ -426,7 +426,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task AddDiscoveredMonitored_NewDialog_CreatesMonitoredRowAndNotifiesMirror()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, FakeTelegramGateway gateway) = Create();
|
||||
(DialogsService service, FakeTelegramStore 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, _, FakeTelegramGateway gateway) = Create();
|
||||
(DialogsService service, FakeTelegramStore 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);
|
||||
@@ -485,7 +485,7 @@ public sealed class DialogsServiceTests
|
||||
[Fact]
|
||||
public async Task AddDiscoveredMonitored_GatewayMirrorFails_RowStillWritten()
|
||||
{
|
||||
(DialogsService service, FakeTelegramStore store, _, FakeTelegramGateway gateway) = Create();
|
||||
(DialogsService service, FakeTelegramStore store, _, TestTelegramGateway gateway) = Create();
|
||||
gateway.SetMonitorFailures.Add("d_100");
|
||||
|
||||
await service.AddDiscoveredMonitoredAsync("d_100", "Канал", string.Empty, "channel", "#666", CancellationToken.None);
|
||||
@@ -497,12 +497,12 @@ public sealed class DialogsServiceTests
|
||||
// ─── Хелперы ────────────────────────────────────────────────────────────
|
||||
|
||||
// Собирает сервис на фейках (FakeTelegramStore — семантика адаптера; настройки/гейт чистые).
|
||||
private static (DialogsService Service, FakeTelegramStore Store, FakeSettingsStore Settings, FakeTelegramGateway Gateway) Create()
|
||||
private static (DialogsService Service, FakeTelegramStore Store, FakeSettingsStore Settings, TestTelegramGateway Gateway) Create()
|
||||
{
|
||||
var store = new FakeTelegramStore();
|
||||
var settings = new FakeSettingsStore();
|
||||
var gateway = new FakeTelegramGateway();
|
||||
return (new DialogsService(store, settings, gateway, NullLogger<DialogsService>.Instance), store, settings, gateway);
|
||||
var gateway = new TestTelegramGateway();
|
||||
return (new DialogsService(store, settings, gateway.Gateway, NullLogger<DialogsService>.Instance), store, settings, gateway);
|
||||
}
|
||||
|
||||
// Строка каталога минимально (id/имя/kind + флаги/хвост каталога).
|
||||
|
||||
@@ -420,7 +420,7 @@ public sealed class DiscoveryWorkerServiceTests
|
||||
private sealed record Fixture(
|
||||
FakeDiscoveryStore Store,
|
||||
FakeSettingsStore Settings,
|
||||
FakeDiscoveryGateway Gateway,
|
||||
TestDiscoveryGateway Gateway,
|
||||
IDiscoveryPacer Pacer,
|
||||
DiscoveryWorkerService Worker,
|
||||
DiscoveryTasksService Tasks);
|
||||
@@ -441,9 +441,9 @@ public sealed class DiscoveryWorkerServiceTests
|
||||
var tasks = new DiscoveryTasksService(store, planGuard, settings);
|
||||
var banGuard = new DiscoveryBanGuard(store, settings);
|
||||
var pacer = Substitute.For<IDiscoveryPacer>();
|
||||
var gateway = new FakeDiscoveryGateway();
|
||||
var gateway = new TestDiscoveryGateway();
|
||||
var worker = new DiscoveryWorkerService(
|
||||
store, tasks, candidates, log, new DiscoveryEvaluator(settings, ml, ai), banGuard, pacer, gateway);
|
||||
store, tasks, candidates, log, new DiscoveryEvaluator(settings, ml, ai), banGuard, pacer, gateway.Gateway);
|
||||
return new Fixture(store, settings, gateway, pacer, worker, tasks);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
using Deal.Contracts.Integrations.Abstractions;
|
||||
using Deal.Contracts.Integrations.Models;
|
||||
|
||||
namespace Deal.Tests.Unit.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Фейковый <see cref="ITelegramGateway"/> для тестов Discovery-воркера
|
||||
/// </summary>
|
||||
public sealed class FakeDiscoveryGateway : ITelegramGateway
|
||||
{
|
||||
/// <summary>
|
||||
/// Результат глобального поиска
|
||||
/// </summary>
|
||||
public IReadOnlyList<TelegramDialogEntryDto> SearchResults { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Исключение SearchAsync
|
||||
/// </summary>
|
||||
public Exception? SearchError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Инфо об источниках по dialogId; отсутствующий — fallback как Local
|
||||
/// </summary>
|
||||
public Dictionary<string, TelegramChannelInfoDto> InfoByDialog { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Выборки сообщений по dialogId; отсутствующий — ok=false "no_history"
|
||||
/// </summary>
|
||||
public Dictionary<string, TelegramEvalReadDto> ReadsByDialog { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Исключение JoinAsync
|
||||
/// </summary>
|
||||
public Exception? JoinError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Сколько сообщений «разобрал» Backfill
|
||||
/// </summary>
|
||||
public int BackfillResult { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Поисковые запросы в порядке вызовов.
|
||||
/// </summary>
|
||||
public List<string> SearchedKeywords { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Запросы Info
|
||||
/// </summary>
|
||||
public List<string> InfoRequests { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Запросы ReadForEval
|
||||
/// </summary>
|
||||
public List<(string DialogId, int Limit)> ReadRequests { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы Join (username без «@») в порядке вызовов.
|
||||
/// </summary>
|
||||
public List<string> JoinedUsernames { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы SetMonitor
|
||||
/// </summary>
|
||||
public List<(string DialogId, bool Enabled)> SetMonitorCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы Backfill
|
||||
/// </summary>
|
||||
public List<(string DialogId, bool Force)> BackfillCalls { get; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TelegramAccountStatusDto> StatusAsync(CancellationToken ct)
|
||||
=> Task.FromResult(new TelegramAccountStatusDto("ready", Connected: true, Listener: true, "@test", Error: null, QrUrl: null));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TelegramAuthResultDto> StartPhoneAsync(
|
||||
string phone,
|
||||
int apiId,
|
||||
string apiHash,
|
||||
CancellationToken ct)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TelegramAuthResultDto> StartQrAsync(
|
||||
int apiId,
|
||||
string apiHash,
|
||||
CancellationToken ct)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> SendCodeAsync(string code, CancellationToken ct) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> SendPasswordAsync(string password, CancellationToken ct) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task LogoutAsync(CancellationToken ct) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<TelegramDialogEntryDto>> RefreshDialogsAsync(CancellationToken ct)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetMonitorAsync(
|
||||
string dialogId,
|
||||
bool enabled,
|
||||
CancellationToken ct)
|
||||
{
|
||||
SetMonitorCalls.Add((dialogId, enabled));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetMonitorAllAsync(bool enabled, CancellationToken ct) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> BackfillAsync(
|
||||
string dialogId,
|
||||
bool force,
|
||||
CancellationToken ct)
|
||||
{
|
||||
BackfillCalls.Add((dialogId, force));
|
||||
return Task.FromResult(BackfillResult);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<TelegramRecentMessageDto>> ReadRecentAsync(
|
||||
string dialogId,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TelegramSourceContentDto> ReadSourceAsync(
|
||||
string dialogId,
|
||||
long msgId,
|
||||
CancellationToken ct)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<TelegramDialogEntryDto>> SearchAsync(
|
||||
string query,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
{
|
||||
SearchedKeywords.Add(query);
|
||||
if (SearchError is not null)
|
||||
{
|
||||
throw SearchError;
|
||||
}
|
||||
|
||||
return Task.FromResult(SearchResults);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TelegramChannelInfoDto> InfoAsync(string dialogId, CancellationToken ct)
|
||||
{
|
||||
InfoRequests.Add(dialogId);
|
||||
return Task.FromResult(
|
||||
InfoByDialog.TryGetValue(dialogId, out TelegramChannelInfoDto? info)
|
||||
? info
|
||||
: new TelegramChannelInfoDto(dialogId, string.Empty, string.Empty, string.Empty, "#666", Participants: null, IsForum: false));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TelegramEvalReadDto> ReadForEvalAsync(
|
||||
string dialogId,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ReadRequests.Add((dialogId, limit));
|
||||
return Task.FromResult(
|
||||
ReadsByDialog.TryGetValue(dialogId, out TelegramEvalReadDto? read)
|
||||
? read
|
||||
: new TelegramEvalReadDto(Ok: false, "no_history", []));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task JoinAsync(string username, CancellationToken ct)
|
||||
{
|
||||
JoinedUsernames.Add(username);
|
||||
if (JoinError is not null)
|
||||
{
|
||||
throw JoinError;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task LeaveAsync(string dialogId, CancellationToken ct) => throw new NotSupportedException();
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
using Deal.Contracts.Integrations.Abstractions;
|
||||
using Deal.Contracts.Integrations.Models;
|
||||
using Grpc.Core;
|
||||
|
||||
namespace Deal.Tests.Unit.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory реализация <see cref="ITelegramGateway"/> для юнит-тестов модуля Telegram/ингресса.
|
||||
/// </summary>
|
||||
public sealed class FakeTelegramGateway : ITelegramGateway
|
||||
{
|
||||
// Фаза idle-формы (по умолчанию не подключён).
|
||||
private const string IdlePhase = "idle";
|
||||
|
||||
private const string NotConnectedDetail = "Telegram не подключён";
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы SetMonitor
|
||||
/// </summary>
|
||||
public List<(string DialogId, bool Enabled)> SetMonitorCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы SetMonitorAll
|
||||
/// </summary>
|
||||
public List<bool> SetMonitorAllCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы Backfill
|
||||
/// </summary>
|
||||
public List<(string DialogId, bool Force)> BackfillCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы ReadSource
|
||||
/// </summary>
|
||||
public List<(string DialogId, long MsgId)> ReadSourceCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Результат ReadSourceAsync
|
||||
/// </summary>
|
||||
public TelegramSourceContentDto SourceContent { get; set; } = new(false, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// Сколько сообщений «разобрал» Backfill
|
||||
/// </summary>
|
||||
public int BackfillResult { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Каталог, который возвращает RefreshDialogsAsync/SearchAsync
|
||||
/// </summary>
|
||||
public IReadOnlyList<TelegramDialogEntryDto> Catalog { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Статус, который возвращает StatusAsync
|
||||
/// </summary>
|
||||
public TelegramAccountStatusDto Status { get; set; } =
|
||||
new(IdlePhase, Connected: false, Listener: false, Account: string.Empty, Error: null, QrUrl: null);
|
||||
|
||||
/// <summary>
|
||||
/// Заданное исключение StatusAsync
|
||||
/// </summary>
|
||||
public RpcException? StatusFailure { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Диалоги, чей BackfillAsync падает
|
||||
/// </summary>
|
||||
public HashSet<string> BackfillFailures { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Диалоги, чей SetMonitorAsync падает
|
||||
/// </summary>
|
||||
public HashSet<string> SetMonitorFailures { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TelegramAccountStatusDto> StatusAsync(CancellationToken ct)
|
||||
{
|
||||
if (StatusFailure is not null)
|
||||
{
|
||||
throw StatusFailure;
|
||||
}
|
||||
|
||||
return Task.FromResult(Status);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TelegramAuthResultDto> StartPhoneAsync(
|
||||
string phone,
|
||||
int apiId,
|
||||
string apiHash,
|
||||
CancellationToken ct)
|
||||
=> Task.FromResult(new TelegramAuthResultDto("code", null));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TelegramAuthResultDto> StartQrAsync(
|
||||
int apiId,
|
||||
string apiHash,
|
||||
CancellationToken ct)
|
||||
=> Task.FromResult(new TelegramAuthResultDto("qr", "https://t.me/qr/test"));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> SendCodeAsync(string code, CancellationToken ct) => Task.FromResult("ready");
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> SendPasswordAsync(string password, CancellationToken ct) => Task.FromResult("ready");
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task LogoutAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<TelegramDialogEntryDto>> RefreshDialogsAsync(CancellationToken ct)
|
||||
=> Task.FromResult(Catalog);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetMonitorAsync(
|
||||
string dialogId,
|
||||
bool enabled,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (SetMonitorFailures.Contains(dialogId))
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unavailable, NotConnectedDetail));
|
||||
}
|
||||
|
||||
SetMonitorCalls.Add((dialogId, enabled));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetMonitorAllAsync(bool enabled, CancellationToken ct)
|
||||
{
|
||||
SetMonitorAllCalls.Add(enabled);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> BackfillAsync(
|
||||
string dialogId,
|
||||
bool force,
|
||||
CancellationToken ct)
|
||||
{
|
||||
BackfillCalls.Add((dialogId, force));
|
||||
if (BackfillFailures.Contains(dialogId))
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unavailable, NotConnectedDetail));
|
||||
}
|
||||
|
||||
return Task.FromResult(BackfillResult);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<TelegramRecentMessageDto>> ReadRecentAsync(
|
||||
string dialogId,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
=> Task.FromResult<IReadOnlyList<TelegramRecentMessageDto>>([]);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TelegramSourceContentDto> ReadSourceAsync(
|
||||
string dialogId,
|
||||
long msgId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ReadSourceCalls.Add((dialogId, msgId));
|
||||
return Task.FromResult(SourceContent);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<TelegramDialogEntryDto>> SearchAsync(
|
||||
string query,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
=> Task.FromResult(Catalog);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TelegramChannelInfoDto> InfoAsync(string dialogId, CancellationToken ct)
|
||||
=> Task.FromResult(new TelegramChannelInfoDto(dialogId, string.Empty, string.Empty, string.Empty, "#666", null, false));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TelegramEvalReadDto> ReadForEvalAsync(
|
||||
string dialogId,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
=> Task.FromResult(new TelegramEvalReadDto(false, "no_history", []));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task JoinAsync(string username, CancellationToken ct) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task LeaveAsync(string dialogId, CancellationToken ct) => Task.CompletedTask;
|
||||
}
|
||||
@@ -101,7 +101,7 @@ internal static class TelegramIngressTestHost
|
||||
|
||||
builder.Services.AddScoped<ISettingsStore>(_ => new FakeSettingsStore());
|
||||
builder.Services.AddScoped<ITelegramStore>(_ => new FakeTelegramStore());
|
||||
builder.Services.AddScoped<ITelegramGateway>(_ => new FakeTelegramGateway());
|
||||
builder.Services.AddScoped<ITelegramGateway>(_ => new TestTelegramGateway().Gateway);
|
||||
builder.Services.AddTelegramModule();
|
||||
|
||||
configureServices(builder.Services);
|
||||
|
||||
@@ -13,8 +13,8 @@ public sealed class TelegramSourceContentProviderTests
|
||||
[Fact]
|
||||
public async Task LoadAsync_ForeignKind_ReturnsNullWithoutGatewayCall()
|
||||
{
|
||||
var gateway = new FakeTelegramGateway();
|
||||
var provider = new TelegramSourceContentProvider(gateway);
|
||||
var gateway = new TestTelegramGateway();
|
||||
var provider = new TelegramSourceContentProvider(gateway.Gateway);
|
||||
|
||||
SourceContent? content = await provider.LoadAsync(
|
||||
SourceItemFactory.Source(kind: "local"), CancellationToken.None);
|
||||
@@ -26,11 +26,11 @@ public sealed class TelegramSourceContentProviderTests
|
||||
[Fact]
|
||||
public async Task LoadAsync_TelegramKindCaseInsensitive_LoadsContent()
|
||||
{
|
||||
var gateway = new FakeTelegramGateway
|
||||
var gateway = new TestTelegramGateway
|
||||
{
|
||||
SourceContent = new TelegramSourceContentDto(true, "текст", null),
|
||||
};
|
||||
var provider = new TelegramSourceContentProvider(gateway);
|
||||
var provider = new TelegramSourceContentProvider(gateway.Gateway);
|
||||
|
||||
SourceContent? content = await provider.LoadAsync(
|
||||
SourceItemFactory.Source(kind: "Telegram", origin: "d_1", externalId: "100"), CancellationToken.None);
|
||||
@@ -42,8 +42,8 @@ public sealed class TelegramSourceContentProviderTests
|
||||
[Fact]
|
||||
public async Task LoadAsync_EmptyOriginRef_ReturnsNullWithoutGatewayCall()
|
||||
{
|
||||
var gateway = new FakeTelegramGateway();
|
||||
var provider = new TelegramSourceContentProvider(gateway);
|
||||
var gateway = new TestTelegramGateway();
|
||||
var provider = new TelegramSourceContentProvider(gateway.Gateway);
|
||||
|
||||
SourceContent? content = await provider.LoadAsync(
|
||||
SourceItemFactory.Source(kind: "telegram", origin: null, externalId: "100"), CancellationToken.None);
|
||||
@@ -55,8 +55,8 @@ public sealed class TelegramSourceContentProviderTests
|
||||
[Fact]
|
||||
public async Task LoadAsync_NonNumericExternalId_ReturnsNullWithoutGatewayCall()
|
||||
{
|
||||
var gateway = new FakeTelegramGateway();
|
||||
var provider = new TelegramSourceContentProvider(gateway);
|
||||
var gateway = new TestTelegramGateway();
|
||||
var provider = new TelegramSourceContentProvider(gateway.Gateway);
|
||||
|
||||
SourceContent? content = await provider.LoadAsync(
|
||||
SourceItemFactory.Source(kind: "telegram", origin: "d_1", externalId: "not-a-number"), CancellationToken.None);
|
||||
@@ -68,11 +68,11 @@ public sealed class TelegramSourceContentProviderTests
|
||||
[Fact]
|
||||
public async Task LoadAsync_GatewayNotFound_ReturnsNull()
|
||||
{
|
||||
var gateway = new FakeTelegramGateway
|
||||
var gateway = new TestTelegramGateway
|
||||
{
|
||||
SourceContent = new TelegramSourceContentDto(false, null, null),
|
||||
};
|
||||
var provider = new TelegramSourceContentProvider(gateway);
|
||||
var provider = new TelegramSourceContentProvider(gateway.Gateway);
|
||||
|
||||
SourceContent? content = await provider.LoadAsync(
|
||||
SourceItemFactory.Source(kind: "telegram", origin: "d_1", externalId: "100"), CancellationToken.None);
|
||||
@@ -83,11 +83,11 @@ public sealed class TelegramSourceContentProviderTests
|
||||
[Fact]
|
||||
public async Task LoadAsync_Found_ReturnsText()
|
||||
{
|
||||
var gateway = new FakeTelegramGateway
|
||||
var gateway = new TestTelegramGateway
|
||||
{
|
||||
SourceContent = new TelegramSourceContentDto(true, "исходный текст", 1_700_000_000_000),
|
||||
};
|
||||
var provider = new TelegramSourceContentProvider(gateway);
|
||||
var provider = new TelegramSourceContentProvider(gateway.Gateway);
|
||||
|
||||
SourceContent? content = await provider.LoadAsync(
|
||||
SourceItemFactory.Source(kind: "telegram", origin: "d_42", externalId: "777"), CancellationToken.None);
|
||||
@@ -99,8 +99,8 @@ public sealed class TelegramSourceContentProviderTests
|
||||
[Fact]
|
||||
public async Task LoadAsync_NumericExternalIdAndOriginRef_CallsGatewayWithParsedArguments()
|
||||
{
|
||||
var gateway = new FakeTelegramGateway();
|
||||
var provider = new TelegramSourceContentProvider(gateway);
|
||||
var gateway = new TestTelegramGateway();
|
||||
var provider = new TelegramSourceContentProvider(gateway.Gateway);
|
||||
|
||||
await provider.LoadAsync(
|
||||
SourceItemFactory.Source(kind: "telegram", origin: "d_42", externalId: "777"), CancellationToken.None);
|
||||
|
||||
@@ -72,7 +72,7 @@ public sealed class TelegramSourceIngestObserverTests
|
||||
var dialogs = new DialogsService(
|
||||
store,
|
||||
new FakeSettingsStore(),
|
||||
new FakeTelegramGateway(),
|
||||
new TestTelegramGateway().Gateway,
|
||||
NullLogger<DialogsService>.Instance);
|
||||
var observer = new TelegramSourceIngestObserver(dialogs, NullLogger<TelegramSourceIngestObserver>.Instance);
|
||||
return (observer, store);
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
using Deal.Contracts.Integrations.Abstractions;
|
||||
using Deal.Contracts.Integrations.Models;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Deal.Tests.Unit.Support;
|
||||
|
||||
/// <summary>
|
||||
/// Подставка <see cref="ITelegramGateway"/> для тестов Discovery-воркера: сервисы получают
|
||||
/// NSubstitute-подставку (<see cref="Gateway"/>), тесты управляют каталогом/сбоями и читают
|
||||
/// журналы вызовов (как раньше — через фейк). Неожиданные методы бросают <see cref="NotSupportedException"/>.
|
||||
/// </summary>
|
||||
public sealed class TestDiscoveryGateway
|
||||
{
|
||||
/// <summary>
|
||||
/// Подставка порта гейтвея (создаётся в конструкторе).
|
||||
/// </summary>
|
||||
public ITelegramGateway Gateway { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Результат SearchAsync
|
||||
/// </summary>
|
||||
public IReadOnlyList<TelegramDialogEntryDto> SearchResults { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Заданное исключение SearchAsync
|
||||
/// </summary>
|
||||
public Exception? SearchError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Инфо по конкретным диалогам
|
||||
/// </summary>
|
||||
public Dictionary<string, TelegramChannelInfoDto> InfoByDialog { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Чтения для оценки по конкретным диалогам
|
||||
/// </summary>
|
||||
public Dictionary<string, TelegramEvalReadDto> ReadsByDialog { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Заданное исключение JoinAsync
|
||||
/// </summary>
|
||||
public Exception? JoinError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Сколько сообщений «разобрал» Backfill
|
||||
/// </summary>
|
||||
public int BackfillResult { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Ключи, по которым был Search
|
||||
/// </summary>
|
||||
public List<string> SearchedKeywords { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Запросы Info
|
||||
/// </summary>
|
||||
public List<string> InfoRequests { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Запросы ReadForEval
|
||||
/// </summary>
|
||||
public List<(string DialogId, int Limit)> ReadRequests { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Юзернеймы, к которым был Join
|
||||
/// </summary>
|
||||
public List<string> JoinedUsernames { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы SetMonitor
|
||||
/// </summary>
|
||||
public List<(string DialogId, bool Enabled)> SetMonitorCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы Backfill
|
||||
/// </summary>
|
||||
public List<(string DialogId, bool Force)> BackfillCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт подставку с поведением по умолчанию.
|
||||
/// </summary>
|
||||
public TestDiscoveryGateway()
|
||||
{
|
||||
Gateway = Substitute.For<ITelegramGateway>();
|
||||
Gateway.StatusAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new TelegramAccountStatusDto("ready", Connected: true, Listener: true, "@test", Error: null, QrUrl: null));
|
||||
Gateway.StartPhoneAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns<TelegramAuthResultDto>(_ => throw new NotSupportedException());
|
||||
Gateway.StartQrAsync(Arg.Any<int>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns<TelegramAuthResultDto>(_ => throw new NotSupportedException());
|
||||
Gateway.SendCodeAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns<string>(_ => throw new NotSupportedException());
|
||||
Gateway.SendPasswordAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns<string>(_ => throw new NotSupportedException());
|
||||
Gateway.When(g => g.LogoutAsync(Arg.Any<CancellationToken>()))
|
||||
.Do(_ => throw new NotSupportedException());
|
||||
Gateway.RefreshDialogsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns<IReadOnlyList<TelegramDialogEntryDto>>(_ => throw new NotSupportedException());
|
||||
Gateway.When(g => g.SetMonitorAsync(Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => SetMonitorCalls.Add((ci.ArgAt<string>(0), ci.ArgAt<bool>(1))));
|
||||
Gateway.SetMonitorAllAsync(Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
.Returns<Task>(_ => throw new NotSupportedException());
|
||||
Gateway.When(g => g.BackfillAsync(Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => BackfillCalls.Add((ci.ArgAt<string>(0), ci.ArgAt<bool>(1))));
|
||||
Gateway.BackfillAsync(Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
.Returns(BackfillResult);
|
||||
Gateway.ReadRecentAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns<IReadOnlyList<TelegramRecentMessageDto>>(_ => throw new NotSupportedException());
|
||||
Gateway.ReadSourceAsync(Arg.Any<string>(), Arg.Any<long>(), Arg.Any<CancellationToken>())
|
||||
.Returns<TelegramSourceContentDto>(_ => throw new NotSupportedException());
|
||||
Gateway.When(g => g.SearchAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => SearchedKeywords.Add(ci.ArgAt<string>(0)));
|
||||
Gateway.SearchAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci =>
|
||||
{
|
||||
if (SearchError is not null)
|
||||
{
|
||||
throw SearchError;
|
||||
}
|
||||
|
||||
return SearchResults;
|
||||
});
|
||||
Gateway.When(g => g.InfoAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => InfoRequests.Add(ci.ArgAt<string>(0)));
|
||||
Gateway.InfoAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => InfoByDialog.TryGetValue(ci.ArgAt<string>(0), out TelegramChannelInfoDto? info)
|
||||
? info
|
||||
: new TelegramChannelInfoDto(ci.ArgAt<string>(0), string.Empty, string.Empty, string.Empty, "#666", Participants: null, IsForum: false));
|
||||
Gateway.When(g => g.ReadForEvalAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => ReadRequests.Add((ci.ArgAt<string>(0), ci.ArgAt<int>(1))));
|
||||
Gateway.ReadForEvalAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => ReadsByDialog.TryGetValue(ci.ArgAt<string>(0), out TelegramEvalReadDto? read)
|
||||
? read
|
||||
: new TelegramEvalReadDto(Ok: false, "no_history", []));
|
||||
Gateway.When(g => g.JoinAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci =>
|
||||
{
|
||||
JoinedUsernames.Add(ci.ArgAt<string>(0));
|
||||
if (JoinError is not null)
|
||||
{
|
||||
throw JoinError;
|
||||
}
|
||||
});
|
||||
Gateway.LeaveAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns<Task>(_ => throw new NotSupportedException());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using Deal.Contracts.Integrations.Abstractions;
|
||||
using Deal.Contracts.Integrations.Models;
|
||||
using Grpc.Core;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Deal.Tests.Unit.Support;
|
||||
|
||||
/// <summary>
|
||||
/// Подставка <see cref="ITelegramGateway"/> для юнит-тестов модуля Telegram/ингресса: сервисы получают
|
||||
/// NSubstitute-подставку (<see cref="Gateway"/>), тесты управляют поведением свойствами и читают
|
||||
/// журналы вызовов (как раньше — через фейк).
|
||||
/// </summary>
|
||||
public sealed class TestTelegramGateway
|
||||
{
|
||||
// Фаза idle-формы (по умолчанию не подключён).
|
||||
private const string IdlePhase = "idle";
|
||||
|
||||
private const string NotConnectedDetail = "Telegram не подключён";
|
||||
|
||||
/// <summary>
|
||||
/// Подставка порта гейтвея (создаётся в конструкторе).
|
||||
/// </summary>
|
||||
public ITelegramGateway Gateway { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы SetMonitor
|
||||
/// </summary>
|
||||
public List<(string DialogId, bool Enabled)> SetMonitorCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы SetMonitorAll
|
||||
/// </summary>
|
||||
public List<bool> SetMonitorAllCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы Backfill
|
||||
/// </summary>
|
||||
public List<(string DialogId, bool Force)> BackfillCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Вызовы ReadSource
|
||||
/// </summary>
|
||||
public List<(string DialogId, long MsgId)> ReadSourceCalls { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Результат ReadSourceAsync
|
||||
/// </summary>
|
||||
public TelegramSourceContentDto SourceContent { get; set; } = new(false, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// Сколько сообщений «разобрал» Backfill
|
||||
/// </summary>
|
||||
public int BackfillResult { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Каталог, который возвращает RefreshDialogsAsync/SearchAsync
|
||||
/// </summary>
|
||||
public IReadOnlyList<TelegramDialogEntryDto> Catalog { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Статус, который возвращает StatusAsync
|
||||
/// </summary>
|
||||
public TelegramAccountStatusDto Status { get; set; } =
|
||||
new(IdlePhase, Connected: false, Listener: false, Account: string.Empty, Error: null, QrUrl: null);
|
||||
|
||||
/// <summary>
|
||||
/// Заданное исключение StatusAsync
|
||||
/// </summary>
|
||||
public RpcException? StatusFailure { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Диалоги, чей BackfillAsync падает
|
||||
/// </summary>
|
||||
public HashSet<string> BackfillFailures { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Диалоги, чей SetMonitorAsync падает
|
||||
/// </summary>
|
||||
public HashSet<string> SetMonitorFailures { get; } = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Создаёт подставку с поведением по умолчанию.
|
||||
/// </summary>
|
||||
public TestTelegramGateway()
|
||||
{
|
||||
Gateway = Substitute.For<ITelegramGateway>();
|
||||
Gateway.StatusAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(ci =>
|
||||
{
|
||||
if (StatusFailure is not null)
|
||||
{
|
||||
throw StatusFailure;
|
||||
}
|
||||
|
||||
return Status;
|
||||
});
|
||||
Gateway.StartPhoneAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new TelegramAuthResultDto("code", null));
|
||||
Gateway.StartQrAsync(Arg.Any<int>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new TelegramAuthResultDto("qr", "https://t.me/qr/test"));
|
||||
Gateway.SendCodeAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()).Returns("ready");
|
||||
Gateway.SendPasswordAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()).Returns("ready");
|
||||
Gateway.RefreshDialogsAsync(Arg.Any<CancellationToken>()).Returns(ci => Catalog);
|
||||
Gateway.When(g => g.SetMonitorAsync(Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci =>
|
||||
{
|
||||
if (SetMonitorFailures.Contains(ci.ArgAt<string>(0)))
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unavailable, NotConnectedDetail));
|
||||
}
|
||||
|
||||
SetMonitorCalls.Add((ci.ArgAt<string>(0), ci.ArgAt<bool>(1)));
|
||||
});
|
||||
Gateway.When(g => g.SetMonitorAllAsync(Arg.Any<bool>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => SetMonitorAllCalls.Add(ci.ArgAt<bool>(0)));
|
||||
Gateway.When(g => g.BackfillAsync(Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => BackfillCalls.Add((ci.ArgAt<string>(0), ci.ArgAt<bool>(1))));
|
||||
Gateway.BackfillAsync(Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci =>
|
||||
{
|
||||
if (BackfillFailures.Contains(ci.ArgAt<string>(0)))
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.Unavailable, NotConnectedDetail));
|
||||
}
|
||||
|
||||
return BackfillResult;
|
||||
});
|
||||
Gateway.ReadRecentAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns<IReadOnlyList<TelegramRecentMessageDto>>([]);
|
||||
Gateway.When(g => g.ReadSourceAsync(Arg.Any<string>(), Arg.Any<long>(), Arg.Any<CancellationToken>()))
|
||||
.Do(ci => ReadSourceCalls.Add((ci.ArgAt<string>(0), ci.ArgAt<long>(1))));
|
||||
Gateway.ReadSourceAsync(Arg.Any<string>(), Arg.Any<long>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => SourceContent);
|
||||
Gateway.SearchAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => Catalog);
|
||||
Gateway.InfoAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => new TelegramChannelInfoDto(ci.ArgAt<string>(0), string.Empty, string.Empty, string.Empty, "#666", null, false));
|
||||
Gateway.ReadForEvalAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new TelegramEvalReadDto(false, "no_history", []));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user