created new config lib
This commit is contained in:
parent
bc14acaeba
commit
68a4c2d550
36 changed files with 2681 additions and 134 deletions
215
libs/config-new/test/config-manager.test.ts
Normal file
215
libs/config-new/test/config-manager.test.ts
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
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');
|
||||
});
|
||||
});
|
||||
213
libs/config-new/test/index.test.ts
Normal file
213
libs/config-new/test/index.test.ts
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { writeFileSync, mkdirSync, rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
initializeConfig,
|
||||
getConfig,
|
||||
getConfigManager,
|
||||
resetConfig,
|
||||
getDatabaseConfig,
|
||||
getServiceConfig,
|
||||
getLoggingConfig,
|
||||
getProviderConfig,
|
||||
isDevelopment,
|
||||
isProduction,
|
||||
isTest,
|
||||
} from '../src';
|
||||
|
||||
describe('Config Module', () => {
|
||||
const testConfigDir = join(process.cwd(), 'test-config-module');
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
resetConfig();
|
||||
mkdirSync(testConfigDir, { recursive: true });
|
||||
|
||||
// Create test configuration files
|
||||
const config = {
|
||||
app: {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
},
|
||||
service: {
|
||||
name: 'test-service',
|
||||
port: 3000,
|
||||
},
|
||||
database: {
|
||||
postgres: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'testdb',
|
||||
username: 'testuser',
|
||||
password: 'testpass',
|
||||
},
|
||||
questdb: {
|
||||
host: 'localhost',
|
||||
httpPort: 9000,
|
||||
pgPort: 8812,
|
||||
},
|
||||
mongodb: {
|
||||
uri: 'mongodb://localhost:27017',
|
||||
database: 'testdb',
|
||||
},
|
||||
dragonfly: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
},
|
||||
logging: {
|
||||
level: 'info',
|
||||
format: 'json',
|
||||
},
|
||||
providers: {
|
||||
yahoo: {
|
||||
enabled: true,
|
||||
rateLimit: 5,
|
||||
},
|
||||
quoteMedia: {
|
||||
enabled: false,
|
||||
apiKey: 'test-key',
|
||||
},
|
||||
},
|
||||
features: {
|
||||
realtime: true,
|
||||
backtesting: true,
|
||||
},
|
||||
environment: 'test',
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(testConfigDir, 'default.json'),
|
||||
JSON.stringify(config, null, 2)
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetConfig();
|
||||
rmSync(testConfigDir, { recursive: true, force: true });
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
test('should initialize configuration', async () => {
|
||||
const config = await initializeConfig(testConfigDir);
|
||||
|
||||
expect(config.app.name).toBe('test-app');
|
||||
expect(config.service.port).toBe(3000);
|
||||
expect(config.environment).toBe('test');
|
||||
});
|
||||
|
||||
test('should get configuration after initialization', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
const config = getConfig();
|
||||
|
||||
expect(config.app.name).toBe('test-app');
|
||||
expect(config.database.postgres.host).toBe('localhost');
|
||||
});
|
||||
|
||||
test('should throw if getting config before initialization', () => {
|
||||
expect(() => getConfig()).toThrow('Configuration not initialized');
|
||||
});
|
||||
|
||||
test('should get config manager instance', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
const manager = getConfigManager();
|
||||
|
||||
expect(manager).toBeDefined();
|
||||
expect(manager.get().app.name).toBe('test-app');
|
||||
});
|
||||
|
||||
test('should get database configuration', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
const dbConfig = getDatabaseConfig();
|
||||
|
||||
expect(dbConfig.postgres.host).toBe('localhost');
|
||||
expect(dbConfig.questdb.httpPort).toBe(9000);
|
||||
expect(dbConfig.mongodb.database).toBe('testdb');
|
||||
});
|
||||
|
||||
test('should get service configuration', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
const serviceConfig = getServiceConfig();
|
||||
|
||||
expect(serviceConfig.name).toBe('test-service');
|
||||
expect(serviceConfig.port).toBe(3000);
|
||||
});
|
||||
|
||||
test('should get logging configuration', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
const loggingConfig = getLoggingConfig();
|
||||
|
||||
expect(loggingConfig.level).toBe('info');
|
||||
expect(loggingConfig.format).toBe('json');
|
||||
});
|
||||
|
||||
test('should get provider configuration', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
|
||||
const yahooConfig = getProviderConfig('yahoo');
|
||||
expect(yahooConfig.enabled).toBe(true);
|
||||
expect(yahooConfig.rateLimit).toBe(5);
|
||||
|
||||
const qmConfig = getProviderConfig('quoteMedia');
|
||||
expect(qmConfig.enabled).toBe(false);
|
||||
expect(qmConfig.apiKey).toBe('test-key');
|
||||
});
|
||||
|
||||
test('should throw for non-existent provider', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
|
||||
expect(() => getProviderConfig('nonexistent')).toThrow(
|
||||
'Provider configuration not found: nonexistent'
|
||||
);
|
||||
});
|
||||
|
||||
test('should check environment correctly', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
|
||||
expect(isTest()).toBe(true);
|
||||
expect(isDevelopment()).toBe(false);
|
||||
expect(isProduction()).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle environment overrides', async () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.STOCKBOT_APP__NAME = 'env-override-app';
|
||||
process.env.STOCKBOT_DATABASE__POSTGRES__HOST = 'prod-db';
|
||||
|
||||
const prodConfig = {
|
||||
database: {
|
||||
postgres: {
|
||||
host: 'prod-host',
|
||||
port: 5432,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(testConfigDir, 'production.json'),
|
||||
JSON.stringify(prodConfig, null, 2)
|
||||
);
|
||||
|
||||
const config = await initializeConfig(testConfigDir);
|
||||
|
||||
expect(config.environment).toBe('production');
|
||||
expect(config.app.name).toBe('env-override-app');
|
||||
expect(config.database.postgres.host).toBe('prod-db');
|
||||
expect(isProduction()).toBe(true);
|
||||
});
|
||||
|
||||
test('should reset configuration', async () => {
|
||||
await initializeConfig(testConfigDir);
|
||||
expect(() => getConfig()).not.toThrow();
|
||||
|
||||
resetConfig();
|
||||
expect(() => getConfig()).toThrow('Configuration not initialized');
|
||||
});
|
||||
|
||||
test('should maintain singleton instance', async () => {
|
||||
const config1 = await initializeConfig(testConfigDir);
|
||||
const config2 = await initializeConfig(testConfigDir);
|
||||
|
||||
expect(config1).toBe(config2);
|
||||
});
|
||||
});
|
||||
181
libs/config-new/test/loaders.test.ts
Normal file
181
libs/config-new/test/loaders.test.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { writeFileSync, mkdirSync, rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { EnvLoader } from '../src/loaders/env.loader';
|
||||
import { FileLoader } from '../src/loaders/file.loader';
|
||||
|
||||
describe('EnvLoader', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
test('should load environment variables with prefix', async () => {
|
||||
process.env.TEST_APP_NAME = 'env-app';
|
||||
process.env.TEST_APP_VERSION = '1.0.0';
|
||||
process.env.TEST_DATABASE_HOST = 'env-host';
|
||||
process.env.TEST_DATABASE_PORT = '5432';
|
||||
process.env.OTHER_VAR = 'should-not-load';
|
||||
|
||||
const loader = new EnvLoader('TEST_', { convertCase: false, nestedDelimiter: null });
|
||||
const config = await loader.load();
|
||||
|
||||
expect(config.APP_NAME).toBe('env-app');
|
||||
expect(config.APP_VERSION).toBe('1.0.0');
|
||||
expect(config.DATABASE_HOST).toBe('env-host');
|
||||
expect(config.DATABASE_PORT).toBe(5432); // Should be parsed as number
|
||||
expect(config.OTHER_VAR).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should convert snake_case to camelCase', async () => {
|
||||
process.env.TEST_DATABASE_CONNECTION_STRING = 'postgres://localhost';
|
||||
process.env.TEST_API_KEY_SECRET = 'secret123';
|
||||
|
||||
const loader = new EnvLoader('TEST_', { convertCase: true });
|
||||
const config = await loader.load();
|
||||
|
||||
expect(config.databaseConnectionString).toBe('postgres://localhost');
|
||||
expect(config.apiKeySecret).toBe('secret123');
|
||||
});
|
||||
|
||||
test('should parse JSON values', async () => {
|
||||
process.env.TEST_SETTINGS = '{"feature": true, "limit": 100}';
|
||||
process.env.TEST_NUMBERS = '[1, 2, 3]';
|
||||
|
||||
const loader = new EnvLoader('TEST_', { parseJson: true });
|
||||
const config = await loader.load();
|
||||
|
||||
expect(config.SETTINGS).toEqual({ feature: true, limit: 100 });
|
||||
expect(config.NUMBERS).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test('should parse boolean and number values', async () => {
|
||||
process.env.TEST_ENABLED = 'true';
|
||||
process.env.TEST_DISABLED = 'false';
|
||||
process.env.TEST_PORT = '3000';
|
||||
process.env.TEST_RATIO = '0.75';
|
||||
|
||||
const loader = new EnvLoader('TEST_', { parseValues: true });
|
||||
const config = await loader.load();
|
||||
|
||||
expect(config.ENABLED).toBe(true);
|
||||
expect(config.DISABLED).toBe(false);
|
||||
expect(config.PORT).toBe(3000);
|
||||
expect(config.RATIO).toBe(0.75);
|
||||
});
|
||||
|
||||
test('should handle nested object structure', async () => {
|
||||
process.env.TEST_APP__NAME = 'nested-app';
|
||||
process.env.TEST_APP__SETTINGS__ENABLED = 'true';
|
||||
process.env.TEST_DATABASE__HOST = 'localhost';
|
||||
|
||||
const loader = new EnvLoader('TEST_', {
|
||||
parseValues: true,
|
||||
nestedDelimiter: '__'
|
||||
});
|
||||
const config = await loader.load();
|
||||
|
||||
expect(config.APP).toEqual({
|
||||
NAME: 'nested-app',
|
||||
SETTINGS: {
|
||||
ENABLED: true
|
||||
}
|
||||
});
|
||||
expect(config.DATABASE).toEqual({
|
||||
HOST: 'localhost'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('FileLoader', () => {
|
||||
const testDir = join(process.cwd(), 'test-config');
|
||||
|
||||
beforeEach(() => {
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('should load JSON configuration file', async () => {
|
||||
const config = {
|
||||
app: { name: 'file-app', version: '1.0.0' },
|
||||
database: { host: 'localhost', port: 5432 }
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(testDir, 'default.json'),
|
||||
JSON.stringify(config, null, 2)
|
||||
);
|
||||
|
||||
const loader = new FileLoader(testDir);
|
||||
const loaded = await loader.load();
|
||||
|
||||
expect(loaded).toEqual(config);
|
||||
});
|
||||
|
||||
test('should load environment-specific configuration', async () => {
|
||||
const defaultConfig = {
|
||||
app: { name: 'app', port: 3000 },
|
||||
database: { host: 'localhost' }
|
||||
};
|
||||
|
||||
const prodConfig = {
|
||||
app: { port: 8080 },
|
||||
database: { host: 'prod-db' }
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(testDir, 'default.json'),
|
||||
JSON.stringify(defaultConfig, null, 2)
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
join(testDir, 'production.json'),
|
||||
JSON.stringify(prodConfig, null, 2)
|
||||
);
|
||||
|
||||
const loader = new FileLoader(testDir, 'production');
|
||||
const loaded = await loader.load();
|
||||
|
||||
expect(loaded).toEqual({
|
||||
app: { name: 'app', port: 8080 },
|
||||
database: { host: 'prod-db' }
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle missing configuration files gracefully', async () => {
|
||||
const loader = new FileLoader(testDir);
|
||||
const loaded = await loader.load();
|
||||
|
||||
expect(loaded).toEqual({});
|
||||
});
|
||||
|
||||
test('should throw on invalid JSON', async () => {
|
||||
writeFileSync(
|
||||
join(testDir, 'default.json'),
|
||||
'invalid json content'
|
||||
);
|
||||
|
||||
const loader = new FileLoader(testDir);
|
||||
|
||||
await expect(loader.load()).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('should support custom configuration', async () => {
|
||||
const config = { custom: 'value' };
|
||||
|
||||
writeFileSync(
|
||||
join(testDir, 'custom.json'),
|
||||
JSON.stringify(config, null, 2)
|
||||
);
|
||||
|
||||
const loader = new FileLoader(testDir);
|
||||
const loaded = await loader.loadFile('custom.json');
|
||||
|
||||
expect(loaded).toEqual(config);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue