Files
Haven/test/importFerryAccess.test.js

147 lines
6.8 KiB
JavaScript

'use strict';
// Discord lists private channels to the Ferry bot but keeps them closed until
// its role is added to them. The import picker marks those channels, and a
// fetch that includes one anyway skips it by name instead of importing an
// empty channel or failing.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
process.env.HAVEN_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'haven-importaccess-'));
process.env.JWT_SECRET = process.env.JWT_SECRET || 'j'.repeat(64);
const auth = require('../src/auth');
auth.verifyToken = (token) => (token === 'admin' ? { id: 1 } : null);
// Ferry itself is not needed, only the headers it hands the import.
const ferryPath = require.resolve('../src/ferry');
require.cache[ferryPath] = { id: ferryPath, filename: ferryPath, loaded: true, exports: { importHeaders: () => ({ Authorization: 'Bot test' }) } };
const registerImport = require('../src/routes/import');
const routes = {};
const app = {
post: (p, ...fns) => { routes['POST ' + p] = fns[fns.length - 1]; },
get: (p, ...fns) => { routes['GET ' + p] = fns[fns.length - 1]; },
};
registerImport({ app, uploadDiskGuard: (q, s, n) => n(), uploadLimiter: (q, s, n) => n(), importUpload: {}, verifyAdminFromDb: () => true });
const call = (key, req) => new Promise((resolve) => {
const res = { statusCode: 200, status(c) { this.statusCode = c; return this; }, json(b) { resolve({ status: this.statusCode, body: b }); } };
routes[key](req, res);
});
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const GUILD = '910000000000000001';
const BOT = '910000000000000002';
const BOT_ROLE = '910000000000000003';
const GENERAL = '910000000000000010';
const SECRET = '910000000000000011';
const OPENED = '910000000000000012';
const STAFF_CAT = '910000000000000020';
const SECRET_THREAD = '910000000000000030';
const VIEW = 1024, HISTORY = 65536;
const everyoneDeny = { id: GUILD, type: 0, allow: '0', deny: String(VIEW) };
const discord = {
[`/users/@me`]: { id: BOT, username: 'Ferry' },
[`/guilds/${GUILD}`]: {
id: GUILD, owner_id: '910000000000000099',
roles: [
{ id: GUILD, name: '@everyone', permissions: String(VIEW + HISTORY) },
{ id: BOT_ROLE, name: 'Ferry', permissions: String(VIEW + HISTORY + 2048 + 536870912) },
],
},
[`/guilds/${GUILD}/members/${BOT}`]: { roles: [BOT_ROLE] },
[`/guilds/${GUILD}/channels`]: [
{ id: STAFF_CAT, type: 4, name: 'Staff', position: 0, permission_overwrites: [everyoneDeny] },
{ id: GENERAL, type: 0, name: 'general', position: 1, permission_overwrites: [] },
// Private, and the bot's role was never added.
{ id: SECRET, type: 0, name: 'secret', position: 2, parent_id: STAFF_CAT, permission_overwrites: [everyoneDeny] },
// Private too, but the bot's role was added with the two permissions.
{ id: OPENED, type: 0, name: 'opened', position: 3, parent_id: STAFF_CAT,
permission_overwrites: [everyoneDeny, { id: BOT_ROLE, type: 0, allow: String(VIEW + HISTORY), deny: '0' }] },
],
[`/guilds/${GUILD}/threads/active`]: { threads: [{ id: SECRET_THREAD, name: 'secret plans', parent_id: SECRET }] },
};
const requested = [];
global.fetch = async (url) => {
const u = new URL(url);
const p = u.pathname.replace(/^\/api\/v10/, '');
requested.push(p);
const json = (b, status = 200) => new Response(JSON.stringify(b), { status, headers: { 'content-type': 'application/json' } });
if (p in discord) return json(discord[p]);
if (/\/threads\/archived\/public$/.test(p)) return json({ threads: [], has_more: false });
if (p === `/channels/${GENERAL}/messages` || p === `/channels/${OPENED}/messages`) {
return json([{ id: '1', type: 0, content: 'hello', timestamp: new Date(2026, 0, 1).toISOString(), author: { id: '5', username: 'a' } }]);
}
if (/\/messages$/.test(p)) return json({ message: 'Missing Access' }, 403);
return json({ message: 'Unknown' }, 404);
};
test('the picker marks a private channel, and its threads, as unreadable', async () => {
const r = await call('POST /api/import/discord/guild-channels', {
headers: { authorization: 'Bearer admin' }, body: { useFerry: true, guildId: GUILD },
});
assert.equal(r.status, 200);
const byName = Object.fromEntries(r.body.channels.map(c => [c.name, c]));
assert.equal(byName.general.readable, true);
assert.equal(byName.secret.readable, false);
assert.equal(byName.opened.readable, true, 'adding the bot role opens it');
assert.equal(r.body.threads.find(t => t.id === SECRET_THREAD).readable, false);
assert.ok(!requested.includes(`/channels/${SECRET}/threads/archived/public`), 'no thread lookups for a channel the bot cannot read');
assert.ok(!JSON.stringify(r.body).includes('Bot test'), 'the token never reaches the client');
});
test('a hidden channel picked anyway is skipped by name, the rest import', async () => {
requested.length = 0;
const started = await call('POST /api/import/discord/fetch', {
headers: { authorization: 'Bearer admin' },
body: {
useFerry: true, guildId: GUILD, guildName: 'Guild',
channels: [
{ id: GENERAL, name: 'general' },
{ id: SECRET, name: 'secret' },
{ id: SECRET_THREAD, name: 'secret plans', kind: 'thread', parentId: SECRET },
{ id: OPENED, name: 'opened' },
],
},
});
assert.equal(started.status, 202);
let done = null;
for (let i = 0; i < 50 && !done; i++) {
await sleep(100);
const r = await call('GET /api/import/discord/fetch/:jobId', { headers: { authorization: 'Bearer admin' }, params: { jobId: started.body.jobId } });
if (r.body.state === 'done') done = r;
}
assert.ok(done, 'the job finishes');
assert.equal(done.status, 200);
assert.deepEqual(done.body.skipped.sort(), ['secret', 'secret plans']);
assert.deepEqual(done.body.channels.map(c => c.name).sort(), ['general', 'opened']);
assert.ok(!requested.includes(`/channels/${SECRET}/messages`), 'a hidden channel is never read');
fs.rmSync(path.join(os.tmpdir(), `haven-import-${done.body.importId}.json`), { force: true });
});
test('when every pick is hidden the error says what to do in Discord', async () => {
const started = await call('POST /api/import/discord/fetch', {
headers: { authorization: 'Bearer admin' },
body: { useFerry: true, guildId: GUILD, channels: [{ id: SECRET, name: 'secret' }] },
});
let done = null;
for (let i = 0; i < 50 && !done; i++) {
await sleep(100);
const r = await call('GET /api/import/discord/fetch/:jobId', { headers: { authorization: 'Bearer admin' }, params: { jobId: started.body.jobId } });
if (r.body.state === 'done') done = r;
}
assert.equal(done.status, 400);
assert.match(done.body.error, /add the bot's role/);
});
test.after(() => {
fs.rmSync(process.env.HAVEN_DATA_DIR, { recursive: true, force: true });
setImmediate(() => process.exit(0));
});