sync-info-sheet.test.ts

6.1 KB · raw

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('hasPaired flag', () => {14  let storage: StorageShape;1516  function installChromeMock(s: StorageShape): void {17    const chromeMock = {18      storage: {19        local: {20          get: vi.fn(async (key?: string | string[]) => {21            if (!key) return s;22            if (typeof key === 'string') return { [key]: s[key] };23            return key.reduce<Record<string, unknown>>((acc, k) => { acc[k] = s[k]; return acc; }, {});24          }),25          set: vi.fn(async (next: Record<string, unknown>) => { Object.assign(s, next); }),26          remove: vi.fn(async (keys: string | string[]) => {27            const list = Array.isArray(keys) ? keys : [keys];28            for (const key of list) delete s[key];29          }),30        },31      },32    } as unknown as typeof chrome;33    (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;34  }3536  beforeEach(async () => {37    vi.resetModules();38    storage = {};39    installChromeMock(storage);40  });4142  async function clearStorage(): Promise<void> {43    for (const key of Object.keys(storage)) delete storage[key];44  }4546  it('getHasPaired returns false when never paired', async () => {47    await clearStorage();48    const { getHasPaired } = await import('./sync-storage');49    const result = await getHasPaired();50    expect(result).toBe(false);51  });5253  it('setHasPaired persists across calls', async () => {54    const { setHasPaired, getHasPaired } = await import('./sync-storage');55    await setHasPaired();56    const result = await getHasPaired();57    expect(result).toBe(true);58  });5960  it('hasPaired remains true after multiple calls to setHasPaired', async () => {61    const { setHasPaired, getHasPaired } = await import('./sync-storage');62    await setHasPaired();63    await setHasPaired(); // idempotent64    const result = await getHasPaired();65    expect(result).toBe(true);66  });67});6869describe('PAIRING_COMPLETED handler', () => {70  let storage: StorageShape;71  let sessionStorage: StorageShape;72  let listener: MessageListener;7374  beforeEach(async () => {75    vi.resetModules();76    vi.useRealTimers();77    storage = { peach_settings: { breachMonitoring: false } };78    sessionStorage = {};7980    vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, status: 200, text: async () => '' })));81    vi.stubGlobal('__PEACH_E2E_PASSKEYS__', false);8283    const chromeMock = {84      runtime: {85        id: 'peach-extension',86        onMessage: { addListener: vi.fn((fn: MessageListener) => { listener = fn; }) },87        sendMessage: vi.fn(async () => ({ ok: true })),88      },89      tabs: {90        query: vi.fn((_, cb?: (tabs: unknown[]) => void) => {91          if (typeof cb === 'function') { cb([{ id: 1 }]); return; }92          return Promise.resolve([{ id: 1 }]);93        }),94        sendMessage: vi.fn(async () => ({ ok: true })),95      },96      storage: {97        local: {98          get: vi.fn(async (key?: string | string[]) => {99            if (!key) return storage;100            if (typeof key === 'string') return { [key]: storage[key] };101            return key.reduce<Record<string, unknown>>((acc, k) => { acc[k] = storage[k]; return acc; }, {});102          }),103          set: vi.fn(async (next: Record<string, unknown>) => { Object.assign(storage, next); }),104          remove: vi.fn(async (keys: string | string[]) => {105            const list = Array.isArray(keys) ? keys : [keys];106            for (const key of list) delete storage[key];107          }),108        },109        session: {110          get: vi.fn(async (key?: string | string[]) => {111            if (!key) return sessionStorage;112            if (typeof key === 'string') return { [key]: sessionStorage[key] };113            return key.reduce<Record<string, unknown>>((acc, k) => { acc[k] = sessionStorage[k]; return acc; }, {});114          }),115          set: vi.fn(async (next: Record<string, unknown>) => { Object.assign(sessionStorage, next); }),116          remove: vi.fn(async (keys: string | string[]) => {117            const list = Array.isArray(keys) ? keys : [keys];118            for (const key of list) delete sessionStorage[key];119          }),120          clear: vi.fn(async () => { for (const k of Object.keys(sessionStorage)) delete sessionStorage[k]; }),121        },122      },123      alarms: {124        clear: vi.fn(), create: vi.fn(),125        onAlarm: { addListener: vi.fn() },126      },127      idle: {128        setDetectionInterval: vi.fn(),129        onStateChanged: { addListener: vi.fn() },130      },131      scripting: { executeScript: vi.fn(async () => []) },132    } as unknown as typeof chrome;133134    (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;135136    const messages = await import('./messages');137    messages.setupMessageHandler();138    const vault = await import('./vault');139    await vault.ensureSettingsLoaded();140  });141142  async function dispatch(message: Message): Promise<MessageResponse> {143    return new Promise<MessageResponse>((resolve) => {144      const keepOpen = listener(message, { id: 'peach-extension' }, resolve);145      expect(keepOpen).toBe(true);146    });147  }148149  it('flag is set when PAIRING_COMPLETED fires', async () => {150    await dispatch({151      type: 'PAIRING_COMPLETED',152      payload: {153        deviceName: 'iPhone',154        publicKeyHex: 'abc123',155        deviceIp: '192.168.1.50',156      },157    });158    const { getHasPaired } = await import('./sync-storage');159    const paired = await getHasPaired();160    expect(paired).toBe(true);161  });162163  it('PAIRING_COMPLETED stores device with lastKnownIp and deviceName', async () => {164    await dispatch({165      type: 'PAIRING_COMPLETED',166      payload: {167        deviceName: 'iPhone',168        publicKeyHex: 'abc123',169        deviceIp: '192.168.1.50',170      },171    });172    const { getPairedDevices } = await import('./sync-storage');173    const devices = await getPairedDevices();174    expect(devices[0].lastKnownIp).toBe('192.168.1.50');175    expect(devices[0].displayName).toBe('iPhone');176  });177});