import { describe, it, expect, beforeEach } from 'vitest'; import { vi } from 'vitest'; import type { Message, MessageResponse } from '../shared/types'; type StorageShape = Record; type MessageListener = ( msg: unknown, sender: chrome.runtime.MessageSender, sendResponse: (response: MessageResponse) => void ) => boolean; describe('hasPaired flag', () => { let storage: StorageShape; function installChromeMock(s: StorageShape): void { const chromeMock = { storage: { local: { get: vi.fn(async (key?: string | string[]) => { if (!key) return s; if (typeof key === 'string') return { [key]: s[key] }; return key.reduce>((acc, k) => { acc[k] = s[k]; return acc; }, {}); }), set: vi.fn(async (next: Record) => { Object.assign(s, next); }), remove: vi.fn(async (keys: string | string[]) => { const list = Array.isArray(keys) ? keys : [keys]; for (const key of list) delete s[key]; }), }, }, } as unknown as typeof chrome; (globalThis as { chrome?: typeof chrome }).chrome = chromeMock; } beforeEach(async () => { vi.resetModules(); storage = {}; installChromeMock(storage); }); async function clearStorage(): Promise { for (const key of Object.keys(storage)) delete storage[key]; } it('getHasPaired returns false when never paired', async () => { await clearStorage(); const { getHasPaired } = await import('./sync-storage'); const result = await getHasPaired(); expect(result).toBe(false); }); it('setHasPaired persists across calls', async () => { const { setHasPaired, getHasPaired } = await import('./sync-storage'); await setHasPaired(); const result = await getHasPaired(); expect(result).toBe(true); }); it('hasPaired remains true after multiple calls to setHasPaired', async () => { const { setHasPaired, getHasPaired } = await import('./sync-storage'); await setHasPaired(); await setHasPaired(); // idempotent const result = await getHasPaired(); expect(result).toBe(true); }); }); describe('PAIRING_COMPLETED handler', () => { let storage: StorageShape; let sessionStorage: StorageShape; let listener: MessageListener; beforeEach(async () => { vi.resetModules(); vi.useRealTimers(); storage = { peach_settings: { breachMonitoring: false } }; sessionStorage = {}; vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, status: 200, text: async () => '' }))); vi.stubGlobal('__PEACH_E2E_PASSKEYS__', false); const chromeMock = { runtime: { id: 'peach-extension', onMessage: { addListener: vi.fn((fn: MessageListener) => { listener = fn; }) }, sendMessage: vi.fn(async () => ({ ok: true })), }, tabs: { query: vi.fn((_, cb?: (tabs: unknown[]) => void) => { if (typeof cb === 'function') { cb([{ id: 1 }]); return; } return Promise.resolve([{ id: 1 }]); }), sendMessage: vi.fn(async () => ({ ok: true })), }, storage: { local: { get: vi.fn(async (key?: string | string[]) => { if (!key) return storage; if (typeof key === 'string') return { [key]: storage[key] }; return key.reduce>((acc, k) => { acc[k] = storage[k]; return acc; }, {}); }), set: vi.fn(async (next: Record) => { Object.assign(storage, next); }), remove: vi.fn(async (keys: string | string[]) => { const list = Array.isArray(keys) ? keys : [keys]; for (const key of list) delete storage[key]; }), }, session: { get: vi.fn(async (key?: string | string[]) => { if (!key) return sessionStorage; if (typeof key === 'string') return { [key]: sessionStorage[key] }; return key.reduce>((acc, k) => { acc[k] = sessionStorage[k]; return acc; }, {}); }), set: vi.fn(async (next: Record) => { Object.assign(sessionStorage, next); }), remove: vi.fn(async (keys: string | string[]) => { const list = Array.isArray(keys) ? keys : [keys]; for (const key of list) delete sessionStorage[key]; }), clear: vi.fn(async () => { for (const k of Object.keys(sessionStorage)) delete sessionStorage[k]; }), }, }, alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn() }, }, idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() }, }, scripting: { executeScript: vi.fn(async () => []) }, } as unknown as typeof chrome; (globalThis as { chrome?: typeof chrome }).chrome = chromeMock; const messages = await import('./messages'); messages.setupMessageHandler(); const vault = await import('./vault'); await vault.ensureSettingsLoaded(); }); async function dispatch(message: Message): Promise { return new Promise((resolve) => { const keepOpen = listener(message, { id: 'peach-extension' }, resolve); expect(keepOpen).toBe(true); }); } it('flag is set when PAIRING_COMPLETED fires', async () => { await dispatch({ type: 'PAIRING_COMPLETED', payload: { deviceName: 'iPhone', publicKeyHex: 'abc123', deviceIp: '192.168.1.50', }, }); const { getHasPaired } = await import('./sync-storage'); const paired = await getHasPaired(); expect(paired).toBe(true); }); it('PAIRING_COMPLETED stores device with lastKnownIp and deviceName', async () => { await dispatch({ type: 'PAIRING_COMPLETED', payload: { deviceName: 'iPhone', publicKeyHex: 'abc123', deviceIp: '192.168.1.50', }, }); const { getPairedDevices } = await import('./sync-storage'); const devices = await getPairedDevices(); expect(devices[0].lastKnownIp).toBe('192.168.1.50'); expect(devices[0].displayName).toBe('iPhone'); }); });