/** * Tests for secure-storage.ts * * These tests verify that credentials are encrypted when stored in localStorage * (fallback mode when OS keyring is unavailable). */ import { describe, it, expect, beforeEach, vi } from 'vitest'; // Mock Tauri runtime to return false (non-Tauri environment) vi.mock('../../src/lib/tauri-gateway', () => ({ isTauriRuntime: () => false, })); // Import after mocking import { secureStorage } from '../../src/lib/secure-storage'; describe('secureStorage', () => { beforeEach(() => { localStorage.clear(); vi.clearAllMocks(); }); describe('encryption fallback', () => { it('should encrypt data when storing to localStorage', async () => { const key = 'test-key'; const value = 'secret-value'; await secureStorage.set(key, value); // Check that localStorage doesn't contain plaintext const encryptedKey = 'enc_' + key; const stored = localStorage.getItem(encryptedKey); expect(stored).not.toBeNull(); expect(stored).not.toBe(value); expect(stored).not.toContain('secret-value'); // Should be JSON with iv and data fields const parsed = JSON.parse(stored!); expect(parsed).toHaveProperty('iv'); expect(parsed).toHaveProperty('data'); }); it('should decrypt data when retrieving from localStorage', async () => { const key = 'test-key'; const value = 'secret-value'; await secureStorage.set(key, value); const retrieved = await secureStorage.get(key); expect(retrieved).toBe(value); }); it('should handle special characters in values', async () => { const key = 'test-key'; const value = 'p@ssw0rd!#$%^&*(){}[]|\\:";\'<>?,./~`'; await secureStorage.set(key, value); const retrieved = await secureStorage.get(key); expect(retrieved).toBe(value); }); it('should handle Unicode characters in values', async () => { const key = 'test-key'; const value = '密码测试123テスト🔑🔐'; await secureStorage.set(key, value); const retrieved = await secureStorage.get(key); expect(retrieved).toBe(value); }); it('should handle empty string by removing the key', async () => { const key = 'test-key'; const value = 'initial-value'; await secureStorage.set(key, value); expect(await secureStorage.get(key)).toBe(value); // Setting empty string should remove the key await secureStorage.set(key, ''); const encryptedKey = 'enc_' + key; expect(localStorage.getItem(encryptedKey)).toBeNull(); expect(await secureStorage.get(key)).toBeNull(); }); it('should handle null by removing the key', async () => { const key = 'test-key'; const value = 'initial-value'; await secureStorage.set(key, value); expect(await secureStorage.get(key)).toBe(value); // Delete should remove the key await secureStorage.delete(key); const encryptedKey = 'enc_' + key; expect(localStorage.getItem(encryptedKey)).toBeNull(); }); }); describe('backward compatibility', () => { it('should read unencrypted legacy data', async () => { const key = 'legacy-key'; const value = 'legacy-value'; // Simulate legacy unencrypted storage localStorage.setItem(key, value); const retrieved = await secureStorage.get(key); expect(retrieved).toBe(value); }); it('should migrate unencrypted data to encrypted on next set', async () => { const key = 'legacy-key'; const value = 'legacy-value'; const newValue = 'new-encrypted-value'; // Simulate legacy unencrypted storage localStorage.setItem(key, value); // Read should return legacy value const retrieved = await secureStorage.get(key); expect(retrieved).toBe(value); // Write should encrypt the new value await secureStorage.set(key, newValue); // Legacy key should be removed, encrypted key should exist expect(localStorage.getItem(key)).toBeNull(); const encryptedKey = 'enc_' + key; const stored = localStorage.getItem(encryptedKey); expect(stored).not.toBeNull(); expect(stored).not.toContain(newValue); // Should retrieve the new encrypted value expect(await secureStorage.get(key)).toBe(newValue); }); }); describe('encryption strength', () => { it('should use different IV for each encryption', async () => { const key = 'test-key'; const value = 'same-value'; await secureStorage.set(key, value); const encrypted1 = localStorage.getItem('enc_' + key); await secureStorage.set(key, value); const encrypted2 = localStorage.getItem('enc_' + key); // Both should be encrypted versions of the same value expect(encrypted1).not.toBe(encrypted2); // But both should decrypt to the same value const parsed1 = JSON.parse(encrypted1!); const parsed2 = JSON.parse(encrypted2!); expect(parsed1.iv).not.toBe(parsed2.iv); // Different IVs expect(parsed1.data).not.toBe(parsed2.data); // Different ciphertext }); }); describe('error handling', () => { it('should return null for non-existent keys', async () => { const retrieved = await secureStorage.get('non-existent-key'); expect(retrieved).toBeNull(); }); it('should handle corrupted encrypted data gracefully', async () => { const key = 'corrupted-key'; const value = 'valid-value'; // Store valid encrypted data await secureStorage.set(key, value); // Corrupt the encrypted data const encryptedKey = 'enc_' + key; const encrypted = localStorage.getItem(encryptedKey); const parsed = JSON.parse(encrypted!); parsed.data = 'corrupted-data'; localStorage.setItem(encryptedKey, JSON.stringify(parsed)); // Should return null for corrupted data const retrieved = await secureStorage.get(key); expect(retrieved).toBeNull(); }); }); });