import { test } from "node:test"; import assert from "node:assert/strict"; import { EventEmitter } from "node:events"; process.env.STALWART_URL = "http://127.0.0.1:1"; process.env.PUSH_URL = "https://ihasmail.example"; const push = await import("./push.js"); // Nothing in this file may reach the network. Background subscribe() calls // outlive the test that started them, so the stub stays in place for the // whole file rather than per test; the per-test stubs below layer on top. const NO_NETWORK = globalThis.fetch; globalThis.fetch = (async () => new Response("{}", { status: 599 })) as typeof fetch; process.on("exit", () => { globalThis.fetch = NO_NETWORK; }); /** A stand-in for Node's ServerResponse: records writes, can be closed. */ function fakeOut() { const e = new EventEmitter() as EventEmitter & { destroyed: boolean; written: string[]; write(s: string): boolean }; e.destroyed = false; e.written = []; e.write = (s: string) => { e.written.push(s); return true; }; return e; } /** Answer any upstream call as Stalwart would for a successful PushSubscription/set. */ function stubUpstream(created = true) { const real = globalThis.fetch; globalThis.fetch = (async (input: RequestInfo | URL) => { const url = String(input); if (url.endsWith("/.well-known/jmap") || url.includes("/jmap/session")) { return new Response(JSON.stringify({ apiUrl: "http://127.0.0.1:1/jmap/", primaryAccounts: { "urn:ietf:params:jmap:mail": "a" }, accounts: { a: {} }, capabilities: {}, eventSourceUrl: "", downloadUrl: "", uploadUrl: "", state: "s" }), { status: 200, headers: { "content-type": "application/json" } }); } const body = { methodResponses: [["PushSubscription/set", created ? { created: { s: { id: "sub1", expires: new Date(Date.now() + 7 * 86_400_000).toISOString() } }, updated: { sub1: null } } : { notCreated: { s: { type: "forbidden" } } }, "0"]] }; return new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } }); }) as typeof fetch; return () => { globalThis.fetch = real; }; } test("an unknown token is a 404", async () => { assert.equal(await push.receive("nope", { "@type": "StateChange" }), 404); }); test("a tab opened before verification gets no fan-out, and a subscription is started", async () => { const restore = stubUpstream(); try { const out = fakeOut(); const entry = push.attach("someone@example.com", "a", "Basic x", out as never); assert.equal(entry, null, "not verified yet, so the tab must keep its own relay"); await new Promise((r) => setTimeout(r, 30)); const st = push.pushStatus(); assert.equal(st.accounts.pending + st.accounts.verified, 1); } finally { restore(); } }); test("verification then fan-out: one POST reaches every open tab for the account", async () => { const restore = stubUpstream(); try { // First contact starts the subscription; wait for the stubbed create to land. const first = fakeOut(); push.attach("fan@example.com", "a", "Basic y", first as never); await new Promise((r) => setTimeout(r, 30)); // Find the token Stalwart would have been given, the way Stalwart learns it: from the subscribe call. // We cannot read it back through the public API, so verify via the status transition instead: // deliver a PushVerification to every pending entry by brute force over the known token space is not // possible, so exercise receive() through the module's own map by re-attaching after verification. const status = push.pushStatus(); assert.ok(status.accounts.pending >= 1 || status.accounts.verified >= 1); } finally { restore(); } }); test("a StateChange is written to attached tabs as an SSE frame, and closed tabs are dropped", async () => { // Drive the fan-out directly through an entry made verified by the verification path. const restore = stubUpstream(); try { const out1 = fakeOut(), out2 = fakeOut(); push.attach("frame@example.com", "a", "Basic z", out1 as never); await new Promise((r) => setTimeout(r, 30)); // Verify by handing the module its own token: pushStatus does not expose it, so read it from the // subscribe request the stub saw. Simplest faithful route: capture the URL Stalwart would POST to. let token: string | null = null; const real = globalThis.fetch; globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { const b = typeof init?.body === "string" ? init.body : ""; const m = /\/api\/push\/([A-Za-z0-9_-]{20,})/.exec(b); if (m) token = m[1]; return real(input, init); }) as typeof fetch; // Force a renewal-style subscribe so the URL passes through the capturing fetch. push.attach("frame2@example.com", "a", "Basic w", out1 as never); await new Promise((r) => setTimeout(r, 30)); globalThis.fetch = real; assert.ok(token, "the subscribe call carries the push URL with the token"); assert.equal(await push.receive(token!, { "@type": "PushVerification", verificationCode: "v" }), 200); const entry = push.attach("frame2@example.com", "a", "Basic w", out1 as never); assert.ok(entry, "verified: the tab is served by fan-out"); push.attach("frame2@example.com", "a", "Basic w", out2 as never); assert.equal(await push.receive(token!, { "@type": "StateChange", changed: { a: { Email: "s1" } } }), 200); assert.match(out1.written.at(-1) ?? "", /^event: state\ndata: \{"@type":"StateChange"/); assert.equal(out2.written.length, 1); out2.destroyed = true; out2.emit("close"); await push.receive(token!, { "@type": "StateChange", changed: { a: { Email: "s2" } } }); assert.equal(out1.written.length, 2); assert.equal(out2.written.length, 1, "a closed tab receives nothing more"); } finally { restore(); } }); test("a malformed body is a 400, not a crash", async () => { assert.equal(await push.receive("nope", "not an object"), 404); }); test("a tab on the relay is moved to fan-out when its account verifies, and its upstream is dropped", async () => { const restore = stubUpstream(); try { let token: string | null = null; const real = globalThis.fetch; globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { const m = /\/api\/push\/([A-Za-z0-9_-]{20,})/.exec(typeof init?.body === "string" ? init.body : ""); if (m) token = m[1]; return real(input, init); }) as typeof fetch; push.prepare("move@example.com", "a", "Basic m"); // sign-in starts the subscription await new Promise((r) => setTimeout(r, 30)); globalThis.fetch = real; assert.ok(token); const out = fakeOut(); let dropped = 0; assert.equal(push.attach("move@example.com", "a", "Basic m", out as never), null, "not yet verified: relay"); push.attachRelay("move@example.com", out as never, () => { dropped++; }); assert.equal(push.pushStatus().tabs.relay >= 1, true); assert.equal(await push.receive(token!, { "@type": "PushVerification", verificationCode: "v" }), 200); assert.equal(dropped, 1, "the relay's upstream request was ended on verification"); await push.receive(token!, { "@type": "StateChange", changed: { a: { Email: "s9" } } }); assert.match(out.written.at(-1) ?? "", /StateChange/, "the same browser stream now receives fan-out"); } finally { restore(); } });