Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
54 lines
2.9 KiB
TypeScript
54 lines
2.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { signInitData, validateInitData } from '../src/auth/telegram.ts';
|
|
import { base32Decode, base32Encode, generateTotpSecret, totpCode, verifyTotp } from '../src/auth/totp.ts';
|
|
|
|
const TOKEN = '123456:test-token';
|
|
const user = { id: 42, first_name: 'Anna', language_code: 'ru' };
|
|
|
|
describe('Telegram initData', () => {
|
|
it('принимает корректную подпись', () => {
|
|
const res = validateInitData(signInitData(TOKEN, user, { startParam: 'l-ru' }), TOKEN, 3600);
|
|
expect(res).toMatchObject({ ok: true, user: { id: 42 }, startParam: 'l-ru' });
|
|
});
|
|
|
|
it('отклоняет отсутствующие, подделанные, чужим токеном и просроченные данные', () => {
|
|
expect(validateInitData(undefined, TOKEN, 3600)).toEqual({ ok: false, reason: 'MISSING' });
|
|
const signed = signInitData(TOKEN, user);
|
|
const tampered = signed.replace('%22id%22%3A42', '%22id%22%3A43');
|
|
expect(tampered).not.toBe(signed);
|
|
expect(validateInitData(tampered, TOKEN, 3600)).toEqual({ ok: false, reason: 'BAD_HASH' });
|
|
expect(validateInitData(signed, '999:other', 3600)).toEqual({ ok: false, reason: 'BAD_HASH' });
|
|
expect(validateInitData(signed.replace(/hash=[0-9a-f]+/, 'hash=zz'), TOKEN, 3600)).toEqual({ ok: false, reason: 'BAD_HASH' });
|
|
const old = signInitData(TOKEN, user, { authDate: new Date(Date.now() - 2 * 3600_000) });
|
|
expect(validateInitData(old, TOKEN, 3600)).toEqual({ ok: false, reason: 'EXPIRED' });
|
|
});
|
|
|
|
it('без токена бота не принимает ничего', () => {
|
|
expect(validateInitData(signInitData(TOKEN, user), '', 3600)).toEqual({ ok: false, reason: 'NO_TOKEN' });
|
|
});
|
|
|
|
it('совпадает с эталонным алгоритмом Telegram (порядок ключей не важен)', () => {
|
|
const signed = new URLSearchParams(signInitData(TOKEN, user));
|
|
const reordered = new URLSearchParams([...signed.entries()].reverse());
|
|
expect(validateInitData(reordered.toString(), TOKEN, 3600).ok).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('TOTP', () => {
|
|
it('RFC 6238: эталонный вектор SHA-1', () => {
|
|
// секрет "12345678901234567890", T = 59 c → 94287082 (8 цифр) → последние 6: 287082
|
|
const secret = base32Encode(Buffer.from('12345678901234567890'));
|
|
expect(totpCode(secret, 59_000)).toBe('287082');
|
|
expect(base32Decode(secret).toString()).toBe('12345678901234567890');
|
|
});
|
|
|
|
it('проверяет код с допуском ±30 с', () => {
|
|
const secret = generateTotpSecret();
|
|
const now = Date.now();
|
|
expect(verifyTotp(secret, totpCode(secret, now), now)).toBe(true);
|
|
expect(verifyTotp(secret, totpCode(secret, now - 30_000), now)).toBe(true);
|
|
expect(verifyTotp(secret, totpCode(secret, now - 120_000), now)).toBe(false);
|
|
expect(verifyTotp(secret, 'abcdef', now)).toBe(false);
|
|
});
|
|
});
|