/** * Secure Config 测试(v0.7.2 覆盖补齐 —— 此前零测试) * * 锁定敏感配置加密存储契约(P0-1 的回归防线): * 1. 敏感 key 判定模式(apikey/token/secret/password/auth_key) * 2. safeStorage 可用 → 加密前缀格式 + 解密还原 * 3. safeStorage 不可用 → 明文降级(可用性优先)+ WARN * 4. 解密失败(跨机器/重装)→ 返回空串(引导重录而非崩溃) * 5. 加密幂等(已加密值不二次加密)与非字符串透传 */ import { describe, it, expect, vi, beforeEach } from 'vitest'; // 可控的 safeStorage 桩:可逆 fake 加密(enc: 前缀),可用性开关可编程 const mockState = vi.hoisted(() => ({ encryptionAvailable: true, failDecrypt: false, })); vi.mock('electron', () => ({ safeStorage: { isEncryptionAvailable: () => mockState.encryptionAvailable, encryptString: (value: string) => Buffer.from(`enc:${value}`, 'utf-8'), decryptString: (buffer: Buffer) => { const raw = buffer.toString('utf-8'); if (mockState.failDecrypt || !raw.startsWith('enc:')) { throw new Error('decryption failed'); } return raw.slice(4); }, }, })); vi.mock('electron-log', () => ({ default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, })); import { isSensitiveConfigKey, isEncryptedValue, encryptConfigValue, decryptConfigValue, } from '../secure-config'; beforeEach(() => { mockState.encryptionAvailable = true; mockState.failDecrypt = false; }); describe('isSensitiveConfigKey — 敏感 key 判定', () => { it.each([ ['llm.apiKey', true], ['llm.api_key', true], ['searxng.auth_key', true], ['llm.fallbackApiKey', true], ['proxy.token', true], ['db.secret', true], ['GITHUB_PASSWORD', true], ['ui.theme', false], ['llm.model', false], ['agent.maxIterations', false], ['network.proxyUrl', false], ])('%s → %j', (key, expected) => { expect(isSensitiveConfigKey(key)).toBe(expected); }); }); describe('encrypt/decrypt — 加密回环', () => { it('加密值带版本化前缀(metona-enc:v1:),解密还原原文', () => { const encrypted = encryptConfigValue('sk-my-secret-key'); expect(typeof encrypted).toBe('string'); expect(isEncryptedValue(encrypted)).toBe(true); expect(String(encrypted)).toMatch(/^metona-enc:v1:/); const decrypted = decryptConfigValue(encrypted); expect(decrypted).toBe('sk-my-secret-key'); }); it('非字符串值原样透传(number/boolean/null 不加密)', () => { expect(encryptConfigValue(42)).toBe(42); expect(encryptConfigValue(true)).toBe(true); expect(encryptConfigValue(null)).toBe(null); expect(decryptConfigValue(42)).toBe(42); }); it('空字符串不加密(避免无意义前缀包裹)', () => { expect(encryptConfigValue('')).toBe(''); }); it('已加密值幂等 —— 二次加密不再包裹前缀', () => { const once = encryptConfigValue('sk-key') as string; const twice = encryptConfigValue(once); expect(twice).toBe(once); expect(decryptConfigValue(twice)).toBe('sk-key'); }); it('非加密格式的值解密时原样返回(历史明文平滑兼容)', () => { expect(decryptConfigValue('plain-old-key')).toBe('plain-old-key'); }); }); describe('加密降级与失败语义', () => { it('safeStorage 不可用 → 明文存储(可用性优先 + WARN)', () => { mockState.encryptionAvailable = false; const value = encryptConfigValue('sk-plaintext-fallback'); expect(value).toBe('sk-plaintext-fallback'); expect(isEncryptedValue(value)).toBe(false); }); it('加密过程抛错 → 回退明文存储(不阻断配置保存)', () => { // decryptString 抛错不影响 encrypt;此处验证 decrypt 失败语义 mockState.failDecrypt = true; const encrypted = encryptConfigValue('sk-x') as string; expect(decryptConfigValue(encrypted)).toBe(''); // 失败 → 空串(createAdapter 判定未配置,引导重录) }); });