Entry.totp-params.test.tsx

4.5 KB · raw

1// @vitest-environment jsdom2//3// PB-26-114: the popup generated TOTP codes from the RFC defaults (SHA-1 / 64// digits / 30 s) regardless of what the entry actually stored. The background5// fill path already honoured the entry's parameters under an explicit comment,6// and the import layer deliberately preserves non-default parameters from7// otpauth:// URIs — so an entry imported from Bitwarden or Proton with8// algorithm=SHA256, digits=8, or a 60 s period displayed and copied the WRONG9// code from the popup while autofill generated the right one.10//11// Both popup call sites (the entry view's live code, and the copy_totp keyboard12// action) must derive their code from the same stored parameters.1314import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";1516const { mockSendMessage } = vi.hoisted(() => ({ mockSendMessage: vi.fn() }));17vi.mock("../../shared/bridge", () => ({ sendMessage: mockSendMessage }));1819import { generateTOTP } from "../../crypto/totp";20import type { VaultEntry } from "../../shared/types";21import { EntryView } from "./Entry";22import { flushView, renderView } from "./view-test-utils";2324let mounted: Array<{ dispose: () => void }> = [];2526/** The view groups the digits for readability ("824 492"), so compare squashed. */27function squashed(view: { container: HTMLElement }): string {28  return (view.container.textContent ?? "").replace(/\s+/g, "");29}3031// A seed that is stable across the default and non-default parameter sets, so32// the two calls actually differ rather than coincidentally agreeing.33const SEED = "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP";3435function totpEntry(overrides: Partial<VaultEntry> = {}): VaultEntry {36  return {37    id: "entry-1",38    type: "login",39    title: "Vault",40    username: "june",41    password: "pw",42    url: "https://example.com",43    totp: SEED,44    totpAlgorithm: "SHA256",45    totpDigits: 8,46    totpPeriod: 60,47    createdAt: 1,48    modifiedAt: 1,49    ...overrides,50  } as VaultEntry;51}5253beforeEach(() => {54  mounted = [];55  mockSendMessage.mockReset();56  mockSendMessage.mockImplementation(() => Promise.resolve({ ok: true, data: null }));57  (globalThis as unknown as { chrome: unknown }).chrome = {58    runtime: {59      onMessage: { addListener: () => undefined, removeListener: () => undefined },60      getURL: (path: string) => `chrome-extension://test/${path}`,61    },62    storage: {63      local: { get: () => Promise.resolve({}), set: () => Promise.resolve() },64      session: { get: () => Promise.resolve({}), set: () => Promise.resolve() },65    },66  };67});6869afterEach(() => {70  for (const view of mounted) view.dispose();71  mounted = [];72  vi.restoreAllMocks();73});7475describe("PB-26-114: popup TOTP honours the entry's stored parameters", () => {76  it("the entry view shows the code for SHA-256 / 8 digits / 60 s", async () => {77    const view = renderView(() => (78      <EntryView79        entry={totpEntry()}80        currentTabUrl=""81        onDelete={() => undefined}82        onFieldSaved={() => undefined}83      />84    ));85    mounted.push(view);86    await flushView();8788    const expected = generateTOTP(SEED, "SHA256", 8, 60);89    const withDefaults = generateTOTP(SEED);90    // Guard the premise: the two calls must not agree, or the test proves91    // nothing about which one the UI used.92    expect(expected.code).not.toBe(withDefaults.code);93    expect(expected.code).toHaveLength(8);9495    expect(squashed(view)).toContain(expected.code);96    expect(squashed(view)).not.toContain(withDefaults.code);97  });9899  it("an entry with RFC-default parameters still shows the default code", async () => {100    const view = renderView(() => (101      <EntryView102        entry={totpEntry({ totpAlgorithm: undefined, totpDigits: undefined, totpPeriod: undefined })}103        currentTabUrl=""104        onDelete={() => undefined}105        onFieldSaved={() => undefined}106      />107    ));108    mounted.push(view);109    await flushView();110111    const expected = generateTOTP(SEED);112    expect(squashed(view)).toContain(expected.code);113  });114115  it("a 6-digit SHA-512 entry is generated with its own parameters", async () => {116    const view = renderView(() => (117      <EntryView118        entry={totpEntry({ totpAlgorithm: "SHA512", totpDigits: 6, totpPeriod: 30 })}119        currentTabUrl=""120        onDelete={() => undefined}121        onFieldSaved={() => undefined}122      />123    ));124    mounted.push(view);125    await flushView();126127    const expected = generateTOTP(SEED, "SHA512", 6, 30);128    expect(expected.code).not.toBe(generateTOTP(SEED).code);129    expect(squashed(view)).toContain(expected.code);130  });131});