format
This commit is contained in:
parent
d858222af7
commit
7d9044ab29
202 changed files with 10755 additions and 10972 deletions
|
|
@ -1,215 +1,221 @@
|
|||
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');
|
||||
});
|
||||
});
|
||||
import { beforeEach, describe, expect, test } from 'bun:test';
|
||||
import { z } from 'zod';
|
||||
import { ConfigManager } from '../src/config-manager';
|
||||
import { ConfigValidationError } from '../src/errors';
|
||||
import { ConfigLoader } from '../src/types';
|
||||
|
||||
// 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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import { ConfigManager } from '../src/config-manager';
|
||||
import { FileLoader } from '../src/loaders/file.loader';
|
||||
import { EnvLoader } from '../src/loaders/env.loader';
|
||||
import { initializeConfig, initializeServiceConfig, resetConfig } from '../src/index';
|
||||
import { EnvLoader } from '../src/loaders/env.loader';
|
||||
import { FileLoader } from '../src/loaders/file.loader';
|
||||
import { appConfigSchema } from '../src/schemas';
|
||||
|
||||
// Test directories setup
|
||||
|
|
@ -23,33 +23,33 @@ describe('Dynamic Location Config Loading', () => {
|
|||
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');
|
||||
|
|
@ -60,13 +60,13 @@ describe('Dynamic Location Config Loading', () => {
|
|||
|
||||
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
|
||||
|
|
@ -79,13 +79,13 @@ describe('Dynamic Location Config Loading', () => {
|
|||
|
||||
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
|
||||
|
|
@ -98,13 +98,13 @@ describe('Dynamic Location Config Loading', () => {
|
|||
|
||||
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)
|
||||
|
|
@ -119,13 +119,13 @@ describe('Dynamic Location Config Loading', () => {
|
|||
|
||||
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');
|
||||
|
|
@ -136,16 +136,16 @@ describe('Dynamic Location Config Loading', () => {
|
|||
|
||||
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');
|
||||
|
|
@ -157,18 +157,18 @@ describe('Dynamic Location Config Loading', () => {
|
|||
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');
|
||||
|
|
@ -181,18 +181,18 @@ describe('Dynamic Location Config Loading', () => {
|
|||
|
||||
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')
|
||||
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);
|
||||
|
|
@ -217,7 +217,7 @@ function setupTestScenarios() {
|
|||
version: '1.0.0',
|
||||
service: {
|
||||
name: 'monorepo-root',
|
||||
port: 3000
|
||||
port: 3000,
|
||||
},
|
||||
database: {
|
||||
postgres: {
|
||||
|
|
@ -225,32 +225,32 @@ function setupTestScenarios() {
|
|||
port: 5432,
|
||||
database: 'test_db',
|
||||
user: 'test_user',
|
||||
password: 'test_pass'
|
||||
password: 'test_pass',
|
||||
},
|
||||
questdb: {
|
||||
host: 'localhost',
|
||||
ilpPort: 9009
|
||||
ilpPort: 9009,
|
||||
},
|
||||
mongodb: {
|
||||
host: 'localhost',
|
||||
port: 27017,
|
||||
database: 'test_mongo'
|
||||
database: 'test_mongo',
|
||||
},
|
||||
dragonfly: {
|
||||
host: 'localhost',
|
||||
port: 6379
|
||||
}
|
||||
port: 6379,
|
||||
},
|
||||
},
|
||||
logging: {
|
||||
level: 'info'
|
||||
}
|
||||
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)
|
||||
|
|
@ -261,20 +261,20 @@ function setupTestScenarios() {
|
|||
name: 'test-service',
|
||||
database: {
|
||||
postgres: {
|
||||
host: 'service-db'
|
||||
}
|
||||
host: 'service-db',
|
||||
},
|
||||
},
|
||||
service: {
|
||||
name: 'test-service',
|
||||
port: 4000
|
||||
}
|
||||
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)
|
||||
|
|
@ -286,15 +286,15 @@ function setupTestScenarios() {
|
|||
version: '2.0.0',
|
||||
service: {
|
||||
name: 'test-lib',
|
||||
port: 5000
|
||||
}
|
||||
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)
|
||||
|
|
@ -305,20 +305,20 @@ function setupTestScenarios() {
|
|||
name: 'deep-service',
|
||||
database: {
|
||||
postgres: {
|
||||
host: 'deep-db'
|
||||
}
|
||||
host: 'deep-db',
|
||||
},
|
||||
},
|
||||
service: {
|
||||
name: 'deep-service',
|
||||
port: 6000
|
||||
}
|
||||
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)
|
||||
|
|
@ -330,7 +330,7 @@ function setupTestScenarios() {
|
|||
version: '0.1.0',
|
||||
service: {
|
||||
name: 'standalone-app',
|
||||
port: 7000
|
||||
port: 7000,
|
||||
},
|
||||
database: {
|
||||
postgres: {
|
||||
|
|
@ -338,32 +338,32 @@ function setupTestScenarios() {
|
|||
port: 5432,
|
||||
database: 'standalone_db',
|
||||
user: 'standalone_user',
|
||||
password: 'standalone_pass'
|
||||
password: 'standalone_pass',
|
||||
},
|
||||
questdb: {
|
||||
host: 'localhost',
|
||||
ilpPort: 9009
|
||||
ilpPort: 9009,
|
||||
},
|
||||
mongodb: {
|
||||
host: 'localhost',
|
||||
port: 27017,
|
||||
database: 'standalone_mongo'
|
||||
database: 'standalone_mongo',
|
||||
},
|
||||
dragonfly: {
|
||||
host: 'localhost',
|
||||
port: 6379
|
||||
}
|
||||
port: 6379,
|
||||
},
|
||||
},
|
||||
logging: {
|
||||
level: 'debug'
|
||||
}
|
||||
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)
|
||||
|
|
@ -383,4 +383,4 @@ DEBUG=true
|
|||
APP_EXTRA_FEATURE=enabled
|
||||
`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { mkdirSync, writeFileSync, rmSync, existsSync, chmodSync } from 'fs';
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import { ConfigManager } from '../src/config-manager';
|
||||
import { FileLoader } from '../src/loaders/file.loader';
|
||||
import { EnvLoader } from '../src/loaders/env.loader';
|
||||
import { initializeConfig, initializeServiceConfig, resetConfig } from '../src/index';
|
||||
import { appConfigSchema } from '../src/schemas';
|
||||
import { ConfigError, ConfigValidationError } from '../src/errors';
|
||||
import { initializeConfig, initializeServiceConfig, resetConfig } from '../src/index';
|
||||
import { EnvLoader } from '../src/loaders/env.loader';
|
||||
import { FileLoader } from '../src/loaders/file.loader';
|
||||
import { appConfigSchema } from '../src/schemas';
|
||||
|
||||
const TEST_DIR = join(__dirname, 'edge-case-tests');
|
||||
|
||||
|
|
@ -17,9 +17,9 @@ describe('Edge Cases and Error Handling', () => {
|
|||
beforeEach(() => {
|
||||
originalEnv = { ...process.env };
|
||||
originalCwd = process.cwd();
|
||||
|
||||
|
||||
resetConfig();
|
||||
|
||||
|
||||
if (existsSync(TEST_DIR)) {
|
||||
rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ describe('Edge Cases and Error Handling', () => {
|
|||
process.env = originalEnv;
|
||||
process.chdir(originalCwd);
|
||||
resetConfig();
|
||||
|
||||
|
||||
if (existsSync(TEST_DIR)) {
|
||||
rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ describe('Edge Cases and Error Handling', () => {
|
|||
test('should handle missing .env files gracefully', async () => {
|
||||
// No .env file exists
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
// Should not throw even without .env file
|
||||
|
|
@ -50,15 +50,12 @@ describe('Edge Cases and Error Handling', () => {
|
|||
test('should handle corrupted JSON config files', async () => {
|
||||
const configDir = join(TEST_DIR, 'config');
|
||||
mkdirSync(configDir, { recursive: true });
|
||||
|
||||
|
||||
// Create corrupted JSON file
|
||||
writeFileSync(
|
||||
join(configDir, 'development.json'),
|
||||
'{ "app": { "name": "test", invalid json }'
|
||||
);
|
||||
writeFileSync(join(configDir, 'development.json'), '{ "app": { "name": "test", invalid json }');
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new FileLoader(configDir, 'development')]
|
||||
loaders: [new FileLoader(configDir, 'development')],
|
||||
});
|
||||
|
||||
// Should throw error for invalid JSON
|
||||
|
|
@ -67,9 +64,9 @@ describe('Edge Cases and Error Handling', () => {
|
|||
|
||||
test('should handle missing config directories', async () => {
|
||||
const nonExistentDir = join(TEST_DIR, 'nonexistent');
|
||||
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new FileLoader(nonExistentDir, 'development')]
|
||||
loaders: [new FileLoader(nonExistentDir, 'development')],
|
||||
});
|
||||
|
||||
// Should not throw, should return empty config
|
||||
|
|
@ -80,16 +77,16 @@ describe('Edge Cases and Error Handling', () => {
|
|||
test('should handle permission denied on config files', async () => {
|
||||
const configDir = join(TEST_DIR, 'config');
|
||||
mkdirSync(configDir, { recursive: true });
|
||||
|
||||
|
||||
const configFile = join(configDir, 'development.json');
|
||||
writeFileSync(configFile, JSON.stringify({ app: { name: 'test' } }));
|
||||
|
||||
|
||||
// Make file unreadable (this might not work on all systems)
|
||||
try {
|
||||
chmodSync(configFile, 0o000);
|
||||
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new FileLoader(configDir, 'development')]
|
||||
loaders: [new FileLoader(configDir, 'development')],
|
||||
});
|
||||
|
||||
// Should handle permission error gracefully
|
||||
|
|
@ -109,26 +106,23 @@ describe('Edge Cases and Error Handling', () => {
|
|||
// This tests deep merge with potential circular references
|
||||
const configDir = join(TEST_DIR, 'config');
|
||||
mkdirSync(configDir, { recursive: true });
|
||||
|
||||
|
||||
writeFileSync(
|
||||
join(configDir, 'development.json'),
|
||||
JSON.stringify({
|
||||
app: {
|
||||
name: 'test',
|
||||
settings: {
|
||||
ref: 'settings'
|
||||
}
|
||||
}
|
||||
ref: 'settings',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
process.env.APP_SETTINGS_NESTED_VALUE = 'deep-value';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [
|
||||
new FileLoader(configDir, 'development'),
|
||||
new EnvLoader('')
|
||||
]
|
||||
loaders: [new FileLoader(configDir, 'development'), new EnvLoader('')],
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
|
@ -138,13 +132,13 @@ describe('Edge Cases and Error Handling', () => {
|
|||
test('should handle extremely deep nesting in environment variables', async () => {
|
||||
// Test very deep nesting
|
||||
process.env.LEVEL1_LEVEL2_LEVEL3_LEVEL4_LEVEL5_VALUE = 'deep-value';
|
||||
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('', { nestedDelimiter: '_' })]
|
||||
loaders: [new EnvLoader('', { nestedDelimiter: '_' })],
|
||||
});
|
||||
|
||||
const config = await manager.initialize();
|
||||
|
||||
|
||||
// Should create nested structure
|
||||
expect((config as any).level1?.level2?.level3?.level4?.level5?.value).toBe('deep-value');
|
||||
});
|
||||
|
|
@ -152,15 +146,15 @@ describe('Edge Cases and Error Handling', () => {
|
|||
test('should handle conflicting data types in config merging', async () => {
|
||||
const configDir = join(TEST_DIR, 'config');
|
||||
mkdirSync(configDir, { recursive: true });
|
||||
|
||||
|
||||
// File config has object
|
||||
writeFileSync(
|
||||
join(configDir, 'development.json'),
|
||||
JSON.stringify({
|
||||
database: {
|
||||
host: 'localhost',
|
||||
port: 5432
|
||||
}
|
||||
port: 5432,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
|
|
@ -168,14 +162,11 @@ describe('Edge Cases and Error Handling', () => {
|
|||
process.env.DATABASE = 'simple-string';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [
|
||||
new FileLoader(configDir, 'development'),
|
||||
new EnvLoader('')
|
||||
]
|
||||
loaders: [new FileLoader(configDir, 'development'), new EnvLoader('')],
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
||||
|
||||
// Environment variable should win
|
||||
expect(config.database).toBe('simple-string');
|
||||
});
|
||||
|
|
@ -184,15 +175,15 @@ describe('Edge Cases and Error Handling', () => {
|
|||
// Create multiple config setups in different directories
|
||||
const dir1 = join(TEST_DIR, 'dir1');
|
||||
const dir2 = join(TEST_DIR, 'dir2');
|
||||
|
||||
|
||||
mkdirSync(join(dir1, 'config'), { recursive: true });
|
||||
mkdirSync(join(dir2, 'config'), { recursive: true });
|
||||
|
||||
|
||||
writeFileSync(
|
||||
join(dir1, 'config', 'development.json'),
|
||||
JSON.stringify({ app: { name: 'dir1-app' } })
|
||||
);
|
||||
|
||||
|
||||
writeFileSync(
|
||||
join(dir2, 'config', 'development.json'),
|
||||
JSON.stringify({ app: { name: 'dir2-app' } })
|
||||
|
|
@ -229,13 +220,13 @@ JSON_VALUE={"key": "value", "nested": {"array": [1, 2, 3]}}
|
|||
);
|
||||
|
||||
process.chdir(TEST_DIR);
|
||||
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
const config = await manager.initialize();
|
||||
|
||||
|
||||
// Should handle valid entries
|
||||
expect(process.env.VALID_KEY).toBe('valid_value');
|
||||
expect(process.env.KEY_WITH_QUOTES).toBe('quoted value');
|
||||
|
|
@ -245,12 +236,12 @@ JSON_VALUE={"key": "value", "nested": {"array": [1, 2, 3]}}
|
|||
test('should handle empty config files', async () => {
|
||||
const configDir = join(TEST_DIR, 'config');
|
||||
mkdirSync(configDir, { recursive: true });
|
||||
|
||||
|
||||
// Create empty JSON file
|
||||
writeFileSync(join(configDir, 'development.json'), '{}');
|
||||
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new FileLoader(configDir, 'development')]
|
||||
loaders: [new FileLoader(configDir, 'development')],
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
|
@ -260,7 +251,7 @@ JSON_VALUE={"key": "value", "nested": {"array": [1, 2, 3]}}
|
|||
|
||||
test('should handle config initialization without schema', async () => {
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
// Initialize without schema
|
||||
|
|
@ -271,7 +262,7 @@ JSON_VALUE={"key": "value", "nested": {"array": [1, 2, 3]}}
|
|||
|
||||
test('should handle accessing config before initialization', () => {
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
// Should throw error when accessing uninitialized config
|
||||
|
|
@ -282,15 +273,15 @@ JSON_VALUE={"key": "value", "nested": {"array": [1, 2, 3]}}
|
|||
|
||||
test('should handle invalid config paths in getValue', async () => {
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
||||
|
||||
// Should throw for invalid paths
|
||||
expect(() => manager.getValue('nonexistent.path')).toThrow('Configuration key not found');
|
||||
expect(() => manager.getValue('app.nonexistent')).toThrow('Configuration key not found');
|
||||
|
||||
|
||||
// Should work for valid paths
|
||||
expect(() => manager.getValue('environment')).not.toThrow();
|
||||
});
|
||||
|
|
@ -301,11 +292,11 @@ JSON_VALUE={"key": "value", "nested": {"array": [1, 2, 3]}}
|
|||
process.env.EMPTY_VALUE = '';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
const config = await manager.initialize();
|
||||
|
||||
|
||||
expect((config as any).null_value).toBe(null);
|
||||
expect((config as any).undefined_value).toBe(undefined);
|
||||
expect((config as any).empty_value).toBe('');
|
||||
|
|
@ -318,7 +309,7 @@ JSON_VALUE={"key": "value", "nested": {"array": [1, 2, 3]}}
|
|||
process.env.SERVICE_PORT = 'not-a-number'; // This should cause validation to fail
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
await expect(manager.initialize(appConfigSchema)).rejects.toThrow(ConfigValidationError);
|
||||
|
|
@ -326,7 +317,7 @@ JSON_VALUE={"key": "value", "nested": {"array": [1, 2, 3]}}
|
|||
|
||||
test('should handle config updates with invalid schema', async () => {
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
await manager.initialize(appConfigSchema);
|
||||
|
|
@ -335,8 +326,8 @@ JSON_VALUE={"key": "value", "nested": {"array": [1, 2, 3]}}
|
|||
expect(() => {
|
||||
manager.set({
|
||||
service: {
|
||||
port: 'invalid-port' as any
|
||||
}
|
||||
port: 'invalid-port' as any,
|
||||
},
|
||||
});
|
||||
}).toThrow(ConfigValidationError);
|
||||
});
|
||||
|
|
@ -344,7 +335,7 @@ JSON_VALUE={"key": "value", "nested": {"array": [1, 2, 3]}}
|
|||
test('should handle loader priority conflicts', async () => {
|
||||
const configDir = join(TEST_DIR, 'config');
|
||||
mkdirSync(configDir, { recursive: true });
|
||||
|
||||
|
||||
writeFileSync(
|
||||
join(configDir, 'development.json'),
|
||||
JSON.stringify({ app: { name: 'file-config' } })
|
||||
|
|
@ -356,12 +347,12 @@ JSON_VALUE={"key": "value", "nested": {"array": [1, 2, 3]}}
|
|||
const manager = new ConfigManager({
|
||||
loaders: [
|
||||
new FileLoader(configDir, 'development'), // priority 50
|
||||
new EnvLoader('') // priority 100
|
||||
]
|
||||
new EnvLoader(''), // priority 100
|
||||
],
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
||||
|
||||
// Environment should win due to higher priority
|
||||
expect(config.app.name).toBe('env-config');
|
||||
});
|
||||
|
|
@ -369,16 +360,16 @@ JSON_VALUE={"key": "value", "nested": {"array": [1, 2, 3]}}
|
|||
test('should handle readonly environment variables', async () => {
|
||||
// Some system environment variables might be readonly
|
||||
const originalPath = process.env.PATH;
|
||||
|
||||
|
||||
// This should not cause the loader to fail
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
const config = await manager.initialize();
|
||||
expect(config).toBeDefined();
|
||||
|
||||
|
||||
// PATH should not be modified
|
||||
expect(process.env.PATH).toBe(originalPath);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,208 +1,202 @@
|
|||
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 = {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
service: {
|
||||
name: 'test-service',
|
||||
port: 3000,
|
||||
},
|
||||
database: {
|
||||
postgres: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'testdb',
|
||||
user: 'testuser',
|
||||
password: 'testpass',
|
||||
},
|
||||
questdb: {
|
||||
host: 'localhost',
|
||||
httpPort: 9000,
|
||||
pgPort: 8812,
|
||||
},
|
||||
mongodb: {
|
||||
host: 'localhost',
|
||||
port: 27017,
|
||||
database: 'testdb',
|
||||
},
|
||||
dragonfly: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
},
|
||||
logging: {
|
||||
level: 'info',
|
||||
format: 'json',
|
||||
},
|
||||
providers: {
|
||||
yahoo: {
|
||||
enabled: true,
|
||||
rateLimit: 5,
|
||||
},
|
||||
qm: {
|
||||
enabled: false,
|
||||
apiKey: 'test-key',
|
||||
},
|
||||
},
|
||||
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);
|
||||
});
|
||||
});
|
||||
import { mkdirSync, rmSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import {
|
||||
getConfig,
|
||||
getConfigManager,
|
||||
getDatabaseConfig,
|
||||
getLoggingConfig,
|
||||
getProviderConfig,
|
||||
getServiceConfig,
|
||||
initializeConfig,
|
||||
isDevelopment,
|
||||
isProduction,
|
||||
isTest,
|
||||
resetConfig,
|
||||
} 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 = {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
service: {
|
||||
name: 'test-service',
|
||||
port: 3000,
|
||||
},
|
||||
database: {
|
||||
postgres: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'testdb',
|
||||
user: 'testuser',
|
||||
password: 'testpass',
|
||||
},
|
||||
questdb: {
|
||||
host: 'localhost',
|
||||
httpPort: 9000,
|
||||
pgPort: 8812,
|
||||
},
|
||||
mongodb: {
|
||||
host: 'localhost',
|
||||
port: 27017,
|
||||
database: 'testdb',
|
||||
},
|
||||
dragonfly: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
},
|
||||
logging: {
|
||||
level: 'info',
|
||||
format: 'json',
|
||||
},
|
||||
providers: {
|
||||
yahoo: {
|
||||
enabled: true,
|
||||
rateLimit: 5,
|
||||
},
|
||||
qm: {
|
||||
enabled: false,
|
||||
apiKey: 'test-key',
|
||||
},
|
||||
},
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,181 +1,166 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
import { mkdirSync, rmSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
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 { getProviderConfig, resetConfig } from '../src/index';
|
||||
import { EnvLoader } from '../src/loaders/env.loader';
|
||||
import { FileLoader } from '../src/loaders/file.loader';
|
||||
import { appConfigSchema } from '../src/schemas';
|
||||
import { resetConfig, getProviderConfig } from '../src/index';
|
||||
import { join } from 'path';
|
||||
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
|
||||
|
||||
const TEST_DIR = join(__dirname, 'provider-tests');
|
||||
|
||||
|
|
@ -15,10 +15,10 @@ describe('Provider Configuration Tests', () => {
|
|||
beforeEach(() => {
|
||||
// Save original environment
|
||||
originalEnv = { ...process.env };
|
||||
|
||||
|
||||
// Reset config singleton
|
||||
resetConfig();
|
||||
|
||||
|
||||
// Clean up test directory
|
||||
if (existsSync(TEST_DIR)) {
|
||||
rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
|
|
@ -29,7 +29,7 @@ describe('Provider Configuration Tests', () => {
|
|||
afterEach(() => {
|
||||
// Restore original environment
|
||||
process.env = originalEnv;
|
||||
|
||||
|
||||
// Clean up
|
||||
resetConfig();
|
||||
if (existsSync(TEST_DIR)) {
|
||||
|
|
@ -44,7 +44,7 @@ describe('Provider Configuration Tests', () => {
|
|||
process.env.WEBSHARE_ENABLED = 'true';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
|
@ -64,7 +64,7 @@ describe('Provider Configuration Tests', () => {
|
|||
process.env.EOD_PRIORITY = '10';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
|
@ -88,7 +88,7 @@ describe('Provider Configuration Tests', () => {
|
|||
process.env.IB_PRIORITY = '5';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
|
@ -113,7 +113,7 @@ describe('Provider Configuration Tests', () => {
|
|||
process.env.QM_PRIORITY = '15';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
|
@ -136,7 +136,7 @@ describe('Provider Configuration Tests', () => {
|
|||
process.env.YAHOO_PRIORITY = '20';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
|
@ -153,27 +153,31 @@ describe('Provider Configuration Tests', () => {
|
|||
// Create a config file
|
||||
const configDir = join(TEST_DIR, 'config');
|
||||
mkdirSync(configDir, { recursive: true });
|
||||
|
||||
|
||||
writeFileSync(
|
||||
join(configDir, 'development.json'),
|
||||
JSON.stringify({
|
||||
providers: {
|
||||
eod: {
|
||||
name: 'EOD Historical Data',
|
||||
apiKey: 'file-eod-key',
|
||||
baseUrl: 'https://file.eod.com/api',
|
||||
tier: 'free',
|
||||
enabled: false,
|
||||
priority: 1
|
||||
JSON.stringify(
|
||||
{
|
||||
providers: {
|
||||
eod: {
|
||||
name: 'EOD Historical Data',
|
||||
apiKey: 'file-eod-key',
|
||||
baseUrl: 'https://file.eod.com/api',
|
||||
tier: 'free',
|
||||
enabled: false,
|
||||
priority: 1,
|
||||
},
|
||||
yahoo: {
|
||||
name: 'Yahoo Finance',
|
||||
baseUrl: 'https://file.yahoo.com',
|
||||
enabled: true,
|
||||
priority: 2,
|
||||
},
|
||||
},
|
||||
yahoo: {
|
||||
name: 'Yahoo Finance',
|
||||
baseUrl: 'https://file.yahoo.com',
|
||||
enabled: true,
|
||||
priority: 2
|
||||
}
|
||||
}
|
||||
}, null, 2)
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
// Set environment variables that should override file config
|
||||
|
|
@ -183,10 +187,7 @@ describe('Provider Configuration Tests', () => {
|
|||
process.env.YAHOO_PRIORITY = '25';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [
|
||||
new FileLoader(configDir, 'development'),
|
||||
new EnvLoader('')
|
||||
]
|
||||
loaders: [new FileLoader(configDir, 'development'), new EnvLoader('')],
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
|
@ -211,7 +212,7 @@ describe('Provider Configuration Tests', () => {
|
|||
process.env.IB_GATEWAY_PORT = 'not-a-number'; // Should be a number
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
// Should throw validation error
|
||||
|
|
@ -226,7 +227,7 @@ describe('Provider Configuration Tests', () => {
|
|||
process.env.WEBSHARE_ENABLED = 'true';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
await manager.initialize(appConfigSchema);
|
||||
|
|
@ -241,7 +242,9 @@ describe('Provider Configuration Tests', () => {
|
|||
expect((webshareConfig as any).apiKey).toBe('test-webshare-key');
|
||||
|
||||
// Test non-existent provider
|
||||
expect(() => getProviderConfig('nonexistent')).toThrow('Provider configuration not found: nonexistent');
|
||||
expect(() => getProviderConfig('nonexistent')).toThrow(
|
||||
'Provider configuration not found: nonexistent'
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle boolean string parsing correctly', async () => {
|
||||
|
|
@ -253,7 +256,7 @@ describe('Provider Configuration Tests', () => {
|
|||
process.env.WEBSHARE_ENABLED = 'yes'; // Should be treated as string, not boolean
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
|
@ -272,7 +275,7 @@ describe('Provider Configuration Tests', () => {
|
|||
process.env.IB_GATEWAY_CLIENT_ID = '999';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
|
@ -300,9 +303,9 @@ YAHOO_BASE_URL=https://env-file.yahoo.com
|
|||
const originalCwd = process.cwd();
|
||||
try {
|
||||
process.chdir(TEST_DIR);
|
||||
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
loaders: [new EnvLoader('')],
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
|
@ -317,4 +320,4 @@ YAHOO_BASE_URL=https://env-file.yahoo.com
|
|||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import {
|
||||
getConfig,
|
||||
getDatabaseConfig,
|
||||
|
|
@ -11,7 +11,7 @@ import {
|
|||
isDevelopment,
|
||||
isProduction,
|
||||
isTest,
|
||||
resetConfig
|
||||
resetConfig,
|
||||
} from '../src/index';
|
||||
|
||||
const TEST_DIR = join(__dirname, 'real-usage-tests');
|
||||
|
|
@ -23,13 +23,13 @@ describe('Real Usage Scenarios', () => {
|
|||
beforeEach(() => {
|
||||
originalEnv = { ...process.env };
|
||||
originalCwd = process.cwd();
|
||||
|
||||
|
||||
resetConfig();
|
||||
|
||||
|
||||
if (existsSync(TEST_DIR)) {
|
||||
rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
|
||||
setupRealUsageScenarios();
|
||||
});
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ describe('Real Usage Scenarios', () => {
|
|||
process.env = originalEnv;
|
||||
process.chdir(originalCwd);
|
||||
resetConfig();
|
||||
|
||||
|
||||
if (existsSync(TEST_DIR)) {
|
||||
rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
|
|
@ -53,18 +53,18 @@ describe('Real Usage Scenarios', () => {
|
|||
// Test typical data-ingestion config access patterns
|
||||
expect(config.app.name).toBe('data-ingestion');
|
||||
expect(config.service.port).toBe(3001);
|
||||
|
||||
|
||||
// Test database config access
|
||||
const dbConfig = getDatabaseConfig();
|
||||
expect(dbConfig.postgres.host).toBe('localhost');
|
||||
expect(dbConfig.postgres.port).toBe(5432);
|
||||
expect(dbConfig.questdb.host).toBe('localhost');
|
||||
|
||||
|
||||
// Test provider access
|
||||
const yahooConfig = getProviderConfig('yahoo');
|
||||
expect(yahooConfig).toBeDefined();
|
||||
expect((yahooConfig as any).enabled).toBe(true);
|
||||
|
||||
|
||||
// Test environment helpers
|
||||
expect(isDevelopment()).toBe(true);
|
||||
expect(isProduction()).toBe(false);
|
||||
|
|
@ -78,11 +78,11 @@ describe('Real Usage Scenarios', () => {
|
|||
|
||||
expect(config.app.name).toBe('web-api');
|
||||
expect(config.service.port).toBe(4000);
|
||||
|
||||
|
||||
// Web API should have access to all the same configs
|
||||
const serviceConfig = getServiceConfig();
|
||||
expect(serviceConfig.name).toBe('web-api');
|
||||
|
||||
|
||||
const loggingConfig = getLoggingConfig();
|
||||
expect(loggingConfig.level).toBe('info');
|
||||
});
|
||||
|
|
@ -96,7 +96,7 @@ describe('Real Usage Scenarios', () => {
|
|||
// Libraries should inherit from root config
|
||||
expect(config.app.name).toBe('cache-lib');
|
||||
expect(config.app.version).toBe('1.0.0'); // From root
|
||||
|
||||
|
||||
// Should have access to cache config
|
||||
const dbConfig = getDatabaseConfig();
|
||||
expect(dbConfig.dragonfly).toBeDefined();
|
||||
|
|
@ -106,7 +106,7 @@ describe('Real Usage Scenarios', () => {
|
|||
|
||||
test('should handle production environment correctly', async () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
|
||||
const dataServiceDir = join(TEST_DIR, 'apps', 'data-ingestion');
|
||||
process.chdir(dataServiceDir);
|
||||
|
||||
|
|
@ -115,14 +115,14 @@ describe('Real Usage Scenarios', () => {
|
|||
|
||||
expect(config.environment).toBe('production');
|
||||
expect(config.logging.level).toBe('warn'); // Production should use different log level
|
||||
|
||||
|
||||
expect(isProduction()).toBe(true);
|
||||
expect(isDevelopment()).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle test environment correctly', async () => {
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
|
||||
const dataServiceDir = join(TEST_DIR, 'apps', 'data-ingestion');
|
||||
process.chdir(dataServiceDir);
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ describe('Real Usage Scenarios', () => {
|
|||
|
||||
expect(config.environment).toBe('test');
|
||||
expect(config.logging.level).toBe('debug'); // Test should use debug level
|
||||
|
||||
|
||||
expect(isTest()).toBe(true);
|
||||
expect(isDevelopment()).toBe(false);
|
||||
});
|
||||
|
|
@ -153,10 +153,10 @@ describe('Real Usage Scenarios', () => {
|
|||
const dbConfig = getDatabaseConfig();
|
||||
expect(dbConfig.postgres.host).toBe('prod-db.example.com');
|
||||
expect(dbConfig.postgres.port).toBe(5433);
|
||||
|
||||
|
||||
const serviceConfig = getServiceConfig();
|
||||
expect(serviceConfig.port).toBe(8080);
|
||||
|
||||
|
||||
const eodConfig = getProviderConfig('eod');
|
||||
expect((eodConfig as any).apiKey).toBe('prod-eod-key');
|
||||
});
|
||||
|
|
@ -168,8 +168,10 @@ describe('Real Usage Scenarios', () => {
|
|||
const config = await initializeServiceConfig();
|
||||
|
||||
// Should throw for non-existent providers
|
||||
expect(() => getProviderConfig('nonexistent')).toThrow('Provider configuration not found: nonexistent');
|
||||
|
||||
expect(() => getProviderConfig('nonexistent')).toThrow(
|
||||
'Provider configuration not found: nonexistent'
|
||||
);
|
||||
|
||||
// Should work for providers that exist but might not be configured
|
||||
// (they should have defaults from schema)
|
||||
const yahooConfig = getProviderConfig('yahoo');
|
||||
|
|
@ -181,18 +183,18 @@ describe('Real Usage Scenarios', () => {
|
|||
process.chdir(dataServiceDir);
|
||||
|
||||
const config = await initializeServiceConfig();
|
||||
|
||||
|
||||
// Test various access patterns used in real applications
|
||||
const configManager = (await import('../src/index')).getConfigManager();
|
||||
|
||||
|
||||
// Direct path access
|
||||
expect(configManager.getValue('app.name')).toBe('data-ingestion');
|
||||
expect(configManager.getValue('service.port')).toBe(3001);
|
||||
|
||||
|
||||
// Check if paths exist
|
||||
expect(configManager.has('app.name')).toBe(true);
|
||||
expect(configManager.has('nonexistent.path')).toBe(false);
|
||||
|
||||
|
||||
// Typed access
|
||||
const port = configManager.getValue<number>('service.port');
|
||||
expect(typeof port).toBe('number');
|
||||
|
|
@ -205,39 +207,39 @@ describe('Real Usage Scenarios', () => {
|
|||
|
||||
await initializeServiceConfig();
|
||||
const configManager = (await import('../src/index')).getConfigManager();
|
||||
|
||||
|
||||
// Update config at runtime (useful for testing)
|
||||
configManager.set({
|
||||
service: {
|
||||
port: 9999
|
||||
}
|
||||
port: 9999,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const updatedConfig = getConfig();
|
||||
expect(updatedConfig.service.port).toBe(9999);
|
||||
|
||||
|
||||
// Other values should be preserved
|
||||
expect(updatedConfig.app.name).toBe('data-ingestion');
|
||||
});
|
||||
|
||||
test('should work across multiple service initializations', async () => {
|
||||
// Simulate multiple services in the same process (like tests)
|
||||
|
||||
|
||||
// First service
|
||||
const dataServiceDir = join(TEST_DIR, 'apps', 'data-ingestion');
|
||||
process.chdir(dataServiceDir);
|
||||
|
||||
|
||||
let config = await initializeServiceConfig();
|
||||
expect(config.app.name).toBe('data-ingestion');
|
||||
|
||||
// Reset and switch to another service
|
||||
|
||||
// Reset and switch to another service
|
||||
resetConfig();
|
||||
const webApiDir = join(TEST_DIR, 'apps', 'web-api');
|
||||
process.chdir(webApiDir);
|
||||
|
||||
|
||||
config = await initializeServiceConfig();
|
||||
expect(config.app.name).toBe('web-api');
|
||||
|
||||
|
||||
// Each service should get its own config
|
||||
expect(config.service.port).toBe(4000); // web-api port
|
||||
});
|
||||
|
|
@ -263,7 +265,7 @@ function setupRealUsageScenarios() {
|
|||
development: {
|
||||
app: {
|
||||
name: 'stock-bot-monorepo',
|
||||
version: '1.0.0'
|
||||
version: '1.0.0',
|
||||
},
|
||||
database: {
|
||||
postgres: {
|
||||
|
|
@ -271,116 +273,125 @@ function setupRealUsageScenarios() {
|
|||
port: 5432,
|
||||
database: 'trading_bot',
|
||||
username: 'trading_user',
|
||||
password: 'trading_pass_dev'
|
||||
password: 'trading_pass_dev',
|
||||
},
|
||||
questdb: {
|
||||
host: 'localhost',
|
||||
port: 9009,
|
||||
database: 'questdb'
|
||||
database: 'questdb',
|
||||
},
|
||||
mongodb: {
|
||||
host: 'localhost',
|
||||
port: 27017,
|
||||
database: 'stock'
|
||||
database: 'stock',
|
||||
},
|
||||
dragonfly: {
|
||||
host: 'localhost',
|
||||
port: 6379
|
||||
}
|
||||
port: 6379,
|
||||
},
|
||||
},
|
||||
logging: {
|
||||
level: 'info',
|
||||
format: 'json'
|
||||
format: 'json',
|
||||
},
|
||||
providers: {
|
||||
yahoo: {
|
||||
name: 'Yahoo Finance',
|
||||
enabled: true,
|
||||
priority: 1,
|
||||
baseUrl: 'https://query1.finance.yahoo.com'
|
||||
baseUrl: 'https://query1.finance.yahoo.com',
|
||||
},
|
||||
eod: {
|
||||
name: 'EOD Historical Data',
|
||||
enabled: false,
|
||||
priority: 2,
|
||||
apiKey: 'demo-api-key',
|
||||
baseUrl: 'https://eodhistoricaldata.com/api'
|
||||
}
|
||||
}
|
||||
baseUrl: 'https://eodhistoricaldata.com/api',
|
||||
},
|
||||
},
|
||||
},
|
||||
production: {
|
||||
logging: {
|
||||
level: 'warn'
|
||||
level: 'warn',
|
||||
},
|
||||
database: {
|
||||
postgres: {
|
||||
host: 'prod-postgres.internal',
|
||||
port: 5432
|
||||
}
|
||||
}
|
||||
port: 5432,
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
logging: {
|
||||
level: 'debug'
|
||||
level: 'debug',
|
||||
},
|
||||
database: {
|
||||
postgres: {
|
||||
database: 'trading_bot_test'
|
||||
}
|
||||
}
|
||||
}
|
||||
database: 'trading_bot_test',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Object.entries(rootConfigs).forEach(([env, config]) => {
|
||||
writeFileSync(
|
||||
join(scenarios.root, 'config', `${env}.json`),
|
||||
JSON.stringify(config, null, 2)
|
||||
);
|
||||
writeFileSync(join(scenarios.root, 'config', `${env}.json`), JSON.stringify(config, null, 2));
|
||||
});
|
||||
|
||||
// Data service config
|
||||
writeFileSync(
|
||||
join(scenarios.dataService, 'config', 'development.json'),
|
||||
JSON.stringify({
|
||||
app: {
|
||||
name: 'data-ingestion'
|
||||
JSON.stringify(
|
||||
{
|
||||
app: {
|
||||
name: 'data-ingestion',
|
||||
},
|
||||
service: {
|
||||
name: 'data-ingestion',
|
||||
port: 3001,
|
||||
workers: 2,
|
||||
},
|
||||
},
|
||||
service: {
|
||||
name: 'data-ingestion',
|
||||
port: 3001,
|
||||
workers: 2
|
||||
}
|
||||
}, null, 2)
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
// Web API config
|
||||
writeFileSync(
|
||||
join(scenarios.webApi, 'config', 'development.json'),
|
||||
JSON.stringify({
|
||||
app: {
|
||||
name: 'web-api'
|
||||
JSON.stringify(
|
||||
{
|
||||
app: {
|
||||
name: 'web-api',
|
||||
},
|
||||
service: {
|
||||
name: 'web-api',
|
||||
port: 4000,
|
||||
cors: {
|
||||
origin: ['http://localhost:3000', 'http://localhost:4200'],
|
||||
},
|
||||
},
|
||||
},
|
||||
service: {
|
||||
name: 'web-api',
|
||||
port: 4000,
|
||||
cors: {
|
||||
origin: ['http://localhost:3000', 'http://localhost:4200']
|
||||
}
|
||||
}
|
||||
}, null, 2)
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
// Cache lib config
|
||||
writeFileSync(
|
||||
join(scenarios.cacheLib, 'config', 'development.json'),
|
||||
JSON.stringify({
|
||||
app: {
|
||||
name: 'cache-lib'
|
||||
JSON.stringify(
|
||||
{
|
||||
app: {
|
||||
name: 'cache-lib',
|
||||
},
|
||||
service: {
|
||||
name: 'cache-lib',
|
||||
},
|
||||
},
|
||||
service: {
|
||||
name: 'cache-lib'
|
||||
}
|
||||
}, null, 2)
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
// Root .env file
|
||||
|
|
@ -401,4 +412,4 @@ WEBSHARE_API_KEY=demo-webshare-key
|
|||
DATA_SERVICE_RATE_LIMIT=1000
|
||||
`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue