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('sync screen handlers', () => { 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); }); } function storageSet(key: string, value: unknown): void { storage[key] = value; } describe('GET_PAIRED_DEVICES handler', () => { it('returns empty array when no devices paired', async () => { const result = await dispatch({ type: 'GET_PAIRED_DEVICES' }); expect(result.ok).toBe(true); const data = (result as { data: { devices: unknown[] } }).data; expect(data.devices).toEqual([]); }); it('returns paired devices list', async () => { storage['peach_paired_devices'] = [ { deviceId: 'abc', displayName: 'MacBook', x25519PublicKey: 'abc', pairedAt: Date.now(), lastKnownIp: '192.168.1.1', }, ]; const result = await dispatch({ type: 'GET_PAIRED_DEVICES' }); const data = (result as { data: { devices: Array> } }).data; expect(data.devices.length).toBe(1); expect(data.devices[0].displayName).toBe('MacBook'); }); it('does not expose private keys in paired devices', async () => { storage['peach_paired_devices'] = [ { deviceId: 'abc', displayName: 'MacBook', x25519PublicKey: 'abc', pairedAt: Date.now(), lastKnownIp: '192.168.1.1', }, ]; const result = await dispatch({ type: 'GET_PAIRED_DEVICES' }); const data = (result as { data: { devices: Array> } }).data; expect(data.devices[0].privateKey).toBeUndefined(); }); }); describe('GET_SYNC_STATUS handler', () => { it('returns never status when never synced', async () => { const result = await dispatch({ type: 'GET_SYNC_STATUS' }); expect(result.ok).toBe(true); const data = (result as { data: { status: string; lastSyncAt: unknown } }).data; expect(data.status).toBe('never'); expect(data.lastSyncAt).toBeNull(); }); it('returns up_to_date after successful sync', async () => { storageSet('peach_last_sync_at', String(Date.now())); const result = await dispatch({ type: 'GET_SYNC_STATUS' }); const data = (result as { data: { status: string; lastSyncAt: unknown } }).data; expect(data.status).toBe('up_to_date'); expect(data.lastSyncAt).toBeTruthy(); }); }); describe('RESET_SYNC_IDENTITY handler', () => { it('is Pro-gated', async () => { const result = await dispatch({ type: 'RESET_SYNC_IDENTITY' }); expect(result.ok).toBe(false); expect((result as { error: string }).error).toMatch(/pro_required/); }); it('clears paired devices on reset for Pro users', async () => { storage['peach_paired_devices'] = [ { deviceId: 'a', displayName: 'iPhone', x25519PublicKey: 'pk-a', pairedAt: 1 }, { deviceId: 'b', displayName: 'MacBook', x25519PublicKey: 'pk-b', pairedAt: 2 }, ]; const license = await import('./license'); vi.spyOn(license, 'isProUser').mockResolvedValue(true); const result = await dispatch({ type: 'RESET_SYNC_IDENTITY' }); expect(result.ok).toBe(true); expect((result as { data: { removedDevices: number } }).data?.removedDevices).toBe(2); expect(storage['peach_paired_devices']).toEqual([]); }); }); });