From 218ccb8efa0b8845ee039178b7e10854db9c17a4 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Tue, 28 Apr 2026 22:20:07 -0400 Subject: [PATCH] test(ext/sw): export/restore handler unit tests --- .../service-worker/__tests__/backup.test.ts | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 extension/src/service-worker/__tests__/backup.test.ts diff --git a/extension/src/service-worker/__tests__/backup.test.ts b/extension/src/service-worker/__tests__/backup.test.ts new file mode 100644 index 0000000..1ff2d51 --- /dev/null +++ b/extension/src/service-worker/__tests__/backup.test.ts @@ -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('../git-host'); + return { + ...actual, + createGitHost: () => fakeNewHost, + }; +}); + +vi.mock('../vault', async () => { + const actual = await vi.importActual('../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 /). + 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'); + }); +});