84 lines
5.5 KiB
JavaScript
84 lines
5.5 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { generateKeyPairSync, sign } from 'node:crypto';
|
|
import { once } from 'node:events';
|
|
import { DiscordBot } from '../src/bots/discord.js';
|
|
import { TelegramBot } from '../src/bots/telegram.js';
|
|
import { createGateway } from '../src/server.js';
|
|
import { fixture } from './helpers.js';
|
|
|
|
test('Telegram /start offers Get access; private callback creates request and admin callback delivers number', async () => {
|
|
const f = fixture();
|
|
const calls = [];
|
|
const bot = new TelegramBot(f.config, f.store, { fetchImpl: async (url, options) => {
|
|
calls.push({ method: url.split('/').at(-1), body: JSON.parse(options.body) });
|
|
return Response.json({ ok: true, result: {} });
|
|
} });
|
|
const user = { id: 5, first_name: 'Player', username: 'player', language_code: 'en' };
|
|
await bot.handle({ message: { from: user, chat: { type: 'private' }, text: '/start access' } }, f.service);
|
|
assert.equal(calls[0].body.reply_markup.inline_keyboard[0][0].callback_data, 'request');
|
|
await bot.handle({ callback_query: { id: 'cb1', from: user, message: { chat: { type: 'private' } }, data: 'request' } }, f.service);
|
|
assert.equal(f.store.state.requests[0].user_id, '5');
|
|
assert.equal(f.store.state.requests[0].language_code, 'en');
|
|
const id = f.store.state.requests[0].id;
|
|
await bot.handle({ callback_query: { id: 'cb2', from: { id: 100 }, message: { chat: { type: 'private' } }, data: `approve:${id}` } }, f.service);
|
|
assert.equal(f.store.getRequest(id).status, 'approved');
|
|
assert.ok(f.messages.some(m => m.userId === '5' && m.text.includes('Your number:')));
|
|
assert.equal(calls.filter(c => c.method === 'answerCallbackQuery').length, 2);
|
|
await bot.handle({ message: { from: user, chat: { type: 'group' }, text: '/start' } }, f.service);
|
|
assert.equal(calls.filter(c => c.method === 'sendMessage').length, 3);
|
|
});
|
|
|
|
test('Discord HTTP endpoint validates the exact signed body, rejects stale/forged signatures, and defers approvals', async t => {
|
|
const f = fixture();
|
|
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
|
f.config.discord.publicKey = publicKey.export({ type: 'spki', format: 'der' }).subarray(-32).toString('hex');
|
|
f.config.discord.applicationId = '321';
|
|
const edits = [];
|
|
const bot = new DiscordBot(f.config, { now: f.now, fetchImpl: async (url, options) => {
|
|
edits.push({ url, body: JSON.parse(options.body) }); return Response.json({ id: 'dm' });
|
|
} });
|
|
const gateway = createGateway(f.config, f.store, { discord: bot, service: f.service });
|
|
gateway.server.listen(0, '127.0.0.1'); await once(gateway.server, 'listening');
|
|
t.after(async () => { await bot.drain(); await gateway.close(); });
|
|
const endpoint = `http://127.0.0.1:${gateway.server.address().port}/_access/discord`;
|
|
const post = async (payload, overrides = {}) => {
|
|
const raw = JSON.stringify(payload);
|
|
const timestamp = String(Math.floor(f.now() / 1000));
|
|
const signature = sign(null, Buffer.from(timestamp + raw), privateKey).toString('hex');
|
|
return fetch(endpoint, { method: 'POST', headers: { 'content-type': 'application/json', 'x-signature-timestamp': timestamp, 'x-signature-ed25519': signature, ...overrides }, body: raw });
|
|
};
|
|
assert.deepEqual(await (await post({ type: 1 })).json(), { type: 1 });
|
|
assert.equal((await post({ type: 1 }, { 'x-signature-ed25519': '00'.repeat(64) })).status, 401);
|
|
assert.equal((await post({ type: 1 }, { 'x-signature-timestamp': '1' })).status, 401);
|
|
assert.equal((await fetch(endpoint, { method: 'POST', body: '{}' })).status, 401);
|
|
const start = { type: 2, application_id: '321', data: { name: 'access' } };
|
|
assert.equal((await (await post(start)).json()).data.components[0].components[0].custom_id, 'request');
|
|
const interaction = { type: 3, id: '111', token: 'interaction-token', application_id: '321', user: { id: '7', username: 'player' }, data: { custom_id: 'request' } };
|
|
assert.deepEqual(await (await post(interaction)).json(), { type: 5, data: { flags: 64 } });
|
|
await bot.drain();
|
|
assert.equal(f.store.state.requests[0].platform, 'discord');
|
|
assert.equal(f.store.state.requests[0].user_id, '7');
|
|
await post(interaction); await bot.drain();
|
|
assert.equal(edits.length, 1, 'replayed interaction must not re-run the action');
|
|
const requestId = f.store.state.requests[0].id;
|
|
await post({ ...interaction, id: '112', user: { id: '999' }, data: { custom_id: `approve:${requestId}` } });
|
|
await bot.drain();
|
|
assert.equal(f.store.getRequest(requestId).status, 'pending');
|
|
await post({ ...interaction, id: '113', user: { id: '200' }, data: { custom_id: `approve:${requestId}` } });
|
|
await bot.drain();
|
|
assert.equal(f.store.getRequest(requestId).status, 'approved');
|
|
assert.equal(f.messages.filter(m => m.userId === '7' && m.text.includes('Your number:')).length, 1);
|
|
});
|
|
|
|
test('Discord DMs disable mentions and render separate approval buttons', async () => {
|
|
const f = fixture();
|
|
f.config.discord.publicKey = generateKeyPairSync('ed25519').publicKey.export({ type: 'spki', format: 'der' }).subarray(-32).toString('hex');
|
|
const bodies = [];
|
|
const bot = new DiscordBot(f.config, { fetchImpl: async (_url, options) => { bodies.push(JSON.parse(options.body)); return Response.json({ id: '123' }); } });
|
|
await bot.sendUser('100', '@everyone malicious display name', [{ label: 'Approve', id: 'approve:123' }, { label: 'Deny', id: 'deny:123' }]);
|
|
assert.deepEqual(bodies[0], { recipient_id: '100' });
|
|
assert.deepEqual(bodies[1].allowed_mentions, { parse: [] });
|
|
assert.equal(bodies[1].components[0].components.length, 2);
|
|
});
|