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 <noreply@anthropic.com> * refactor: clean up fs mock naming in encryption tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: hyunjinee <leehj0110@kakao.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
hyunjinee
parent
942b8cd8ee
commit
01172eaa44
+116
-52
@@ -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 * as crypto from 'crypto';
|
||||||
import {
|
import {
|
||||||
encryptData,
|
encryptData,
|
||||||
decryptData,
|
decryptData,
|
||||||
getEncryptionKey,
|
getEncryptionKey,
|
||||||
|
getKeyFilePath,
|
||||||
isEncryptedPayload,
|
isEncryptedPayload,
|
||||||
ENCRYPTION_KEY_ENV,
|
ENCRYPTION_KEY_ENV,
|
||||||
IV_LENGTH,
|
IV_LENGTH,
|
||||||
type EncryptedPayload,
|
type EncryptedPayload,
|
||||||
} from './encryption.js';
|
} 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<typeof import('node:fs'), 'existsSync' | 'readFileSync'>,
|
||||||
|
}));
|
||||||
|
vi.mock('node:fs', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import('node:fs')>();
|
||||||
|
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)
|
// Generate a valid test key (256 bits = 32 bytes = 64 hex chars)
|
||||||
const generateTestKey = () => crypto.randomBytes(32);
|
const generateTestKey = () => crypto.randomBytes(32);
|
||||||
const generateTestKeyHex = () => crypto.randomBytes(32).toString('hex');
|
const generateTestKeyHex = () => crypto.randomBytes(32).toString('hex');
|
||||||
@@ -250,9 +265,25 @@ describe('encryption', () => {
|
|||||||
|
|
||||||
describe('getEncryptionKey', () => {
|
describe('getEncryptionKey', () => {
|
||||||
const originalEnv = process.env[ENCRYPTION_KEY_ENV];
|
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(() => {
|
afterEach(() => {
|
||||||
// Restore original env
|
mockFs.existsSync.mockImplementation(mockFs.originals.existsSync);
|
||||||
|
mockFs.readFileSync.mockImplementation(mockFs.originals.readFileSync);
|
||||||
if (originalEnv !== undefined) {
|
if (originalEnv !== undefined) {
|
||||||
process.env[ENCRYPTION_KEY_ENV] = originalEnv;
|
process.env[ENCRYPTION_KEY_ENV] = originalEnv;
|
||||||
} else {
|
} else {
|
||||||
@@ -260,62 +291,95 @@ describe('encryption', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null when env var is not set', () => {
|
describe('from env var', () => {
|
||||||
delete process.env[ENCRYPTION_KEY_ENV];
|
beforeEach(() => {
|
||||||
expect(getEncryptionKey()).toBeNull();
|
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', () => {
|
describe('from key file fallback', () => {
|
||||||
process.env[ENCRYPTION_KEY_ENV] = '';
|
beforeEach(() => {
|
||||||
expect(getEncryptionKey()).toBeNull();
|
delete process.env[ENCRYPTION_KEY_ENV];
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null for invalid hex (too short)', () => {
|
it('should return key when key file exists with valid hex', () => {
|
||||||
process.env[ENCRYPTION_KEY_ENV] = 'abc123'; // Only 6 chars, need 64
|
const keyHex = generateTestKeyHex();
|
||||||
expect(getEncryptionKey()).toBeNull();
|
mockKeyFile(keyHex);
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null for invalid hex (too long)', () => {
|
const key = getEncryptionKey();
|
||||||
process.env[ENCRYPTION_KEY_ENV] = 'a'.repeat(128); // 128 chars, need 64
|
expect(key).not.toBeNull();
|
||||||
expect(getEncryptionKey()).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', () => {
|
it('should return null when key file does not exist', () => {
|
||||||
process.env[ENCRYPTION_KEY_ENV] = 'g'.repeat(64); // 'g' is not hex
|
mockKeyFile();
|
||||||
expect(getEncryptionKey()).toBeNull();
|
expect(getEncryptionKey()).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return valid key buffer for correct hex string', () => {
|
it('should return null when key file contains invalid hex', () => {
|
||||||
const keyHex = generateTestKeyHex();
|
mockKeyFile('not-valid-hex');
|
||||||
process.env[ENCRYPTION_KEY_ENV] = keyHex;
|
expect(getEncryptionKey()).toBeNull();
|
||||||
|
});
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user