stock-bot/libs/core/config/test/dynamic-location.test.ts
2025-06-23 18:14:43 -04:00

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
`
);
}