entry.lock-port.test.ts

4.3 KB · raw

1// LV-5: the lock-on-close onConnect listener accepts only our nonce'd2// "popup-<uuid>" port and ignores anything else.3import { 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"); // registers the onConnect listener114    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.each(["popup", "evil"])("ignores the non-nonce port name %s", async (name) => {125    await import("./entry");126    const vault = await setupUnlockedVaultWithLockOnClose();127128    const port = makePort(name);129    connectListeners[0](port);130    // The listener returned early, so no onDisconnect handler was registered.131    await port.fireDisconnect();132133    expect(vault.isUnlocked()).toBe(true);134  });135});