@snow-tzu/type-config
Version:
Core configuration management system with Spring Boot-like features
469 lines (388 loc) • 15.7 kB
text/typescript
import 'reflect-metadata';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { ConfigManager, InMemoryConfigSource } from '../src';
describe('Cross-Format and Cross-Source Consistency', () => {
let originalEnv: NodeJS.ProcessEnv;
const createdManagers: ConfigManager[] = [];
let tempDir: string;
beforeEach(() => {
originalEnv = { ...process.env };
// Create a temporary directory for test config files
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'config-test-'));
});
afterEach(async () => {
process.env = originalEnv;
for (const manager of createdManagers) {
await manager.dispose();
}
createdManagers.length = 0;
// Clean up temp directory
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
describe('Cross-format consistency (JSON vs YAML)', () => {
it('should resolve placeholders identically in JSON and YAML files', async () => {
process.env.DB_HOST = 'prod-server';
process.env.DB_PORT = '5432';
// Create JSON config
const jsonConfig = {
database: {
host: '${DB_HOST}',
port: '${DB_PORT:3306}',
username: '${DB_USER:postgres}',
password: '${DB_PASSWORD}',
},
};
fs.writeFileSync(path.join(tempDir, 'application.json'), JSON.stringify(jsonConfig, null, 2));
// Create YAML config with identical structure
const yamlConfig = `database:
host: '\${DB_HOST}'
port: '\${DB_PORT:3306}'
username: '\${DB_USER:postgres}'
password: '\${DB_PASSWORD}'
`;
fs.writeFileSync(path.join(tempDir, 'application.yml'), yamlConfig);
// Load JSON config
const jsonManager = new ConfigManager({ configDir: tempDir });
createdManagers.push(jsonManager);
await jsonManager.initialize();
// Load YAML config (create new temp dir to avoid merging)
const tempDir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'config-test-'));
fs.writeFileSync(path.join(tempDir2, 'application.yml'), yamlConfig);
const yamlManager = new ConfigManager({ configDir: tempDir2 });
createdManagers.push(yamlManager);
await yamlManager.initialize();
// Both should resolve identically
expect(jsonManager.get('database.host')).toBe('prod-server');
expect(yamlManager.get('database.host')).toBe('prod-server');
expect(jsonManager.get('database.port')).toBe('5432');
expect(yamlManager.get('database.port')).toBe('5432');
expect(jsonManager.get('database.username')).toBe('postgres');
expect(yamlManager.get('database.username')).toBe('postgres');
expect(jsonManager.get('database.password')).toBeUndefined();
expect(yamlManager.get('database.password')).toBeUndefined();
// Clean up second temp dir
fs.rmSync(tempDir2, { recursive: true, force: true });
});
it('should resolve multiple placeholders identically in JSON and YAML', async () => {
process.env.DB_USER = 'admin';
process.env.DB_NAME = 'mydb';
// JSON config
const jsonConfig = {
database: {
url: 'postgres://${DB_USER}@${DB_HOST:localhost}/${DB_NAME}',
},
};
fs.writeFileSync(path.join(tempDir, 'application.json'), JSON.stringify(jsonConfig, null, 2));
// YAML config
const yamlConfig = `database:
url: 'postgres://\${DB_USER}@\${DB_HOST:localhost}/\${DB_NAME}'
`;
const tempDir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'config-test-'));
fs.writeFileSync(path.join(tempDir2, 'application.yml'), yamlConfig);
const jsonManager = new ConfigManager({ configDir: tempDir });
createdManagers.push(jsonManager);
await jsonManager.initialize();
const yamlManager = new ConfigManager({ configDir: tempDir2 });
createdManagers.push(yamlManager);
await yamlManager.initialize();
const expectedUrl = 'postgres://admin@localhost/mydb';
expect(jsonManager.get('database.url')).toBe(expectedUrl);
expect(yamlManager.get('database.url')).toBe(expectedUrl);
fs.rmSync(tempDir2, { recursive: true, force: true });
});
it('should handle nested structures identically in JSON and YAML', async () => {
process.env.PRIMARY_HOST = 'primary-server';
process.env.SECONDARY_HOST = 'secondary-server';
// JSON config
const jsonConfig = {
databases: {
connections: {
primary: {
host: '${PRIMARY_HOST}',
port: '${PRIMARY_PORT:5432}',
},
secondary: {
host: '${SECONDARY_HOST}',
port: '${SECONDARY_PORT:5433}',
},
},
},
};
fs.writeFileSync(path.join(tempDir, 'application.json'), JSON.stringify(jsonConfig, null, 2));
// YAML config
const yamlConfig = `databases:
connections:
primary:
host: '\${PRIMARY_HOST}'
port: '\${PRIMARY_PORT:5432}'
secondary:
host: '\${SECONDARY_HOST}'
port: '\${SECONDARY_PORT:5433}'
`;
const tempDir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'config-test-'));
fs.writeFileSync(path.join(tempDir2, 'application.yml'), yamlConfig);
const jsonManager = new ConfigManager({ configDir: tempDir });
createdManagers.push(jsonManager);
await jsonManager.initialize();
const yamlManager = new ConfigManager({ configDir: tempDir2 });
createdManagers.push(yamlManager);
await yamlManager.initialize();
expect(jsonManager.get('databases.connections.primary.host')).toBe('primary-server');
expect(yamlManager.get('databases.connections.primary.host')).toBe('primary-server');
expect(jsonManager.get('databases.connections.primary.port')).toBe('5432');
expect(yamlManager.get('databases.connections.primary.port')).toBe('5432');
expect(jsonManager.get('databases.connections.secondary.host')).toBe('secondary-server');
expect(yamlManager.get('databases.connections.secondary.host')).toBe('secondary-server');
expect(jsonManager.get('databases.connections.secondary.port')).toBe('5433');
expect(yamlManager.get('databases.connections.secondary.port')).toBe('5433');
fs.rmSync(tempDir2, { recursive: true, force: true });
});
});
describe('Custom source consistency (InMemoryConfigSource)', () => {
it('should resolve placeholders from InMemoryConfigSource', async () => {
process.env.API_KEY = 'secret-key-123';
process.env.API_URL = 'https://api.example.com';
const config = {
api: {
key: '${API_KEY}',
url: '${API_URL}',
timeout: '${API_TIMEOUT:5000}',
},
};
const manager = new ConfigManager({
additionalSources: [new InMemoryConfigSource(config, 100)],
});
createdManagers.push(manager);
await manager.initialize();
expect(manager.get('api.key')).toBe('secret-key-123');
expect(manager.get('api.url')).toBe('https://api.example.com');
expect(manager.get('api.timeout')).toBe('5000');
});
it('should resolve placeholders consistently across file and custom sources', async () => {
process.env.DB_HOST = 'prod-server';
process.env.CACHE_HOST = 'redis-server';
// File config
const fileConfig = {
database: {
host: '${DB_HOST}',
port: '${DB_PORT:5432}',
},
};
fs.writeFileSync(path.join(tempDir, 'application.json'), JSON.stringify(fileConfig, null, 2));
// Custom source config
const customConfig = {
cache: {
host: '${CACHE_HOST}',
port: '${CACHE_PORT:6379}',
},
};
const manager = new ConfigManager({
configDir: tempDir,
additionalSources: [new InMemoryConfigSource(customConfig, 100)],
});
createdManagers.push(manager);
await manager.initialize();
// Both sources should resolve placeholders identically
expect(manager.get('database.host')).toBe('prod-server');
expect(manager.get('database.port')).toBe('5432');
expect(manager.get('cache.host')).toBe('redis-server');
expect(manager.get('cache.port')).toBe('6379');
});
it('should handle custom sources with complex nested placeholders', async () => {
process.env.SERVICE_NAME = 'my-service';
process.env.REGION = 'us-east-1';
const config = {
services: {
primary: {
name: '${SERVICE_NAME}',
endpoint: 'https://${SERVICE_NAME}.${REGION}.example.com',
config: {
region: '${REGION}',
timeout: '${TIMEOUT:30000}',
},
},
},
};
const manager = new ConfigManager({
additionalSources: [new InMemoryConfigSource(config, 100)],
});
createdManagers.push(manager);
await manager.initialize();
expect(manager.get('services.primary.name')).toBe('my-service');
expect(manager.get('services.primary.endpoint')).toBe(
'https://my-service.us-east-1.example.com'
);
expect(manager.get('services.primary.config.region')).toBe('us-east-1');
expect(manager.get('services.primary.config.timeout')).toBe('30000');
});
});
describe('Consistent multi-reference resolution', () => {
it('should resolve the same environment variable consistently across multiple references', async () => {
process.env.SHARED_HOST = 'shared-server';
const config = {
service1: {
host: '${SHARED_HOST}',
},
service2: {
host: '${SHARED_HOST}',
},
service3: {
endpoint: 'https://${SHARED_HOST}/api',
},
};
const manager = new ConfigManager({
additionalSources: [new InMemoryConfigSource(config, 100)],
});
createdManagers.push(manager);
await manager.initialize();
const host1 = manager.get('service1.host');
const host2 = manager.get('service2.host');
const endpoint = manager.get('service3.endpoint');
expect(host1).toBe('shared-server');
expect(host2).toBe('shared-server');
expect(endpoint).toBe('https://shared-server/api');
// All references should be identical
expect(host1).toBe(host2);
});
it('should resolve the same placeholder with fallback consistently', async () => {
// Don't set DEFAULT_PORT in environment
const config = {
service1: {
port: '${DEFAULT_PORT:8080}',
},
service2: {
port: '${DEFAULT_PORT:8080}',
},
service3: {
url: 'http://localhost:${DEFAULT_PORT:8080}',
},
};
const manager = new ConfigManager({
additionalSources: [new InMemoryConfigSource(config, 100)],
});
createdManagers.push(manager);
await manager.initialize();
const port1 = manager.get('service1.port');
const port2 = manager.get('service2.port');
const url = manager.get('service3.url');
expect(port1).toBe('8080');
expect(port2).toBe('8080');
expect(url).toBe('http://localhost:8080');
// All should use the same fallback value
expect(port1).toBe(port2);
});
it('should resolve consistently when environment variable changes', async () => {
process.env.DYNAMIC_VALUE = 'initial-value';
const config = {
field1: '${DYNAMIC_VALUE}',
field2: '${DYNAMIC_VALUE}',
field3: 'prefix-${DYNAMIC_VALUE}-suffix',
};
const manager = new ConfigManager({
additionalSources: [new InMemoryConfigSource(config, 100)],
});
createdManagers.push(manager);
await manager.initialize();
expect(manager.get('field1')).toBe('initial-value');
expect(manager.get('field2')).toBe('initial-value');
expect(manager.get('field3')).toBe('prefix-initial-value-suffix');
// All references should be consistent
expect(manager.get('field1')).toBe(manager.get('field2'));
});
});
describe('Cross-source with arrays', () => {
it('should resolve placeholders in arrays from custom sources', async () => {
process.env.HOST1 = 'server1.example.com';
process.env.HOST2 = 'server2.example.com';
const config = {
servers: ['${HOST1}', '${HOST2}', '${HOST3:server3.example.com}'],
};
const manager = new ConfigManager({
additionalSources: [new InMemoryConfigSource(config, 100)],
});
createdManagers.push(manager);
await manager.initialize();
expect(manager.get('servers')).toEqual([
'server1.example.com',
'server2.example.com',
'server3.example.com',
]);
});
it('should resolve placeholders in arrays identically across JSON and YAML', async () => {
process.env.ENDPOINT1 = 'https://api1.example.com';
process.env.ENDPOINT2 = 'https://api2.example.com';
// JSON config
const jsonConfig = {
endpoints: ['${ENDPOINT1}', '${ENDPOINT2}', '${ENDPOINT3:https://api3.example.com}'],
};
fs.writeFileSync(path.join(tempDir, 'application.json'), JSON.stringify(jsonConfig, null, 2));
// YAML config
const yamlConfig = `endpoints:
- '\${ENDPOINT1}'
- '\${ENDPOINT2}'
- '\${ENDPOINT3:https://api3.example.com}'
`;
const tempDir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'config-test-'));
fs.writeFileSync(path.join(tempDir2, 'application.yml'), yamlConfig);
const jsonManager = new ConfigManager({ configDir: tempDir });
createdManagers.push(jsonManager);
await jsonManager.initialize();
const yamlManager = new ConfigManager({ configDir: tempDir2 });
createdManagers.push(yamlManager);
await yamlManager.initialize();
const expectedEndpoints = [
'https://api1.example.com',
'https://api2.example.com',
'https://api3.example.com',
];
expect(jsonManager.get('endpoints')).toEqual(expectedEndpoints);
expect(yamlManager.get('endpoints')).toEqual(expectedEndpoints);
fs.rmSync(tempDir2, { recursive: true, force: true });
});
});
describe('Undefined on resolution failure', () => {
it('should return undefined when placeholder has no fallback and env var not set', async () => {
const config = {
database: {
host: '${DB_HOST}',
port: '${DB_PORT:5432}',
},
};
const manager = new ConfigManager({
additionalSources: [new InMemoryConfigSource(config, 100)],
});
createdManagers.push(manager);
await manager.initialize();
expect(manager.get('database.host')).toBeUndefined();
expect(manager.get('database.port')).toBe('5432');
});
it('should handle undefined consistently across all source types', async () => {
// File source
const fileConfig = {
file: {
value: '${MISSING_VAR}',
},
};
fs.writeFileSync(path.join(tempDir, 'application.json'), JSON.stringify(fileConfig, null, 2));
// Custom source
const customConfig = {
custom: {
value: '${MISSING_VAR}',
},
};
const manager = new ConfigManager({
configDir: tempDir,
additionalSources: [new InMemoryConfigSource(customConfig, 100)],
});
createdManagers.push(manager);
await manager.initialize();
// Both should be undefined
expect(manager.get('file.value')).toBeUndefined();
expect(manager.get('custom.value')).toBeUndefined();
});
});
});