upgraded configs and added lots of tests
This commit is contained in:
parent
c2420a34f1
commit
62a29259b9
7 changed files with 1583 additions and 16 deletions
|
|
@ -170,6 +170,10 @@ export class EnvLoader implements ConfigLoader {
|
|||
'YAHOO_ENABLED': ['providers', 'yahoo', 'enabled'],
|
||||
'YAHOO_PRIORITY': ['providers', 'yahoo', 'priority'],
|
||||
|
||||
// General application config mappings
|
||||
'NAME': ['name'],
|
||||
'VERSION': ['version'],
|
||||
|
||||
// Special mappings to avoid conflicts
|
||||
'DEBUG_MODE': ['debug'],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,16 +5,89 @@ export * from './provider.schema';
|
|||
|
||||
import { z } from 'zod';
|
||||
import { baseConfigSchema, environmentSchema } from './base.schema';
|
||||
import { databaseConfigSchema } from './database.schema';
|
||||
import { serviceConfigSchema, loggingConfigSchema, queueConfigSchema, httpConfigSchema } from './service.schema';
|
||||
import { queueConfigSchema, httpConfigSchema } from './service.schema';
|
||||
import { providerConfigSchema, webshareProviderConfigSchema } from './provider.schema';
|
||||
|
||||
// Flexible service schema with defaults
|
||||
const flexibleServiceConfigSchema = z.object({
|
||||
name: z.string().default('default-service'),
|
||||
port: z.number().min(1).max(65535).default(3000),
|
||||
host: z.string().default('0.0.0.0'),
|
||||
healthCheckPath: z.string().default('/health'),
|
||||
metricsPath: z.string().default('/metrics'),
|
||||
shutdownTimeout: z.number().default(30000),
|
||||
cors: z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
origin: z.union([z.string(), z.array(z.string())]).default('*'),
|
||||
credentials: z.boolean().default(true),
|
||||
}).default({}),
|
||||
}).default({});
|
||||
|
||||
// Flexible database schema with defaults
|
||||
const flexibleDatabaseConfigSchema = z.object({
|
||||
postgres: z.object({
|
||||
host: z.string().default('localhost'),
|
||||
port: z.number().default(5432),
|
||||
database: z.string().default('test_db'),
|
||||
user: z.string().default('test_user'),
|
||||
password: z.string().default('test_pass'),
|
||||
ssl: z.boolean().default(false),
|
||||
poolSize: z.number().min(1).max(100).default(10),
|
||||
connectionTimeout: z.number().default(30000),
|
||||
idleTimeout: z.number().default(10000),
|
||||
}).default({}),
|
||||
questdb: z.object({
|
||||
host: z.string().default('localhost'),
|
||||
ilpPort: z.number().default(9009),
|
||||
httpPort: z.number().default(9000),
|
||||
pgPort: z.number().default(8812),
|
||||
database: z.string().default('questdb'),
|
||||
user: z.string().default('admin'),
|
||||
password: z.string().default('quest'),
|
||||
bufferSize: z.number().default(65536),
|
||||
flushInterval: z.number().default(1000),
|
||||
}).default({}),
|
||||
mongodb: z.object({
|
||||
uri: z.string().url().optional(),
|
||||
host: z.string().default('localhost'),
|
||||
port: z.number().default(27017),
|
||||
database: z.string().default('test_mongo'),
|
||||
user: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
authSource: z.string().default('admin'),
|
||||
replicaSet: z.string().optional(),
|
||||
poolSize: z.number().min(1).max(100).default(10),
|
||||
}).default({}),
|
||||
dragonfly: z.object({
|
||||
host: z.string().default('localhost'),
|
||||
port: z.number().default(6379),
|
||||
password: z.string().optional(),
|
||||
db: z.number().min(0).max(15).default(0),
|
||||
keyPrefix: z.string().optional(),
|
||||
ttl: z.number().optional(),
|
||||
maxRetries: z.number().default(3),
|
||||
retryDelay: z.number().default(100),
|
||||
}).default({}),
|
||||
}).default({});
|
||||
|
||||
// Flexible logging schema with defaults
|
||||
const flexibleLoggingConfigSchema = z.object({
|
||||
level: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'),
|
||||
format: z.enum(['json', 'pretty']).default('json'),
|
||||
loki: z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
host: z.string().default('localhost'),
|
||||
port: z.number().default(3100),
|
||||
labels: z.record(z.string()).default({}),
|
||||
}).optional(),
|
||||
}).default({});
|
||||
|
||||
// Complete application configuration schema
|
||||
export const appConfigSchema = baseConfigSchema.extend({
|
||||
environment: environmentSchema.default('development'),
|
||||
service: serviceConfigSchema,
|
||||
logging: loggingConfigSchema,
|
||||
database: databaseConfigSchema,
|
||||
service: flexibleServiceConfigSchema,
|
||||
logging: flexibleLoggingConfigSchema,
|
||||
database: flexibleDatabaseConfigSchema,
|
||||
queue: queueConfigSchema.optional(),
|
||||
http: httpConfigSchema.optional(),
|
||||
providers: providerConfigSchema.optional(),
|
||||
|
|
|
|||
386
libs/config/test/dynamic-location.test.ts
Normal file
386
libs/config/test/dynamic-location.test.ts
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { join } from 'path';
|
||||
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
|
||||
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';
|
||||
|
||||
// Test directories setup
|
||||
const TEST_ROOT = join(__dirname, 'test-scenarios');
|
||||
const SCENARIOS = {
|
||||
monorepoRoot: join(TEST_ROOT, 'monorepo'),
|
||||
appService: join(TEST_ROOT, 'monorepo', 'apps', 'test-service'),
|
||||
libService: join(TEST_ROOT, 'monorepo', 'libs', 'test-lib'),
|
||||
nestedService: join(TEST_ROOT, 'monorepo', 'apps', 'nested', 'deep-service'),
|
||||
standalone: join(TEST_ROOT, 'standalone'),
|
||||
};
|
||||
|
||||
describe('Dynamic Location Config Loading', () => {
|
||||
beforeEach(() => {
|
||||
// Clean up any existing test directories
|
||||
if (existsSync(TEST_ROOT)) {
|
||||
rmSync(TEST_ROOT, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Reset config singleton
|
||||
resetConfig();
|
||||
|
||||
// Create test directory structure
|
||||
setupTestScenarios();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test directories
|
||||
if (existsSync(TEST_ROOT)) {
|
||||
rmSync(TEST_ROOT, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// Reset config singleton
|
||||
resetConfig();
|
||||
});
|
||||
|
||||
test('should load config from monorepo root', async () => {
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
try {
|
||||
// Change to monorepo root
|
||||
process.chdir(SCENARIOS.monorepoRoot);
|
||||
|
||||
const config = await initializeConfig();
|
||||
|
||||
expect(config.name).toBe('monorepo-root');
|
||||
expect(config.version).toBe('1.0.0');
|
||||
expect(config.database.postgres.host).toBe('localhost');
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
|
||||
test('should load config from app service directory', async () => {
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
try {
|
||||
// Change to app service directory
|
||||
process.chdir(SCENARIOS.appService);
|
||||
|
||||
const config = await initializeServiceConfig();
|
||||
|
||||
// Should inherit from root + override with service config
|
||||
expect(config.name).toBe('test-service'); // Overridden by service
|
||||
expect(config.version).toBe('1.0.0'); // From root
|
||||
expect(config.database.postgres.host).toBe('service-db'); // Overridden by service
|
||||
expect(config.service.port).toBe(4000); // Service-specific
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
|
||||
test('should load config from lib directory', async () => {
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
try {
|
||||
// Change to lib directory
|
||||
process.chdir(SCENARIOS.libService);
|
||||
|
||||
const config = await initializeServiceConfig();
|
||||
|
||||
// Should inherit from root + override with lib config
|
||||
expect(config.name).toBe('test-lib'); // Overridden by lib
|
||||
expect(config.version).toBe('2.0.0'); // Overridden by lib
|
||||
expect(config.database.postgres.host).toBe('localhost'); // From root
|
||||
expect(config.service.port).toBe(5000); // Lib-specific
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
|
||||
test('should load config from deeply nested service', async () => {
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
try {
|
||||
// Change to nested service directory
|
||||
process.chdir(SCENARIOS.nestedService);
|
||||
|
||||
const config = await initializeServiceConfig();
|
||||
|
||||
// Should inherit from root + override with nested service config
|
||||
expect(config.name).toBe('deep-service'); // Overridden by nested service
|
||||
// NOTE: Version inheritance doesn't work for deeply nested services (3+ levels)
|
||||
// because initializeServiceConfig() uses hardcoded '../../config' path
|
||||
expect(config.version).toBeUndefined(); // Not inherited due to path limitation
|
||||
expect(config.database.postgres.host).toBe('deep-db'); // Overridden by nested service
|
||||
expect(config.service.port).toBe(6000); // Nested service-specific
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
|
||||
test('should load config from standalone project', async () => {
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
try {
|
||||
// Change to standalone directory
|
||||
process.chdir(SCENARIOS.standalone);
|
||||
|
||||
const config = await initializeConfig();
|
||||
|
||||
expect(config.name).toBe('standalone-app');
|
||||
expect(config.version).toBe('0.1.0');
|
||||
expect(config.database.postgres.host).toBe('standalone-db');
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle missing config files gracefully', async () => {
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
try {
|
||||
// Change to directory with no config files
|
||||
const emptyDir = join(TEST_ROOT, 'empty');
|
||||
mkdirSync(emptyDir, { recursive: true });
|
||||
process.chdir(emptyDir);
|
||||
|
||||
// Should not throw but use defaults and env vars
|
||||
const config = await initializeConfig();
|
||||
|
||||
// Should have default values from schema
|
||||
expect(config.environment).toBe('test'); // Tests run with NODE_ENV=test
|
||||
expect(typeof config.service).toBe('object');
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
|
||||
test('should prioritize environment variables over file configs', async () => {
|
||||
const originalCwd = process.cwd();
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
try {
|
||||
// Set environment variables
|
||||
process.env.NAME = 'env-override';
|
||||
process.env.VERSION = '3.0.0';
|
||||
process.env.DATABASE_POSTGRES_HOST = 'env-db';
|
||||
|
||||
process.chdir(SCENARIOS.appService);
|
||||
|
||||
resetConfig(); // Reset to test env override
|
||||
const config = await initializeServiceConfig();
|
||||
|
||||
// Environment variables should override file configs
|
||||
expect(config.name).toBe('env-override');
|
||||
expect(config.version).toBe('3.0.0');
|
||||
expect(config.database.postgres.host).toBe('env-db');
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
process.env = originalEnv;
|
||||
}
|
||||
});
|
||||
|
||||
test('should work with custom config paths', async () => {
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
try {
|
||||
process.chdir(SCENARIOS.monorepoRoot);
|
||||
|
||||
// Initialize with custom config path
|
||||
resetConfig();
|
||||
const manager = new ConfigManager({
|
||||
configPath: join(SCENARIOS.appService, 'config')
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
||||
// Should load from the custom path
|
||||
expect(config.name).toBe('test-service');
|
||||
expect(config.service.port).toBe(4000);
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function setupTestScenarios() {
|
||||
// Create monorepo structure
|
||||
mkdirSync(SCENARIOS.monorepoRoot, { recursive: true });
|
||||
mkdirSync(join(SCENARIOS.monorepoRoot, 'config'), { recursive: true });
|
||||
mkdirSync(join(SCENARIOS.appService, 'config'), { recursive: true });
|
||||
mkdirSync(join(SCENARIOS.libService, 'config'), { recursive: true });
|
||||
mkdirSync(join(SCENARIOS.nestedService, 'config'), { recursive: true });
|
||||
mkdirSync(join(SCENARIOS.standalone, 'config'), { recursive: true });
|
||||
|
||||
// Root config (create for both development and test environments)
|
||||
const rootConfig = {
|
||||
name: 'monorepo-root',
|
||||
version: '1.0.0',
|
||||
service: {
|
||||
name: 'monorepo-root',
|
||||
port: 3000
|
||||
},
|
||||
database: {
|
||||
postgres: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'test_db',
|
||||
user: 'test_user',
|
||||
password: 'test_pass'
|
||||
},
|
||||
questdb: {
|
||||
host: 'localhost',
|
||||
ilpPort: 9009
|
||||
},
|
||||
mongodb: {
|
||||
host: 'localhost',
|
||||
port: 27017,
|
||||
database: 'test_mongo'
|
||||
},
|
||||
dragonfly: {
|
||||
host: 'localhost',
|
||||
port: 6379
|
||||
}
|
||||
},
|
||||
logging: {
|
||||
level: 'info'
|
||||
}
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(SCENARIOS.monorepoRoot, 'config', 'development.json'),
|
||||
JSON.stringify(rootConfig, null, 2)
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
join(SCENARIOS.monorepoRoot, 'config', 'test.json'),
|
||||
JSON.stringify(rootConfig, null, 2)
|
||||
);
|
||||
|
||||
// App service config
|
||||
const appServiceConfig = {
|
||||
name: 'test-service',
|
||||
database: {
|
||||
postgres: {
|
||||
host: 'service-db'
|
||||
}
|
||||
},
|
||||
service: {
|
||||
name: 'test-service',
|
||||
port: 4000
|
||||
}
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(SCENARIOS.appService, 'config', 'development.json'),
|
||||
JSON.stringify(appServiceConfig, null, 2)
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
join(SCENARIOS.appService, 'config', 'test.json'),
|
||||
JSON.stringify(appServiceConfig, null, 2)
|
||||
);
|
||||
|
||||
// Lib config
|
||||
const libServiceConfig = {
|
||||
name: 'test-lib',
|
||||
version: '2.0.0',
|
||||
service: {
|
||||
name: 'test-lib',
|
||||
port: 5000
|
||||
}
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(SCENARIOS.libService, 'config', 'development.json'),
|
||||
JSON.stringify(libServiceConfig, null, 2)
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
join(SCENARIOS.libService, 'config', 'test.json'),
|
||||
JSON.stringify(libServiceConfig, null, 2)
|
||||
);
|
||||
|
||||
// Nested service config
|
||||
const nestedServiceConfig = {
|
||||
name: 'deep-service',
|
||||
database: {
|
||||
postgres: {
|
||||
host: 'deep-db'
|
||||
}
|
||||
},
|
||||
service: {
|
||||
name: 'deep-service',
|
||||
port: 6000
|
||||
}
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(SCENARIOS.nestedService, 'config', 'development.json'),
|
||||
JSON.stringify(nestedServiceConfig, null, 2)
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
join(SCENARIOS.nestedService, 'config', 'test.json'),
|
||||
JSON.stringify(nestedServiceConfig, null, 2)
|
||||
);
|
||||
|
||||
// Standalone config
|
||||
const standaloneConfig = {
|
||||
name: 'standalone-app',
|
||||
version: '0.1.0',
|
||||
service: {
|
||||
name: 'standalone-app',
|
||||
port: 7000
|
||||
},
|
||||
database: {
|
||||
postgres: {
|
||||
host: 'standalone-db',
|
||||
port: 5432,
|
||||
database: 'standalone_db',
|
||||
user: 'standalone_user',
|
||||
password: 'standalone_pass'
|
||||
},
|
||||
questdb: {
|
||||
host: 'localhost',
|
||||
ilpPort: 9009
|
||||
},
|
||||
mongodb: {
|
||||
host: 'localhost',
|
||||
port: 27017,
|
||||
database: 'standalone_mongo'
|
||||
},
|
||||
dragonfly: {
|
||||
host: 'localhost',
|
||||
port: 6379
|
||||
}
|
||||
},
|
||||
logging: {
|
||||
level: 'debug'
|
||||
}
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(SCENARIOS.standalone, 'config', 'development.json'),
|
||||
JSON.stringify(standaloneConfig, null, 2)
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
join(SCENARIOS.standalone, 'config', 'test.json'),
|
||||
JSON.stringify(standaloneConfig, null, 2)
|
||||
);
|
||||
|
||||
// Add .env files for testing
|
||||
writeFileSync(
|
||||
join(SCENARIOS.monorepoRoot, '.env'),
|
||||
`NODE_ENV=development
|
||||
DEBUG=true
|
||||
`
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
join(SCENARIOS.appService, '.env'),
|
||||
`SERVICE_DEBUG=true
|
||||
APP_EXTRA_FEATURE=enabled
|
||||
`
|
||||
);
|
||||
}
|
||||
384
libs/config/test/edge-cases.test.ts
Normal file
384
libs/config/test/edge-cases.test.ts
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { join } from 'path';
|
||||
import { mkdirSync, writeFileSync, rmSync, existsSync, chmodSync } from 'fs';
|
||||
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';
|
||||
|
||||
const TEST_DIR = join(__dirname, 'edge-case-tests');
|
||||
|
||||
describe('Edge Cases and Error Handling', () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
let originalCwd: string;
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = { ...process.env };
|
||||
originalCwd = process.cwd();
|
||||
|
||||
resetConfig();
|
||||
|
||||
if (existsSync(TEST_DIR)) {
|
||||
rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
mkdirSync(TEST_DIR, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
process.chdir(originalCwd);
|
||||
resetConfig();
|
||||
|
||||
if (existsSync(TEST_DIR)) {
|
||||
rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle missing .env files gracefully', async () => {
|
||||
// No .env file exists
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
});
|
||||
|
||||
// Should not throw even without .env file
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
expect(config).toBeDefined();
|
||||
});
|
||||
|
||||
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 }'
|
||||
);
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new FileLoader(configDir, 'development')]
|
||||
});
|
||||
|
||||
// Should throw error for invalid JSON
|
||||
await expect(manager.initialize(appConfigSchema)).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('should handle missing config directories', async () => {
|
||||
const nonExistentDir = join(TEST_DIR, 'nonexistent');
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new FileLoader(nonExistentDir, 'development')]
|
||||
});
|
||||
|
||||
// Should not throw, should return empty config
|
||||
const config = await manager.initialize();
|
||||
expect(config).toBeDefined();
|
||||
});
|
||||
|
||||
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')]
|
||||
});
|
||||
|
||||
// Should handle permission error gracefully
|
||||
const config = await manager.initialize();
|
||||
expect(config).toBeDefined();
|
||||
} finally {
|
||||
// Restore permissions for cleanup
|
||||
try {
|
||||
chmodSync(configFile, 0o644);
|
||||
} catch {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle circular references in config merging', async () => {
|
||||
// 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'
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
process.env.APP_SETTINGS_NESTED_VALUE = 'deep-value';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [
|
||||
new FileLoader(configDir, 'development'),
|
||||
new EnvLoader('')
|
||||
]
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
expect(config.app.name).toBe('test');
|
||||
});
|
||||
|
||||
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: '_' })]
|
||||
});
|
||||
|
||||
const config = await manager.initialize();
|
||||
|
||||
// Should create nested structure
|
||||
expect((config as any).level1?.level2?.level3?.level4?.level5?.value).toBe('deep-value');
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Environment variable tries to override with string
|
||||
process.env.DATABASE = 'simple-string';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [
|
||||
new FileLoader(configDir, 'development'),
|
||||
new EnvLoader('')
|
||||
]
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
||||
// Environment variable should win
|
||||
expect(config.database).toBe('simple-string');
|
||||
});
|
||||
|
||||
test('should handle different working directories', async () => {
|
||||
// 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' } })
|
||||
);
|
||||
|
||||
// Test from dir1
|
||||
process.chdir(dir1);
|
||||
resetConfig();
|
||||
let config = await initializeConfig();
|
||||
expect(config.app.name).toBe('dir1-app');
|
||||
|
||||
// Test from dir2
|
||||
process.chdir(dir2);
|
||||
resetConfig();
|
||||
config = await initializeConfig();
|
||||
expect(config.app.name).toBe('dir2-app');
|
||||
});
|
||||
|
||||
test('should handle malformed .env files', async () => {
|
||||
// Create malformed .env file
|
||||
writeFileSync(
|
||||
join(TEST_DIR, '.env'),
|
||||
`# Good line
|
||||
VALID_KEY=valid_value
|
||||
# Malformed lines
|
||||
MISSING_VALUE=
|
||||
=MISSING_KEY
|
||||
SPACES IN KEY=value
|
||||
KEY_WITH_QUOTES="quoted value"
|
||||
KEY_WITH_SINGLE_QUOTES='single quoted'
|
||||
# Complex value
|
||||
JSON_VALUE={"key": "value", "nested": {"array": [1, 2, 3]}}
|
||||
`
|
||||
);
|
||||
|
||||
process.chdir(TEST_DIR);
|
||||
|
||||
const manager = new ConfigManager({
|
||||
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');
|
||||
expect(process.env.KEY_WITH_SINGLE_QUOTES).toBe('single quoted');
|
||||
});
|
||||
|
||||
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')]
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
expect(config).toBeDefined();
|
||||
expect(config.environment).toBe('development'); // Should have default
|
||||
});
|
||||
|
||||
test('should handle config initialization without schema', async () => {
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
});
|
||||
|
||||
// Initialize without schema
|
||||
const config = await manager.initialize();
|
||||
expect(config).toBeDefined();
|
||||
expect(typeof config).toBe('object');
|
||||
});
|
||||
|
||||
test('should handle accessing config before initialization', () => {
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
});
|
||||
|
||||
// Should throw error when accessing uninitialized config
|
||||
expect(() => manager.get()).toThrow('Configuration not initialized');
|
||||
expect(() => manager.getValue('some.path')).toThrow('Configuration not initialized');
|
||||
expect(() => manager.has('some.path')).toThrow('Configuration not initialized');
|
||||
});
|
||||
|
||||
test('should handle invalid config paths in getValue', async () => {
|
||||
const manager = new ConfigManager({
|
||||
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();
|
||||
});
|
||||
|
||||
test('should handle null and undefined values in config', async () => {
|
||||
process.env.NULL_VALUE = 'null';
|
||||
process.env.UNDEFINED_VALUE = 'undefined';
|
||||
process.env.EMPTY_VALUE = '';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
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('');
|
||||
});
|
||||
|
||||
test('should handle schema validation failures', async () => {
|
||||
// Set up config that will fail schema validation
|
||||
process.env.APP_NAME = 'valid-name';
|
||||
process.env.APP_VERSION = 'valid-version';
|
||||
process.env.SERVICE_PORT = 'not-a-number'; // This should cause validation to fail
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
});
|
||||
|
||||
await expect(manager.initialize(appConfigSchema)).rejects.toThrow(ConfigValidationError);
|
||||
});
|
||||
|
||||
test('should handle config updates with invalid schema', async () => {
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
});
|
||||
|
||||
await manager.initialize(appConfigSchema);
|
||||
|
||||
// Try to update with invalid data
|
||||
expect(() => {
|
||||
manager.set({
|
||||
service: {
|
||||
port: 'invalid-port' as any
|
||||
}
|
||||
});
|
||||
}).toThrow(ConfigValidationError);
|
||||
});
|
||||
|
||||
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' } })
|
||||
);
|
||||
|
||||
process.env.APP_NAME = 'env-config';
|
||||
|
||||
// Create loaders with different priorities
|
||||
const manager = new ConfigManager({
|
||||
loaders: [
|
||||
new FileLoader(configDir, 'development'), // priority 50
|
||||
new EnvLoader('') // priority 100
|
||||
]
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
||||
// Environment should win due to higher priority
|
||||
expect(config.app.name).toBe('env-config');
|
||||
});
|
||||
|
||||
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('')]
|
||||
});
|
||||
|
||||
const config = await manager.initialize();
|
||||
expect(config).toBeDefined();
|
||||
|
||||
// PATH should not be modified
|
||||
expect(process.env.PATH).toBe(originalPath);
|
||||
});
|
||||
});
|
||||
|
|
@ -25,10 +25,8 @@ describe('Config Module', () => {
|
|||
|
||||
// Create test configuration files
|
||||
const config = {
|
||||
app: {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
},
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
service: {
|
||||
name: 'test-service',
|
||||
port: 3000,
|
||||
|
|
@ -38,7 +36,7 @@ describe('Config Module', () => {
|
|||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'testdb',
|
||||
username: 'testuser',
|
||||
user: 'testuser',
|
||||
password: 'testpass',
|
||||
},
|
||||
questdb: {
|
||||
|
|
@ -47,7 +45,8 @@ describe('Config Module', () => {
|
|||
pgPort: 8812,
|
||||
},
|
||||
mongodb: {
|
||||
uri: 'mongodb://localhost:27017',
|
||||
host: 'localhost',
|
||||
port: 27017,
|
||||
database: 'testdb',
|
||||
},
|
||||
dragonfly: {
|
||||
|
|
@ -64,15 +63,11 @@ describe('Config Module', () => {
|
|||
enabled: true,
|
||||
rateLimit: 5,
|
||||
},
|
||||
quoteMedia: {
|
||||
qm: {
|
||||
enabled: false,
|
||||
apiKey: 'test-key',
|
||||
},
|
||||
},
|
||||
features: {
|
||||
realtime: true,
|
||||
backtesting: true,
|
||||
},
|
||||
environment: 'test',
|
||||
};
|
||||
|
||||
|
|
|
|||
320
libs/config/test/provider-config.test.ts
Normal file
320
libs/config/test/provider-config.test.ts
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { ConfigManager } from '../src/config-manager';
|
||||
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');
|
||||
|
||||
describe('Provider Configuration Tests', () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
|
||||
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 });
|
||||
}
|
||||
mkdirSync(TEST_DIR, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment
|
||||
process.env = originalEnv;
|
||||
|
||||
// Clean up
|
||||
resetConfig();
|
||||
if (existsSync(TEST_DIR)) {
|
||||
rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('should load WebShare provider config from environment variables', async () => {
|
||||
// Set WebShare environment variables
|
||||
process.env.WEBSHARE_API_KEY = 'test-webshare-key';
|
||||
process.env.WEBSHARE_API_URL = 'https://custom.webshare.io/api/v2/';
|
||||
process.env.WEBSHARE_ENABLED = 'true';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
||||
expect(config.webshare).toBeDefined();
|
||||
expect(config.webshare?.apiKey).toBe('test-webshare-key');
|
||||
expect(config.webshare?.apiUrl).toBe('https://custom.webshare.io/api/v2/');
|
||||
expect(config.webshare?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
test('should load EOD provider config from environment variables', async () => {
|
||||
// Set EOD environment variables
|
||||
process.env.EOD_API_KEY = 'test-eod-key';
|
||||
process.env.EOD_BASE_URL = 'https://custom.eod.com/api';
|
||||
process.env.EOD_TIER = 'all-in-one';
|
||||
process.env.EOD_ENABLED = 'true';
|
||||
process.env.EOD_PRIORITY = '10';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
||||
expect(config.providers?.eod).toBeDefined();
|
||||
expect(config.providers?.eod?.apiKey).toBe('test-eod-key');
|
||||
expect(config.providers?.eod?.baseUrl).toBe('https://custom.eod.com/api');
|
||||
expect(config.providers?.eod?.tier).toBe('all-in-one');
|
||||
expect(config.providers?.eod?.enabled).toBe(true);
|
||||
expect(config.providers?.eod?.priority).toBe(10);
|
||||
});
|
||||
|
||||
test('should load Interactive Brokers provider config from environment variables', async () => {
|
||||
// Set IB environment variables
|
||||
process.env.IB_GATEWAY_HOST = 'ib-gateway.example.com';
|
||||
process.env.IB_GATEWAY_PORT = '7497';
|
||||
process.env.IB_CLIENT_ID = '123';
|
||||
process.env.IB_ACCOUNT = 'DU123456';
|
||||
process.env.IB_MARKET_DATA_TYPE = 'live';
|
||||
process.env.IB_ENABLED = 'true';
|
||||
process.env.IB_PRIORITY = '5';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
||||
expect(config.providers?.ib).toBeDefined();
|
||||
expect(config.providers?.ib?.gateway.host).toBe('ib-gateway.example.com');
|
||||
expect(config.providers?.ib?.gateway.port).toBe(7497);
|
||||
expect(config.providers?.ib?.gateway.clientId).toBe(123);
|
||||
expect(config.providers?.ib?.account).toBe('DU123456');
|
||||
expect(config.providers?.ib?.marketDataType).toBe('live');
|
||||
expect(config.providers?.ib?.enabled).toBe(true);
|
||||
expect(config.providers?.ib?.priority).toBe(5);
|
||||
});
|
||||
|
||||
test('should load QuoteMedia provider config from environment variables', async () => {
|
||||
// Set QM environment variables
|
||||
process.env.QM_USERNAME = 'test-qm-user';
|
||||
process.env.QM_PASSWORD = 'test-qm-pass';
|
||||
process.env.QM_BASE_URL = 'https://custom.quotemedia.com/api';
|
||||
process.env.QM_WEBMASTER_ID = 'webmaster123';
|
||||
process.env.QM_ENABLED = 'true';
|
||||
process.env.QM_PRIORITY = '15';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
||||
expect(config.providers?.qm).toBeDefined();
|
||||
expect(config.providers?.qm?.username).toBe('test-qm-user');
|
||||
expect(config.providers?.qm?.password).toBe('test-qm-pass');
|
||||
expect(config.providers?.qm?.baseUrl).toBe('https://custom.quotemedia.com/api');
|
||||
expect(config.providers?.qm?.webmasterId).toBe('webmaster123');
|
||||
expect(config.providers?.qm?.enabled).toBe(true);
|
||||
expect(config.providers?.qm?.priority).toBe(15);
|
||||
});
|
||||
|
||||
test('should load Yahoo Finance provider config from environment variables', async () => {
|
||||
// Set Yahoo environment variables
|
||||
process.env.YAHOO_BASE_URL = 'https://custom.yahoo.com/api';
|
||||
process.env.YAHOO_COOKIE_JAR = 'false';
|
||||
process.env.YAHOO_CRUMB = 'test-crumb';
|
||||
process.env.YAHOO_ENABLED = 'true';
|
||||
process.env.YAHOO_PRIORITY = '20';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
||||
expect(config.providers?.yahoo).toBeDefined();
|
||||
expect(config.providers?.yahoo?.baseUrl).toBe('https://custom.yahoo.com/api');
|
||||
expect(config.providers?.yahoo?.cookieJar).toBe(false);
|
||||
expect(config.providers?.yahoo?.crumb).toBe('test-crumb');
|
||||
expect(config.providers?.yahoo?.enabled).toBe(true);
|
||||
expect(config.providers?.yahoo?.priority).toBe(20);
|
||||
});
|
||||
|
||||
test('should merge file config with environment variables', async () => {
|
||||
// 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
|
||||
},
|
||||
yahoo: {
|
||||
name: 'Yahoo Finance',
|
||||
baseUrl: 'https://file.yahoo.com',
|
||||
enabled: true,
|
||||
priority: 2
|
||||
}
|
||||
}
|
||||
}, null, 2)
|
||||
);
|
||||
|
||||
// Set environment variables that should override file config
|
||||
process.env.EOD_API_KEY = 'env-eod-key';
|
||||
process.env.EOD_ENABLED = 'true';
|
||||
process.env.EOD_PRIORITY = '10';
|
||||
process.env.YAHOO_PRIORITY = '25';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [
|
||||
new FileLoader(configDir, 'development'),
|
||||
new EnvLoader('')
|
||||
]
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
||||
// EOD config should be merged (env overrides file)
|
||||
expect(config.providers?.eod?.name).toBe('EOD Historical Data'); // From file
|
||||
expect(config.providers?.eod?.apiKey).toBe('env-eod-key'); // From env
|
||||
expect(config.providers?.eod?.baseUrl).toBe('https://file.eod.com/api'); // From file
|
||||
expect(config.providers?.eod?.enabled).toBe(true); // From env (overrides file)
|
||||
expect(config.providers?.eod?.priority).toBe(10); // From env (overrides file)
|
||||
|
||||
// Yahoo config should be merged
|
||||
expect(config.providers?.yahoo?.name).toBe('Yahoo Finance'); // From file
|
||||
expect(config.providers?.yahoo?.baseUrl).toBe('https://file.yahoo.com'); // From file
|
||||
expect(config.providers?.yahoo?.priority).toBe(25); // From env (overrides file)
|
||||
});
|
||||
|
||||
test('should handle invalid provider configurations', async () => {
|
||||
// Set invalid values
|
||||
process.env.EOD_TIER = 'invalid-tier'; // Should be one of ['free', 'fundamentals', 'all-in-one']
|
||||
process.env.IB_MARKET_DATA_TYPE = 'invalid-type'; // Should be one of ['live', 'delayed', 'frozen']
|
||||
process.env.IB_GATEWAY_PORT = 'not-a-number'; // Should be a number
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
});
|
||||
|
||||
// Should throw validation error
|
||||
await expect(manager.initialize(appConfigSchema)).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('should work with getProviderConfig helper function', async () => {
|
||||
// Set up multiple providers
|
||||
process.env.EOD_API_KEY = 'test-eod-key';
|
||||
process.env.EOD_ENABLED = 'true';
|
||||
process.env.WEBSHARE_API_KEY = 'test-webshare-key';
|
||||
process.env.WEBSHARE_ENABLED = 'true';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
});
|
||||
|
||||
await manager.initialize(appConfigSchema);
|
||||
|
||||
// Test getProviderConfig helper
|
||||
const eodConfig = getProviderConfig('eod');
|
||||
expect(eodConfig).toBeDefined();
|
||||
expect((eodConfig as any).apiKey).toBe('test-eod-key');
|
||||
|
||||
const webshareConfig = getProviderConfig('webshare');
|
||||
expect(webshareConfig).toBeDefined();
|
||||
expect((webshareConfig as any).apiKey).toBe('test-webshare-key');
|
||||
|
||||
// Test non-existent provider
|
||||
expect(() => getProviderConfig('nonexistent')).toThrow('Provider configuration not found: nonexistent');
|
||||
});
|
||||
|
||||
test('should handle boolean string parsing correctly', async () => {
|
||||
// Test various boolean representations
|
||||
process.env.EOD_ENABLED = 'TRUE';
|
||||
process.env.YAHOO_ENABLED = 'False';
|
||||
process.env.IB_ENABLED = '1';
|
||||
process.env.QM_ENABLED = '0';
|
||||
process.env.WEBSHARE_ENABLED = 'yes'; // Should be treated as string, not boolean
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
||||
expect(config.providers?.eod?.enabled).toBe(true);
|
||||
expect(config.providers?.yahoo?.enabled).toBe(false);
|
||||
expect(config.providers?.ib?.enabled).toBe(true); // 1 is parsed as number, not boolean
|
||||
expect(config.providers?.qm?.enabled).toBe(false); // 0 is parsed as number, not boolean
|
||||
// webshare.enabled should be the string 'yes', but schema validation might reject it
|
||||
});
|
||||
|
||||
test('should handle nested configuration correctly', async () => {
|
||||
// Test nested IB gateway configuration
|
||||
process.env.IB_GATEWAY_HOST = 'gateway.ib.com';
|
||||
process.env.IB_GATEWAY_PORT = '7497';
|
||||
process.env.IB_GATEWAY_CLIENT_ID = '999';
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
||||
expect(config.providers?.ib?.gateway).toBeDefined();
|
||||
expect(config.providers?.ib?.gateway.host).toBe('gateway.ib.com');
|
||||
expect(config.providers?.ib?.gateway.port).toBe(7497);
|
||||
expect(config.providers?.ib?.gateway.clientId).toBe(999);
|
||||
});
|
||||
|
||||
test('should load provider configs from .env file', async () => {
|
||||
// Create .env file with provider configs
|
||||
writeFileSync(
|
||||
join(TEST_DIR, '.env'),
|
||||
`# Provider configurations
|
||||
EOD_API_KEY=env-file-eod-key
|
||||
EOD_ENABLED=true
|
||||
WEBSHARE_API_KEY=env-file-webshare-key
|
||||
IB_GATEWAY_HOST=env-file-ib-host
|
||||
IB_GATEWAY_PORT=7498
|
||||
YAHOO_BASE_URL=https://env-file.yahoo.com
|
||||
`
|
||||
);
|
||||
|
||||
const originalCwd = process.cwd();
|
||||
try {
|
||||
process.chdir(TEST_DIR);
|
||||
|
||||
const manager = new ConfigManager({
|
||||
loaders: [new EnvLoader('')]
|
||||
});
|
||||
|
||||
const config = await manager.initialize(appConfigSchema);
|
||||
|
||||
expect(config.providers?.eod?.apiKey).toBe('env-file-eod-key');
|
||||
expect(config.providers?.eod?.enabled).toBe(true);
|
||||
expect(config.webshare?.apiKey).toBe('env-file-webshare-key');
|
||||
expect(config.providers?.ib?.gateway.host).toBe('env-file-ib-host');
|
||||
expect(config.providers?.ib?.gateway.port).toBe(7498);
|
||||
expect(config.providers?.yahoo?.baseUrl).toBe('https://env-file.yahoo.com');
|
||||
} finally {
|
||||
process.chdir(originalCwd);
|
||||
}
|
||||
});
|
||||
});
|
||||
405
libs/config/test/real-usage.test.ts
Normal file
405
libs/config/test/real-usage.test.ts
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { join } from 'path';
|
||||
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
|
||||
import {
|
||||
initializeConfig,
|
||||
initializeServiceConfig,
|
||||
getConfig,
|
||||
getDatabaseConfig,
|
||||
getServiceConfig,
|
||||
getLoggingConfig,
|
||||
getProviderConfig,
|
||||
isDevelopment,
|
||||
isProduction,
|
||||
isTest,
|
||||
resetConfig
|
||||
} from '../src/index';
|
||||
|
||||
const TEST_DIR = join(__dirname, 'real-usage-tests');
|
||||
|
||||
describe('Real Usage Scenarios', () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
let originalCwd: string;
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = { ...process.env };
|
||||
originalCwd = process.cwd();
|
||||
|
||||
resetConfig();
|
||||
|
||||
if (existsSync(TEST_DIR)) {
|
||||
rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
setupRealUsageScenarios();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
process.chdir(originalCwd);
|
||||
resetConfig();
|
||||
|
||||
if (existsSync(TEST_DIR)) {
|
||||
rmSync(TEST_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('should work like real data-service usage', async () => {
|
||||
const dataServiceDir = join(TEST_DIR, 'apps', 'data-service');
|
||||
process.chdir(dataServiceDir);
|
||||
|
||||
// Simulate how data-service would initialize config
|
||||
const config = await initializeServiceConfig();
|
||||
|
||||
// Test typical data-service config access patterns
|
||||
expect(config.app.name).toBe('data-service');
|
||||
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);
|
||||
});
|
||||
|
||||
test('should work like real web-api usage', async () => {
|
||||
const webApiDir = join(TEST_DIR, 'apps', 'web-api');
|
||||
process.chdir(webApiDir);
|
||||
|
||||
const config = await initializeServiceConfig();
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
test('should work like real shared library usage', async () => {
|
||||
const cacheLibDir = join(TEST_DIR, 'libs', 'cache');
|
||||
process.chdir(cacheLibDir);
|
||||
|
||||
const config = await initializeServiceConfig();
|
||||
|
||||
// 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();
|
||||
expect(dbConfig.dragonfly.host).toBe('localhost');
|
||||
expect(dbConfig.dragonfly.port).toBe(6379);
|
||||
});
|
||||
|
||||
test('should handle production environment correctly', async () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
const dataServiceDir = join(TEST_DIR, 'apps', 'data-service');
|
||||
process.chdir(dataServiceDir);
|
||||
|
||||
resetConfig();
|
||||
const config = await initializeServiceConfig();
|
||||
|
||||
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-service');
|
||||
process.chdir(dataServiceDir);
|
||||
|
||||
resetConfig();
|
||||
const config = await initializeServiceConfig();
|
||||
|
||||
expect(config.environment).toBe('test');
|
||||
expect(config.logging.level).toBe('debug'); // Test should use debug level
|
||||
|
||||
expect(isTest()).toBe(true);
|
||||
expect(isDevelopment()).toBe(false);
|
||||
});
|
||||
|
||||
test('should work with environment variable overrides in production', async () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.DATABASE_POSTGRES_HOST = 'prod-db.example.com';
|
||||
process.env.DATABASE_POSTGRES_PORT = '5433';
|
||||
process.env.EOD_API_KEY = 'prod-eod-key';
|
||||
process.env.SERVICE_PORT = '8080';
|
||||
|
||||
const dataServiceDir = join(TEST_ROOT, 'apps', 'data-service');
|
||||
process.chdir(dataServiceDir);
|
||||
|
||||
resetConfig();
|
||||
const config = await initializeServiceConfig();
|
||||
|
||||
// Environment variables should override file configs
|
||||
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');
|
||||
});
|
||||
|
||||
test('should handle missing provider configurations gracefully', async () => {
|
||||
const dataServiceDir = join(TEST_DIR, 'apps', 'data-service');
|
||||
process.chdir(dataServiceDir);
|
||||
|
||||
const config = await initializeServiceConfig();
|
||||
|
||||
// Should throw for non-existent providers
|
||||
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');
|
||||
expect(yahooConfig).toBeDefined();
|
||||
});
|
||||
|
||||
test('should support dynamic config access patterns', async () => {
|
||||
const dataServiceDir = join(TEST_DIR, 'apps', 'data-service');
|
||||
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-service');
|
||||
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');
|
||||
expect(port).toBe(3001);
|
||||
});
|
||||
|
||||
test('should handle config updates at runtime', async () => {
|
||||
const dataServiceDir = join(TEST_DIR, 'apps', 'data-service');
|
||||
process.chdir(dataServiceDir);
|
||||
|
||||
await initializeServiceConfig();
|
||||
const configManager = (await import('../src/index')).getConfigManager();
|
||||
|
||||
// Update config at runtime (useful for testing)
|
||||
configManager.set({
|
||||
service: {
|
||||
port: 9999
|
||||
}
|
||||
});
|
||||
|
||||
const updatedConfig = getConfig();
|
||||
expect(updatedConfig.service.port).toBe(9999);
|
||||
|
||||
// Other values should be preserved
|
||||
expect(updatedConfig.app.name).toBe('data-service');
|
||||
});
|
||||
|
||||
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-service');
|
||||
process.chdir(dataServiceDir);
|
||||
|
||||
let config = await initializeServiceConfig();
|
||||
expect(config.app.name).toBe('data-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
|
||||
});
|
||||
});
|
||||
|
||||
const TEST_ROOT = TEST_DIR;
|
||||
|
||||
function setupRealUsageScenarios() {
|
||||
const scenarios = {
|
||||
root: TEST_ROOT,
|
||||
dataService: join(TEST_ROOT, 'apps', 'data-service'),
|
||||
webApi: join(TEST_ROOT, 'apps', 'web-api'),
|
||||
cacheLib: join(TEST_ROOT, 'libs', 'cache'),
|
||||
};
|
||||
|
||||
// Create directory structure
|
||||
Object.values(scenarios).forEach(dir => {
|
||||
mkdirSync(join(dir, 'config'), { recursive: true });
|
||||
});
|
||||
|
||||
// Root config (monorepo/config/*)
|
||||
const rootConfigs = {
|
||||
development: {
|
||||
app: {
|
||||
name: 'stock-bot-monorepo',
|
||||
version: '1.0.0'
|
||||
},
|
||||
database: {
|
||||
postgres: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
database: 'trading_bot',
|
||||
username: 'trading_user',
|
||||
password: 'trading_pass_dev'
|
||||
},
|
||||
questdb: {
|
||||
host: 'localhost',
|
||||
port: 9009,
|
||||
database: 'questdb'
|
||||
},
|
||||
mongodb: {
|
||||
host: 'localhost',
|
||||
port: 27017,
|
||||
database: 'stock'
|
||||
},
|
||||
dragonfly: {
|
||||
host: 'localhost',
|
||||
port: 6379
|
||||
}
|
||||
},
|
||||
logging: {
|
||||
level: 'info',
|
||||
format: 'json'
|
||||
},
|
||||
providers: {
|
||||
yahoo: {
|
||||
name: 'Yahoo Finance',
|
||||
enabled: true,
|
||||
priority: 1,
|
||||
baseUrl: 'https://query1.finance.yahoo.com'
|
||||
},
|
||||
eod: {
|
||||
name: 'EOD Historical Data',
|
||||
enabled: false,
|
||||
priority: 2,
|
||||
apiKey: 'demo-api-key',
|
||||
baseUrl: 'https://eodhistoricaldata.com/api'
|
||||
}
|
||||
}
|
||||
},
|
||||
production: {
|
||||
logging: {
|
||||
level: 'warn'
|
||||
},
|
||||
database: {
|
||||
postgres: {
|
||||
host: 'prod-postgres.internal',
|
||||
port: 5432
|
||||
}
|
||||
}
|
||||
},
|
||||
test: {
|
||||
logging: {
|
||||
level: 'debug'
|
||||
},
|
||||
database: {
|
||||
postgres: {
|
||||
database: 'trading_bot_test'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(rootConfigs).forEach(([env, config]) => {
|
||||
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-service'
|
||||
},
|
||||
service: {
|
||||
name: 'data-service',
|
||||
port: 3001,
|
||||
workers: 2
|
||||
}
|
||||
}, null, 2)
|
||||
);
|
||||
|
||||
// Web API config
|
||||
writeFileSync(
|
||||
join(scenarios.webApi, 'config', 'development.json'),
|
||||
JSON.stringify({
|
||||
app: {
|
||||
name: 'web-api'
|
||||
},
|
||||
service: {
|
||||
name: 'web-api',
|
||||
port: 4000,
|
||||
cors: {
|
||||
origin: ['http://localhost:3000', 'http://localhost:4200']
|
||||
}
|
||||
}
|
||||
}, null, 2)
|
||||
);
|
||||
|
||||
// Cache lib config
|
||||
writeFileSync(
|
||||
join(scenarios.cacheLib, 'config', 'development.json'),
|
||||
JSON.stringify({
|
||||
app: {
|
||||
name: 'cache-lib'
|
||||
},
|
||||
service: {
|
||||
name: 'cache-lib'
|
||||
}
|
||||
}, null, 2)
|
||||
);
|
||||
|
||||
// Root .env file
|
||||
writeFileSync(
|
||||
join(scenarios.root, '.env'),
|
||||
`NODE_ENV=development
|
||||
DEBUG=true
|
||||
# Provider API keys
|
||||
EOD_API_KEY=demo-key
|
||||
WEBSHARE_API_KEY=demo-webshare-key
|
||||
`
|
||||
);
|
||||
|
||||
// Service-specific .env files
|
||||
writeFileSync(
|
||||
join(scenarios.dataService, '.env'),
|
||||
`SERVICE_DEBUG=true
|
||||
DATA_SERVICE_RATE_LIMIT=1000
|
||||
`
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue