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 },79 session: {80 get: vi.fn(async (key?: string | string[]) => {81 if (!key) return sessionStorage;82 if (typeof key === "string") {83 return { [key]: sessionStorage[key] };84 }85 return key.reduce<Record<string, unknown>>((result, entry) => {86 result[entry] = sessionStorage[entry];87 return result;88 }, {});89 }),90 set: vi.fn(async (next: Record<string, unknown>) => {91 Object.assign(sessionStorage, next);92 }),93 remove: vi.fn(async (keys: string | string[]) => {94 const list = Array.isArray(keys) ? keys : [keys];95 for (const key of list) {96 delete sessionStorage[key];97 }98 }),99 clear: vi.fn(async () => {100 for (const key of Object.keys(sessionStorage)) delete sessionStorage[key];101 }),102 },103 },104 alarms: {105 clear: vi.fn(),106 create: vi.fn(),107 onAlarm: { addListener: vi.fn() },108 },109 idle: {110 setDetectionInterval: vi.fn(),111 onStateChanged: { addListener: vi.fn() },112 },113 scripting: {114 executeScript: vi.fn(async () => []),115 },116 } as unknown as typeof chrome;117118 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;119120 const messages = await import("./messages");121 messages.setupMessageHandler();122 const vault = await import("./vault");123 await vault.ensureSettingsLoaded();124 });125126 async function dispatch(127 message: Message,128 sender: chrome.runtime.MessageSender = { id: "peach-extension" }129 ): Promise<MessageResponse> {130 return await new Promise<MessageResponse>((resolve) => {131 const keepChannelOpen = listener(message, sender, (response) => resolve(response));132 expect(keepChannelOpen).toBe(true);133 });134 }135136 it("GET_ENTRIES when unlocked returns the entry array", async () => {137 const vault = await import("./vault");138 await vault.createVault("correct-pass-12345");139 await vault.addEntry({140 title: "GitHub",141 username: "june",142 password: "secret123",143 url: "https://github.com",144 });145146 await expect(dispatch({ type: "GET_ENTRIES" })).resolves.toMatchObject({147 ok: true,148 data: [149 expect.objectContaining({150 title: "GitHub",151 username: "june",152 }),153 ],154 });155 });156157 it("GET_ENTRIES when locked returns a vault locked error", async () => {158 const vault = await import("./vault");159 await vault.createVault("correct-pass-12345");160 vault.lock();161162 await expect(dispatch({ type: "GET_ENTRIES" })).resolves.toMatchObject({163 ok: false,164 error: "vault locked",165 });166 });167168 it("ADD_ENTRY with a valid payload returns the created entry", async () => {169 const vault = await import("./vault");170 await vault.createVault("correct-pass-12345");171172 const response = await dispatch({173 type: "ADD_ENTRY",174 payload: {175 title: "GitHub",176 username: "june",177 password: "secret123",178 url: "https://github.com",179 },180 });181182 expect(response).toMatchObject({183 ok: true,184 data: expect.objectContaining({185 id: expect.any(String),186 title: "GitHub",187 }),188 });189 expect(vault.getEntries()).toHaveLength(1);190 });191192 it("ADD_ENTRY when locked returns a vault locked error", async () => {193 const vault = await import("./vault");194 await vault.createVault("correct-pass-12345");195 vault.lock();196197 await expect(198 dispatch({199 type: "ADD_ENTRY",200 payload: {201 title: "GitHub",202 username: "june",203 password: "secret123",204 url: "https://github.com",205 },206 })207 ).resolves.toMatchObject({ ok: false, error: "vault locked" });208 });209210 it("ADD_ENTRY with a missing password returns a validation error", async () => {211 const vault = await import("./vault");212 await vault.createVault("correct-pass-12345");213214 await expect(215 dispatch({216 type: "ADD_ENTRY",217 payload: {218 title: "GitHub",219 username: "june",220 password: "",221 url: "https://github.com",222 },223 })224 ).resolves.toMatchObject({ ok: false, error: "password or passkey required" });225 });226227 it("UPDATE_ENTRY updates an existing entry and returns ok", async () => {228 const vault = await import("./vault");229 await vault.createVault("correct-pass-12345");230 const entry = await vault.addEntry({231 title: "GitHub",232 username: "june",233 password: "secret123",234 url: "https://github.com",235 });236237 await expect(238 dispatch({239 type: "UPDATE_ENTRY",240 payload: { id: entry.id, patch: { password: "new-secret" } },241 })242 ).resolves.toMatchObject({ ok: true });243244 expect(vault.getEntryById(entry.id)?.password).toBe("new-secret");245 });246247 it("UPDATE_ENTRY with an empty login password returns an error", async () => {248 const vault = await import("./vault");249 await vault.createVault("correct-pass-12345");250 const entry = await vault.addEntry({251 title: "GitHub",252 username: "june",253 password: "secret123",254 url: "https://github.com",255 });256257 await expect(258 dispatch({259 type: "UPDATE_ENTRY",260 payload: { id: entry.id, patch: { password: "" } },261 })262 ).resolves.toMatchObject({ ok: false, error: "password or passkey required" });263 });264265 it("DELETE_ENTRY soft-deletes the target entry", async () => {266 const vault = await import("./vault");267 await vault.createVault("correct-pass-12345");268 const entry = await vault.addEntry({269 title: "GitHub",270 username: "june",271 password: "secret123",272 url: "https://github.com",273 });274275 await expect(276 dispatch({277 type: "DELETE_ENTRY",278 payload: { id: entry.id },279 })280 ).resolves.toMatchObject({ ok: true });281282 expect(vault.getEntries()).toEqual([]);283 expect(vault.getDeletedEntries()).toHaveLength(1);284 });285286 it("DELETE_ENTRY of a missing id returns an error", async () => {287 const vault = await import("./vault");288 await vault.createVault("correct-pass-12345");289290 await expect(291 dispatch({292 type: "DELETE_ENTRY",293 payload: { id: "missing" },294 })295 ).resolves.toMatchObject({ ok: false, error: "entry not found" });296 });297298 it("GET_ENTRY_FOR_FILL returns a safe login entry without privateKeyJwk", async () => {299 const vault = await import("./vault");300 await vault.createVault("correct-pass-12345");301 const entry = await vault.addEntry({302 title: "GitHub",303 username: "june",304 password: "secret123",305 url: "https://github.com",306 });307308 const response = await dispatch({309 type: "GET_ENTRY_FOR_FILL",310 payload: { id: entry.id },311 });312313 expect(response).toMatchObject({314 ok: true,315 data: expect.objectContaining({316 id: entry.id,317 username: "june",318 password: "secret123",319 }),320 });321 expect((response.data as Record<string, unknown>).privateKeyJwk).toBeUndefined();322 });323324 it("GET_ENTRY_FOR_FILL when locked returns an error", async () => {325 const vault = await import("./vault");326 await vault.createVault("correct-pass-12345");327 vault.lock();328329 await expect(330 dispatch({331 type: "GET_ENTRY_FOR_FILL",332 payload: { id: "missing" },333 })334 ).resolves.toMatchObject({ ok: false, error: "vault locked" });335 });336337 it("GET_ENTRY_FOR_FILL rejects passkey-only entries", async () => {338 const vault = await import("./vault");339 await vault.createVault("correct-pass-12345");340 const generated = await generatePasskeyKeypair(-7);341 const entry = await vault.addEntry({342 type: "login",343 title: "WebAuthn.io",344 passkey: {345 rpId: "webauthn.io",346 rpName: "WebAuthn.io",347 credentialId: base64urlEncode(crypto.getRandomValues(new Uint8Array(32))),348 publicKeyCose: base64urlEncode(generated.publicKeyCose),349 privateKeyJwk: generated.privateKeyJwk,350 userHandle: base64urlEncode(crypto.getRandomValues(new Uint8Array(32))),351 userName: "june",352 userDisplayName: "June",353 algorithm: -7,354 signCount: 0,355 aaguid: "AAAAAAAAAAAAAAAAAAAAAA",356 backupEligible: true,357 backupState: false,358 createdAt: Date.now(),359 },360 });361362 await expect(363 dispatch({364 type: "GET_ENTRY_FOR_FILL",365 payload: { id: entry.id },366 })367 ).resolves.toMatchObject({ ok: false, error: "passkey not fillable" });368 });369370 it("AUTOFILL_QUERY returns matched domain metadata without secrets", async () => {371 const vault = await import("./vault");372 await vault.createVault("correct-pass-12345");373 await vault.addEntry({374 title: "GitHub",375 username: "june",376 password: "secret123",377 url: "https://github.com",378 totp: "JBSWY3DPEHPK3PXP",379 });380381 const response = await dispatch({382 type: "AUTOFILL_QUERY",383 payload: { domain: "github.com" },384 });385386 expect(response).toMatchObject({387 ok: true,388 data: [389 {390 id: expect.any(String),391 title: "GitHub",392 username: "june",393 url: "https://github.com",394 hasTotp: true,395 },396 ],397 });398 expect((response.data as Array<Record<string, unknown>>)[0].password).toBeUndefined();399 expect((response.data as Array<Record<string, unknown>>)[0].totp).toBeUndefined();400 });401402 it("AUTOFILL_QUERY includeAll returns only id and title for non-note, non-passkey entries", async () => {403 const vault = await import("./vault");404 await vault.createVault("correct-pass-12345");405 await vault.addEntry({406 title: "GitHub",407 username: "june",408 password: "secret123",409 url: "https://github.com",410 });411 await vault.addEntry({412 type: "note",413 title: "Secure note",414 notes: "hello",415 });416417 const response = await dispatch({418 type: "AUTOFILL_QUERY",419 payload: { domain: "github.com", includeAll: true },420 });421422 expect(response).toMatchObject({423 ok: true,424 data: [{ id: expect.any(String), title: "GitHub" }],425 });426 expect(response.data).toHaveLength(1);427 });428429 it("RESTORE_ENTRIES replace mode restores the provided entries", async () => {430 const vault = await import("./vault");431 await vault.createVault("correct-pass-12345");432 await vault.addEntry({ title: "A", username: "", password: "one", url: "" });433434 const response = await dispatch({435 type: "RESTORE_ENTRIES",436 payload: {437 mode: "replace",438 entries: [439 {440 id: "restored-1",441 type: "login",442 title: "B",443 username: "june",444 password: "two",445 url: "https://b.test",446 createdAt: 1,447 modifiedAt: 1,448 },449 ],450 },451 });452453 expect(response).toMatchObject({ ok: true, data: { added: 1, updated: 0, skipped: 0 } });454 expect(vault.getEntries().map((entry) => entry.id)).toEqual(["restored-1"]);455 });456457 it("RESTORE_ENTRIES replace mode with an empty array returns an error", async () => {458 const vault = await import("./vault");459 await vault.createVault("correct-pass-12345");460461 await expect(462 dispatch({463 type: "RESTORE_ENTRIES",464 payload: { mode: "replace", entries: [] },465 })466 ).resolves.toMatchObject({ ok: false, error: "refuse to replace with empty entry set" });467 });468469 it("CHANGE_PASSWORD responds true for the happy path and false for a wrong current password", async () => {470 const vault = await import("./vault");471 await vault.createVault("correct-pass-12345");472473 await expect(474 dispatch({475 type: "CHANGE_PASSWORD",476 payload: {477 currentPassword: "correct-pass-12345",478 newPassword: "new-pass-67890",479 },480 })481 ).resolves.toMatchObject({ ok: true });482483 vault.lock();484 expect(await vault.unlock("new-pass-67890")).toBe(true);485486 await expect(487 dispatch({488 type: "CHANGE_PASSWORD",489 payload: {490 currentPassword: "wrong-pass",491 newPassword: "another-pass",492 },493 })494 ).resolves.toMatchObject({ ok: false, error: "incorrect current password" });495 });496497 it("UPDATE_SETTINGS reschedules auto-lock using the new minutes value", async () => {498 const vault = await import("./vault");499 await vault.createVault("correct-pass-12345");500501 (502 chrome.alarms.create as unknown as ReturnType<typeof vi.fn>503 ).mockClear();504 (505 chrome.idle.setDetectionInterval as unknown as ReturnType<typeof vi.fn>506 ).mockClear();507508 await expect(509 dispatch({510 type: "UPDATE_SETTINGS",511 payload: { autoLockMinutes: 30 },512 })513 ).resolves.toMatchObject({ ok: true });514515 expect(vault.getSettings().autoLockMinutes).toBe(30);516 expect(chrome.idle.setDetectionInterval).toHaveBeenLastCalledWith(1800);517 expect(chrome.alarms.create).toHaveBeenLastCalledWith("autolock", {518 delayInMinutes: 30,519 });520 });521522 it("CHECK_ALL_BREACHES updates matching entries without corrupting the vault", async () => {523 vi.useFakeTimers();524 const vault = await import("./vault");525 await vault.createVault("correct-pass-12345");526 const entry = await vault.addEntry({527 title: "GitHub",528 username: "june",529 password: "secret123",530 url: "https://github.com",531 });532 const breachSpy = vi.spyOn(vault, "checkBreach").mockResolvedValue(7);533534 const responsePromise = dispatch({ type: "CHECK_ALL_BREACHES" });535 await vi.runAllTimersAsync();536 const response = await responsePromise;537538 expect(response).toMatchObject({ ok: true, data: { [entry.id]: 7 } });539 expect(vault.getEntryById(entry.id)?.breach).toBe(7);540 breachSpy.mockRestore();541 });542543 it("messages from a different extension id are rejected while undefined sender.id is allowed", async () => {544 const vault = await import("./vault");545 await vault.createVault("correct-pass-12345");546547 await expect(dispatch({ type: "GET_ENTRIES" }, { id: "other-extension" })).resolves.toMatchObject({548 ok: false,549 error: "invalid sender",550 });551552 await expect(dispatch({ type: "GET_ENTRIES" }, {})).resolves.toMatchObject({553 ok: true,554 data: [],555 });556 });557558 it("LOCK and UNLOCK messages update vault state correctly", async () => {559 const vault = await import("./vault");560561 await expect(562 dispatch({ type: "CREATE_VAULT", payload: { password: "correct-pass-12345" } })563 ).resolves.toMatchObject({ ok: true });564 expect(vault.isUnlocked()).toBe(true);565566 await expect(dispatch({ type: "LOCK" })).resolves.toMatchObject({ ok: true });567 expect(vault.isUnlocked()).toBe(false);568569 await expect(570 dispatch({ type: "UNLOCK", payload: { password: "wrong-pass" } })571 ).resolves.toMatchObject({ ok: false });572 expect(vault.isUnlocked()).toBe(false);573574 await expect(575 dispatch({ type: "UNLOCK", payload: { password: "correct-pass-12345" } })576 ).resolves.toMatchObject({ ok: true });577 expect(vault.isUnlocked()).toBe(true);578 });579580 it("FIRE_CANARY_PING sends a minimal payload to allowed HTTPS webhooks", async () => {581 const response = await dispatch({582 type: "FIRE_CANARY_PING",583 payload: {584 webhookUrl: "https://example.com/hook",585 entryTitle: "GitHub",586 trigger: "password_copied",587 },588 });589590 expect(response).toMatchObject({ ok: true });591 expect(fetchMock).toHaveBeenCalledWith(592 "https://example.com/hook",593 expect.objectContaining({594 method: "POST",595 headers: { "Content-Type": "application/json" },596 body: expect.any(String),597 })598 );599 const body = JSON.parse(fetchMock.mock.calls[0][1].body as string) as Record<string, unknown>;600 expect(body).toMatchObject({601 entry: "GitHub",602 trigger: "password_copied",603 source: "peach-extension",604 });605 });606607 it("FIRE_CANARY_PING rejects insecure webhook urls", async () => {608 await expect(609 dispatch({610 type: "FIRE_CANARY_PING",611 payload: {612 webhookUrl: "http://example.com/hook",613 entryTitle: "GitHub",614 trigger: "password_copied",615 },616 })617 ).resolves.toMatchObject({ ok: false, error: "invalid webhook url" });618619 expect(fetchMock).not.toHaveBeenCalled();620 });621622 it("SUBMIT_FEEDBACK returns ok:true and skips network when FEEDBACK_ENDPOINT is empty", async () => {623 const response = await dispatch({624 type: "SUBMIT_FEEDBACK",625 payload: {626 category: "Bug report",627 message: "the popup flashes",628 version: "2.1.0",629 browser: "Chrome",630 },631 });632633 expect(response).toMatchObject({ ok: true });634 expect(fetchMock).not.toHaveBeenCalled();635 });636637 it("SUBMIT_FEEDBACK returns ok:false when message is empty whitespace", async () => {638 const response = await dispatch({639 type: "SUBMIT_FEEDBACK",640 payload: {641 category: "Bug report",642 message: " ",643 version: "2.1.0",644 browser: "Chrome",645 },646 });647648 expect(response).toMatchObject({ ok: false, error: "message is empty" });649 expect(fetchMock).not.toHaveBeenCalled();650 });651652 it("GET_IDENTITIES when locked returns vault locked error", async () => {653 const vault = await import("./vault");654 await vault.createVault("correct-pass-12345");655 vault.lock();656 await expect(dispatch({ type: "GET_IDENTITIES" })).resolves.toMatchObject({657 ok: false,658 error: "vault locked",659 });660 });661662 it("GET_IDENTITIES returns emails and usernames derived from login entries", async () => {663 const vault = await import("./vault");664 await vault.createVault("correct-pass-12345");665 await vault.addEntry({ title: "Gmail", username: "[email protected]", password: "pw1", url: "https://gmail.com" });666 await vault.addEntry({ title: "GitHub", username: "[email protected]", password: "pw2", url: "https://github.com" });667 await vault.addEntry({ title: "Slack", username: "juni_patel", password: "pw3", url: "https://slack.com" });668669 const resp = await dispatch({ type: "GET_IDENTITIES" }) as { ok: boolean; data: unknown[] };670 expect(resp.ok).toBe(true);671 const data = resp.data as Array<{ value: string; type: string; useCount: number }>;672 expect(data.find((d) => d.value === "[email protected]")).toMatchObject({673 type: "email",674 useCount: 2,675 });676 expect(data.find((d) => d.value === "juni_patel")).toMatchObject({677 type: "username",678 useCount: 1,679 });680 681 const emailIdx = data.findIndex((d) => d.value === "[email protected]");682 const userIdx = data.findIndex((d) => d.value === "juni_patel");683 expect(emailIdx).toBeLessThan(userIdx);684 });685686 it("GET_IDENTITIES deduplicates correctly — same email across 3 entries appears once with useCount 3", async () => {687 const vault = await import("./vault");688 await vault.createVault("correct-pass-12345");689 for (let i = 0; i < 3; i++) {690 await vault.addEntry({ title: `Site${i}`, username: "[email protected]", password: "pw", url: `https://site${i}.com` });691 }692693 const resp = await dispatch({ type: "GET_IDENTITIES" }) as { ok: boolean; data: unknown[] };694 const data = resp.data as Array<{ value: string; useCount: number }>;695 const hits = data.filter((d) => d.value === "[email protected]");696 expect(hits).toHaveLength(1);697 expect(hits[0]!.useCount).toBe(3);698 });699700 it("START_PEER_DISCOVERY is Pro-gated and rejects free users", async () => {701 const res = await dispatch({ type: "START_PEER_DISCOVERY" });702 expect(res.ok).toBe(false);703 expect((res as { error?: string }).error).toBe("pro_required");704 });705706 it("INITIATE_PAIR_WITH_PEER is Pro-gated and rejects free users", async () => {707 const res = await dispatch({708 type: "INITIATE_PAIR_WITH_PEER",709 payload: {710 peer: {711 deviceId: "p1",712 deviceName: "iPhone",713 host: "192.168.1.10",714 port: 44100,715 x25519PublicKey: "abc",716 },717 },718 });719 expect(res.ok).toBe(false);720 expect((res as { error?: string }).error).toBe("pro_required");721 });722723 it("STOP_PEER_DISCOVERY succeeds even when discovery never started", async () => {724 const res = await dispatch({ type: "STOP_PEER_DISCOVERY" });725 expect(res.ok).toBe(true);726 });727728 it("DEBUG_INJECT_INCOMING_PAIRING + GET returns the request and 60s expiry window", async () => {729 const inject = await dispatch({730 type: "DEBUG_INJECT_INCOMING_PAIRING",731 payload: { deviceName: "iPhone", publicKeyHex: "deadbeef" },732 });733 expect(inject.ok).toBe(true);734735 const get = await dispatch({ type: "GET_INCOMING_PAIRING_REQUEST" });736 expect(get.ok).toBe(true);737 const data = (get as { data?: { deviceName?: string; expiresAt?: number; receivedAt?: number } }).data;738 expect(data?.deviceName).toBe("iPhone");739 expect((data?.expiresAt ?? 0) - (data?.receivedAt ?? 0)).toBe(60_000);740 });741742 it("RESPOND_INCOMING_PAIRING accept clears the request and reports the device name", async () => {743 await dispatch({744 type: "DEBUG_INJECT_INCOMING_PAIRING",745 payload: { deviceName: "MacBook", publicKeyHex: "cafe" },746 });747 const respond = await dispatch({748 type: "RESPOND_INCOMING_PAIRING",749 payload: { accept: true },750 });751 expect(respond.ok).toBe(true);752 expect((respond as { data?: { accepted?: boolean; deviceName?: string } }).data).toEqual({753 accepted: true,754 deviceName: "MacBook",755 });756 const after = await dispatch({ type: "GET_INCOMING_PAIRING_REQUEST" });757 expect((after as { data?: unknown }).data).toBeNull();758 });759760 it("RESPOND_INCOMING_PAIRING with no pending request returns no_pending_request", async () => {761 const res = await dispatch({762 type: "RESPOND_INCOMING_PAIRING",763 payload: { accept: false },764 });765 expect(res.ok).toBe(false);766 expect((res as { error?: string }).error).toBe("no_pending_request");767 });768769 it("RESET_SYNC_IDENTITY is Pro-gated and rejects free users", async () => {770 const res = await dispatch({ type: "RESET_SYNC_IDENTITY" });771 expect(res.ok).toBe(false);772 expect((res as { error?: string }).error).toBe("pro_required");773 });774775 it("RESET_SYNC_IDENTITY clears local paired devices for Pro users", async () => {776 storage["peach_paired_devices"] = [777 { deviceId: "a", displayName: "iPhone", x25519PublicKey: "pk-a", pairedAt: 1 },778 { deviceId: "b", displayName: "MacBook", x25519PublicKey: "pk-b", pairedAt: 2 },779 ];780 const license = await import("./license");781 vi.spyOn(license, "isProUser").mockResolvedValue(true);782 const res = await dispatch({ type: "RESET_SYNC_IDENTITY" });783 expect(res.ok).toBe(true);784 expect((res as { data?: { removedDevices: number } }).data?.removedDevices).toBe(2);785 expect(storage["peach_paired_devices"]).toEqual([]);786 });787788 it("SUBMIT_FEEDBACK does not refresh the auto-lock timer", async () => {789 const vault = await import("./vault");790 await vault.createVault("correct-pass-12345");791 const alarmsCreate = chrome.alarms.create as unknown as ReturnType<typeof vi.fn>;792793 794 alarmsCreate.mockClear();795 await dispatch({ type: "GET_ENTRIES" });796 expect(alarmsCreate).toHaveBeenCalled();797798 799 alarmsCreate.mockClear();800 await dispatch({801 type: "SUBMIT_FEEDBACK",802 payload: { category: "Bug report", message: "hi", version: "2.1.0", browser: "Chrome" },803 });804 expect(alarmsCreate).not.toHaveBeenCalled();805 });806});