123import { beforeEach, describe, expect, it, vi } from "vitest";45type StorageShape = Record<string, unknown>;67interface FakePort {8 name: string;9 onDisconnect: { addListener: (fn: () => void) => void };10 fireDisconnect: () => Promise<void>;11}1213function makePort(name: string): FakePort {14 let handler: (() => void) | null = null;15 return {16 name,17 onDisconnect: {18 addListener: (fn: () => void) => {19 handler = fn;20 },21 },22 fireDisconnect: async () => {23 await handler?.();24 },25 };26}2728describe("LV-5 lock-on-close port", () => {29 let storage: StorageShape;30 let connectListeners: Array<(port: FakePort) => void>;3132 beforeEach(() => {33 vi.resetModules();34 storage = {};35 connectListeners = [];3637 const chromeMock = {38 runtime: {39 onInstalled: { addListener: vi.fn() },40 onConnect: {41 addListener: vi.fn((fn: (port: FakePort) => void) => {42 connectListeners.push(fn);43 }),44 },45 onMessage: { addListener: vi.fn() },46 sendMessage: vi.fn(async () => undefined),47 connect: vi.fn(),48 getURL: vi.fn((p: string) => p),49 },50 storage: {51 local: {52 get: vi.fn(async (key?: string | string[]) => {53 if (!key) return storage;54 if (typeof key === "string") return { [key]: storage[key] };55 return key.reduce<Record<string, unknown>>((r, k) => {56 r[k] = storage[k];57 return r;58 }, {});59 }),60 set: vi.fn(async (next: Record<string, unknown>) => {61 Object.assign(storage, next);62 }),63 remove: vi.fn(async (keys: string | string[]) => {64 for (const k of Array.isArray(keys) ? keys : [keys]) delete storage[k];65 }),66 },67 session: {68 get: vi.fn(async () => ({})),69 set: vi.fn(async () => undefined),70 remove: vi.fn(async () => undefined),71 },72 },73 alarms: {74 clear: vi.fn(async () => true),75 create: vi.fn(async () => undefined),76 onAlarm: { addListener: vi.fn() },77 },78 idle: {79 setDetectionInterval: vi.fn(),80 onStateChanged: { addListener: vi.fn() },81 },82 scripting: { executeScript: vi.fn(async () => []) },83 tabs: { query: vi.fn(async () => []) },84 commands: { onCommand: { addListener: vi.fn() } },85 webAuthenticationProxy: {86 onCreateRequest: { addListener: vi.fn() },87 onGetRequest: { addListener: vi.fn() },88 onIsUvpaaRequest: { addListener: vi.fn() },89 onRequestCanceled: { addListener: vi.fn() },90 attach: vi.fn(async () => undefined),91 completeCreateRequest: vi.fn(async () => undefined),92 completeGetRequest: vi.fn(async () => undefined),93 completeIsUvpaaRequest: vi.fn(async () => undefined),94 },95 } as unknown as typeof chrome;9697 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;98 vi.stubGlobal("__PEACH_E2E_PASSKEYS__", false);99 vi.stubGlobal("fetch", vi.fn(async () => {100 throw new Error("network disabled in tests");101 }));102 });103104 async function setupUnlockedVaultWithLockOnClose() {105 const vault = await import("./vault");106 await vault.createVault("correct horse battery staple");107 await vault.updateSettings({ lockOnClose: true });108 expect(vault.isUnlocked()).toBe(true);109 return vault;110 }111112 it("locks the vault when a nonce'd popup-<uuid> port disconnects", async () => {113 await import("./entry"); 114 const vault = await setupUnlockedVaultWithLockOnClose();115 expect(connectListeners).toHaveLength(1);116117 const port = makePort(`popup-${"a1b2c3"}`);118 connectListeners[0](port);119 await port.fireDisconnect();120121 expect(vault.isUnlocked()).toBe(false);122 });123124 it("ignores a port with an unrelated name — no lock", async () => {125 await import("./entry");126 const vault = await setupUnlockedVaultWithLockOnClose();127128 const port = makePort("evil");129 connectListeners[0](port);130 131 await port.fireDisconnect();132133 expect(vault.isUnlocked()).toBe(true);134 });135});