Telemax/tests/client-request.test.ts
Folist f6ee386a61 Аудит безопасности: HTTPS-панель, скоуп доверия CA, сериализация MAX-запросов
- redactSecrets: маскируются password/trackId/phone и любые строки от 512
  символов — пароль MAX больше не попадает в лог веб-панели открытым текстом
- Веб-панель по умолчанию работает по HTTPS с самоподписанным сертификатом
  (генерируется при первом старте в .data/tls); старые http-ссылки получают
  301-редирект на том же порту (polyglot по первому байту; resume строго в
  process.nextTick — иначе TLS-хендшейк зависает); /apikey даёт https-ссылку;
  PANEL_TLS=off для установки за своим reverse proxy
- Сертификаты Минцифры больше не доверяются всему контейнеру через
  NODE_EXTRA_CA_CERTS — src/max/ca.ts скоупит их только на соединения с MAX
  (ca в tls.connect + undici-агент для CDN-загрузок); Telegram и GitHub
  проверяются только по стандартным корням Mozilla
- Запросы одного опкода к MAX сериализуются (request() в client.ts) — два
  конкурентных MSG_SEND больше не перепутают messageId-связки редактирования
  и удаления
- Починена гонка первого чтения в ChatMapStore: конкурентные обращения
  форкали кэш, и часть upsert'ов молча терялась на диске
- Graceful shutdown по SIGTERM/SIGINT; в панели — реконнект WebSocket и
  периодическое обновление метрик; сравнение id в patchCachedChatLastMessage
  переведено на String() (BigInt-чаты не совпадали)
- Удалён мёртвый код: эндпоинты /api/debug/*, getPollVoters,
  createTelegramBot, цепочка historySynced, getChats(marker)
- React/Tailwind/lucide перенесены в devDependencies — рантайм-образ легче;
  vitest 4, npm audit: 0 уязвимостей; chmod 600 для .env в setup.sh/update.sh
- Новые тесты: redactSecrets, сторы (эвикшн/нормализация/конкурентность),
  сериализация запросов — всего 41
2026-08-14 21:13:26 +03:00

59 lines
2.2 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { MaxClient } from '../src/max/client.js';
/**
* Fakes the socket layer: every send() gets a distinct async response for its
* own opcode, mimicking a server that answers strictly one response per request.
*/
class FakeMaxClient extends MaxClient {
readonly sentPayloads: unknown[] = [];
private responseCounter = 0;
override send(opcode: number, payload?: unknown): void {
this.sentPayloads.push(payload);
const n = ++this.responseCounter;
queueMicrotask(() => {
this.emit('message', {
dir: 1,
seq: n,
opcode,
payload: { message: { id: BigInt(n * 100), attaches: [] } },
length: 0,
});
});
}
}
describe('MaxClient request serialization', () => {
it('concurrent same-opcode requests each get their own response, in send order', async () => {
const client = new FakeMaxClient();
// Two concurrent MSG_SENDs — exactly what Telegraf produces for two quick
// Telegram messages (it handles a poll batch's updates concurrently).
// Before serialization both promises resolved on the FIRST response frame,
// cross-wiring the messageId links that edit/delete depend on.
const [first, second] = await Promise.all([client.sendMessage(1, 'первое'), client.sendMessage(1, 'второе')]);
expect(first.messageId).toBe(100n);
expect(second.messageId).toBe(200n);
const texts = client.sentPayloads.map((p) => (p as { message: { text: string } }).message.text);
expect(texts).toEqual(['первое', 'второе']);
});
it('a failed request does not wedge the queue for the next one', async () => {
const client = new FakeMaxClient();
// First call throws at send time (e.g. socket gone) — the chain must survive.
const realSend = FakeMaxClient.prototype.send;
let calls = 0;
client.send = (opcode: number, payload?: unknown): void => {
calls += 1;
if (calls === 1) throw new Error('not connected');
realSend.call(client, opcode, payload);
};
await expect(client.sendMessage(1, 'сломается')).rejects.toThrow('not connected');
const ok = await client.sendMessage(1, 'пройдёт');
expect(ok.messageId).toBe(100n);
});
});