Index: test/push.test.js
===================================================================
--- test/push.test.js	(revision ad107154f7df0224781cab7686f02aa6148d5a9e)
+++ test/push.test.js	(revision ad107154f7df0224781cab7686f02aa6148d5a9e)
@@ -0,0 +1,70 @@
+// Web Push slice 1: VAPID key management + the subscription store. Actual
+// delivery needs a real push service, so tests cover the pure parts: key
+// auto-generation/persistence (no env editing) and the subscription CRUD with
+// per-type alert preferences. Own process (node --test), so env tweaks here
+// don't leak into other test files.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+delete process.env.VAPID_PUBLIC_KEY;
+delete process.env.VAPID_PRIVATE_KEY;
+const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pushkey-'));
+process.env.DATABASE_PATH = path.join(dir, 'database.sqlite');
+
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+dbMod.initializeDatabase();
+const Push = (await import('../src/services/PushService.js')).default;
+const keyFile = path.join(dir, '.vapid');
+
+test('VAPID keys auto-generate to a 0600 file and persist', async () => {
+  const pub = await Push.publicKey();
+  assert.ok(pub && typeof pub === 'string' && pub.length > 20, 'public key exists');
+  assert.ok(fs.existsSync(keyFile), 'key file written next to the database');
+  if (process.platform !== 'win32') {
+    assert.equal(fs.statSync(keyFile).mode & 0o777, 0o600, 'key file is 0600');
+  }
+  const onDisk = JSON.parse(fs.readFileSync(keyFile, 'utf8'));
+  assert.equal(onDisk.publicKey, pub, 'served key matches the persisted one');
+  assert.equal(await Push.pushReady(), true);
+});
+
+test('subscription store: save, list, update alerts, delete', () => {
+  const ok = Push.saveSubscription({
+    endpoint: 'https://push.example/ep1', userId: 'u1',
+    p256dh: 'PK', auth: 'AUTH', uaLabel: 'Firefox op laptop',
+  });
+  assert.equal(ok, true);
+  const list = Push.listSubscriptions('u1');
+  assert.equal(list.length, 1);
+  const alerts = JSON.parse(list[0].alert_types);
+  assert.equal(alerts.follow, 1);   // defaults applied
+  assert.equal(alerts.like, 0);
+  // update preferences
+  assert.equal(Push.updateAlerts('https://push.example/ep1', 'u1', { like: 1, follow: 0 }), true);
+  const upd = JSON.parse(Push.listSubscriptions('u1')[0].alert_types);
+  assert.equal(upd.like, 1);
+  assert.equal(upd.follow, 0);
+  assert.equal(upd.dm, 1);          // untouched default survives
+  // wrong user can't update
+  assert.equal(Push.updateAlerts('https://push.example/ep1', 'u2', { like: 0 }), false);
+  // delete
+  assert.equal(Push.deleteSubscription('https://push.example/ep1'), true);
+  assert.equal(Push.listSubscriptions('u1').length, 0);
+});
+
+test('re-subscribing the same endpoint upserts instead of duplicating', () => {
+  Push.saveSubscription({ endpoint: 'https://push.example/ep2', userId: 'u1', p256dh: 'A', auth: 'B' });
+  Push.saveSubscription({ endpoint: 'https://push.example/ep2', userId: 'u1', p256dh: 'C', auth: 'D' });
+  const rows = db.prepare('SELECT * FROM push_subscriptions WHERE endpoint = ?').all('https://push.example/ep2');
+  assert.equal(rows.length, 1);
+  assert.equal(rows[0].p256dh, 'C');
+});
+
+test('incomplete subscription payloads are refused', () => {
+  assert.equal(Push.saveSubscription({ endpoint: '', userId: 'u1', p256dh: 'x', auth: 'y' }), false);
+  assert.equal(Push.saveSubscription({ endpoint: 'https://e', userId: 'u1', p256dh: '', auth: 'y' }), false);
+});
