import { test } from "node:test"; import assert from "node:assert/strict"; import { generateKeyPairSync } from "node:crypto"; import { readFileSync } from "node:fs"; import { runInThisContext } from "node:vm"; globalThis.self = globalThis; globalThis.location = { origin: "https://app.example" }; // Load the helper the way a worker does: importScripts evaluates a // classic script in the global scope. vm.runInThisContext is that. globalThis.importScripts = (path) => runInThisContext(readFileSync(new URL(path, import.meta.url), "utf8"), { filename: path }); importScripts("./aviso-sw.js"); const { AvisoSW } = globalThis; // Real P-256 points: a browser's subscribe() rejects anything else. function realPoint() { const { publicKey } = generateKeyPairSync("ec", { namedCurve: "prime256v1" }); const jwk = publicKey.export({ format: "jwk" }); return Uint8Array.from([4, ...Buffer.from(jwk.x, "base64url"), ...Buffer.from(jwk.y, "base64url")]); } const KEY_BYTES = realPoint(); const KEY = Buffer.from(KEY_BYTES).toString("base64url"); const OLD_KEY_BYTES = realPoint(); const asBuffer = (u8) => u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength); function fakeSub(endpoint, keyBytes) { return { endpoint, options: { applicationServerKey: asBuffer(keyBytes) }, unsubscribed: false, toJSON() { return { endpoint: this.endpoint, expirationTime: null, keys: { p256dh: "P", auth: "A" } }; }, async unsubscribe() { this.unsubscribed = true; return true; }, }; } // The platform refuses options it cannot store: a function inside // data is a DataCloneError in every browser. function cloneable(v) { if (typeof v === "function") return false; if (v && typeof v === "object") return Object.values(v).every(cloneable); return true; } function rig() { const shown = [], opened = [], subscribed = []; globalThis.registration = { async showNotification(title, options) { if (!cloneable(options)) throw new Error("DataCloneError"); shown.push({ title, options }); }, pushManager: { async subscribe(opts) { assert.equal(opts.userVisibleOnly, true); assert.deepEqual(Array.from(opts.applicationServerKey), Array.from(KEY_BYTES)); const s = fakeSub("https://push.example/renewed", KEY_BYTES); subscribed.push(s); return s; }, }, }; globalThis.clients = { windows: [], async matchAll(opts) { // A worker that forgets includeUncontrolled misses windows it // does not yet control and opens duplicates; the fake refuses. assert.deepEqual(opts, { type: "window", includeUncontrolled: true }); return this.windows; }, async openWindow(u) { opened.push(u); }, }; return { shown, opened, subscribed }; } function pushEvent(payload) { return { data: payload === undefined ? null : { json() { return JSON.parse(payload); } } }; } test("validateURL admits only root-relative same-origin paths, absolute refused even on-origin", () => { const o = "https://app.example"; assert.equal(AvisoSW.validateURL("/inbox?x=1", o), "https://app.example/inbox?x=1"); for (const bad of ["", "inbox", "//evil.example/x", "https://evil.example/x", "https://app.example/inbox", "/a\\b", "javascript:alert(1)", "https://app.example.evil/x", 42, null]) { assert.equal(AvisoSW.validateURL(bad, o), null, String(bad)); } }); test("handlePush shows the default payload and validates url", async () => { const r = rig(); await AvisoSW.handlePush(pushEvent('{"title":"Hi","body":"b","url":"/inbox","tag":"t"}'), { fallback: () => ({ title: "fb" }) }); assert.equal(r.shown.length, 1); assert.equal(r.shown[0].title, "Hi"); assert.equal(r.shown[0].options.body, "b"); assert.equal(r.shown[0].options.tag, "t"); assert.equal(r.shown[0].options.data.url, "https://app.example/inbox"); }); test("handlePush drops an off-origin or absolute url but still shows", async () => { const r = rig(); await AvisoSW.handlePush(pushEvent('{"title":"Hi","url":"https://evil.example/"}'), { fallback: () => ({ title: "fb" }) }); await AvisoSW.handlePush(pushEvent('{"title":"Hi2","url":"https://app.example/inbox"}'), { fallback: () => ({ title: "fb" }) }); assert.equal(r.shown[0].title, "Hi"); assert.equal("url" in r.shown[0].options.data, false); assert.equal(r.shown[1].title, "Hi2"); assert.equal("url" in r.shown[1].options.data, false); }); test("handlePush falls back on malformed or missing payload", async () => { const r = rig(); await AvisoSW.handlePush(pushEvent('{"nope":1}'), { fallback: () => ({ title: "fb", options: { data: { url: "/" } } }) }); await AvisoSW.handlePush(pushEvent(undefined), { fallback: () => ({ title: "fb2" }) }); await AvisoSW.handlePush({ data: { json() { throw new Error("not json"); } } }, { fallback: () => ({ title: "fb3" }) }); assert.deepEqual(r.shown.map((s) => s.title), ["fb", "fb2", "fb3"]); assert.equal(r.shown[0].options.data.url, "https://app.example/"); }); test("handlePush falls back when the platform refuses the decoded options", async () => { const r = rig(); await AvisoSW.handlePush(pushEvent('{"blob":"..."}'), { decode: async () => ({ title: "Custom", options: { data: { url: "/x", fn() {} } } }), fallback: () => ({ title: "fb" }), }); assert.deepEqual(r.shown.map((s) => s.title), ["fb"]); // A fallback the platform refuses is the app's bug: it surfaces. await assert.rejects(AvisoSW.handlePush(pushEvent('{"nope":1}'), { fallback: () => ({ title: "fb", options: { data: { fn() {} } } }) }), /DataCloneError/); }); test("handlePush requires a fallback", async () => { rig(); await assert.rejects(AvisoSW.handlePush(pushEvent('{"title":"fine"}'), {}), /fallback is required/); }); test("handlePush uses a custom decoder and validates its url; a throwing decoder falls back", async () => { const r = rig(); await AvisoSW.handlePush(pushEvent('{"blob":"..."}'), { decode: async () => ({ title: "Custom", options: { data: { url: "//evil.example/" } } }), fallback: () => ({ title: "fb" }), }); assert.equal(r.shown[0].title, "Custom"); assert.equal("url" in r.shown[0].options.data, false); await AvisoSW.handlePush(pushEvent('{"blob":"..."}'), { decode: async () => { throw new Error("cannot decrypt"); }, fallback: () => ({ title: "fb" }), }); assert.equal(r.shown[1].title, "fb"); }); test("handleClick focuses a matching window, else opens, else fallback, never off-origin", async () => { const r = rig(); let focusedURL = null; globalThis.clients.windows = [{ url: "https://app.example/inbox", async focus() { focusedURL = this.url; } }]; let closed = 0; const ev = (url) => ({ notification: { close() { closed++; }, data: url === undefined ? {} : { url } } }); await AvisoSW.handleClick(ev("https://app.example/inbox"), {}); assert.equal(focusedURL, "https://app.example/inbox"); await AvisoSW.handleClick(ev("https://app.example/other"), {}); assert.deepEqual(r.opened, ["https://app.example/other"]); await AvisoSW.handleClick(ev(undefined), { fallbackURL: "/" }); assert.deepEqual(r.opened, ["https://app.example/other", "https://app.example/"]); await AvisoSW.handleClick(ev("https://evil.example/"), {}); await AvisoSW.handleClick(ev("https://app.example.evil/"), {}); await AvisoSW.handleClick(ev("https://evil.example/"), { fallbackURL: "//evil.example/" }); await AvisoSW.handleClick(ev(undefined), { fallbackURL: "https://app.example/abs" }); // fallbackURL is external input: relative only assert.equal(r.opened.length, 2); assert.equal(closed, 7); }); test("handleSubscriptionChange saves a new subscription under the current key", async () => { rig(); const saved = []; const sub = fakeSub("https://push.example/new", KEY_BYTES); const ok = await AvisoSW.handleSubscriptionChange( { newSubscription: sub, oldSubscription: { endpoint: "https://push.example/old" } }, { publicKey: async () => KEY, save: async (b) => { saved.push(b); return { ok: true }; } }, ); assert.equal(ok, true); assert.deepEqual(saved[0], { subscription: { endpoint: "https://push.example/new", keys: { p256dh: "P", auth: "A" } }, publicKey: KEY, previousEndpoint: "https://push.example/old", }); }); test("handleSubscriptionChange replaces a renewal made under a rotated key", async () => { const r = rig(); const saved = []; const stale = fakeSub("https://push.example/stale", OLD_KEY_BYTES); const ok = await AvisoSW.handleSubscriptionChange( { newSubscription: stale, oldSubscription: { endpoint: "https://push.example/old" } }, { publicKey: async () => KEY, save: async (b) => { saved.push(b); } }, ); assert.equal(ok, true); assert.equal(stale.unsubscribed, true); assert.equal(r.subscribed.length, 1); assert.equal(saved[0].subscription.endpoint, "https://push.example/renewed"); assert.equal(saved[0].publicKey, KEY); assert.equal(saved[0].previousEndpoint, "https://push.example/old"); }); test("handleSubscriptionChange renews itself when the event carries no subscription", async () => { const r = rig(); const saved = []; const ok = await AvisoSW.handleSubscriptionChange({}, { publicKey: async () => KEY, save: async (b) => { saved.push(b); } }); assert.equal(ok, true); assert.equal(r.subscribed.length, 1); assert.equal(saved[0].subscription.endpoint, "https://push.example/renewed"); assert.equal("previousEndpoint" in saved[0], false); }); test("handleSubscriptionChange fails silently on an expired session, a failed key fetch, or a throw", async () => { rig(); const sub = fakeSub("e", KEY_BYTES); assert.equal(await AvisoSW.handleSubscriptionChange({ newSubscription: sub }, { publicKey: async () => KEY, save: async () => ({ ok: false, status: 401 }) }), false); assert.equal(await AvisoSW.handleSubscriptionChange({ newSubscription: sub }, { publicKey: async () => { throw new Error("offline"); }, save: async () => ({ ok: true }) }), false); assert.equal(await AvisoSW.handleSubscriptionChange({ newSubscription: sub }, { publicKey: async () => KEY, save: async () => { throw new Error("net"); } }), false); await assert.rejects(AvisoSW.handleSubscriptionChange({ newSubscription: sub }, {}), /publicKey\(\) and save\(\) are required/); }); // The app's sw.js, as SKILL.md shows it: listeners that hand the // helper's promise to event.waitUntil synchronously. A worker that // awaited before calling waitUntil would be terminated mid-flight. test("an app worker wires the helper through event.waitUntil synchronously", async () => { const r = rig(); const listeners = {}; globalThis.addEventListener = (type, fn) => { listeners[type] = fn; }; runInThisContext(` self.addEventListener("push", (e) => e.waitUntil(AvisoSW.handlePush(e, { fallback: () => ({ title: "New activity", options: { data: { url: "/" } } }), }))); self.addEventListener("notificationclick", (e) => e.waitUntil(AvisoSW.handleClick(e, { fallbackURL: "/" }))); `, { filename: "sw.js" }); let waited = null; const ev = Object.assign(pushEvent('{"title":"Hi","url":"/inbox"}'), { waitUntil(p) { waited = p; } }); listeners.push(ev); assert.ok(waited && typeof waited.then === "function", "waitUntil not called synchronously with a promise"); await waited; assert.equal(r.shown[0].title, "Hi"); waited = null; listeners.notificationclick({ notification: { close() {}, data: { url: "https://app.example/inbox" } }, waitUntil(p) { waited = p; } }); assert.ok(waited); await waited; assert.deepEqual(r.opened, ["https://app.example/inbox"]); });