215 lines
No EOL
5.9 KiB
TypeScript
215 lines
No EOL
5.9 KiB
TypeScript
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
import { z } from 'zod';
|
|
import { ConfigManager } from '../src/config-manager';
|
|
import { ConfigLoader } from '../src/types';
|
|
import { ConfigValidationError } from '../src/errors';
|
|
|
|
// Mock loader for testing
|
|
class MockLoader implements ConfigLoader {
|
|
priority = 0;
|
|
|
|
constructor(
|
|
private data: Record<string, unknown>,
|
|
public override priority: number = 0
|
|
) {}
|
|
|
|
async load(): Promise<Record<string, unknown>> {
|
|
return this.data;
|
|
}
|
|
}
|
|
|
|
// Test schema
|
|
const testSchema = z.object({
|
|
app: z.object({
|
|
name: z.string(),
|
|
version: z.string(),
|
|
port: z.number().positive(),
|
|
}),
|
|
database: z.object({
|
|
host: z.string(),
|
|
port: z.number(),
|
|
}),
|
|
environment: z.enum(['development', 'test', 'production']),
|
|
});
|
|
|
|
type TestConfig = z.infer<typeof testSchema>;
|
|
|
|
describe('ConfigManager', () => {
|
|
let manager: ConfigManager<TestConfig>;
|
|
|
|
beforeEach(() => {
|
|
manager = new ConfigManager<TestConfig>({
|
|
loaders: [
|
|
new MockLoader({
|
|
app: {
|
|
name: 'test-app',
|
|
version: '1.0.0',
|
|
port: 3000,
|
|
},
|
|
database: {
|
|
host: 'localhost',
|
|
port: 5432,
|
|
},
|
|
}),
|
|
],
|
|
environment: 'test',
|
|
});
|
|
});
|
|
|
|
test('should initialize configuration', async () => {
|
|
const config = await manager.initialize(testSchema);
|
|
|
|
expect(config.app.name).toBe('test-app');
|
|
expect(config.app.version).toBe('1.0.0');
|
|
expect(config.environment).toBe('test');
|
|
});
|
|
|
|
test('should merge multiple loaders by priority', async () => {
|
|
manager = new ConfigManager<TestConfig>({
|
|
loaders: [
|
|
new MockLoader({ app: { name: 'base', port: 3000 } }, 0),
|
|
new MockLoader({ app: { name: 'override', version: '2.0.0' } }, 10),
|
|
new MockLoader({ database: { host: 'prod-db' } }, 5),
|
|
],
|
|
environment: 'test',
|
|
});
|
|
|
|
const config = await manager.initialize();
|
|
|
|
expect(config.app.name).toBe('override');
|
|
expect(config.app.version).toBe('2.0.0');
|
|
expect(config.app.port).toBe(3000);
|
|
expect(config.database.host).toBe('prod-db');
|
|
});
|
|
|
|
test('should validate configuration with schema', async () => {
|
|
manager = new ConfigManager<TestConfig>({
|
|
loaders: [
|
|
new MockLoader({
|
|
app: {
|
|
name: 'test-app',
|
|
version: '1.0.0',
|
|
port: 'invalid', // Should be number
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
|
|
await expect(manager.initialize(testSchema)).rejects.toThrow(ConfigValidationError);
|
|
});
|
|
|
|
test('should get configuration value by path', async () => {
|
|
await manager.initialize(testSchema);
|
|
|
|
expect(manager.getValue('app.name')).toBe('test-app');
|
|
expect(manager.getValue<number>('database.port')).toBe(5432);
|
|
});
|
|
|
|
test('should check if configuration path exists', async () => {
|
|
await manager.initialize(testSchema);
|
|
|
|
expect(manager.has('app.name')).toBe(true);
|
|
expect(manager.has('app.nonexistent')).toBe(false);
|
|
});
|
|
|
|
test('should update configuration at runtime', async () => {
|
|
await manager.initialize(testSchema);
|
|
|
|
manager.set({
|
|
app: {
|
|
name: 'updated-app',
|
|
},
|
|
});
|
|
|
|
const config = manager.get();
|
|
expect(config.app.name).toBe('updated-app');
|
|
expect(config.app.version).toBe('1.0.0'); // Should preserve other values
|
|
});
|
|
|
|
test('should validate updates against schema', async () => {
|
|
await manager.initialize(testSchema);
|
|
|
|
expect(() => {
|
|
manager.set({
|
|
app: {
|
|
port: 'invalid' as any,
|
|
},
|
|
});
|
|
}).toThrow(ConfigValidationError);
|
|
});
|
|
|
|
test('should reset configuration', async () => {
|
|
await manager.initialize(testSchema);
|
|
manager.reset();
|
|
|
|
expect(() => manager.get()).toThrow('Configuration not initialized');
|
|
});
|
|
|
|
test('should create typed getter', async () => {
|
|
await manager.initialize(testSchema);
|
|
|
|
const appSchema = z.object({
|
|
app: z.object({
|
|
name: z.string(),
|
|
version: z.string(),
|
|
}),
|
|
});
|
|
|
|
const getAppConfig = manager.createTypedGetter(appSchema);
|
|
const appConfig = getAppConfig();
|
|
|
|
expect(appConfig.app.name).toBe('test-app');
|
|
});
|
|
|
|
test('should detect environment correctly', () => {
|
|
const originalEnv = process.env.NODE_ENV;
|
|
|
|
process.env.NODE_ENV = 'production';
|
|
const prodManager = new ConfigManager({ loaders: [] });
|
|
expect(prodManager.getEnvironment()).toBe('production');
|
|
|
|
process.env.NODE_ENV = 'test';
|
|
const testManager = new ConfigManager({ loaders: [] });
|
|
expect(testManager.getEnvironment()).toBe('test');
|
|
|
|
process.env.NODE_ENV = originalEnv;
|
|
});
|
|
|
|
test('should handle deep merge correctly', async () => {
|
|
manager = new ConfigManager({
|
|
loaders: [
|
|
new MockLoader({
|
|
app: {
|
|
settings: {
|
|
feature1: true,
|
|
feature2: false,
|
|
nested: {
|
|
value: 'base',
|
|
},
|
|
},
|
|
},
|
|
}, 0),
|
|
new MockLoader({
|
|
app: {
|
|
settings: {
|
|
feature2: true,
|
|
feature3: true,
|
|
nested: {
|
|
value: 'override',
|
|
extra: 'new',
|
|
},
|
|
},
|
|
},
|
|
}, 10),
|
|
],
|
|
});
|
|
|
|
const config = await manager.initialize();
|
|
|
|
expect(config.app.settings.feature1).toBe(true);
|
|
expect(config.app.settings.feature2).toBe(true);
|
|
expect(config.app.settings.feature3).toBe(true);
|
|
expect(config.app.settings.nested.value).toBe('override');
|
|
expect(config.app.settings.nested.extra).toBe('new');
|
|
});
|
|
}); |