DUNGEONS-30: add access gateway with Telegram and Discord approvals
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
83
test/bots.test.js
Normal file
83
test/bots.test.js
Normal file
@@ -0,0 +1,83 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user