Files
hub/tools/tasks-test.js
T

52 lines
5.1 KiB
JavaScript

#!/usr/bin/env node
// Tasks: repeating tasks roll forward, subtasks nest one level and go away with their parent.
const { spawn } = require('child_process');
const fs = require('fs'), os = require('os'), path = require('path'), crypto = require('crypto');
const root = path.join(__dirname, '..'), dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-tasks-'));
const port = 20000 + Math.floor(Math.random() * 20000), base = `http://127.0.0.1:${port}`;
let failed = 0;
const ok = (name, cond, extra) => { console.log((cond ? 'PASS ' : 'FAIL ') + name + (cond ? '' : ' -> ' + JSON.stringify(extra))); if (!cond) failed++; };
const SECRET = 'tasks-'.repeat(8), UID = 'utasks';
fs.writeFileSync(path.join(dir, 'users.json'), JSON.stringify({ users: [{ id: UID, name: 'Admin', role: 'admin', tabs: [], handle: 'aGFuZGxl', epoch: 1, disabled: false, created: Date.now() }] }));
const child = spawn(process.execPath, ['server.js'], { cwd: root, env: { ...process.env, PORT: String(port), DATA_DIR: dir, COOKIE_SECRET: SECRET } });
const exp = Date.now() + 864e5, cookie = `hub_session=${UID}.1.${exp}.${crypto.createHmac('sha256', SECRET).update(`s${UID}.1.${exp}`).digest('base64url')}`;
const call = async (p, body, method) => { const r = await fetch(base + p, { method: method || (body ? 'POST' : 'GET'), headers: { cookie, 'content-type': 'application/json' }, body: body && JSON.stringify(body) }); return { status: r.status, json: await r.json().catch(() => ({})) }; };
const ymd = d => d.toISOString().slice(0, 10), plus = n => { const d = new Date(); d.setUTCDate(d.getUTCDate() + n); return ymd(d); };
const wait = ms => new Promise(r => setTimeout(r, ms));
(async () => {
for (let i = 0; i < 50; i++) { try { await fetch(base + '/login'); break; } catch { await wait(100); } }
const L = 'local:inbox', list = async () => (await call('/api/tasks?list=' + L)).json;
await call('/api/tasks', { list: L, fields: { title: 'Water plants', notes: '', due: plus(-3), done: false, repeat: 'daily' } });
await call('/api/tasks', { list: L, fields: { title: 'Plain', notes: '', due: '', done: false } });
let ts = await list(), rep = ts.find(t => t.title === 'Water plants'), plain = ts.find(t => t.title === 'Plain');
ok('a task remembers that it repeats', rep.repeat === 'daily' && plain.repeat === 'none', ts);
let r = await call('/api/tasks/toggle', { list: L, id: rep.id, done: true });
ts = await list(); rep = ts.find(t => t.title === 'Water plants');
ok('completing a repeating task moves it to the next date (after today) and keeps it open', r.json.rolled === plus(1) && rep.due === plus(1) && !rep.done, [r.json, rep]);
r = await call('/api/tasks/toggle', { list: L, id: plain.id, done: true }); ts = await list();
ok('a plain task is simply completed', ts.find(t => t.title === 'Plain').done === true && !r.json.rolled, r.json);
r = await call('/api/tasks', { list: L, id: rep.id, fields: { title: 'Water plants', notes: '', due: rep.due, done: false, repeat: 'bogus' } }); ts = await list();
ok('an unknown repeat is treated as none', ts.find(t => t.id === rep.id).repeat === 'none', ts);
await call('/api/tasks', { list: L, fields: { title: 'Sub 1', notes: '', due: '', done: false, parent: rep.id } });
ts = await list(); const sub = ts.find(t => t.title === 'Sub 1');
ok('a subtask is stored under its parent', sub && sub.parent === rep.id, ts);
r = await call('/api/tasks', { list: L, fields: { title: 'Deep', notes: '', due: '', done: false, parent: sub.id } });
ok('a subtask cannot have subtasks of its own', r.status === 400, r);
r = await call('/api/tasks', { list: L, fields: { title: 'Orphan', notes: '', due: '', done: false, parent: 'nope' } });
ok('a missing parent is refused', r.status === 400, r);
r = await call('/api/tasks/delete', { list: L, id: rep.id });
ts = await list(); ok('deleting the parent deletes its subtasks', !ts.some(t => t.id === rep.id || t.id === sub.id), ts);
// tasks due in a date range, for the calendar
await call('/api/tasks', { list: L, fields: { title: 'Due soon', notes: '', due: plus(2), done: false, repeat: 'weekly' } });
await call('/api/tasks', { list: L, fields: { title: 'Due later', notes: '', due: plus(40), done: false } });
await call('/api/tasks', { list: L, fields: { title: 'Done already', notes: '', due: plus(2), done: true } });
ts = await list(); const soonT = ts.find(t => t.title === 'Due soon');
await call('/api/tasks', { list: L, fields: { title: 'A subtask', notes: '', due: plus(2), done: false, parent: soonT.id } });
r = await call(`/api/tasks/due?from=${plus(0)}&to=${plus(7)}`);
ok('the calendar gets open tasks due in the range (not done ones, subtasks or other dates)', r.status === 200 && r.json.length === 1 && r.json[0].title === 'Due soon' && r.json[0].repeat === 'weekly' && r.json[0].listName, r.json);
r = await call('/api/tasks/due?from=bad&to=worse'); ok('a bad date range is refused', r.status === 400, r.status);
ok('the server is still running', child.exitCode === null);
child.kill(); fs.rmSync(dir, { recursive: true, force: true });
console.log(failed ? `\n${failed} check(s) failed` : '\nAll task checks passed'); process.exit(failed ? 1 : 0);
})();