From 7a6cfb56590523007859078578cebf1c6b3600f0 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 26 Jun 2026 22:51:10 -0400 Subject: [PATCH] test(ext/sw): cover handleUnlock keyfile error paths (forward-compat guard, invalid_key_file, key-file-not-set) --- .../__tests__/keyfile-unlock.test.ts | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/extension/src/service-worker/__tests__/keyfile-unlock.test.ts b/extension/src/service-worker/__tests__/keyfile-unlock.test.ts index 2a138bf..cee2cbb 100644 --- a/extension/src/service-worker/__tests__/keyfile-unlock.test.ts +++ b/extension/src/service-worker/__tests__/keyfile-unlock.test.ts @@ -339,3 +339,103 @@ describe('handleUnlock — keyfile branch (Task 4)', () => { expect(wasm.unlock).not.toHaveBeenCalled(); }); }); + +// ---- Task 4 error paths: forward-compat guard, invalid_key_file, key-file-not-set ---- + +describe('handleUnlock — keyfile error paths', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('forward-compat guard: rejects unknown second_factor without calling unlock or unlock_with_secret', async () => { + const fakeVaultConfig = { hostType: 'gitea' as const, hostUrl: 'https://g', repoPath: 'u/v', apiToken: 't' }; + const keyfileBase64 = btoa(String.fromCharCode(...new Uint8Array(32))); + mockChromeStorage({ vaultConfig: fakeVaultConfig, keyfileBase64 }); + vi.mocked(gitHostMod.createGitHost).mockImplementation(() => { + const h = makeHostMock(); + (h.readFile as ReturnType).mockImplementation(async (path: string) => { + if (path === '.relicario/salt') return new Uint8Array(32); + if (path === '.relicario/params.json') { + return new TextEncoder().encode( + '{"argon2_m":65536,"argon2_t":3,"argon2_p":4,"second_factor":"totp"}', + ); + } + if (path === 'manifest.enc') return new Uint8Array([0xab, 0xcd]); + throw new Error(`404: ${path}`); + }); + (globalThis as { __lastFakeGitHost?: ReturnType | null }).__lastFakeGitHost = h; + return h; + }); + const wasm = makeWasm({ keyfile_decode: vi.fn(), unlock_with_secret: vi.fn() }); + const state = makeState(wasm); + vault.setWasm(wasm); + + const resp = await handleUnlock({ type: 'unlock', passphrase: 'pw' }, state); + + expect(resp).toEqual({ ok: false, error: 'unsupported second factor in vault params' }); + expect(wasm.unlock).not.toHaveBeenCalled(); + expect(wasm.unlock_with_secret).not.toHaveBeenCalled(); + }); + + it('returns invalid_key_file when keyfile_decode throws (bad armor)', async () => { + const fakeVaultConfig = { hostType: 'gitea' as const, hostUrl: 'https://g', repoPath: 'u/v', apiToken: 't' }; + const keyfileBase64 = btoa(String.fromCharCode(...new Uint8Array(32))); + mockChromeStorage({ vaultConfig: fakeVaultConfig, keyfileBase64 }); + vi.mocked(gitHostMod.createGitHost).mockImplementation(() => { + const h = makeHostMock(); + (h.readFile as ReturnType).mockImplementation(async (path: string) => { + if (path === '.relicario/salt') return new Uint8Array(32); + if (path === '.relicario/params.json') { + return new TextEncoder().encode( + '{"argon2_m":65536,"argon2_t":3,"argon2_p":4,"second_factor":"keyfile"}', + ); + } + if (path === 'manifest.enc') return new Uint8Array([0xab, 0xcd]); + throw new Error(`404: ${path}`); + }); + (globalThis as { __lastFakeGitHost?: ReturnType | null }).__lastFakeGitHost = h; + return h; + }); + const wasm = makeWasm({ + keyfile_decode: vi.fn(() => { throw new Error('bad armor'); }), + unlock_with_secret: vi.fn(), + }); + const state = makeState(wasm); + vault.setWasm(wasm); + + const resp = await handleUnlock({ type: 'unlock', passphrase: 'pw' }, state); + + expect(resp).toEqual({ ok: false, error: 'invalid_key_file' }); + expect(wasm.unlock_with_secret).not.toHaveBeenCalled(); + }); + + it('returns key-file-not-set error when keyfileBase64 absent from storage', async () => { + const fakeVaultConfig = { hostType: 'gitea' as const, hostUrl: 'https://g', repoPath: 'u/v', apiToken: 't' }; + // Intentionally no keyfileBase64 in storage — only vaultConfig. + mockChromeStorage({ vaultConfig: fakeVaultConfig }); + vi.mocked(gitHostMod.createGitHost).mockImplementation(() => { + const h = makeHostMock(); + (h.readFile as ReturnType).mockImplementation(async (path: string) => { + if (path === '.relicario/salt') return new Uint8Array(32); + if (path === '.relicario/params.json') { + return new TextEncoder().encode( + '{"argon2_m":65536,"argon2_t":3,"argon2_p":4,"second_factor":"keyfile"}', + ); + } + if (path === 'manifest.enc') return new Uint8Array([0xab, 0xcd]); + throw new Error(`404: ${path}`); + }); + (globalThis as { __lastFakeGitHost?: ReturnType | null }).__lastFakeGitHost = h; + return h; + }); + const wasm = makeWasm({ keyfile_decode: vi.fn(), unlock_with_secret: vi.fn() }); + const state = makeState(wasm); + vault.setWasm(wasm); + + const resp = await handleUnlock({ type: 'unlock', passphrase: 'pw' }, state); + + expect(resp).toEqual({ ok: false, error: 'Key file not set. Run setup first.' }); + expect(wasm.keyfile_decode).not.toHaveBeenCalled(); + expect(wasm.unlock_with_secret).not.toHaveBeenCalled(); + }); +});