Files
Haven/test/importFetchJob.test.js

103 lines
4.5 KiB
JavaScript

'use strict';
// Reading channels from Discord runs as a background job. Held open as one
// request it outlived proxies and tunnels in front of Haven (Cloudflare
// Tunnel answers with an HTML page after 100 seconds), and the page showed
// "Unexpected token '<'". The start request now answers at once with a job
// id, and the page polls for progress and the result.
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');
// Keep the requires below away from the real data folder.
process.env.HAVEN_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'haven-importjob-'));
process.env.JWT_SECRET = process.env.JWT_SECRET || 'j'.repeat(64);
const auth = require('../src/auth');
auth.verifyToken = (token) => (token === 'admin' ? { id: 1 } : token === 'other' ? { id: 2 } : null);
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));
// A slow Discord: 130 messages in two pages.
let reads = 0;
global.fetch = async (url) => {
reads++;
await sleep(150);
const before = new URL(url).searchParams.get('before');
const count = before ? 30 : 100;
const start = before ? 1000 : 2000;
const batch = Array.from({ length: count }, (_, i) => ({
id: String(start - i), type: 0, content: `message ${start - i}`, timestamp: new Date(2026, 0, 1).toISOString(),
author: { id: '99999', username: 'someone' },
}));
return new Response(JSON.stringify(batch), { status: 200, headers: { 'content-type': 'application/json' } });
};
test('starting a fetch answers at once with a job, and polling returns the result', async () => {
const started = await call('POST /api/import/discord/fetch', {
headers: { authorization: 'Bearer admin' },
body: { discordToken: 'tok', guildName: 'Guild', channels: [{ id: '111111111', name: 'general' }] },
});
assert.equal(started.status, 202);
assert.match(started.body.jobId, /^[0-9a-f]{32}$/);
assert.equal(started.body.importId, undefined, 'nothing is read before the answer');
const poll = () => call('GET /api/import/discord/fetch/:jobId', { headers: { authorization: 'Bearer admin' }, params: { jobId: started.body.jobId } });
const first = await poll();
assert.equal(first.status, 200);
assert.equal(first.body.state, 'running');
const stranger = await call('GET /api/import/discord/fetch/:jobId', { headers: { authorization: 'Bearer other' }, params: { jobId: started.body.jobId } });
assert.equal(stranger.status, 404, 'another admin cannot read this job');
let done = null;
for (let i = 0; i < 40 && !done; i++) {
await sleep(100);
const r = await poll();
if (r.body.state === 'done') done = r;
else assert.ok(r.body.messages >= 0 && r.body.channelsTotal === 1);
}
assert.ok(done, 'the job finishes');
assert.equal(done.status, 200);
assert.equal(done.body.totalMessages, 130);
assert.equal(done.body.channels[0].messageCount, 130);
assert.ok(reads >= 2);
fs.rmSync(path.join(os.tmpdir(), `haven-import-${done.body.importId}.json`), { force: true });
const again = await poll();
assert.equal(again.status, 404, 'a delivered result is not kept');
});
test('a failed read comes back as the job result', async () => {
global.fetch = async () => new Response('{}', { status: 401 });
const started = await call('POST /api/import/discord/fetch', {
headers: { authorization: 'Bearer admin' },
body: { discordToken: 'tok', channels: [{ id: '222222222', name: 'x' }] },
});
assert.equal(started.status, 202);
await sleep(100);
const r = await call('GET /api/import/discord/fetch/:jobId', { headers: { authorization: 'Bearer admin' }, params: { jobId: started.body.jobId } });
assert.equal(r.status, 400);
assert.match(r.body.error, /Invalid or expired Discord token/);
});
// import.js keeps a cleanup timer running, so end the process once done.
test.after(() => {
fs.rmSync(process.env.HAVEN_DATA_DIR, { recursive: true, force: true });
setImmediate(() => process.exit(0));
});