mirror of
https://github.com/ancsemi/Haven.git
synced 2026-10-26 08:38:20 +00:00
103 lines
4.5 KiB
JavaScript
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));
|
|
});
|