messages.autolock-refresh.test.ts

5.3 KB · raw

1// PB-26-107: passive page traffic must not refresh the auto-lock timer.2//3// Every inbound message used to call touchAutoLock(), which reschedules the4// autolock alarm. The content script fires AUTOFILL_QUERY and ML_PREDICT_BATCH5// on every `focusin` — an event browsers deliver for programmatic6// element.focus() too — so a page running7//   setInterval(() => passwordField.focus(), 1000)8// pinned the timer open for as long as the tab existed. Idle auto-lock is the9// control a shared-machine user relies on, and it silently never fired.10//11// Both passive types are now excluded from the refresh; genuinely user-initiated12// traffic still reschedules it.1314import { beforeEach, describe, expect, it, vi } from "vitest";1516type StorageShape = Record<string, unknown>;1718function makeChromeStorageMock() {19  const storage: StorageShape = {};20  const chrome = {21    storage: {22      local: {23        get: vi.fn(async (key?: string | string[]) => {24          if (!key) return { ...storage };25          const keys = Array.isArray(key) ? key : [key];26          return keys.reduce<StorageShape>((acc, k) => { acc[k] = storage[k]; return acc; }, {});27        }),28        set: vi.fn(async (next: StorageShape) => { Object.assign(storage, next); }),29        remove: vi.fn(async () => {}),30      },31      session: {32        get: vi.fn(async () => ({})),33        set: vi.fn(async () => {}),34        remove: vi.fn(async () => {}),35      },36    },37    alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn() } },38    idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() } },39    runtime: {40      id: "test-extension-id",41      getURL: vi.fn((path: string) => `chrome-extension://test-extension-id/${path}`),42    },43  } as unknown as typeof globalThis.chrome;44  return { chrome };45}4647const pageSender = {48  tab: { url: "https://example.com/login" },49  url: "https://example.com/login",50} as unknown as chrome.runtime.MessageSender;5152describe("PB-26-107: passive page traffic does not refresh auto-lock", () => {53  let vault: typeof import("./vault");54  let messages: typeof import("./messages");5556  beforeEach(async () => {57    vi.resetModules();58    (globalThis as Record<string, unknown>).chrome = makeChromeStorageMock().chrome;59    vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("no network"); }));60    vi.stubGlobal("__PEACH_E2E_PASSKEYS__", false);6162    vault = await import("./vault");63    messages = await import("./messages");64    await vault.createVault("test-autolock-pass");65  });6667  function alarmCreations(): number {68    return (chrome.alarms.create as unknown as ReturnType<typeof vi.fn>).mock.calls.length;69  }7071  it("content-script GET_SETTINGS does not reschedule the lock alarm", async () => {72    (chrome.alarms.create as unknown as ReturnType<typeof vi.fn>).mockClear();7374    await messages.handleMessage({ type: "GET_SETTINGS" }, pageSender);7576    expect(alarmCreations()).toBe(0);77  });7879  it("repeated AUTOFILL_QUERY does not reschedule the lock alarm", async () => {80    (chrome.alarms.create as unknown as ReturnType<typeof vi.fn>).mockClear();8182    for (let i = 0; i < 50; i += 1) {83      await messages.handleMessage(84        { type: "AUTOFILL_QUERY", payload: { domain: "example.com", kind: "login" } },85        pageSender,86      );87    }8889    expect(alarmCreations()).toBe(0);90  });9192  it("repeated ML_PREDICT_BATCH does not reschedule the lock alarm", async () => {93    (chrome.alarms.create as unknown as ReturnType<typeof vi.fn>).mockClear();9495    for (let i = 0; i < 50; i += 1) {96      await messages.handleMessage(97        { type: "ML_PREDICT_BATCH", payload: { fields: [] } },98        pageSender,99      );100    }101102    expect(alarmCreations()).toBe(0);103  });104105  it("page TOUCH_AUTO_LOCK is rejected without rescheduling the lock alarm", async () => {106    (chrome.alarms.create as unknown as ReturnType<typeof vi.fn>).mockClear();107108    await expect(109      messages.handleMessage({ type: "TOUCH_AUTO_LOCK", payload: {} }, pageSender),110    ).resolves.toMatchObject({ ok: false, error: "invalid_sender" });111    expect(alarmCreations()).toBe(0);112  });113114  it("an extension-page TOUCH_AUTO_LOCK reschedules the lock alarm", async () => {115    (chrome.alarms.create as unknown as ReturnType<typeof vi.fn>).mockClear();116117    await messages.handleMessage(118      { type: "TOUCH_AUTO_LOCK", payload: {} },119      { url: "chrome-extension://test-extension-id/popup.html" },120    );121122    expect(alarmCreations()).toBe(1);123  });124125  // A focus loop interleaved with genuine activity must not be able to build a126  // longer session than the activity alone would.127  it("a focus loop between real actions adds nothing to the deadline", async () => {128    (chrome.alarms.create as unknown as ReturnType<typeof vi.fn>).mockClear();129130    for (let i = 0; i < 20; i += 1) {131      await messages.handleMessage(132        { type: "AUTOFILL_QUERY", payload: { domain: "example.com", kind: "login" } },133        pageSender,134      );135    }136    await messages.handleMessage(137      { type: "TOUCH_AUTO_LOCK", payload: {} },138      { url: "chrome-extension://test-extension-id/popup.html" },139    );140    for (let i = 0; i < 20; i += 1) {141      await messages.handleMessage(142        { type: "AUTOFILL_QUERY", payload: { domain: "example.com", kind: "login" } },143        pageSender,144      );145    }146147    expect(alarmCreations()).toBe(1);148  });149});