Files

69 lines
5.7 KiB
JavaScript

#!/usr/bin/env node
// Push reminders: who gets told, when, and never twice. The push service itself is replaced by a recorder.
const fs = require('fs'), os = require('os'), path = require('path');
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-push-'));
process.env.DATA_DIR = dir; process.env.COOKIE_SECRET = 'push-test-secret-'.repeat(3);
let failed = 0;
const ok = (n, c, x) => { console.log((c ? 'PASS ' : 'FAIL ') + n + (c ? '' : ' -> ' + JSON.stringify(x))); if (!c) failed++; };
const webpush = require('web-push');
const sent = []; let dead = false;
webpush.sendNotification = async (sub, payload) => { if (dead) throw Object.assign(new Error('gone'), { statusCode: 410 }); sent.push({ sub, ...JSON.parse(payload) }); };
const users = require('../users'), push = require('../push');
(async () => {
const u = users.create({ name: 'Pat', role: 'user', tabs: ['tasks', 'calendar'] });
const d = users.dirOf(u.id);
const TZ = 'America/New_York', now = new Date('2026-10-22T13:00:00Z'); // 09:00 in New York
const { day, hm } = push.localNow(TZ, now);
ok('the person\'s own clock is used (09:00 in New York at 13:00 UTC)', day === '2026-10-22' && hm === '09:00', { day, hm });
const writePush = o => fs.writeFileSync(path.join(d, 'push.json'), JSON.stringify({ tz: TZ, tasksAt: '08:00', eventMins: 15, tasks: true, events: true, subs: [{ sub: { endpoint: 'https://push.example/abc', keys: { p256dh: 'x', auth: 'y' } } }], ...o }));
fs.writeFileSync(path.join(d, 'tasks.json'), JSON.stringify({ lists: [{ id: 'inbox', name: 'Tasks' }], tasks: [
{ id: 't1', list: 'inbox', title: 'Pay rent', due: day, done: false }, { id: 't2', list: 'inbox', title: 'Old thing', due: '2026-10-01', done: false },
{ id: 't3', list: 'inbox', title: 'Done thing', due: day, done: true }, { id: 't4', list: 'inbox', title: 'Later', due: '2026-12-01', done: false }] }));
writePush({}); await push.tick(new Date('2026-10-22T11:00:00Z')); // 07:00 local: too early
ok('nothing is sent before the morning time', sent.length === 0, sent);
await push.tick(now);
ok('at the morning time one summary is sent for tasks due or overdue', sent.length === 1 && /2 tasks/.test(sent[0].title) && /1 overdue/.test(sent[0].body) && !/Done thing|Later/.test(sent[0].body), sent);
await push.tick(new Date(now.getTime() + 60e3));
ok('and not again the same day', sent.length === 1, sent.length);
await push.tick(new Date(now.getTime() + 864e5));
ok('but again the next day', sent.length === 2, sent.length);
sent.length = 0; writePush({ tasks: false, subs: [] }); fs.writeFileSync(path.join(d, 'push.json'), JSON.stringify({ tz: TZ, subs: [] }));
await push.tick(new Date(now.getTime() + 2 * 864e5));
ok('a person with no device signed up is never contacted', sent.length === 0, sent);
// events: a local event starting in 10 minutes
const soon = new Date(Date.now() + 10 * 60e3), pad = n => String(n).padStart(2, '0');
const st = `${soon.getUTCFullYear()}-${pad(soon.getUTCMonth() + 1)}-${pad(soon.getUTCDate())}T${pad(soon.getUTCHours())}:${pad(soon.getUTCMinutes())}`;
fs.writeFileSync(path.join(d, 'events.json'), JSON.stringify([{ uid: 'e1', fields: { title: 'Dentist', location: 'Main St', notes: '', allDay: false, start: st, end: st.replace(/T(\d\d)/, (m, h) => 'T' + pad((+h + 1) % 24)), tz: 'UTC', repeat: 'none' }, created: 1, updated: 1 }]));
writePush({ tasks: false }); sent.length = 0;
await push.tick(new Date(Date.now() - 3 * 36e5), true);
ok('an event hours away sends nothing', sent.length === 0, sent);
await push.tick(new Date(), true);
ok('an event about to start is announced with its place', sent.some(s => s.title === 'Dentist' && /Main St/.test(s.body)), sent);
await push.tick(new Date(Date.now() + 60e3), true);
ok('and only once', sent.filter(s => s.title === 'Dentist').length === 1, sent);
// the Microsoft client secret for Mail is about to expire: admins are warned once per stage
const admin = users.create({ name: 'Boss', role: 'admin' }), ad = users.dirOf(admin.id);
fs.writeFileSync(path.join(ad, 'push.json'), JSON.stringify({ tz: TZ, tasks: false, events: false, subs: [{ sub: { endpoint: 'https://push.example/admin', keys: { p256dh: 'x', auth: 'y' } } }] }));
const in5 = new Date(Date.now() + 5 * 864e5).toISOString().slice(0, 10);
fs.writeFileSync(path.join(dir, 'mail-config.json'), JSON.stringify({ data: require('../vault')('nehub-mail').seal({ clientId: 'aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb', clientSecret: 's', secretExpires: in5 }) }));
sent.length = 0; await push.tick(new Date(), true);
ok('an admin is warned that the mail secret expires soon', sent.some(s => /Mail sign-in expires in [456] days/.test(s.title) && s.sub.endpoint.endsWith('/admin')), sent);
const n1 = sent.length; await push.tick(new Date(Date.now() + 60e3), true);
ok('and only once for that stage', sent.length === n1, sent.length);
ok('an ordinary person is not told about it', !sent.some(s => /Mail sign-in/.test(s.title) && !s.sub.endpoint.endsWith('/admin')), sent);
// a subscription the push service says is gone is dropped
dead = true; writePush({ tasks: true, tasksAt: '00:00', tasksDay: '' }); fs.writeFileSync(path.join(d, 'tasks.json'), JSON.stringify({ lists: [{ id: 'inbox', name: 'Tasks' }], tasks: [{ id: 't1', list: 'inbox', title: 'X', due: '2000-01-01', done: false }] }));
await push.tick(new Date());
ok('a device that has unsubscribed (410) is removed', JSON.parse(fs.readFileSync(path.join(d, 'push.json'), 'utf8')).subs.length === 0, fs.readFileSync(path.join(d, 'push.json'), 'utf8'));
fs.rmSync(dir, { recursive: true, force: true });
console.log(failed ? `\n${failed} check(s) failed` : '\nAll push checks passed'); process.exit(failed ? 1 : 0);
})().catch(e => { console.error('TEST CRASHED:', e); process.exit(1); });