Deal — единая кодовая база
ci / build-test (push) Canceled after 0s

SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/
Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue,
контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер),
Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог).

Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0,
тесты 1340/130/52/38/9 зелёные.
This commit is contained in:
Rustam Khalimov
2026-09-11 23:56:47 +03:00
commit 27c7831910
1383 changed files with 158436 additions and 0 deletions
@@ -0,0 +1,141 @@
using Deal.Api.Telegram;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Settings.Application.Models;
using Deal.Modules.Telegram.Application;
using Deal.Modules.Telegram.Application.Models;
using Deal.Tests.Unit.Contracts;
using Deal.Tests.Unit.Modules.Settings;
using Deal.Tests.Unit.Support;
using Grpc.Core;
using Microsoft.Extensions.Logging.Abstractions;
namespace Deal.Tests.Unit.Api;
/// <summary>
/// Тесты сборки статуса вкладки Telegram — GET /api/tg/status.
/// </summary>
public sealed class TgStatusServiceTests
{
/// <summary>
/// Гейт по умолчанию
/// </summary>
[Fact]
public async Task GetAsync_GatewayIdleAndEmptyTenant_ReturnsIdleForm()
{
(TgStatusService service, _, _, _, _, _) = Create();
TgStatusDto status = await service.GetAsync(CancellationToken.None);
Assert.Equal("idle", status.Phase);
Assert.False(status.Connected);
Assert.False(status.Listener);
Assert.Equal(string.Empty, status.Account);
Assert.Equal(0, status.Monitored);
Assert.False(status.KeysSet);
Assert.Null(status.Error);
Assert.Null(status.QrUrl);
}
/// <summary>
/// Сервис недоступен
/// </summary>
[Fact]
public async Task GetAsync_GatewayUnavailable_ReturnsIdleFormWithKvAccountCountAndKeys()
{
(TgStatusService service, FakeTelegramStore store, FakeSettingsStore settings, FakeTelegramGateway gateway, _, FakeGlobalSettingsStore 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));
settings.Preload(SettingsKeys.TgAccount, "\"@user\"");
PreloadKeys(globalSettings, "123456", "abcdefghijklmnop");
gateway.StatusFailure = new RpcException(new Status(StatusCode.Unavailable, "Telegram не подключён"));
TgStatusDto status = await service.GetAsync(CancellationToken.None);
Assert.Equal("idle", status.Phase);
Assert.False(status.Connected);
Assert.False(status.Listener);
Assert.Equal("@user", status.Account);
Assert.Equal(2, status.Monitored);
Assert.True(status.KeysSet);
Assert.Null(status.Error);
Assert.Null(status.QrUrl);
}
/// <summary>
/// Готовый аккаунт
/// </summary>
[Fact]
public async Task GetAsync_ReadyGateway_ComposesLiveFieldsWithKvAccountMonitoredAndKeys()
{
(TgStatusService service, FakeTelegramStore store, FakeSettingsStore settings, FakeTelegramGateway gateway, _, FakeGlobalSettingsStore globalSettings) = Create();
store.Seed(Dialog("d_1", "Канал", "channel", Monitor: true));
settings.Preload(SettingsKeys.TgAccount, "\"@realuser\"");
PreloadKeys(globalSettings, "123456", "abcdefghijklmnop");
gateway.Status = new TelegramAccountStatusDto(
Phase: "ready", Connected: true, Listener: true, Account: "gateway-account", Error: null, QrUrl: null);
TgStatusDto status = await service.GetAsync(CancellationToken.None);
Assert.Equal("ready", status.Phase);
Assert.True(status.Connected);
Assert.True(status.Listener);
Assert.Equal("@realuser", status.Account); // KV tgAccount перекрывает справочное поле гейта (Ruling 8)
Assert.Equal(1, status.Monitored);
Assert.True(status.KeysSet);
}
/// <summary>
/// Фаза qr несёт qrUrl из гейта
/// </summary>
[Fact]
public async Task GetAsync_QrPhase_ReturnsQrUrlFromGateway()
{
(TgStatusService service, _, _, FakeTelegramGateway gateway, _, _) = Create();
gateway.Status = new TelegramAccountStatusDto(
Phase: "qr", Connected: false, Listener: false, Account: string.Empty, Error: null, QrUrl: "https://t.me/qr/test");
TgStatusDto status = await service.GetAsync(CancellationToken.None);
Assert.Equal("qr", status.Phase);
Assert.Equal("https://t.me/qr/test", status.QrUrl);
Assert.False(status.Connected);
}
// ─── Хелперы ────────────────────────────────────────────────────────────
// Собирает сервис на фейках (FakeTelegramGateway/Store/SettingsStore/GlobalSettingsStore + FakeSecretCipher).
private static (TgStatusService Service, FakeTelegramStore Store, FakeSettingsStore Settings, FakeTelegramGateway Gateway, FakeSecretCipher Cipher, FakeGlobalSettingsStore GlobalSettings) Create()
{
var store = new FakeTelegramStore();
var settings = new FakeSettingsStore();
var gateway = new FakeTelegramGateway();
var cipher = new FakeSecretCipher();
var globalSettings = new FakeGlobalSettingsStore();
var keys = new TelegramKeysService(globalSettings, cipher);
var dialogs = new DialogsService(store, settings, gateway, NullLogger<DialogsService>.Instance);
var service = new TgStatusService(gateway, dialogs, settings, keys);
return (service, store, settings, gateway, cipher, globalSettings);
}
// Кладёт глобальную настройку telegramKeys (apiHash шифруется — enc: через шифр сценария).
// settings: Глобальное KV-хранилище сценария.
// apiId: apiId приложения (5..9 цифр).
// apiHash: Открытый api_hash (ядро шифрует при сохранении).
private static void PreloadKeys(
FakeGlobalSettingsStore settings,
string apiId,
string apiHash)
{
string encrypted = new FakeSecretCipher().Encrypt(apiHash);
settings.Preload(GlobalSettingsKeys.TelegramKeys, $"{{\"apiId\":\"{apiId}\",\"apiHash\":\"{encrypted}\"}}");
}
// Строка каталога минимально (id/имя/kind + флаг мониторинга).
private static FakeTelegramDialogRow Dialog(
string id,
string name,
string kind,
bool Monitor)
=> new(id, name, Handle: string.Empty, kind, Hue: "#666", Monitor, LastText: string.Empty, LastAt: null, Backfilled: false, UpdatedAt: DateTimeOffset.UtcNow);
}