test(ext/sw): export/restore handler unit tests
This commit is contained in:
153
extension/src/service-worker/__tests__/backup.test.ts
Normal file
153
extension/src/service-worker/__tests__/backup.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user