mirror of
https://github.com/Trollobot/Telemax.git
synced 2026-08-29 09:31:42 +00:00
- 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
117 lines
4.7 KiB
TypeScript
117 lines
4.7 KiB
TypeScript
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { MessageLinkStore } from '../src/bridge/sync.js';
|
|
import { ChatMapStore } from '../src/store/chatMapStore.js';
|
|
|
|
describe('MessageLinkStore', () => {
|
|
it('resolves links in both directions, including extra Telegram ids', () => {
|
|
const store = new MessageLinkStore();
|
|
store.add({ maxChatId: 1n, maxMessageId: 100n, telegramMessageId: 7, extraTelegramMessageIds: [8] });
|
|
|
|
expect(store.getByMax(1n, 100n)?.telegramMessageId).toBe(7);
|
|
expect(store.getByTelegram(7)?.maxMessageId).toBe(100n);
|
|
expect(store.getByTelegram(8)?.maxMessageId).toBe(100n);
|
|
});
|
|
|
|
it('matches BigInt and string forms of the same id (String()-keyed)', () => {
|
|
const store = new MessageLinkStore();
|
|
store.add({ maxChatId: '55', maxMessageId: 900n, telegramMessageId: 1 });
|
|
expect(store.getByMax(55n, '900')?.telegramMessageId).toBe(1);
|
|
});
|
|
|
|
it('ignores links with a null maxMessageId', () => {
|
|
const store = new MessageLinkStore();
|
|
store.add({ maxChatId: 1, maxMessageId: null, telegramMessageId: 5 });
|
|
expect(store.getByTelegram(5)).toBeUndefined();
|
|
});
|
|
|
|
it('evicts the oldest link (and its extra ids) past capacity', () => {
|
|
const store = new MessageLinkStore(2);
|
|
store.add({ maxChatId: 1, maxMessageId: 1, telegramMessageId: 11, extraTelegramMessageIds: [111] });
|
|
store.add({ maxChatId: 1, maxMessageId: 2, telegramMessageId: 22 });
|
|
store.add({ maxChatId: 1, maxMessageId: 3, telegramMessageId: 33 });
|
|
|
|
expect(store.getByMax(1, 1)).toBeUndefined();
|
|
expect(store.getByTelegram(11)).toBeUndefined();
|
|
expect(store.getByTelegram(111)).toBeUndefined();
|
|
expect(store.getByMax(1, 2)?.telegramMessageId).toBe(22);
|
|
expect(store.getByMax(1, 3)?.telegramMessageId).toBe(33);
|
|
});
|
|
|
|
it('clear() wipes everything', () => {
|
|
const store = new MessageLinkStore();
|
|
store.add({ maxChatId: 1, maxMessageId: 1, telegramMessageId: 11 });
|
|
store.clear();
|
|
expect(store.getByMax(1, 1)).toBeUndefined();
|
|
expect(store.getByTelegram(11)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('ChatMapStore', () => {
|
|
let dir: string;
|
|
let filePath: string;
|
|
|
|
beforeEach(async () => {
|
|
dir = await mkdtemp(path.join(tmpdir(), 'telemax-chatmap-'));
|
|
filePath = path.join(dir, 'chat-map.json');
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('normalizes maxChatId to a decimal string on upsert', async () => {
|
|
const store = new ChatMapStore(filePath);
|
|
await store.upsert({ maxChatId: 123456789012345n, telegramTopicId: 5, createdAt: 'now' });
|
|
|
|
const raw = JSON.parse(await readFile(filePath, 'utf8')) as Array<{ maxChatId: unknown }>;
|
|
expect(raw[0]?.maxChatId).toBe('123456789012345');
|
|
});
|
|
|
|
it('getByMaxChatId matches across number/string/BigInt forms', async () => {
|
|
const store = new ChatMapStore(filePath);
|
|
await store.upsert({ maxChatId: 42, telegramTopicId: 9, createdAt: 'now' });
|
|
|
|
expect((await store.getByMaxChatId('42'))?.telegramTopicId).toBe(9);
|
|
expect((await store.getByMaxChatId(42n))?.telegramTopicId).toBe(9);
|
|
});
|
|
|
|
it('upsert replaces an existing entry instead of duplicating it', async () => {
|
|
const store = new ChatMapStore(filePath);
|
|
await store.upsert({ maxChatId: 1, telegramTopicId: 10, createdAt: 'now' });
|
|
await store.upsert({ maxChatId: '1', telegramTopicId: 20, createdAt: 'later' });
|
|
|
|
const all = await store.list();
|
|
expect(all).toHaveLength(1);
|
|
expect(all[0]?.telegramTopicId).toBe(20);
|
|
});
|
|
|
|
it('advanceHistoryCursor persists the cursor for an existing mapping only', async () => {
|
|
const store = new ChatMapStore(filePath);
|
|
await store.upsert({ maxChatId: 1, telegramTopicId: 10, createdAt: 'now' });
|
|
|
|
await store.advanceHistoryCursor(1, 1723600000000);
|
|
expect((await store.getByMaxChatId(1))?.historyBackfillCursor).toBe('1723600000000');
|
|
|
|
// No mapping for chat 2 — must be a no-op, not a crash or a phantom entry.
|
|
await store.advanceHistoryCursor(2, 123);
|
|
expect(await store.getByMaxChatId(2)).toBeUndefined();
|
|
});
|
|
|
|
it('survives concurrent upserts without corrupting the file (serialized writes)', async () => {
|
|
const store = new ChatMapStore(filePath);
|
|
await Promise.all(
|
|
Array.from({ length: 20 }, (_, i) => store.upsert({ maxChatId: i, telegramTopicId: i, createdAt: 'now' })),
|
|
);
|
|
|
|
const raw = JSON.parse(await readFile(filePath, 'utf8')) as unknown[];
|
|
expect(raw).toHaveLength(20);
|
|
});
|
|
|
|
it('starts empty when the file does not exist', async () => {
|
|
const store = new ChatMapStore(path.join(dir, 'missing.json'));
|
|
expect(await store.list()).toEqual([]);
|
|
});
|
|
});
|