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