123import { beforeEach, describe, expect, it, vi } from "vitest";45type StorageShape = Record<string, unknown>;67interface FakePort {8 name: string;9 sender?: chrome.runtime.MessageSender;10 onDisconnect: { addListener: (fn: () => void) => void };11 fireDisconnect: () => Promise<void>;12}1314function makePort(name: string, sender?: chrome.runtime.MessageSender): FakePort {15 let handler: (() => void) | null = null;16 return {17 name,18 sender,19 onDisconnect: {20 addListener: (fn: () => void) => {21 handler = fn;22 },23 },24 fireDisconnect: async () => {25 await handler?.();26 },27 };28}2930describe("LV-5 lock-on-close port", () => {31 let storage: StorageShape;32 let connectListeners: Array<(port: FakePort) => void>;3334 beforeEach(() => {35 vi.resetModules();36 storage = {};37 connectListeners = [];3839 const chromeMock = {40 runtime: {41 onInstalled: { addListener: vi.fn() },42 onConnect: {43 addListener: vi.fn((fn: (port: FakePort) => void) => {44 connectListeners.push(fn);45 }),46 },47 onMessage: { addListener: vi.fn() },48 sendMessage: vi.fn(async () => undefined),49 connect: vi.fn(),50 getURL: vi.fn((p: string) => `chrome-extension://test/${p}`),51 },52 storage: {53 local: {54 get: vi.fn(async (key?: string | string[]) => {55 if (!key) return storage;56 if (typeof key === "string") return { [key]: storage[key] };57 return key.reduce<Record<string, unknown>>((r, k) => {58 r[k] = storage[k];59 return r;60 }, {});61 }),62 set: vi.fn(async (next: Record<string, unknown>) => {63 Object.assign(storage, next);64 }),65 remove: vi.fn(async (keys: string | string[]) => {66 for (const k of Array.isArray(keys) ? keys : [keys]) delete storage[k];67 }),68 },69 session: {70 get: vi.fn(async () => ({})),71 set: vi.fn(async () => undefined),72 remove: vi.fn(async () => undefined),73 },74 },75 alarms: {76 clear: vi.fn(async () => true),77 create: vi.fn(async () => undefined),78 onAlarm: { addListener: vi.fn() },79 },80 idle: {81 setDetectionInterval: vi.fn(),82 onStateChanged: { addListener: vi.fn() },83 },84 scripting: { executeScript: vi.fn(async () => []) },85 tabs: { query: vi.fn(async () => []) },86 commands: { onCommand: { addListener: vi.fn() } },87 webAuthenticationProxy: {88 onCreateRequest: { addListener: vi.fn() },89 onGetRequest: { addListener: vi.fn() },90 onIsUvpaaRequest: { addListener: vi.fn() },91 onRequestCanceled: { addListener: vi.fn() },92 attach: vi.fn(async () => undefined),93 completeCreateRequest: vi.fn(async () => undefined),94 completeGetRequest: vi.fn(async () => undefined),95 completeIsUvpaaRequest: vi.fn(async () => undefined),96 },97 } as unknown as typeof chrome;9899 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;100 vi.stubGlobal("__PEACH_E2E_PASSKEYS__", false);101 vi.stubGlobal("fetch", vi.fn(async () => {102 throw new Error("network disabled in tests");103 }));104 });105106 async function setupUnlockedVaultWithLockOnClose() {107 const vault = await import("./vault");108 await vault.createVault("correct horse battery staple");109 await vault.updateSettings({ lockOnClose: true });110 expect(vault.isUnlocked()).toBe(true);111 return vault;112 }113114 it("locks the vault when a nonce'd popup-<uuid> port disconnects", async () => {115 await import("./entry"); 116 const vault = await setupUnlockedVaultWithLockOnClose();117 expect(connectListeners).toHaveLength(1);118119 const port = makePort(`popup-${"a1b2c3"}`, { url: "chrome-extension://test/src/popup/index.html" });120 connectListeners[0](port);121 await port.fireDisconnect();122123 expect(vault.isUnlocked()).toBe(false);124 });125126 it("ignores a nonce'd popup port from a content script", async () => {127 await import("./entry");128 const vault = await setupUnlockedVaultWithLockOnClose();129130 const port = makePort(`popup-${"a1b2c3"}`, {131 url: "https://example.com/",132 tab: { id: 7 } as chrome.tabs.Tab,133 });134 connectListeners[0](port);135 await port.fireDisconnect();136137 expect(vault.isUnlocked()).toBe(true);138 });139140 it.each(["popup", "evil"])("ignores the non-nonce port name %s", async (name) => {141 await import("./entry");142 const vault = await setupUnlockedVaultWithLockOnClose();143144 const port = makePort(name);145 connectListeners[0](port);146 147 await port.fireDisconnect();148149 expect(vault.isUnlocked()).toBe(true);150 });151});