import { randomUUID } from 'node:crypto'; import { INestApplication } from '@nestjs/common'; import { NestExpressApplication } from '@nestjs/platform-express'; import { Test } from '@nestjs/testing'; import { App } from 'supertest/types'; import { AppModule } from '../../src/app.module'; import { configureApp } from '../../src/app.config'; import { THROTTLER_NAMESPACE } from '../../src/common/throttler/prisma-throttler.storage'; import { TelegramApiError } from '../../src/telegram/telegram-api-error'; import { TelegramClient, type TelegramUpdate, type TelegramUpdateBatch, } from '../../src/telegram/telegram-client'; import { TelegramModule } from '../../src/telegram/telegram.module'; export interface SentMessage { chatId: bigint; text: string; } /** * Stands in for the Bot API: no network. Every sendMessage is recorded; * `failWith` scripts the next answers (an error is thrown once per entry, * in order, for the given chat or any chat). */ export class FakeTelegramClient { readonly sent: SentMessage[] = []; private readonly failures: { chatId: bigint | null; error: Error }[] = []; private readonly updates: TelegramUpdate[] = []; /** Every offset getUpdates was asked for, in order. */ readonly polledOffsets: bigint[] = []; /** Runs inside getUpdates, after the updates are chosen (e.g. steal the lease). */ onGetUpdates: (() => Promise) | null = null; private getUpdatesError: Error | null = null; isConfigured(): boolean { return true; } failWith(statusCode: number, description: string, chatId?: bigint): void { this.failures.push({ chatId: chatId ?? null, error: new TelegramApiError('sendMessage', statusCode, description, null), }); } sendMessage(chatId: bigint, text: string): Promise { const index = this.failures.findIndex( (failure) => failure.chatId === null || failure.chatId === chatId, ); if (index >= 0) { const [failure] = this.failures.splice(index, 1); return Promise.reject(failure.error); } this.sent.push({ chatId, text }); return Promise.resolve(); } /** What the next getUpdates calls return (from `offset` on, like Telegram). */ queueUpdate(update: TelegramUpdate): void { this.updates.push(update); } failNextGetUpdates(error: Error): void { this.getUpdatesError = error; } async getUpdates(offset: bigint): Promise { this.polledOffsets.push(offset); if (this.getUpdatesError !== null) { const error = this.getUpdatesError; this.getUpdatesError = null; throw error; } const due = this.updates.filter((update) => update.updateId >= offset); await this.onGetUpdates?.(); const highestUpdateId = due.reduce( (highest, update) => highest === null || update.updateId > highest ? update.updateId : highest, null, ); return { updates: due, highestUpdateId }; } sentTo(chatId: bigint): SentMessage[] { return this.sent.filter((message) => message.chatId === chatId); } } export interface TelegramTestApp { app: INestApplication; telegram: FakeTelegramClient; } /** * The whole API plus TelegramModule, with the Bot API client replaced by a * FakeTelegramClient. The TelegramSender is taken from the app and driven * by the test (no worker loop runs). */ export async function createTelegramTestApp(): Promise { const telegram = new FakeTelegramClient(); const moduleFixture = await Test.createTestingModule({ imports: [AppModule, TelegramModule], }) .overrideProvider(THROTTLER_NAMESPACE) .useValue(randomUUID()) .overrideProvider(TelegramClient) .useValue(telegram) .compile(); const app = moduleFixture.createNestApplication(); configureApp(app); await app.init(); return { app, telegram }; }