// credential-manager depends on keychain-manager and crypto-utils (both singletons). // crypto-utils is already initialized via jest.setup.js env var. // keychain-manager may not have OS keychain available in test env. const fs = require('fs'); const os = require('os'); const path = require('path'); const credentialManager = require('../credential-manager'); // Use a temp file for credentials in tests const TEMP_CREDS_FILE = path.join(os.tmpdir(), 'dashcaddy-test-creds.json'); beforeEach(() => { // Reset singleton state credentialManager.cache.clear(); // Clean up temp file if (fs.existsSync(TEMP_CREDS_FILE)) { fs.unlinkSync(TEMP_CREDS_FILE); } }); afterAll(() => { if (fs.existsSync(TEMP_CREDS_FILE)) { fs.unlinkSync(TEMP_CREDS_FILE); } }); describe('store', () => { test('rejects invalid key (null)', async () => { const result = await credentialManager.store(null, 'value'); expect(result).toBe(false); }); test('rejects invalid key (non-string)', async () => { const result = await credentialManager.store(123, 'value'); expect(result).toBe(false); }); test('rejects invalid value (null)', async () => { const result = await credentialManager.store('key', null); expect(result).toBe(false); }); test('rejects invalid value (non-string)', async () => { const result = await credentialManager.store('key', 123); expect(result).toBe(false); }); test('stores credential and caches it', async () => { const result = await credentialManager.store('test.key', 'secret123'); expect(result).toBe(true); expect(credentialManager.cache.get('test.key')).toBe('secret123'); }); }); describe('retrieve', () => { test('returns cached value when available', async () => { credentialManager.cache.set('cached.key', 'cached-value'); const result = await credentialManager.retrieve('cached.key'); expect(result).toBe('cached-value'); }); test('returns null for non-existent key', async () => { const result = await credentialManager.retrieve('nonexistent'); expect(result).toBeNull(); }); }); describe('store + retrieve round-trip', () => { test('retrieves what was stored', async () => { await credentialManager.store('roundtrip.key', 'my-secret'); // Clear cache to force file read credentialManager.cache.clear(); const result = await credentialManager.retrieve('roundtrip.key'); expect(result).toBe('my-secret'); }); }); describe('delete', () => { test('removes from cache', async () => { await credentialManager.store('delete.key', 'value'); expect(credentialManager.cache.has('delete.key')).toBe(true); await credentialManager.delete('delete.key'); expect(credentialManager.cache.has('delete.key')).toBe(false); }); test('deleted credential cannot be retrieved', async () => { await credentialManager.store('delete2.key', 'value'); await credentialManager.delete('delete2.key'); credentialManager.cache.clear(); const result = await credentialManager.retrieve('delete2.key'); expect(result).toBeNull(); }); }); describe('list', () => { test('returns array of credential keys', async () => { await credentialManager.store('list.a', 'val1'); await credentialManager.store('list.b', 'val2'); const keys = await credentialManager.list(); expect(keys).toContain('list.a'); expect(keys).toContain('list.b'); }); test('returns empty array when no credentials', async () => { const keys = await credentialManager.list(); expect(Array.isArray(keys)).toBe(true); }); }); describe('getMetadata', () => { test('returns metadata for existing key', async () => { await credentialManager.store('meta.key', 'val', { description: 'Test credential' }); const meta = await credentialManager.getMetadata('meta.key'); expect(meta).toEqual({ description: 'Test credential' }); }); test('returns null for non-existent key', async () => { const meta = await credentialManager.getMetadata('nonexistent'); expect(meta).toBeNull(); }); }); describe('exportBackup / importBackup', () => { test('export returns encrypted string', async () => { await credentialManager.store('backup.key', 'backup-value'); const backup = await credentialManager.exportBackup(); expect(typeof backup).toBe('string'); expect(backup.split(':').length).toBe(3); // iv:authTag:ciphertext }); test('import restores credentials from backup', async () => { await credentialManager.store('backup.key', 'backup-value'); const backup = await credentialManager.exportBackup(); // Clear everything await credentialManager.delete('backup.key'); credentialManager.cache.clear(); // Import backup const result = await credentialManager.importBackup(backup); expect(result).toBe(true); // Verify restored const keys = await credentialManager.list(); expect(keys).toContain('backup.key'); }); test('importBackup rejects unsupported version', async () => { const cryptoUtils = require('../crypto-utils'); const badBackup = cryptoUtils.encrypt(JSON.stringify({ version: '99.0', credentials: {} })); const result = await credentialManager.importBackup(badBackup); expect(result).toBe(false); }); }); describe('migrateToEncrypted', () => { test('returns migration count', async () => { const result = await credentialManager.migrateToEncrypted(); expect(result).toHaveProperty('migrated'); expect(result).toHaveProperty('skipped'); expect(result).toHaveProperty('total'); }); });