import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { SecretManager } from '../SecretManager'; import { SecretManagerConfig, SecretError, SecretNotFoundError, SecretTimeoutError } from 'symbi-types'; import { MockSecretManager } from '../../../testing/src/mocks/MockSecretManager'; describe('SecretManager', () => { let secretManager: SecretManager; let mockConfig: SecretManagerConfig; beforeEach(async () => { mockConfig = { providers: [ { name: 'environment', priority: 100, prefix: 'TEST_' }, { name: 'file', priority: 50, filePath: '/tmp/test-secrets.json', format: 'json' } ], defaultTimeout: 5000, cacheEnabled: true, cacheTtl: 300000, debug: false }; secretManager = new SecretManager(mockConfig); await secretManager.initialize(); }); afterEach(async () => { await secretManager.cleanup(); }); describe('constructor', () => { it('should initialize with default configuration', () => { const manager = new SecretManager({ providers: [] }); expect(manager).toBeDefined(); }); it('should set default values for optional configuration', async () => { const minimalConfig = { providers: [{ name: 'environment' as const, priority: 100 }] }; const manager = new SecretManager(minimalConfig); await manager.initialize(); expect(manager).toBeDefined(); await manager.cleanup(); }); it('should initialize providers on construction', async () => { // After beforeEach initialization, providers should be available const providers = secretManager.getProviders(); expect(providers.length).toBeGreaterThan(0); }); }); describe('getSecret', () => { beforeEach(() => { // Mock environment variables process.env.TEST_API_KEY = 'env-api-key'; process.env.TEST_JWT_TOKEN = 'env-jwt-token'; }); afterEach(() => { delete process.env.TEST_API_KEY; delete process.env.TEST_JWT_TOKEN; }); it('should resolve secret from environment provider', async () => { const secret = await secretManager.getSecret('API_KEY'); expect(secret).toBe('env-api-key'); }); it('should return default value when secret not found and required=false', async () => { const secret = await secretManager.getSecret('NON_EXISTENT_KEY', { required: false, defaultValue: 'default-value' }); expect(secret).toBe('default-value'); }); it('should throw SecretNotFoundError when required secret not found', async () => { await expect( secretManager.getSecret('NON_EXISTENT_KEY', { required: true }) ).rejects.toThrow(SecretNotFoundError); }); it('should return empty string when required=false and no default', async () => { const secret = await secretManager.getSecret('NON_EXISTENT_KEY', { required: false }); expect(secret).toBe(''); }); it('should respect provider name filter', async () => { // This test would need mock providers to work properly const config = { providers: [ { name: 'environment' as const, priority: 100, prefix: 'TEST_' } ] }; const manager = new SecretManager(config); await manager.initialize(); const secret = await manager.getSecret('API_KEY', { providerName: 'environment' }); expect(secret).toBe('env-api-key'); await manager.cleanup(); }); it('should handle timeout correctly', async () => { // Create a manager with a provider that will find the secret but delay long enough to timeout process.env.SLOW_TEST_KEY = 'slow-value'; const slowConfig = { providers: [ { name: 'environment' as const, priority: 100, prefix: 'SLOW_' } ] }; const manager = new SecretManager(slowConfig); await manager.initialize(); // Mock the provider's getSecret method to simulate a slow response const provider = manager.getProvider('environment'); if (provider) { const originalGetSecret = provider.getSecret.bind(provider); provider.getSecret = async (key: string) => { // Simulate a delay longer than the timeout await new Promise(resolve => setTimeout(resolve, 200)); return originalGetSecret(key); }; } await expect( manager.getSecret('TEST_KEY', { timeout: 100, // Shorter than the simulated delay required: true }) ).rejects.toThrow(SecretTimeoutError); delete process.env.SLOW_TEST_KEY; await manager.cleanup(); }); it('should use cache when enabled', async () => { const cachedManager = new SecretManager({ ...mockConfig, cacheEnabled: true, cacheTtl: 60000 }); await cachedManager.initialize(); // First call should resolve from provider const secret1 = await cachedManager.getSecret('API_KEY'); // Second call should use cache (would be faster in real implementation) const secret2 = await cachedManager.getSecret('API_KEY'); expect(secret1).toBe(secret2); await cachedManager.cleanup(); }); it('should bypass cache when disabled', async () => { const uncachedManager = new SecretManager({ ...mockConfig, cacheEnabled: false }); await uncachedManager.initialize(); const secret1 = await uncachedManager.getSecret('API_KEY'); const secret2 = await uncachedManager.getSecret('API_KEY'); expect(secret1).toBe(secret2); // Values should still be the same await uncachedManager.cleanup(); }); }); describe('hasSecret', () => { beforeEach(() => { process.env.TEST_API_KEY = 'env-api-key'; }); afterEach(() => { delete process.env.TEST_API_KEY; }); it('should return true for existing secrets', async () => { const exists = await secretManager.hasSecret('API_KEY'); expect(exists).toBe(true); }); it('should return false for non-existent secrets', async () => { const exists = await secretManager.hasSecret('NON_EXISTENT_KEY'); expect(exists).toBe(false); }); it('should work with provider name filter', async () => { const exists = await secretManager.hasSecret('API_KEY', 'environment'); expect(exists).toBe(true); }); }); describe('provider management', () => { it('should return list of providers', () => { const providers = secretManager.getProviders(); expect(Array.isArray(providers)).toBe(true); expect(providers.length).toBeGreaterThan(0); }); it('should return specific provider by name', () => { const envProvider = secretManager.getProvider('environment'); expect(envProvider).toBeDefined(); expect(envProvider?.name).toBe('environment'); }); it('should return undefined for non-existent provider', () => { const provider = secretManager.getProvider('non-existent'); expect(provider).toBeUndefined(); }); it('should sort providers by priority', () => { const providers = secretManager.getProviders(); // Check that providers are sorted by priority (higher first) for (let i = 0; i < providers.length - 1; i++) { expect(providers[i].priority).toBeGreaterThanOrEqual(providers[i + 1].priority); } }); }); describe('cache management', () => { it('should clear cache', () => { expect(() => secretManager.clearCache()).not.toThrow(); }); }); describe('lifecycle management', () => { it('should initialize successfully', async () => { await expect(secretManager.initialize()).resolves.toBeUndefined(); }); it('should cleanup successfully', async () => { await expect(secretManager.cleanup()).resolves.toBeUndefined(); }); it('should handle provider initialization errors gracefully', async () => { // This would need special setup to test provider initialization failures await expect(secretManager.initialize()).resolves.toBeUndefined(); }); }); describe('error handling', () => { it('should throw SecretError for unknown provider types', async () => { const invalidConfig = { providers: [ { name: 'unknown-provider' as any, priority: 100 } ] }; // This should not throw during construction but during provider creation const manager = new SecretManager(invalidConfig); await manager.initialize(); // This might create providers, but unknown providers will be skipped await manager.cleanup(); }); it('should handle provider failures gracefully', async () => { // Mock a failing provider scenario const manager = new SecretManager({ providers: [], debug: false }); await manager.initialize(); await expect( manager.getSecret('ANY_KEY', { required: true }) ).rejects.toThrow(SecretError); await manager.cleanup(); }); }); describe('debug mode', () => { it('should log debug information when enabled', async () => { const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); const debugManager = new SecretManager({ ...mockConfig, debug: true }); await debugManager.initialize(); process.env.TEST_DEBUG_KEY = 'debug-value'; try { await debugManager.getSecret('DEBUG_KEY'); // In a real implementation, this would trigger debug logs } catch { // Ignore errors for this test } delete process.env.TEST_DEBUG_KEY; await debugManager.cleanup(); consoleSpy.mockRestore(); }); }); }); describe('SecretManager Provider Priority', () => { let envManager: SecretManager; beforeEach(async () => { // Set up environment variables process.env.TEST_HIGH_PRIORITY = 'env-value'; process.env.TEST_LOW_PRIORITY = 'env-value'; }); afterEach(async () => { delete process.env.TEST_HIGH_PRIORITY; delete process.env.TEST_LOW_PRIORITY; if (envManager) { await envManager.cleanup(); } }); it('should respect provider priority ordering', async () => { const config = { providers: [ { name: 'environment' as const, priority: 100, // Higher priority prefix: 'TEST_' }, { name: 'file' as const, priority: 50, // Lower priority filePath: '/tmp/test-secrets.json', format: 'json' as const } ] }; envManager = new SecretManager(config); await envManager.initialize(); // Environment provider should be tried first due to higher priority const secret = await envManager.getSecret('HIGH_PRIORITY'); expect(secret).toBe('env-value'); }); it('should fallback to lower priority providers', async () => { const config = { providers: [ { name: 'environment' as const, priority: 100, prefix: 'MISSING_' // This won't match our env vars }, { name: 'environment' as const, priority: 50, prefix: 'TEST_' // This will match } ] }; envManager = new SecretManager(config); await envManager.initialize(); // Should fallback to second provider when first doesn't have the secret const secret = await envManager.getSecret('LOW_PRIORITY'); expect(secret).toBe('env-value'); }); }); describe('SecretManager Integration with MockSecretManager', () => { let mockManager: MockSecretManager; beforeEach(() => { mockManager = new MockSecretManager(); }); afterEach(() => { mockManager.clearSecrets(); }); it('should work with mock implementation for testing', async () => { mockManager.setSecret('TEST_KEY', 'mock-value'); const secret = await mockManager.getSecret('TEST_KEY'); expect(secret).toBe('mock-value'); }); it('should track secret resolution calls', async () => { mockManager.setSecret('TRACKED_KEY', 'tracked-value'); await mockManager.getSecret('TRACKED_KEY'); await mockManager.getSecret('TRACKED_KEY'); const calls = mockManager.getSecretCalls(); expect(calls).toHaveLength(2); expect(calls[0].key).toBe('TRACKED_KEY'); expect(calls[1].key).toBe('TRACKED_KEY'); }); it('should simulate provider-specific secrets', async () => { mockManager.setSecret('ENV_KEY', 'env-secret', 'environment'); mockManager.setSecret('FILE_KEY', 'file-secret', 'file'); const envSecret = await mockManager.getSecret('ENV_KEY', { providerName: 'environment' }); const fileSecret = await mockManager.getSecret('FILE_KEY', { providerName: 'file' }); expect(envSecret).toBe('env-secret'); expect(fileSecret).toBe('file-secret'); }); it('should simulate error conditions', async () => { mockManager.setShouldThrow(new Error('Mock provider failure')); await expect( mockManager.getSecret('ANY_KEY') ).rejects.toThrow('Mock provider failure'); }); });