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