test(ext/sw): unit tests for parse + commit handlers
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 <noreply@anthropic.com>
This commit is contained in:
178
extension/src/service-worker/__tests__/import.test.ts
Normal file
178
extension/src/service-worker/__tests__/import.test.ts
Normal file
@@ -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<typeof import('../vault')>();
|
||||||
|
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<typeof vi.fn>).mockClear();
|
||||||
|
(vault.encryptAndWriteManifest as ReturnType<typeof vi.fn>).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' });
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user