143 lines
5.2 KiB
TypeScript
143 lines
5.2 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import type { FastifyInstance } from 'fastify';
|
|
import {
|
|
telegramRoutes,
|
|
pushTelegramMessage,
|
|
BOT_TOKEN_PATTERN,
|
|
CHAT_ID_PATTERN,
|
|
} from '../../src/local/routes/telegram.js';
|
|
|
|
describe('telegram route exports', () => {
|
|
it('exports telegramRoutes + pushTelegramMessage', () => {
|
|
expect(typeof telegramRoutes).toBe('function');
|
|
expect(typeof pushTelegramMessage).toBe('function');
|
|
});
|
|
});
|
|
|
|
describe('BOT_TOKEN_PATTERN', () => {
|
|
it('accepts a well-formed BotFather token', () => {
|
|
expect(BOT_TOKEN_PATTERN.test('123456789:AAH-abcDEF_ghiJKLmnoPQRstuVWXyz01234')).toBe(true);
|
|
});
|
|
it('rejects a token with no colon', () => {
|
|
expect(BOT_TOKEN_PATTERN.test('123456789AAHabcdefghi')).toBe(false);
|
|
});
|
|
it('rejects a too-short secret half', () => {
|
|
expect(BOT_TOKEN_PATTERN.test('123456789:short')).toBe(false);
|
|
});
|
|
it('rejects free text', () => {
|
|
expect(BOT_TOKEN_PATTERN.test('not-a-real-token')).toBe(false);
|
|
});
|
|
it('rejects a secret with disallowed chars (spaces)', () => {
|
|
expect(BOT_TOKEN_PATTERN.test('123456789:AAH abcDEF ghiJKLmnoPQRstuVWXyz0')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('CHAT_ID_PATTERN', () => {
|
|
it('accepts a positive user chat id', () => {
|
|
expect(CHAT_ID_PATTERN.test('123456789')).toBe(true);
|
|
});
|
|
it('accepts a negative group/supergroup chat id', () => {
|
|
expect(CHAT_ID_PATTERN.test('-1001234567890')).toBe(true);
|
|
});
|
|
it('rejects non-numeric', () => {
|
|
expect(CHAT_ID_PATTERN.test('abc123')).toBe(false);
|
|
});
|
|
it('rejects too-short ids', () => {
|
|
expect(CHAT_ID_PATTERN.test('12')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// Minimal fake FastifyInstance carrying just a vault stub — pushTelegramMessage
|
|
// only reads server.vault.get(). Cast through unknown to avoid stubbing the
|
|
// entire Fastify surface.
|
|
function fakeServer(creds: { token?: string; chatId?: string }): FastifyInstance {
|
|
return {
|
|
vault: {
|
|
get: (key: string) => {
|
|
if (key === 'telegram_bot_token' && creds.token) return { value: creds.token };
|
|
if (key === 'telegram_chat_id' && creds.chatId) return { value: creds.chatId };
|
|
return null;
|
|
},
|
|
},
|
|
} as unknown as FastifyInstance;
|
|
}
|
|
|
|
describe('pushTelegramMessage', () => {
|
|
afterEach(() => { vi.unstubAllGlobals(); });
|
|
|
|
it('returns ok:false for empty text without touching the network', async () => {
|
|
const fetchSpy = vi.fn();
|
|
vi.stubGlobal('fetch', fetchSpy);
|
|
const res = await pushTelegramMessage(fakeServer({ token: 't', chatId: 'c' }), '');
|
|
expect(res.ok).toBe(false);
|
|
expect(res.reason).toBe('empty text');
|
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns ok:false when telegram is not configured', async () => {
|
|
const fetchSpy = vi.fn();
|
|
vi.stubGlobal('fetch', fetchSpy);
|
|
const res = await pushTelegramMessage(fakeServer({}), 'hello');
|
|
expect(res.ok).toBe(false);
|
|
expect(res.reason).toBe('telegram not configured');
|
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('posts to the Telegram API and returns ok:true on success', async () => {
|
|
const fetchSpy = vi.fn().mockResolvedValue({
|
|
json: async () => ({ ok: true, result: { message_id: 42 } }),
|
|
});
|
|
vi.stubGlobal('fetch', fetchSpy);
|
|
|
|
const res = await pushTelegramMessage(
|
|
fakeServer({ token: '123456789:AAH-abcDEF_ghiJKLmnoPQRstuVWXyz01234', chatId: '123456789' }),
|
|
'digest line',
|
|
);
|
|
|
|
expect(res.ok).toBe(true);
|
|
expect(res.messageId).toBe(42);
|
|
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
const [url, init] = fetchSpy.mock.calls[0];
|
|
// Host is hard-coded — token only interpolates into the path (no SSRF).
|
|
expect(String(url)).toContain('https://api.telegram.org/bot');
|
|
expect(String(url)).toContain('/sendMessage');
|
|
expect(init.method).toBe('POST');
|
|
const body = JSON.parse(init.body);
|
|
expect(body.chat_id).toBe('123456789');
|
|
expect(body.text).toBe('digest line');
|
|
});
|
|
|
|
it('returns ok:false when the Telegram API rejects', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
json: async () => ({ ok: false, description: 'chat not found' }),
|
|
}));
|
|
const res = await pushTelegramMessage(
|
|
fakeServer({ token: '123456789:AAH-abcDEF_ghiJKLmnoPQRstuVWXyz01234', chatId: '999999' }),
|
|
'hi',
|
|
);
|
|
expect(res.ok).toBe(false);
|
|
expect(res.reason).toBe('chat not found');
|
|
});
|
|
|
|
it('never throws when fetch rejects (cron-safe)', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down')));
|
|
const res = await pushTelegramMessage(
|
|
fakeServer({ token: '123456789:AAH-abcDEF_ghiJKLmnoPQRstuVWXyz01234', chatId: '123456789' }),
|
|
'hi',
|
|
);
|
|
expect(res.ok).toBe(false);
|
|
expect(res.reason).toContain('network down');
|
|
});
|
|
|
|
it('caps text at Telegram\'s 4096-char limit before sending', async () => {
|
|
const fetchSpy = vi.fn().mockResolvedValue({ json: async () => ({ ok: true, result: {} }) });
|
|
vi.stubGlobal('fetch', fetchSpy);
|
|
await pushTelegramMessage(
|
|
fakeServer({ token: '123456789:AAH-abcDEF_ghiJKLmnoPQRstuVWXyz01234', chatId: '1234' }),
|
|
'x'.repeat(5000),
|
|
);
|
|
const body = JSON.parse(fetchSpy.mock.calls[0][1].body);
|
|
expect(body.text.length).toBeLessThanOrEqual(4096);
|
|
});
|
|
});
|