import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; import { join } from 'path'; import { mkdirSync, writeFileSync, rmSync, existsSync, chmodSync } from 'fs'; import { ConfigManager } from '../src/config-manager'; import { FileLoader } from '../src/loaders/file.loader'; import { EnvLoader } from '../src/loaders/env.loader'; import { initializeConfig, initializeServiceConfig, resetConfig } from '../src/index'; import { appConfigSchema } from '../src/schemas'; import { ConfigError, ConfigValidationError } from '../src/errors'; const TEST_DIR = join(__dirname, 'edge-case-tests'); describe('Edge Cases and Error Handling', () => { let originalEnv: NodeJS.ProcessEnv; let originalCwd: string; beforeEach(() => { originalEnv = { ...process.env }; originalCwd = process.cwd(); resetConfig(); if (existsSync(TEST_DIR)) { rmSync(TEST_DIR, { recursive: true, force: true }); } mkdirSync(TEST_DIR, { recursive: true }); }); afterEach(() => { process.env = originalEnv; process.chdir(originalCwd); resetConfig(); if (existsSync(TEST_DIR)) { rmSync(TEST_DIR, { recursive: true, force: true }); } }); test('should handle missing .env files gracefully', async () => { // No .env file exists const manager = new ConfigManager({ loaders: [new EnvLoader('')] }); // Should not throw even without .env file const config = await manager.initialize(appConfigSchema); expect(config).toBeDefined(); }); test('should handle corrupted JSON config files', async () => { const configDir = join(TEST_DIR, 'config'); mkdirSync(configDir, { recursive: true }); // Create corrupted JSON file writeFileSync( join(configDir, 'development.json'), '{ "app": { "name": "test", invalid json }' ); const manager = new ConfigManager({ loaders: [new FileLoader(configDir, 'development')] }); // Should throw error for invalid JSON await expect(manager.initialize(appConfigSchema)).rejects.toThrow(); }); test('should handle missing config directories', async () => { const nonExistentDir = join(TEST_DIR, 'nonexistent'); const manager = new ConfigManager({ loaders: [new FileLoader(nonExistentDir, 'development')] }); // Should not throw, should return empty config const config = await manager.initialize(); expect(config).toBeDefined(); }); test('should handle permission denied on config files', async () => { const configDir = join(TEST_DIR, 'config'); mkdirSync(configDir, { recursive: true }); const configFile = join(configDir, 'development.json'); writeFileSync(configFile, JSON.stringify({ app: { name: 'test' } })); // Make file unreadable (this might not work on all systems) try { chmodSync(configFile, 0o000); const manager = new ConfigManager({ loaders: [new FileLoader(configDir, 'development')] }); // Should handle permission error gracefully const config = await manager.initialize(); expect(config).toBeDefined(); } finally { // Restore permissions for cleanup try { chmodSync(configFile, 0o644); } catch { // Ignore errors during cleanup } } }); test('should handle circular references in config merging', async () => { // This tests deep merge with potential circular references const configDir = join(TEST_DIR, 'config'); mkdirSync(configDir, { recursive: true }); writeFileSync( join(configDir, 'development.json'), JSON.stringify({ app: { name: 'test', settings: { ref: 'settings' } } }) ); process.env.APP_SETTINGS_NESTED_VALUE = 'deep-value'; const manager = new ConfigManager({ loaders: [ new FileLoader(configDir, 'development'), new EnvLoader('') ] }); const config = await manager.initialize(appConfigSchema); expect(config.app.name).toBe('test'); }); test('should handle extremely deep nesting in environment variables', async () => { // Test very deep nesting process.env.LEVEL1_LEVEL2_LEVEL3_LEVEL4_LEVEL5_VALUE = 'deep-value'; const manager = new ConfigManager({ loaders: [new EnvLoader('', { nestedDelimiter: '_' })] }); const config = await manager.initialize(); // Should create nested structure expect((config as any).level1?.level2?.level3?.level4?.level5?.value).toBe('deep-value'); }); test('should handle conflicting data types in config merging', async () => { const configDir = join(TEST_DIR, 'config'); mkdirSync(configDir, { recursive: true }); // File config has object writeFileSync( join(configDir, 'development.json'), JSON.stringify({ database: { host: 'localhost', port: 5432 } }) ); // Environment variable tries to override with string process.env.DATABASE = 'simple-string'; const manager = new ConfigManager({ loaders: [ new FileLoader(configDir, 'development'), new EnvLoader('') ] }); const config = await manager.initialize(appConfigSchema); // Environment variable should win expect(config.database).toBe('simple-string'); }); test('should handle different working directories', async () => { // Create multiple config setups in different directories const dir1 = join(TEST_DIR, 'dir1'); const dir2 = join(TEST_DIR, 'dir2'); mkdirSync(join(dir1, 'config'), { recursive: true }); mkdirSync(join(dir2, 'config'), { recursive: true }); writeFileSync( join(dir1, 'config', 'development.json'), JSON.stringify({ app: { name: 'dir1-app' } }) ); writeFileSync( join(dir2, 'config', 'development.json'), JSON.stringify({ app: { name: 'dir2-app' } }) ); // Test from dir1 process.chdir(dir1); resetConfig(); let config = await initializeConfig(); expect(config.app.name).toBe('dir1-app'); // Test from dir2 process.chdir(dir2); resetConfig(); config = await initializeConfig(); expect(config.app.name).toBe('dir2-app'); }); test('should handle malformed .env files', async () => { // Create malformed .env file writeFileSync( join(TEST_DIR, '.env'), `# Good line VALID_KEY=valid_value # Malformed lines MISSING_VALUE= =MISSING_KEY SPACES IN KEY=value KEY_WITH_QUOTES="quoted value" KEY_WITH_SINGLE_QUOTES='single quoted' # Complex value JSON_VALUE={"key": "value", "nested": {"array": [1, 2, 3]}} ` ); process.chdir(TEST_DIR); const manager = new ConfigManager({ loaders: [new EnvLoader('')] }); const config = await manager.initialize(); // Should handle valid entries expect(process.env.VALID_KEY).toBe('valid_value'); expect(process.env.KEY_WITH_QUOTES).toBe('quoted value'); expect(process.env.KEY_WITH_SINGLE_QUOTES).toBe('single quoted'); }); test('should handle empty config files', async () => { const configDir = join(TEST_DIR, 'config'); mkdirSync(configDir, { recursive: true }); // Create empty JSON file writeFileSync(join(configDir, 'development.json'), '{}'); const manager = new ConfigManager({ loaders: [new FileLoader(configDir, 'development')] }); const config = await manager.initialize(appConfigSchema); expect(config).toBeDefined(); expect(config.environment).toBe('development'); // Should have default }); test('should handle config initialization without schema', async () => { const manager = new ConfigManager({ loaders: [new EnvLoader('')] }); // Initialize without schema const config = await manager.initialize(); expect(config).toBeDefined(); expect(typeof config).toBe('object'); }); test('should handle accessing config before initialization', () => { const manager = new ConfigManager({ loaders: [new EnvLoader('')] }); // Should throw error when accessing uninitialized config expect(() => manager.get()).toThrow('Configuration not initialized'); expect(() => manager.getValue('some.path')).toThrow('Configuration not initialized'); expect(() => manager.has('some.path')).toThrow('Configuration not initialized'); }); test('should handle invalid config paths in getValue', async () => { const manager = new ConfigManager({ loaders: [new EnvLoader('')] }); const config = await manager.initialize(appConfigSchema); // Should throw for invalid paths expect(() => manager.getValue('nonexistent.path')).toThrow('Configuration key not found'); expect(() => manager.getValue('app.nonexistent')).toThrow('Configuration key not found'); // Should work for valid paths expect(() => manager.getValue('environment')).not.toThrow(); }); test('should handle null and undefined values in config', async () => { process.env.NULL_VALUE = 'null'; process.env.UNDEFINED_VALUE = 'undefined'; process.env.EMPTY_VALUE = ''; const manager = new ConfigManager({ loaders: [new EnvLoader('')] }); const config = await manager.initialize(); expect((config as any).null_value).toBe(null); expect((config as any).undefined_value).toBe(undefined); expect((config as any).empty_value).toBe(''); }); test('should handle schema validation failures', async () => { // Set up config that will fail schema validation process.env.APP_NAME = 'valid-name'; process.env.APP_VERSION = 'valid-version'; process.env.SERVICE_PORT = 'not-a-number'; // This should cause validation to fail const manager = new ConfigManager({ loaders: [new EnvLoader('')] }); await expect(manager.initialize(appConfigSchema)).rejects.toThrow(ConfigValidationError); }); test('should handle config updates with invalid schema', async () => { const manager = new ConfigManager({ loaders: [new EnvLoader('')] }); await manager.initialize(appConfigSchema); // Try to update with invalid data expect(() => { manager.set({ service: { port: 'invalid-port' as any } }); }).toThrow(ConfigValidationError); }); test('should handle loader priority conflicts', async () => { const configDir = join(TEST_DIR, 'config'); mkdirSync(configDir, { recursive: true }); writeFileSync( join(configDir, 'development.json'), JSON.stringify({ app: { name: 'file-config' } }) ); process.env.APP_NAME = 'env-config'; // Create loaders with different priorities const manager = new ConfigManager({ loaders: [ new FileLoader(configDir, 'development'), // priority 50 new EnvLoader('') // priority 100 ] }); const config = await manager.initialize(appConfigSchema); // Environment should win due to higher priority expect(config.app.name).toBe('env-config'); }); test('should handle readonly environment variables', async () => { // Some system environment variables might be readonly const originalPath = process.env.PATH; // This should not cause the loader to fail const manager = new ConfigManager({ loaders: [new EnvLoader('')] }); const config = await manager.initialize(); expect(config).toBeDefined(); // PATH should not be modified expect(process.env.PATH).toBe(originalPath); }); });