entry.lock-port.test.ts

4.9 KB · raw

1// The lock-on-close onConnect listener accepts only our nonce'd2// "popup-<uuid>" port from the extension page and ignores anything else.3import { 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"); // registers the onConnect listener116    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    // The listener returned early, so no onDisconnect handler was registered.147    await port.fireDisconnect();148149    expect(vault.isUnlocked()).toBe(true);150  });151});