From 01172eaa44533c2b8c79efd7afa3971dd4ad1295 Mon Sep 17 00:00:00 2001 From: "jin.2" Date: Fri, 13 Mar 2026 03:45:05 +0900 Subject: [PATCH] fix: isolate getEncryptionKey tests from local filesystem (#737) * fix: isolate getEncryptionKey tests from local filesystem Tests for getEncryptionKey() failed on machines where ~/.agent-browser/.encryption-key existed, because the file-based fallback was not mocked out. Mock node:fs to isolate both env var and key file paths, and add missing tests for the file fallback. Co-Authored-By: Claude Opus 4.6 * refactor: clean up fs mock naming in encryption tests Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: hyunjinee Co-authored-by: Claude Opus 4.6 --- src/encryption.test.ts | 168 ++++++++++++++++++++++++++++------------- 1 file changed, 116 insertions(+), 52 deletions(-) diff --git a/src/encryption.test.ts b/src/encryption.test.ts index 6215a91..06fd567 100644 --- a/src/encryption.test.ts +++ b/src/encryption.test.ts @@ -1,15 +1,30 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import * as crypto from 'crypto'; import { encryptData, decryptData, getEncryptionKey, + getKeyFilePath, isEncryptedPayload, ENCRYPTION_KEY_ENV, IV_LENGTH, type EncryptedPayload, } from './encryption.js'; +// Mock node:fs to isolate getEncryptionKey from the local filesystem +const mockFs = vi.hoisted(() => ({ + existsSync: vi.fn(), + readFileSync: vi.fn(), + originals: {} as Pick, +})); +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal(); + mockFs.originals = { existsSync: actual.existsSync, readFileSync: actual.readFileSync }; + mockFs.existsSync.mockImplementation(actual.existsSync); + mockFs.readFileSync.mockImplementation(actual.readFileSync); + return { ...actual, existsSync: mockFs.existsSync, readFileSync: mockFs.readFileSync }; +}); + // Generate a valid test key (256 bits = 32 bytes = 64 hex chars) const generateTestKey = () => crypto.randomBytes(32); const generateTestKeyHex = () => crypto.randomBytes(32).toString('hex'); @@ -250,9 +265,25 @@ describe('encryption', () => { describe('getEncryptionKey', () => { const originalEnv = process.env[ENCRYPTION_KEY_ENV]; + const keyFilePath = getKeyFilePath(); + + function mockKeyFile(content?: string): void { + const exists = content !== undefined; + mockFs.existsSync.mockImplementation((path: string) => { + if (path === keyFilePath) return exists; + return mockFs.originals.existsSync(path); + }); + if (exists) { + mockFs.readFileSync.mockImplementation((path: string, encoding?: string) => { + if (path === keyFilePath) return content; + return mockFs.originals.readFileSync(path, encoding as BufferEncoding); + }); + } + } afterEach(() => { - // Restore original env + mockFs.existsSync.mockImplementation(mockFs.originals.existsSync); + mockFs.readFileSync.mockImplementation(mockFs.originals.readFileSync); if (originalEnv !== undefined) { process.env[ENCRYPTION_KEY_ENV] = originalEnv; } else { @@ -260,62 +291,95 @@ describe('encryption', () => { } }); - it('should return null when env var is not set', () => { - delete process.env[ENCRYPTION_KEY_ENV]; - expect(getEncryptionKey()).toBeNull(); + describe('from env var', () => { + beforeEach(() => { + mockKeyFile(); + }); + + it('should return null when env var is not set', () => { + delete process.env[ENCRYPTION_KEY_ENV]; + expect(getEncryptionKey()).toBeNull(); + }); + + it('should return null for empty string', () => { + process.env[ENCRYPTION_KEY_ENV] = ''; + expect(getEncryptionKey()).toBeNull(); + }); + + it('should return null for invalid hex (too short)', () => { + process.env[ENCRYPTION_KEY_ENV] = 'abc123'; // Only 6 chars, need 64 + expect(getEncryptionKey()).toBeNull(); + }); + + it('should return null for invalid hex (too long)', () => { + process.env[ENCRYPTION_KEY_ENV] = 'a'.repeat(128); // 128 chars, need 64 + expect(getEncryptionKey()).toBeNull(); + }); + + it('should return null for non-hex characters', () => { + process.env[ENCRYPTION_KEY_ENV] = 'g'.repeat(64); // 'g' is not hex + expect(getEncryptionKey()).toBeNull(); + }); + + it('should return valid key buffer for correct hex string', () => { + const keyHex = generateTestKeyHex(); + process.env[ENCRYPTION_KEY_ENV] = keyHex; + + const key = getEncryptionKey(); + expect(key).not.toBeNull(); + expect(key).toBeInstanceOf(Buffer); + expect(key!.length).toBe(32); // 256 bits + expect(key!.toString('hex')).toBe(keyHex.toLowerCase()); + }); + + it('should accept uppercase hex', () => { + const keyHex = generateTestKeyHex().toUpperCase(); + process.env[ENCRYPTION_KEY_ENV] = keyHex; + + const key = getEncryptionKey(); + expect(key).not.toBeNull(); + expect(key!.length).toBe(32); + }); + + it('should accept mixed case hex', () => { + const keyHex = generateTestKeyHex(); + const mixedCase = keyHex + .split('') + .map((c, i) => (i % 2 === 0 ? c.toUpperCase() : c.toLowerCase())) + .join(''); + process.env[ENCRYPTION_KEY_ENV] = mixedCase; + + const key = getEncryptionKey(); + expect(key).not.toBeNull(); + expect(key!.length).toBe(32); + }); }); - it('should return null for empty string', () => { - process.env[ENCRYPTION_KEY_ENV] = ''; - expect(getEncryptionKey()).toBeNull(); - }); + describe('from key file fallback', () => { + beforeEach(() => { + delete process.env[ENCRYPTION_KEY_ENV]; + }); - it('should return null for invalid hex (too short)', () => { - process.env[ENCRYPTION_KEY_ENV] = 'abc123'; // Only 6 chars, need 64 - expect(getEncryptionKey()).toBeNull(); - }); + it('should return key when key file exists with valid hex', () => { + const keyHex = generateTestKeyHex(); + mockKeyFile(keyHex); - it('should return null for invalid hex (too long)', () => { - process.env[ENCRYPTION_KEY_ENV] = 'a'.repeat(128); // 128 chars, need 64 - expect(getEncryptionKey()).toBeNull(); - }); + const key = getEncryptionKey(); + expect(key).not.toBeNull(); + expect(key).toBeInstanceOf(Buffer); + expect(key!.length).toBe(32); + expect(key!.toString('hex')).toBe(keyHex.toLowerCase()); + }); - it('should return null for non-hex characters', () => { - process.env[ENCRYPTION_KEY_ENV] = 'g'.repeat(64); // 'g' is not hex - expect(getEncryptionKey()).toBeNull(); - }); + it('should return null when key file does not exist', () => { + mockKeyFile(); + expect(getEncryptionKey()).toBeNull(); + }); - it('should return valid key buffer for correct hex string', () => { - const keyHex = generateTestKeyHex(); - process.env[ENCRYPTION_KEY_ENV] = keyHex; - - const key = getEncryptionKey(); - expect(key).not.toBeNull(); - expect(key).toBeInstanceOf(Buffer); - expect(key!.length).toBe(32); // 256 bits - expect(key!.toString('hex')).toBe(keyHex.toLowerCase()); - }); - - it('should accept uppercase hex', () => { - const keyHex = generateTestKeyHex().toUpperCase(); - process.env[ENCRYPTION_KEY_ENV] = keyHex; - - const key = getEncryptionKey(); - expect(key).not.toBeNull(); - expect(key!.length).toBe(32); - }); - - it('should accept mixed case hex', () => { - const keyHex = generateTestKeyHex(); - const mixedCase = keyHex - .split('') - .map((c, i) => (i % 2 === 0 ? c.toUpperCase() : c.toLowerCase())) - .join(''); - process.env[ENCRYPTION_KEY_ENV] = mixedCase; - - const key = getEncryptionKey(); - expect(key).not.toBeNull(); - expect(key!.length).toBe(32); + it('should return null when key file contains invalid hex', () => { + mockKeyFile('not-valid-hex'); + expect(getEncryptionKey()).toBeNull(); + }); }); });