Files
ApiCore/ApiCore-Backend/test/utils/telegram.ts
T
Nick SzaunigandClaude Opus 5.5 fab7f7e12f feat: T4 wave 10 – bottling quality, phenology and honey chain UI, Telegram link/receive and ops
- Bottling quality: water content above the legal limit (strict >) answers
  409 CONFIRMATION_REQUIRED bound to the measured value; prefill for jar
  size, best-before (shelfLifeMonths, month-end clamping) and water content;
  impossible dates give 400; water-content checks in src/common/quality.
- Frontend phenology: "Entwicklung melden" sheet, IndexedDB v4 queue,
  season header on the apiary page, confirmed revoke for all apiaries.
- Frontend honey chain: "Vorschlag aus …" hints with explicit take-over,
  prefill status note, movement ledger with withdraw/correct dialogs.
- Telegram: link tokens (hashed, single use), private-chat receiver with
  lease and offset CAS, bounded poison-update skip, Retry-After, pending
  deliveries skipped on disconnect; system API for deliveries and retry;
  worker-only secret wiring and manifest policy tests.
- ARCHITECTURE.md §7, §9, §10, §11.11–11.13, Annahmen 51–55.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-14 22:47:17 +02:00

118 lines
3.8 KiB
TypeScript

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<void>) | 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<void> {
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<TelegramUpdateBatch> {
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<bigint | null>(
(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<App>;
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<TelegramTestApp> {
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<NestExpressApplication>();
configureApp(app);
await app.init();
return { app, telegram };
}