1import { beforeEach, describe, expect, it, vi } from "vitest";23import { base64urlEncode, generatePasskeyKeypair } from "./passkey-crypto";4import type { Message, MessageResponse } from "../shared/types";56type StorageShape = Record<string, unknown>;7type MessageListener = (8 msg: unknown,9 sender: chrome.runtime.MessageSender,10 sendResponse: (response: MessageResponse) => void11) => boolean;1213describe("background messages", () => {14 let storage: StorageShape;15 let sessionStorage: StorageShape;16 let listener: MessageListener;17 let fetchMock: ReturnType<typeof vi.fn>;1819 beforeEach(async () => {20 vi.resetModules();21 vi.useRealTimers();22 storage = {23 peach_settings: { breachMonitoring: false },24 };25 sessionStorage = {};2627 fetchMock = vi.fn(async () => ({28 ok: true,29 status: 200,30 text: async () => "",31 }));32 vi.stubGlobal("fetch", fetchMock);33 vi.stubGlobal("__PEACH_E2E_PASSKEYS__", false);3435 const tabs = [{ id: 1 }];36 const chromeMock = {37 runtime: {38 id: "peach-extension",39 onMessage: {40 addListener: vi.fn((fn: MessageListener) => {41 listener = fn;42 }),43 },44 sendMessage: vi.fn(async () => ({ ok: true })),45 },46 tabs: {47 query: vi.fn((_query: unknown, callback?: (tabs: Array<{ id?: number }>) => void) => {48 if (typeof callback === "function") {49 callback(tabs);50 return;51 }52 return Promise.resolve(tabs);53 }),54 sendMessage: vi.fn(async () => ({ ok: true })),55 },56 storage: {57 local: {58 get: vi.fn(async (key?: string | string[]) => {59 if (!key) return storage;60 if (typeof key === "string") {61 return { [key]: storage[key] };62 }6364 return key.reduce<Record<string, unknown>>((result, entry) => {65 result[entry] = storage[entry];66 return result;67 }, {});68 }),69 set: vi.fn(async (next: Record<string, unknown>) => {70 Object.assign(storage, next);71 }),72 remove: vi.fn(async (keys: string | string[]) => {73 const list = Array.isArray(keys) ? keys : [keys];74 for (const key of list) {75 delete storage[key];76 }77 }),78 clear: vi.fn(async () => {79 for (const key of Object.keys(storage)) delete storage[key];80 }),81 },82 session: {83 get: vi.fn(async (key?: string | string[]) => {84 if (!key) return sessionStorage;85 if (typeof key === "string") {86 return { [key]: sessionStorage[key] };87 }88 return key.reduce<Record<string, unknown>>((result, entry) => {89 result[entry] = sessionStorage[entry];90 return result;91 }, {});92 }),93 set: vi.fn(async (next: Record<string, unknown>) => {94 Object.assign(sessionStorage, next);95 }),96 remove: vi.fn(async (keys: string | string[]) => {97 const list = Array.isArray(keys) ? keys : [keys];98 for (const key of list) {99 delete sessionStorage[key];100 }101 }),102 clear: vi.fn(async () => {103 for (const key of Object.keys(sessionStorage)) delete sessionStorage[key];104 }),105 },106 },107 alarms: {108 clear: vi.fn(),109 create: vi.fn(),110 onAlarm: { addListener: vi.fn() },111 },112 idle: {113 setDetectionInterval: vi.fn(),114 onStateChanged: { addListener: vi.fn() },115 },116 scripting: {117 executeScript: vi.fn(async () => []),118 },119 } as unknown as typeof chrome;120121 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;122123 const messages = await import("./messages");124 messages.setupMessageHandler();125 const vault = await import("./vault");126 await vault.ensureSettingsLoaded();127 });128129 async function dispatch(130 message: Message,131 sender: chrome.runtime.MessageSender = { id: "peach-extension" }132 ): Promise<MessageResponse> {133 return await new Promise<MessageResponse>((resolve) => {134 const keepChannelOpen = listener(message, sender, (response) => resolve(response));135 expect(keepChannelOpen).toBe(true);136 });137 }138139 it("GET_ENTRIES when unlocked returns the entry array", async () => {140 const vault = await import("./vault");141 await vault.createVault("correct-pass-12345");142 await vault.addEntry({143 title: "GitHub",144 username: "june",145 password: "secret123",146 url: "https://github.com",147 });148149 await expect(dispatch({ type: "GET_ENTRIES" })).resolves.toMatchObject({150 ok: true,151 data: [152 expect.objectContaining({153 title: "GitHub",154 username: "june",155 }),156 ],157 });158 });159160 it("GET_ENTRIES when locked returns a vault locked error", async () => {161 const vault = await import("./vault");162 await vault.createVault("correct-pass-12345");163 vault.lock();164165 await expect(dispatch({ type: "GET_ENTRIES" })).resolves.toMatchObject({166 ok: false,167 error: "vault_locked",168 });169 });170171 it("ADD_ENTRY with a valid payload returns the created entry", async () => {172 const vault = await import("./vault");173 await vault.createVault("correct-pass-12345");174175 const response = await dispatch({176 type: "ADD_ENTRY",177 payload: {178 title: "GitHub",179 username: "june",180 password: "secret123",181 url: "https://github.com",182 },183 });184185 expect(response).toMatchObject({186 ok: true,187 data: expect.objectContaining({188 id: expect.any(String),189 title: "GitHub",190 }),191 });192 expect(vault.getEntries()).toHaveLength(1);193 });194195 it("ADD_ENTRY when locked returns a vault locked error", async () => {196 const vault = await import("./vault");197 await vault.createVault("correct-pass-12345");198 vault.lock();199200 await expect(201 dispatch({202 type: "ADD_ENTRY",203 payload: {204 title: "GitHub",205 username: "june",206 password: "secret123",207 url: "https://github.com",208 },209 })210 ).resolves.toMatchObject({ ok: false, error: "vault_locked" });211 });212213 it("ADD_ENTRY with no password succeeds for email-only accounts", async () => {214 const vault = await import("./vault");215 await vault.createVault("correct-pass-12345");216217 await expect(218 dispatch({219 type: "ADD_ENTRY",220 payload: {221 title: "GitHub",222 username: "june",223 password: "",224 url: "https://github.com",225 },226 })227 ).resolves.toMatchObject({ ok: true });228 });229230 it("UPDATE_ENTRY updates an existing entry and returns ok", async () => {231 const vault = await import("./vault");232 await vault.createVault("correct-pass-12345");233 const entry = await vault.addEntry({234 title: "GitHub",235 username: "june",236 password: "secret123",237 url: "https://github.com",238 });239240 await expect(241 dispatch({242 type: "UPDATE_ENTRY",243 payload: { id: entry.id, patch: { password: "new-secret" } },244 })245 ).resolves.toMatchObject({ ok: true });246247 expect(vault.getEntryById(entry.id)?.password).toBe("new-secret");248 });249250 it("UPDATE_ENTRY with an empty password clears it for email-only accounts", async () => {251 const vault = await import("./vault");252 await vault.createVault("correct-pass-12345");253 const entry = await vault.addEntry({254 title: "GitHub",255 username: "june",256 password: "secret123",257 url: "https://github.com",258 });259260 await expect(261 dispatch({262 type: "UPDATE_ENTRY",263 payload: { id: entry.id, patch: { password: "" } },264 })265 ).resolves.toMatchObject({ ok: true });266267 expect(vault.getEntryById(entry.id)?.password).toBeUndefined();268 });269270 it("DELETE_ENTRY soft-deletes the target entry", async () => {271 const vault = await import("./vault");272 await vault.createVault("correct-pass-12345");273 const entry = await vault.addEntry({274 title: "GitHub",275 username: "june",276 password: "secret123",277 url: "https://github.com",278 });279280 await expect(281 dispatch({282 type: "DELETE_ENTRY",283 payload: { id: entry.id },284 })285 ).resolves.toMatchObject({ ok: true });286287 expect(vault.getEntries()).toEqual([]);288 expect(vault.getDeletedEntries()).toHaveLength(1);289 });290291 it("DELETE_ENTRY of a missing id returns an error", async () => {292 const vault = await import("./vault");293 await vault.createVault("correct-pass-12345");294295 await expect(296 dispatch({297 type: "DELETE_ENTRY",298 payload: { id: "missing" },299 })300 ).resolves.toMatchObject({ ok: false, error: "entry_not_found" });301 });302303 it("GET_ENTRY_FOR_FILL returns a safe login entry without privateKeyJwk", async () => {304 const vault = await import("./vault");305 await vault.createVault("correct-pass-12345");306 const entry = await vault.addEntry({307 title: "GitHub",308 username: "june",309 password: "secret123",310 url: "https://github.com",311 });312313 const response = await dispatch({314 type: "GET_ENTRY_FOR_FILL",315 payload: { id: entry.id },316 });317318 expect(response).toMatchObject({319 ok: true,320 data: expect.objectContaining({321 id: entry.id,322 username: "june",323 password: "secret123",324 }),325 });326 expect((response.data as Record<string, unknown>).privateKeyJwk).toBeUndefined();327 });328329 it("GET_ENTRY_FOR_FILL when locked returns an error", async () => {330 const vault = await import("./vault");331 await vault.createVault("correct-pass-12345");332 vault.lock();333334 await expect(335 dispatch({336 type: "GET_ENTRY_FOR_FILL",337 payload: { id: "missing" },338 })339 ).resolves.toMatchObject({ ok: false, error: "vault_locked" });340 });341342 it("GET_ENTRY_FOR_FILL rejects passkey-only entries", async () => {343 const vault = await import("./vault");344 await vault.createVault("correct-pass-12345");345 const generated = await generatePasskeyKeypair(-7);346 const entry = await vault.addEntry({347 type: "login",348 title: "WebAuthn.io",349 passkey: {350 rpId: "webauthn.io",351 rpName: "WebAuthn.io",352 credentialId: base64urlEncode(crypto.getRandomValues(new Uint8Array(32))),353 publicKeyCose: base64urlEncode(generated.publicKeyCose),354 privateKeyJwk: generated.privateKeyJwk,355 userHandle: base64urlEncode(crypto.getRandomValues(new Uint8Array(32))),356 userName: "june",357 userDisplayName: "June",358 algorithm: -7,359 signCount: 0,360 aaguid: "AAAAAAAAAAAAAAAAAAAAAA",361 backupEligible: true,362 backupState: false,363 createdAt: Date.now(),364 },365 });366367 await expect(368 dispatch({369 type: "GET_ENTRY_FOR_FILL",370 payload: { id: entry.id },371 })372 ).resolves.toMatchObject({ ok: false, error: "passkey_not_fillable" });373 });374375 it("AUTOFILL_QUERY returns matched domain metadata without secrets", async () => {376 const vault = await import("./vault");377 await vault.createVault("correct-pass-12345");378 await vault.addEntry({379 title: "GitHub",380 username: "june",381 password: "secret123",382 url: "https://github.com",383 totp: "JBSWY3DPEHPK3PXP",384 });385386 const response = await dispatch({387 type: "AUTOFILL_QUERY",388 payload: { domain: "github.com" },389 });390391 expect(response).toMatchObject({392 ok: true,393 data: [394 {395 id: expect.any(String),396 title: "GitHub",397 username: "june",398 url: "https://github.com",399 hasTotp: true,400 },401 ],402 });403 expect((response.data as Array<Record<string, unknown>>)[0].password).toBeUndefined();404 expect((response.data as Array<Record<string, unknown>>)[0].totp).toBeUndefined();405 });406407 it("AUTOFILL_QUERY includeAll returns search-ready summaries for non-note, non-passkey entries", async () => {408 const vault = await import("./vault");409 await vault.createVault("correct-pass-12345");410 await vault.addEntry({411 title: "GitHub",412 username: "june",413 password: "secret123",414 url: "https://github.com",415 });416 await vault.addEntry({417 type: "note",418 title: "Secure note",419 notes: "hello",420 });421422 const response = await dispatch({423 type: "AUTOFILL_QUERY",424 payload: { domain: "github.com", includeAll: true },425 });426427 expect(response).toMatchObject({428 ok: true,429 data: [430 {431 id: expect.any(String),432 title: "GitHub",433 username: "june",434 url: "https://github.com",435 hasPasskey: false,436 },437 ],438 });439 const data = response.data as Array<Record<string, unknown>>;440 expect(data).toHaveLength(1);441 442 expect(data[0]).not.toHaveProperty("password");443 expect(data[0]).not.toHaveProperty("totp");444 });445446 it("AUTOFILL_QUERY kind=totp returns only matching entries with 2FA summaries", async () => {447 const vault = await import("./vault");448 await vault.createVault("correct-pass-12345");449 await vault.addEntry({450 title: "GitHub",451 username: "june",452 password: "secret123",453 url: "https://github.com",454 totp: "JBSWY3DPEHPK3PXP",455 });456 await vault.addEntry({457 title: "No 2FA",458 username: "person",459 password: "secret123",460 url: "https://github.com",461 });462463 const response = await dispatch({464 type: "AUTOFILL_QUERY",465 payload: { domain: "github.com", kind: "totp" },466 });467468 expect(response).toMatchObject({469 ok: true,470 data: [471 {472 id: expect.any(String),473 title: "GitHub",474 username: "june",475 url: "https://github.com",476 hasTotp: true,477 },478 ],479 });480 expect((response.data as Array<Record<string, unknown>>)[0].totp).toBeUndefined();481 expect(response.data).toHaveLength(1);482 });483484 it("RESTORE_ENTRIES replace mode restores the provided entries", async () => {485 const vault = await import("./vault");486 await vault.createVault("correct-pass-12345");487 await vault.addEntry({ title: "A", username: "", password: "one", url: "" });488489 const response = await dispatch({490 type: "RESTORE_ENTRIES",491 payload: {492 mode: "replace",493 entries: [494 {495 id: "restored-1",496 type: "login",497 title: "B",498 username: "june",499 password: "two",500 url: "https://b.test",501 createdAt: 1,502 modifiedAt: 1,503 },504 ],505 },506 });507508 expect(response).toMatchObject({ ok: true, data: { added: 1, updated: 0, skipped: 0 } });509 expect(vault.getEntries().map((entry) => entry.id)).toEqual(["restored-1"]);510 });511512 it("RESTORE_ENTRIES replace mode with an empty array returns an error", async () => {513 const vault = await import("./vault");514 await vault.createVault("correct-pass-12345");515516 await expect(517 dispatch({518 type: "RESTORE_ENTRIES",519 payload: { mode: "replace", entries: [] },520 })521 ).resolves.toMatchObject({ ok: false, error: "refuse to replace with empty entry set" });522 });523524 it("CHANGE_PASSWORD responds true for the happy path and false for a wrong current password", async () => {525 const vault = await import("./vault");526 await vault.createVault("correct-pass-12345");527528 await expect(529 dispatch({530 type: "CHANGE_PASSWORD",531 payload: {532 currentPassword: "correct-pass-12345",533 newPassword: "new-pass-67890",534 },535 })536 ).resolves.toMatchObject({ ok: true });537538 vault.lock();539 expect(await vault.unlock("new-pass-67890")).toBe(true);540541 await expect(542 dispatch({543 type: "CHANGE_PASSWORD",544 payload: {545 currentPassword: "wrong-pass",546 newPassword: "another-pass",547 },548 })549 ).resolves.toMatchObject({ ok: false, error: "incorrect_current_password" });550 });551552 it("UPDATE_SETTINGS reschedules auto-lock using the new minutes value", async () => {553 const vault = await import("./vault");554 await vault.createVault("correct-pass-12345");555556 (557 chrome.alarms.create as unknown as ReturnType<typeof vi.fn>558 ).mockClear();559 (560 chrome.idle.setDetectionInterval as unknown as ReturnType<typeof vi.fn>561 ).mockClear();562563 await expect(564 dispatch({565 type: "UPDATE_SETTINGS",566 payload: { autoLockMinutes: 30 },567 })568 ).resolves.toMatchObject({ ok: true });569570 expect(vault.getSettings().autoLockMinutes).toBe(30);571 expect(chrome.idle.setDetectionInterval).toHaveBeenLastCalledWith(1800);572 expect(chrome.alarms.create).toHaveBeenLastCalledWith("autolock", {573 delayInMinutes: 30,574 });575 });576577 it("CHECK_ALL_BREACHES updates matching entries without corrupting the vault", async () => {578 vi.useFakeTimers();579 const vault = await import("./vault");580 await vault.createVault("correct-pass-12345");581 const entry = await vault.addEntry({582 title: "GitHub",583 username: "june",584 password: "secret123",585 url: "https://github.com",586 });587 const breachSpy = vi.spyOn(vault, "checkBreach").mockResolvedValue(7);588589 const responsePromise = dispatch({ type: "CHECK_ALL_BREACHES" });590 await vi.runAllTimersAsync();591 const response = await responsePromise;592593 expect(response).toMatchObject({ ok: true, data: { [entry.id]: 7 } });594 expect(vault.getEntryById(entry.id)?.breach).toBe(7);595 breachSpy.mockRestore();596 });597598 it("messages from a different extension id are rejected while undefined sender.id is allowed", async () => {599 const vault = await import("./vault");600 await vault.createVault("correct-pass-12345");601602 await expect(dispatch({ type: "GET_ENTRIES" }, { id: "other-extension" })).resolves.toMatchObject({603 ok: false,604 error: "invalid_sender",605 });606607 await expect(dispatch({ type: "GET_ENTRIES" }, {})).resolves.toMatchObject({608 ok: true,609 data: [],610 });611 });612613 it("LOCK and UNLOCK messages update vault state correctly", async () => {614 const vault = await import("./vault");615616 await expect(617 dispatch({ type: "CREATE_VAULT", payload: { password: "correct-pass-12345" } })618 ).resolves.toMatchObject({ ok: true });619 expect(vault.isUnlocked()).toBe(true);620621 await expect(dispatch({ type: "LOCK" })).resolves.toMatchObject({ ok: true });622 expect(vault.isUnlocked()).toBe(false);623624 await expect(625 dispatch({ type: "UNLOCK", payload: { password: "wrong-pass" } })626 ).resolves.toMatchObject({ ok: false });627 expect(vault.isUnlocked()).toBe(false);628629 await expect(630 dispatch({ type: "UNLOCK", payload: { password: "correct-pass-12345" } })631 ).resolves.toMatchObject({ ok: true });632 expect(vault.isUnlocked()).toBe(true);633 });634635 it("FIRE_CANARY_PING sends a minimal payload to allowed HTTPS webhooks", async () => {636 const response = await dispatch({637 type: "FIRE_CANARY_PING",638 payload: {639 webhookUrl: "https://example.com/hook",640 entryTitle: "GitHub",641 trigger: "password_copied",642 },643 });644645 expect(response).toMatchObject({ ok: true });646 expect(fetchMock).toHaveBeenCalledWith(647 "https://example.com/hook",648 expect.objectContaining({649 method: "POST",650 headers: { "Content-Type": "application/json" },651 body: expect.any(String),652 })653 );654 const body = JSON.parse(fetchMock.mock.calls[0][1].body as string) as Record<string, unknown>;655 expect(body).toMatchObject({656 entry: "GitHub",657 trigger: "password_copied",658 source: "peach-extension",659 });660 });661662 it("FIRE_CANARY_PING rejects insecure webhook urls", async () => {663 await expect(664 dispatch({665 type: "FIRE_CANARY_PING",666 payload: {667 webhookUrl: "http://example.com/hook",668 entryTitle: "GitHub",669 trigger: "password_copied",670 },671 })672 ).resolves.toMatchObject({ ok: false, error: "invalid_webhook_url" });673674 expect(fetchMock).not.toHaveBeenCalled();675 });676677 it("SUBMIT_FEEDBACK returns ok:true and skips network when FEEDBACK_ENDPOINT is empty", async () => {678 const response = await dispatch({679 type: "SUBMIT_FEEDBACK",680 payload: {681 category: "Bug report",682 message: "the popup flashes",683 version: "2.1.0",684 browser: "Chrome",685 },686 });687688 expect(response).toMatchObject({ ok: true });689 expect(fetchMock).not.toHaveBeenCalled();690 });691692 it("SUBMIT_FEEDBACK returns ok:false when message is empty whitespace", async () => {693 const response = await dispatch({694 type: "SUBMIT_FEEDBACK",695 payload: {696 category: "Bug report",697 message: " ",698 version: "2.1.0",699 browser: "Chrome",700 },701 });702703 expect(response).toMatchObject({ ok: false, error: "message_empty" });704 expect(fetchMock).not.toHaveBeenCalled();705 });706707 it("GET_IDENTITIES when locked returns vault locked error", async () => {708 const vault = await import("./vault");709 await vault.createVault("correct-pass-12345");710 vault.lock();711 await expect(dispatch({ type: "GET_IDENTITIES" })).resolves.toMatchObject({712 ok: false,713 error: "vault_locked",714 });715 });716717 it("GET_IDENTITIES returns emails and usernames derived from login entries", async () => {718 const vault = await import("./vault");719 await vault.createVault("correct-pass-12345");720 await vault.addEntry({ title: "Gmail", username: "[email protected]", password: "pw1", url: "https://gmail.com" });721 await vault.addEntry({ title: "GitHub", username: "[email protected]", password: "pw2", url: "https://github.com" });722 await vault.addEntry({ title: "Slack", username: "juni_patel", password: "pw3", url: "https://slack.com" });723724 const resp = await dispatch({ type: "GET_IDENTITIES" }) as { ok: boolean; data: unknown[] };725 expect(resp.ok).toBe(true);726 const data = resp.data as Array<{ value: string; type: string; useCount: number }>;727 expect(data.find((d) => d.value === "[email protected]")).toMatchObject({728 type: "email",729 useCount: 2,730 });731 expect(data.find((d) => d.value === "juni_patel")).toMatchObject({732 type: "username",733 useCount: 1,734 });735 736 const emailIdx = data.findIndex((d) => d.value === "[email protected]");737 const userIdx = data.findIndex((d) => d.value === "juni_patel");738 expect(emailIdx).toBeLessThan(userIdx);739 });740741 it("GET_IDENTITIES deduplicates correctly — same email across 3 entries appears once with useCount 3", async () => {742 const vault = await import("./vault");743 await vault.createVault("correct-pass-12345");744 for (let i = 0; i < 3; i++) {745 await vault.addEntry({ title: `Site${i}`, username: "[email protected]", password: "pw", url: `https://site${i}.com` });746 }747748 const resp = await dispatch({ type: "GET_IDENTITIES" }) as { ok: boolean; data: unknown[] };749 const data = resp.data as Array<{ value: string; useCount: number }>;750 const hits = data.filter((d) => d.value === "[email protected]");751 expect(hits).toHaveLength(1);752 expect(hits[0]!.useCount).toBe(3);753 });754755 it("SUBMIT_FEEDBACK does not refresh the auto-lock timer", async () => {756 const vault = await import("./vault");757 await vault.createVault("correct-pass-12345");758 const alarmsCreate = chrome.alarms.create as unknown as ReturnType<typeof vi.fn>;759760 761 alarmsCreate.mockClear();762 await dispatch({ type: "GET_ENTRIES" });763 expect(alarmsCreate).toHaveBeenCalled();764765 766 alarmsCreate.mockClear();767 await dispatch({768 type: "SUBMIT_FEEDBACK",769 payload: { category: "Bug report", message: "hi", version: "2.1.0", browser: "Chrome" },770 });771 expect(alarmsCreate).not.toHaveBeenCalled();772 });773774 it("DESTROY_VAULT preserves the Pro license and celebration flag", async () => {775 const vault = await import("./vault");776 await vault.createVault("correct-pass-12345");777 await vault.addEntry({ title: "Gmail", username: "june", password: "s", url: "https://mail.google.com" });778779 780 storage.peach_license = "PEACH-PRO-TEST-KEY";781 storage.peach_pro_celebrated = true;782783 const res = await dispatch({ type: "DESTROY_VAULT" });784 expect(res).toMatchObject({ ok: true });785786 787 expect(storage.peach_vault_a).toBeUndefined();788 789 expect(storage.peach_license).toBe("PEACH-PRO-TEST-KEY");790 expect(storage.peach_pro_celebrated).toBe(true);791 });792793 it("handleMessage waits for the restore gate before serving (cold-start: no premature onboarding/lock)", async () => {794 const messages = await import("./messages");795 const vault = await import("./vault");796797 let resolveGate: () => void = () => {};798 messages.setRestoreReady(new Promise<void>((r) => { resolveGate = r; }));799800 let settled = false;801 const pending = dispatch({ type: "HAS_VAULT" }).then((r) => { settled = true; return r; });802803 804 805 await new Promise((r) => setTimeout(r, 20));806 expect(settled).toBe(false);807808 809 await vault.createVault("correct-pass-12345");810 resolveGate();811812 await expect(pending).resolves.toMatchObject({ ok: true, data: true });813 });814815 it("CREATE_VAULT rejects an empty master password (audit HIGH — no empty-key vault)", async () => {816 const res = await dispatch({ type: "CREATE_VAULT", payload: { password: "" } });817 expect(res.ok).toBe(false);818 819 const has = await dispatch({ type: "HAS_VAULT" });820 expect((has as { data?: boolean }).data).toBe(false);821 });822823 it("GET_ENTRY_FOR_FILL sends a 6-digit TOTP code, never the seed (SEV-3-C)", async () => {824 const vault = await import("./vault");825 await vault.createVault("correct-pass-12345");826 const e = await vault.addEntry({827 title: "Mail",828 username: "june",829 password: "pw",830 url: "https://mail.test",831 totp: "JBSWY3DPEHPK3PXP",832 });833834 const res = await dispatch({ type: "GET_ENTRY_FOR_FILL", payload: { id: e.id } });835 expect(res.ok).toBe(true);836 const data = res.data as { totp?: string; totpCode?: string; totpRemaining?: number };837 expect(data.totp).toBeUndefined();838 expect(data.totpCode).toMatch(/^\d{6}$/);839 expect(data.totpRemaining).toBeGreaterThan(0);840 expect(data.totpRemaining).toBeLessThanOrEqual(30);841 });842843 it("GET_ENTRY_FOR_EDIT returns the raw TOTP seed for the trusted edit form", async () => {844 const vault = await import("./vault");845 await vault.createVault("correct-pass-12345");846 const e = await vault.addEntry({847 title: "Mail",848 username: "june",849 password: "pw",850 url: "https://mail.test",851 totp: "JBSWY3DPEHPK3PXP",852 });853854 const res = await dispatch({ type: "GET_ENTRY_FOR_EDIT", payload: { id: e.id } });855 expect(res.ok).toBe(true);856 const data = res.data as { totp?: string };857 expect(data.totp).toBe("JBSWY3DPEHPK3PXP");858 });859860 it("DESTROY_VAULT with no license clears storage without resurrecting keys", async () => {861 const vault = await import("./vault");862 await vault.createVault("correct-pass-12345");863864 const res = await dispatch({ type: "DESTROY_VAULT" });865 expect(res).toMatchObject({ ok: true });866867 expect(storage.peach_vault_a).toBeUndefined();868 expect(storage.peach_license).toBeUndefined();869 expect(storage.peach_pro_celebrated).toBeUndefined();870 });871});