// LV-5: the lock-on-close onConnect listener accepts only our nonce'd // "popup-" port and ignores anything else. import { beforeEach, describe, expect, it, vi } from "vitest"; type StorageShape = Record; interface FakePort { name: string; onDisconnect: { addListener: (fn: () => void) => void }; fireDisconnect: () => Promise; } function makePort(name: string): FakePort { let handler: (() => void) | null = null; return { name, onDisconnect: { addListener: (fn: () => void) => { handler = fn; }, }, fireDisconnect: async () => { await handler?.(); }, }; } describe("LV-5 lock-on-close port", () => { let storage: StorageShape; let connectListeners: Array<(port: FakePort) => void>; beforeEach(() => { vi.resetModules(); storage = {}; connectListeners = []; const chromeMock = { runtime: { onInstalled: { addListener: vi.fn() }, onConnect: { addListener: vi.fn((fn: (port: FakePort) => void) => { connectListeners.push(fn); }), }, onMessage: { addListener: vi.fn() }, sendMessage: vi.fn(async () => undefined), connect: vi.fn(), getURL: vi.fn((p: string) => p), }, storage: { local: { get: vi.fn(async (key?: string | string[]) => { if (!key) return storage; if (typeof key === "string") return { [key]: storage[key] }; return key.reduce>((r, k) => { r[k] = storage[k]; return r; }, {}); }), set: vi.fn(async (next: Record) => { Object.assign(storage, next); }), remove: vi.fn(async (keys: string | string[]) => { for (const k of Array.isArray(keys) ? keys : [keys]) delete storage[k]; }), }, session: { get: vi.fn(async () => ({})), set: vi.fn(async () => undefined), remove: vi.fn(async () => undefined), }, }, alarms: { clear: vi.fn(async () => true), create: vi.fn(async () => undefined), onAlarm: { addListener: vi.fn() }, }, idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() }, }, scripting: { executeScript: vi.fn(async () => []) }, tabs: { query: vi.fn(async () => []) }, commands: { onCommand: { addListener: vi.fn() } }, webAuthenticationProxy: { onCreateRequest: { addListener: vi.fn() }, onGetRequest: { addListener: vi.fn() }, onIsUvpaaRequest: { addListener: vi.fn() }, onRequestCanceled: { addListener: vi.fn() }, attach: vi.fn(async () => undefined), completeCreateRequest: vi.fn(async () => undefined), completeGetRequest: vi.fn(async () => undefined), completeIsUvpaaRequest: vi.fn(async () => undefined), }, } as unknown as typeof chrome; (globalThis as { chrome?: typeof chrome }).chrome = chromeMock; vi.stubGlobal("__PEACH_E2E_PASSKEYS__", false); vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("network disabled in tests"); })); }); async function setupUnlockedVaultWithLockOnClose() { const vault = await import("./vault"); await vault.createVault("correct horse battery staple"); await vault.updateSettings({ lockOnClose: true }); expect(vault.isUnlocked()).toBe(true); return vault; } it("locks the vault when a nonce'd popup- port disconnects", async () => { await import("./entry"); // registers the onConnect listener const vault = await setupUnlockedVaultWithLockOnClose(); expect(connectListeners).toHaveLength(1); const port = makePort(`popup-${"a1b2c3"}`); connectListeners[0](port); await port.fireDisconnect(); expect(vault.isUnlocked()).toBe(false); }); it.each(["popup", "evil"])("ignores the non-nonce port name %s", async (name) => { await import("./entry"); const vault = await setupUnlockedVaultWithLockOnClose(); const port = makePort(name); connectListeners[0](port); // The listener returned early, so no onDisconnect handler was registered. await port.fireDisconnect(); expect(vault.isUnlocked()).toBe(true); }); });