1import { describe, it, expect, beforeEach } from 'vitest';2import { vi } from 'vitest';34import 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('sync screen handlers', () => {14 let storage: StorageShape;15 let sessionStorage: StorageShape;16 let listener: MessageListener;1718 beforeEach(async () => {19 vi.resetModules();20 vi.useRealTimers();21 storage = { peach_settings: { breachMonitoring: false } };22 sessionStorage = {};2324 vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, status: 200, text: async () => '' })));25 vi.stubGlobal('__PEACH_E2E_PASSKEYS__', false);2627 const chromeMock = {28 runtime: {29 id: 'peach-extension',30 onMessage: { addListener: vi.fn((fn: MessageListener) => { listener = fn; }) },31 sendMessage: vi.fn(async () => ({ ok: true })),32 },33 tabs: {34 query: vi.fn((_, cb?: (tabs: unknown[]) => void) => {35 if (typeof cb === 'function') { cb([{ id: 1 }]); return; }36 return Promise.resolve([{ id: 1 }]);37 }),38 sendMessage: vi.fn(async () => ({ ok: true })),39 },40 storage: {41 local: {42 get: vi.fn(async (key?: string | string[]) => {43 if (!key) return storage;44 if (typeof key === 'string') return { [key]: storage[key] };45 return key.reduce<Record<string, unknown>>((acc, k) => { acc[k] = storage[k]; return acc; }, {});46 }),47 set: vi.fn(async (next: Record<string, unknown>) => { Object.assign(storage, next); }),48 remove: vi.fn(async (keys: string | string[]) => {49 const list = Array.isArray(keys) ? keys : [keys];50 for (const key of list) delete storage[key];51 }),52 },53 session: {54 get: vi.fn(async (key?: string | string[]) => {55 if (!key) return sessionStorage;56 if (typeof key === 'string') return { [key]: sessionStorage[key] };57 return key.reduce<Record<string, unknown>>((acc, k) => { acc[k] = sessionStorage[k]; return acc; }, {});58 }),59 set: vi.fn(async (next: Record<string, unknown>) => { Object.assign(sessionStorage, next); }),60 remove: vi.fn(async (keys: string | string[]) => {61 const list = Array.isArray(keys) ? keys : [keys];62 for (const key of list) delete sessionStorage[key];63 }),64 clear: vi.fn(async () => { for (const k of Object.keys(sessionStorage)) delete sessionStorage[k]; }),65 },66 },67 alarms: {68 clear: vi.fn(), create: vi.fn(),69 onAlarm: { addListener: vi.fn() },70 },71 idle: {72 setDetectionInterval: vi.fn(),73 onStateChanged: { addListener: vi.fn() },74 },75 scripting: { executeScript: vi.fn(async () => []) },76 } as unknown as typeof chrome;7778 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;7980 const messages = await import('./messages');81 messages.setupMessageHandler();82 const vault = await import('./vault');83 await vault.ensureSettingsLoaded();84 });8586 async function dispatch(message: Message): Promise<MessageResponse> {87 return new Promise<MessageResponse>((resolve) => {88 const keepOpen = listener(message, { id: 'peach-extension' }, resolve);89 expect(keepOpen).toBe(true);90 });91 }9293 function storageSet(key: string, value: unknown): void {94 storage[key] = value;95 }9697 describe('GET_PAIRED_DEVICES handler', () => {98 it('returns empty array when no devices paired', async () => {99 const result = await dispatch({ type: 'GET_PAIRED_DEVICES' });100 expect(result.ok).toBe(true);101 const data = (result as { data: { devices: unknown[] } }).data;102 expect(data.devices).toEqual([]);103 });104105 it('returns paired devices list', async () => {106 storage['peach_paired_devices'] = [107 {108 deviceId: 'abc',109 displayName: 'MacBook',110 x25519PublicKey: 'abc',111 pairedAt: Date.now(),112 lastKnownIp: '192.168.1.1',113 },114 ];115 const result = await dispatch({ type: 'GET_PAIRED_DEVICES' });116 const data = (result as { data: { devices: Array<Record<string, unknown>> } }).data;117 expect(data.devices.length).toBe(1);118 expect(data.devices[0].displayName).toBe('MacBook');119 });120121 it('does not expose private keys in paired devices', async () => {122 storage['peach_paired_devices'] = [123 {124 deviceId: 'abc',125 displayName: 'MacBook',126 x25519PublicKey: 'abc',127 pairedAt: Date.now(),128 lastKnownIp: '192.168.1.1',129 },130 ];131 const result = await dispatch({ type: 'GET_PAIRED_DEVICES' });132 const data = (result as { data: { devices: Array<Record<string, unknown>> } }).data;133 expect(data.devices[0].privateKey).toBeUndefined();134 });135 });136137 describe('GET_SYNC_STATUS handler', () => {138 it('returns never status when never synced', async () => {139 const result = await dispatch({ type: 'GET_SYNC_STATUS' });140 expect(result.ok).toBe(true);141 const data = (result as { data: { status: string; lastSyncAt: unknown } }).data;142 expect(data.status).toBe('never');143 expect(data.lastSyncAt).toBeNull();144 });145146 it('returns up_to_date after successful sync', async () => {147 storageSet('peach_last_sync_at', String(Date.now()));148 const result = await dispatch({ type: 'GET_SYNC_STATUS' });149 const data = (result as { data: { status: string; lastSyncAt: unknown } }).data;150 expect(data.status).toBe('up_to_date');151 expect(data.lastSyncAt).toBeTruthy();152 });153 });154155 describe('RESET_SYNC_IDENTITY handler', () => {156 it('is Pro-gated', async () => {157 const result = await dispatch({ type: 'RESET_SYNC_IDENTITY' });158 expect(result.ok).toBe(false);159 expect((result as { error: string }).error).toMatch(/pro_required/);160 });161162 it('clears paired devices on reset for Pro users', async () => {163 storage['peach_paired_devices'] = [164 { deviceId: 'a', displayName: 'iPhone', x25519PublicKey: 'pk-a', pairedAt: 1 },165 { deviceId: 'b', displayName: 'MacBook', x25519PublicKey: 'pk-b', pairedAt: 2 },166 ];167 const license = await import('./license');168 vi.spyOn(license, 'isProUser').mockResolvedValue(true);169 const result = await dispatch({ type: 'RESET_SYNC_IDENTITY' });170 expect(result.ok).toBe(true);171 expect((result as { data: { removedDevices: number } }).data?.removedDevices).toBe(2);172 expect(storage['peach_paired_devices']).toEqual([]);173 });174 });175});