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); });