| 1 | // Web Push slice 1: VAPID key management + the subscription store. Actual
|
|---|
| 2 | // delivery needs a real push service, so tests cover the pure parts: key
|
|---|
| 3 | // auto-generation/persistence (no env editing) and the subscription CRUD with
|
|---|
| 4 | // per-type alert preferences. Own process (node --test), so env tweaks here
|
|---|
| 5 | // don't leak into other test files.
|
|---|
| 6 | import { test } from 'node:test';
|
|---|
| 7 | import assert from 'node:assert/strict';
|
|---|
| 8 | import fs from 'fs';
|
|---|
| 9 | import os from 'os';
|
|---|
| 10 | import path from 'path';
|
|---|
| 11 |
|
|---|
| 12 | delete process.env.VAPID_PUBLIC_KEY;
|
|---|
| 13 | delete process.env.VAPID_PRIVATE_KEY;
|
|---|
| 14 | const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pushkey-'));
|
|---|
| 15 | process.env.DATABASE_PATH = path.join(dir, 'database.sqlite');
|
|---|
| 16 |
|
|---|
| 17 | const dbMod = await import('../src/config/database.js');
|
|---|
| 18 | const db = dbMod.default;
|
|---|
| 19 | dbMod.initializeDatabase();
|
|---|
| 20 | const Push = (await import('../src/services/PushService.js')).default;
|
|---|
| 21 | const keyFile = path.join(dir, '.vapid');
|
|---|
| 22 |
|
|---|
| 23 | test('VAPID keys auto-generate to a 0600 file and persist', async () => {
|
|---|
| 24 | const pub = await Push.publicKey();
|
|---|
| 25 | assert.ok(pub && typeof pub === 'string' && pub.length > 20, 'public key exists');
|
|---|
| 26 | assert.ok(fs.existsSync(keyFile), 'key file written next to the database');
|
|---|
| 27 | if (process.platform !== 'win32') {
|
|---|
| 28 | assert.equal(fs.statSync(keyFile).mode & 0o777, 0o600, 'key file is 0600');
|
|---|
| 29 | }
|
|---|
| 30 | const onDisk = JSON.parse(fs.readFileSync(keyFile, 'utf8'));
|
|---|
| 31 | assert.equal(onDisk.publicKey, pub, 'served key matches the persisted one');
|
|---|
| 32 | assert.equal(await Push.pushReady(), true);
|
|---|
| 33 | });
|
|---|
| 34 |
|
|---|
| 35 | test('subscription store: save, list, update alerts, delete', () => {
|
|---|
| 36 | const ok = Push.saveSubscription({
|
|---|
| 37 | endpoint: 'https://push.example/ep1', userId: 'u1',
|
|---|
| 38 | p256dh: 'PK', auth: 'AUTH', uaLabel: 'Firefox op laptop',
|
|---|
| 39 | });
|
|---|
| 40 | assert.equal(ok, true);
|
|---|
| 41 | const list = Push.listSubscriptions('u1');
|
|---|
| 42 | assert.equal(list.length, 1);
|
|---|
| 43 | const alerts = JSON.parse(list[0].alert_types);
|
|---|
| 44 | assert.equal(alerts.follow, 1); // defaults applied
|
|---|
| 45 | assert.equal(alerts.like, 0);
|
|---|
| 46 | // update preferences
|
|---|
| 47 | assert.equal(Push.updateAlerts('https://push.example/ep1', 'u1', { like: 1, follow: 0 }), true);
|
|---|
| 48 | const upd = JSON.parse(Push.listSubscriptions('u1')[0].alert_types);
|
|---|
| 49 | assert.equal(upd.like, 1);
|
|---|
| 50 | assert.equal(upd.follow, 0);
|
|---|
| 51 | assert.equal(upd.dm, 1); // untouched default survives
|
|---|
| 52 | // wrong user can't update
|
|---|
| 53 | assert.equal(Push.updateAlerts('https://push.example/ep1', 'u2', { like: 0 }), false);
|
|---|
| 54 | // delete
|
|---|
| 55 | assert.equal(Push.deleteSubscription('https://push.example/ep1'), true);
|
|---|
| 56 | assert.equal(Push.listSubscriptions('u1').length, 0);
|
|---|
| 57 | });
|
|---|
| 58 |
|
|---|
| 59 | test('re-subscribing the same endpoint upserts instead of duplicating', () => {
|
|---|
| 60 | Push.saveSubscription({ endpoint: 'https://push.example/ep2', userId: 'u1', p256dh: 'A', auth: 'B' });
|
|---|
| 61 | Push.saveSubscription({ endpoint: 'https://push.example/ep2', userId: 'u1', p256dh: 'C', auth: 'D' });
|
|---|
| 62 | const rows = db.prepare('SELECT * FROM push_subscriptions WHERE endpoint = ?').all('https://push.example/ep2');
|
|---|
| 63 | assert.equal(rows.length, 1);
|
|---|
| 64 | assert.equal(rows[0].p256dh, 'C');
|
|---|
| 65 | });
|
|---|
| 66 |
|
|---|
| 67 | test('burst throttle: one ping per window per (user,type); test type never throttles', () => {
|
|---|
| 68 | assert.equal(Push.throttled('tu1', 'like', 1000), false); // first passes
|
|---|
| 69 | assert.equal(Push.throttled('tu1', 'like', 1100), true); // within 300s window
|
|---|
| 70 | assert.equal(Push.throttled('tu1', 'like', 1301), false); // window elapsed
|
|---|
| 71 | assert.equal(Push.throttled('tu1', 'boost', 1000), false); // other type independent
|
|---|
| 72 | assert.equal(Push.throttled('tu2', 'like', 1000), false); // other user independent
|
|---|
| 73 | assert.equal(Push.throttled('tu1', 'test', 1000), false); // test bypasses
|
|---|
| 74 | assert.equal(Push.throttled('tu1', 'test', 1001), false);
|
|---|
| 75 | });
|
|---|
| 76 |
|
|---|
| 77 | test('incomplete subscription payloads are refused', () => {
|
|---|
| 78 | assert.equal(Push.saveSubscription({ endpoint: '', userId: 'u1', p256dh: 'x', auth: 'y' }), false);
|
|---|
| 79 | assert.equal(Push.saveSubscription({ endpoint: 'https://e', userId: 'u1', p256dh: '', auth: 'y' }), false);
|
|---|
| 80 | });
|
|---|