1234567891011121314import { 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 }> = [];252627function squashed(view: { container: HTMLElement }): string {28 return (view.container.textContent ?? "").replace(/\s+/g, "");29}30313233const 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 91 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});