test(ext/sw): export/restore handler unit tests

This commit is contained in:
adlee-was-taken
2026-04-28 22:20:07 -04:00
parent c1f48ecb71
commit 218ccb8efa

View File

@@ -0,0 +1,153 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
// Top-level mocks: hoisted before the SUT import.
const fakeNewHost = {
readFile: vi.fn().mockRejectedValue(new Error('not-found')),
writeFile: vi.fn().mockResolvedValue(undefined),
writeFileCreateOnly: vi.fn().mockResolvedValue(undefined),
deleteFile: vi.fn(),
listDir: vi.fn().mockResolvedValue([]),
lastCommit: vi.fn().mockResolvedValue(null),
putBlob: vi.fn(),
getBlob: vi.fn(),
deleteBlob: vi.fn(),
};
vi.mock('../git-host', async () => {
const actual = await vi.importActual<typeof import('../git-host')>('../git-host');
return {
...actual,
createGitHost: () => fakeNewHost,
};
});
vi.mock('../vault', async () => {
const actual = await vi.importActual<typeof import('../vault')>('../vault');
return {
...actual,
fetchVaultStateForBackup: vi.fn().mockResolvedValue({
salt_b64: 'AAAA',
params_json: '{}',
devices_json: '[]',
manifest_enc_b64: 'bWZzdA==',
settings_enc_b64: 'c3RuZw==',
items: [{ id: 'aaa1', ciphertext_b64: 'aXRlbS1jdA==' }],
attachments: [],
}),
fetchVaultMeta: vi.fn().mockRejectedValue(new Error('no vault')),
};
});
import { handle, type PopupState } from '../router/popup-only';
import type { Manifest } 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() {
return {
pack_backup_json: vi.fn().mockReturnValue(new Uint8Array([0x52, 0x42, 0x41, 0x4b, 0x01])),
unpack_backup_json: vi.fn().mockReturnValue(JSON.stringify({
salt: btoa(String.fromCharCode(...new Uint8Array(32))),
params_json: '{}',
devices_json: '[]',
manifest_enc: btoa('mfst'),
settings_enc: btoa('stng'),
items: [{ id: 'aaa1', ciphertext: btoa('item-ct') }],
attachments: [{ item_id: 'aaa1', attachment_id: 'bbbb', ciphertext: btoa('att-ct') }],
reference_jpg: null,
})),
};
}
describe('export_backup handler', () => {
beforeEach(() => {
(globalThis as { chrome?: unknown }).chrome = {
storage: {
local: {
get: vi.fn().mockResolvedValue({}),
set: vi.fn().mockResolvedValue(undefined),
},
},
};
});
it('returns ArrayBuffer of pack output on success', async () => {
const state: PopupState = {
manifest: EMPTY_MANIFEST,
gitHost: fakeNewHost as never,
wasm: fakeWasm(),
};
const result = await handle(
{ type: 'export_backup', passphrase: 'p', includeImage: false },
state,
FAKE_SENDER,
);
expect(result.ok).toBe(true);
if (result.ok) {
const data = result.data as { bytes: ArrayBuffer };
expect(data.bytes.byteLength).toBe(5);
}
});
it('rejects when manifest is missing (vault_locked)', async () => {
const state: PopupState = {
manifest: null,
gitHost: fakeNewHost as never,
wasm: fakeWasm(),
};
const result = await handle(
{ type: 'export_backup', passphrase: 'p', includeImage: false },
state,
FAKE_SENDER,
);
expect(result).toEqual({ ok: false, error: 'vault_locked' });
});
});
describe('restore_backup handler', () => {
beforeEach(() => {
fakeNewHost.writeFileCreateOnly.mockClear();
(globalThis as { chrome?: unknown }).chrome = {
storage: {
local: {
get: vi.fn().mockResolvedValue({}),
set: vi.fn().mockResolvedValue(undefined),
},
},
};
});
it('writes vault layout via writeFileCreateOnly', async () => {
const state: PopupState = {
manifest: null,
gitHost: null as never,
wasm: fakeWasm(),
};
const result = await handle(
{
type: 'restore_backup',
bytes: new ArrayBuffer(5),
passphrase: 'p',
newRemote: { hostType: 'gitea', hostUrl: 'https://x', repoPath: 'a/b', apiToken: 't' },
},
state,
FAKE_SENDER,
);
expect(result.ok).toBe(true);
// 5 baseline files (salt, params, devices, manifest, settings) +
// 1 item + 1 attachment = 7 writes.
expect(fakeNewHost.writeFileCreateOnly).toHaveBeenCalledTimes(7);
// Confirm flat-layout attachment path (not <item_id>/<aid>).
const attachCall = fakeNewHost.writeFileCreateOnly.mock.calls.find(
(c: unknown[]) => typeof c[0] === 'string' && (c[0] as string).startsWith('attachments/'),
);
expect(attachCall![0]).toBe('attachments/bbbb.bin');
});
});