From ecb137a120ab4b4928a47bea791b9471a734c05d Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Wed, 29 Apr 2026 23:33:16 -0400 Subject: [PATCH] test(ext/sw): unit tests for parse + commit handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mocks the WASM bridge and vault helpers. Covers: - parse_lastpass_csv pass-through + error surface - commit happy path: 3 items → 3 encryptAndWriteItem + 1 encryptAndWriteManifest call - vault_locked + empty-items rejections - IDs re-minted by SW so manifest keys match the new IDs Co-Authored-By: Claude Opus 4.7 --- .../service-worker/__tests__/import.test.ts | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 extension/src/service-worker/__tests__/import.test.ts diff --git a/extension/src/service-worker/__tests__/import.test.ts b/extension/src/service-worker/__tests__/import.test.ts new file mode 100644 index 0000000..3b6b418 --- /dev/null +++ b/extension/src/service-worker/__tests__/import.test.ts @@ -0,0 +1,178 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const fakeHost = { + readFile: vi.fn(), + writeFile: vi.fn().mockResolvedValue(undefined), + writeFileCreateOnly: vi.fn(), + deleteFile: vi.fn(), + listDir: vi.fn(), + lastCommit: vi.fn(), + putBlob: vi.fn(), + getBlob: vi.fn(), + deleteBlob: vi.fn(), +}; + +vi.mock('../session', () => ({ + setCurrent: vi.fn(), + getCurrent: vi.fn(() => ({ value: 1 })), + clearCurrent: vi.fn(), + requireCurrent: vi.fn(), +})); + +vi.mock('../vault', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + encryptAndWriteItem: vi.fn().mockResolvedValue(undefined), + encryptAndWriteManifest: vi.fn().mockResolvedValue(undefined), + }; +}); + +import { handle, type PopupState } from '../router/popup-only'; +import * as vault from '../vault'; +import type { Manifest, Item } from '../../shared/types'; + +const FAKE_SENDER = { + url: 'chrome-extension://x/vault.html', + id: 'x', + frameId: 0, +} as unknown as chrome.runtime.MessageSender; + +const EMPTY_MANIFEST: Manifest = { schema_version: 2, items: {} } as Manifest; + +function fakeWasm() { + let counter = 0; + return { + parse_lastpass_csv_json: vi.fn().mockReturnValue(JSON.stringify({ + items: [ + sampleItem('GitHub'), + sampleItem('Gmail'), + ], + warnings: [{ row: 3, title: 'No Pass', message: 'missing `password` — skipped' }], + })), + new_item_id: vi.fn(() => `newid${++counter}`.padEnd(16, '0')), + }; +} + +function sampleItem(title: string): Item { + return { + id: 'placeholder-id00', + title, + type: 'login', + tags: [], + favorite: false, + created: 1000, + modified: 1000, + core: { type: 'login', username: 'u', password: 'p' }, + sections: [], + attachments: [], + field_history: {}, + } as unknown as Item; +} + +describe('parse_lastpass_csv handler', () => { + beforeEach(() => { + (globalThis as { chrome?: unknown }).chrome = { + storage: { local: { get: vi.fn().mockResolvedValue({}), set: vi.fn() } }, + }; + }); + + it('returns items + warnings from the WASM bridge', async () => { + const state: PopupState = { + manifest: EMPTY_MANIFEST, + gitHost: fakeHost as never, + wasm: fakeWasm(), + }; + const result = await handle( + { type: 'parse_lastpass_csv', bytes: new ArrayBuffer(8) }, + state, + FAKE_SENDER, + ); + expect(result.ok).toBe(true); + if (result.ok) { + const data = result.data as { items: Item[]; warnings: unknown[] }; + expect(data.items).toHaveLength(2); + expect(data.warnings).toHaveLength(1); + } + }); + + it('surfaces WASM errors as ok:false', async () => { + const state: PopupState = { + manifest: EMPTY_MANIFEST, + gitHost: fakeHost as never, + wasm: { + parse_lastpass_csv_json: vi.fn(() => { throw new Error('bad header'); }), + }, + }; + const result = await handle( + { type: 'parse_lastpass_csv', bytes: new ArrayBuffer(0) }, + state, + FAKE_SENDER, + ); + expect(result).toEqual({ ok: false, error: 'bad header' }); + }); +}); + +describe('import_lastpass_commit handler', () => { + beforeEach(() => { + (vault.encryptAndWriteItem as ReturnType).mockClear(); + (vault.encryptAndWriteManifest as ReturnType).mockClear(); + (globalThis as { chrome?: unknown }).chrome = { + storage: { local: { get: vi.fn().mockResolvedValue({}), set: vi.fn() } }, + }; + }); + + it('encrypts + writes each item, manifest last', async () => { + const state: PopupState = { + manifest: { ...EMPTY_MANIFEST, items: {} }, + gitHost: fakeHost as never, + wasm: fakeWasm(), + }; + + const result = await handle( + { + type: 'import_lastpass_commit', + items: [sampleItem('A'), sampleItem('B'), sampleItem('C')], + }, + state, + FAKE_SENDER, + ); + expect(result.ok).toBe(true); + if (result.ok) { + const data = result.data as { summary: { itemCount: number } }; + expect(data.summary.itemCount).toBe(3); + } + expect(vault.encryptAndWriteItem).toHaveBeenCalledTimes(3); + expect(vault.encryptAndWriteManifest).toHaveBeenCalledTimes(1); + + // Manifest must have been re-keyed with the WASM-minted IDs (newid1, newid2, newid3). + expect(Object.keys(state.manifest!.items)).toHaveLength(3); + for (const key of Object.keys(state.manifest!.items)) { + expect(key).toMatch(/^newid\d/); + } + }); + + it('rejects when vault is locked', async () => { + const state: PopupState = { manifest: null, gitHost: null, wasm: fakeWasm() }; + const result = await handle( + { type: 'import_lastpass_commit', items: [sampleItem('X')] }, + state, + FAKE_SENDER, + ); + expect(result).toEqual({ ok: false, error: 'vault_locked' }); + }); + + it('rejects an empty items list', async () => { + const state: PopupState = { + manifest: EMPTY_MANIFEST, + gitHost: fakeHost as never, + wasm: fakeWasm(), + }; + const result = await handle( + { type: 'import_lastpass_commit', items: [] }, + state, + FAKE_SENDER, + ); + expect(result).toEqual({ ok: false, error: 'no items to import' }); + }); +});