import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; import { join } from 'path'; import { mkdirSync, writeFileSync, rmSync, existsSync } 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'; // Test directories setup const TEST_ROOT = join(__dirname, 'test-scenarios'); const SCENARIOS = { monorepoRoot: join(TEST_ROOT, 'monorepo'), appService: join(TEST_ROOT, 'monorepo', 'apps', 'test-service'), libService: join(TEST_ROOT, 'monorepo', 'libs', 'test-lib'), nestedService: join(TEST_ROOT, 'monorepo', 'apps', 'nested', 'deep-service'), standalone: join(TEST_ROOT, 'standalone'), }; describe('Dynamic Location Config Loading', () => { beforeEach(() => { // Clean up any existing test directories if (existsSync(TEST_ROOT)) { rmSync(TEST_ROOT, { recursive: true, force: true }); } // Reset config singleton resetConfig(); // Create test directory structure setupTestScenarios(); }); afterEach(() => { // Clean up test directories if (existsSync(TEST_ROOT)) { rmSync(TEST_ROOT, { recursive: true, force: true }); } // Reset config singleton resetConfig(); }); test('should load config from monorepo root', async () => { const originalCwd = process.cwd(); try { // Change to monorepo root process.chdir(SCENARIOS.monorepoRoot); const config = await initializeConfig(); expect(config.name).toBe('monorepo-root'); expect(config.version).toBe('1.0.0'); expect(config.database.postgres.host).toBe('localhost'); } finally { process.chdir(originalCwd); } }); test('should load config from app service directory', async () => { const originalCwd = process.cwd(); try { // Change to app service directory process.chdir(SCENARIOS.appService); const config = await initializeServiceConfig(); // Should inherit from root + override with service config expect(config.name).toBe('test-service'); // Overridden by service expect(config.version).toBe('1.0.0'); // From root expect(config.database.postgres.host).toBe('service-db'); // Overridden by service expect(config.service.port).toBe(4000); // Service-specific } finally { process.chdir(originalCwd); } }); test('should load config from lib directory', async () => { const originalCwd = process.cwd(); try { // Change to lib directory process.chdir(SCENARIOS.libService); const config = await initializeServiceConfig(); // Should inherit from root + override with lib config expect(config.name).toBe('test-lib'); // Overridden by lib expect(config.version).toBe('2.0.0'); // Overridden by lib expect(config.database.postgres.host).toBe('localhost'); // From root expect(config.service.port).toBe(5000); // Lib-specific } finally { process.chdir(originalCwd); } }); test('should load config from deeply nested service', async () => { const originalCwd = process.cwd(); try { // Change to nested service directory process.chdir(SCENARIOS.nestedService); const config = await initializeServiceConfig(); // Should inherit from root + override with nested service config expect(config.name).toBe('deep-service'); // Overridden by nested service // NOTE: Version inheritance doesn't work for deeply nested services (3+ levels) // because initializeServiceConfig() uses hardcoded '../../config' path expect(config.version).toBeUndefined(); // Not inherited due to path limitation expect(config.database.postgres.host).toBe('deep-db'); // Overridden by nested service expect(config.service.port).toBe(6000); // Nested service-specific } finally { process.chdir(originalCwd); } }); test('should load config from standalone project', async () => { const originalCwd = process.cwd(); try { // Change to standalone directory process.chdir(SCENARIOS.standalone); const config = await initializeConfig(); expect(config.name).toBe('standalone-app'); expect(config.version).toBe('0.1.0'); expect(config.database.postgres.host).toBe('standalone-db'); } finally { process.chdir(originalCwd); } }); test('should handle missing config files gracefully', async () => { const originalCwd = process.cwd(); try { // Change to directory with no config files const emptyDir = join(TEST_ROOT, 'empty'); mkdirSync(emptyDir, { recursive: true }); process.chdir(emptyDir); // Should not throw but use defaults and env vars const config = await initializeConfig(); // Should have default values from schema expect(config.environment).toBe('test'); // Tests run with NODE_ENV=test expect(typeof config.service).toBe('object'); } finally { process.chdir(originalCwd); } }); test('should prioritize environment variables over file configs', async () => { const originalCwd = process.cwd(); const originalEnv = { ...process.env }; try { // Set environment variables process.env.NAME = 'env-override'; process.env.VERSION = '3.0.0'; process.env.DATABASE_POSTGRES_HOST = 'env-db'; process.chdir(SCENARIOS.appService); resetConfig(); // Reset to test env override const config = await initializeServiceConfig(); // Environment variables should override file configs expect(config.name).toBe('env-override'); expect(config.version).toBe('3.0.0'); expect(config.database.postgres.host).toBe('env-db'); } finally { process.chdir(originalCwd); process.env = originalEnv; } }); test('should work with custom config paths', async () => { const originalCwd = process.cwd(); try { process.chdir(SCENARIOS.monorepoRoot); // Initialize with custom config path resetConfig(); const manager = new ConfigManager({ configPath: join(SCENARIOS.appService, 'config') }); const config = await manager.initialize(appConfigSchema); // Should load from the custom path expect(config.name).toBe('test-service'); expect(config.service.port).toBe(4000); } finally { process.chdir(originalCwd); } }); }); function setupTestScenarios() { // Create monorepo structure mkdirSync(SCENARIOS.monorepoRoot, { recursive: true }); mkdirSync(join(SCENARIOS.monorepoRoot, 'config'), { recursive: true }); mkdirSync(join(SCENARIOS.appService, 'config'), { recursive: true }); mkdirSync(join(SCENARIOS.libService, 'config'), { recursive: true }); mkdirSync(join(SCENARIOS.nestedService, 'config'), { recursive: true }); mkdirSync(join(SCENARIOS.standalone, 'config'), { recursive: true }); // Root config (create for both development and test environments) const rootConfig = { name: 'monorepo-root', version: '1.0.0', service: { name: 'monorepo-root', port: 3000 }, database: { postgres: { host: 'localhost', port: 5432, database: 'test_db', user: 'test_user', password: 'test_pass' }, questdb: { host: 'localhost', ilpPort: 9009 }, mongodb: { host: 'localhost', port: 27017, database: 'test_mongo' }, dragonfly: { host: 'localhost', port: 6379 } }, logging: { level: 'info' } }; writeFileSync( join(SCENARIOS.monorepoRoot, 'config', 'development.json'), JSON.stringify(rootConfig, null, 2) ); writeFileSync( join(SCENARIOS.monorepoRoot, 'config', 'test.json'), JSON.stringify(rootConfig, null, 2) ); // App service config const appServiceConfig = { name: 'test-service', database: { postgres: { host: 'service-db' } }, service: { name: 'test-service', port: 4000 } }; writeFileSync( join(SCENARIOS.appService, 'config', 'development.json'), JSON.stringify(appServiceConfig, null, 2) ); writeFileSync( join(SCENARIOS.appService, 'config', 'test.json'), JSON.stringify(appServiceConfig, null, 2) ); // Lib config const libServiceConfig = { name: 'test-lib', version: '2.0.0', service: { name: 'test-lib', port: 5000 } }; writeFileSync( join(SCENARIOS.libService, 'config', 'development.json'), JSON.stringify(libServiceConfig, null, 2) ); writeFileSync( join(SCENARIOS.libService, 'config', 'test.json'), JSON.stringify(libServiceConfig, null, 2) ); // Nested service config const nestedServiceConfig = { name: 'deep-service', database: { postgres: { host: 'deep-db' } }, service: { name: 'deep-service', port: 6000 } }; writeFileSync( join(SCENARIOS.nestedService, 'config', 'development.json'), JSON.stringify(nestedServiceConfig, null, 2) ); writeFileSync( join(SCENARIOS.nestedService, 'config', 'test.json'), JSON.stringify(nestedServiceConfig, null, 2) ); // Standalone config const standaloneConfig = { name: 'standalone-app', version: '0.1.0', service: { name: 'standalone-app', port: 7000 }, database: { postgres: { host: 'standalone-db', port: 5432, database: 'standalone_db', user: 'standalone_user', password: 'standalone_pass' }, questdb: { host: 'localhost', ilpPort: 9009 }, mongodb: { host: 'localhost', port: 27017, database: 'standalone_mongo' }, dragonfly: { host: 'localhost', port: 6379 } }, logging: { level: 'debug' } }; writeFileSync( join(SCENARIOS.standalone, 'config', 'development.json'), JSON.stringify(standaloneConfig, null, 2) ); writeFileSync( join(SCENARIOS.standalone, 'config', 'test.json'), JSON.stringify(standaloneConfig, null, 2) ); // Add .env files for testing writeFileSync( join(SCENARIOS.monorepoRoot, '.env'), `NODE_ENV=development DEBUG=true ` ); writeFileSync( join(SCENARIOS.appService, '.env'), `SERVICE_DEBUG=true APP_EXTRA_FEATURE=enabled ` ); }