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'); }); });