384 lines
10 KiB
TypeScript
384 lines
10 KiB
TypeScript
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
import { ConfigManager } from '../src/config-manager';
|
|
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
|
|
`
|
|
);
|
|
}
|