#!/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); 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); })();