switched to new config and removed old

This commit is contained in:
Boki 2025-06-18 21:03:45 -04:00
parent 6b69bcbcaa
commit 269364fbc8
70 changed files with 889 additions and 2978 deletions

View file

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

View file

@ -0,0 +1,213 @@
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { writeFileSync, mkdirSync, rmSync } from 'fs';
import { join } from 'path';
import {
initializeConfig,
getConfig,
getConfigManager,
resetConfig,
getDatabaseConfig,
getServiceConfig,
getLoggingConfig,
getProviderConfig,
isDevelopment,
isProduction,
isTest,
} from '../src';
describe('Config Module', () => {
const testConfigDir = join(process.cwd(), 'test-config-module');
const originalEnv = { ...process.env };
beforeEach(() => {
resetConfig();
mkdirSync(testConfigDir, { recursive: true });
// Create test configuration files
const config = {
app: {
name: 'test-app',
version: '1.0.0',
},
service: {
name: 'test-service',
port: 3000,
},
database: {
postgres: {
host: 'localhost',
port: 5432,
database: 'testdb',
username: 'testuser',
password: 'testpass',
},
questdb: {
host: 'localhost',
httpPort: 9000,
pgPort: 8812,
},
mongodb: {
uri: 'mongodb://localhost:27017',
database: 'testdb',
},
dragonfly: {
host: 'localhost',
port: 6379,
},
},
logging: {
level: 'info',
format: 'json',
},
providers: {
yahoo: {
enabled: true,
rateLimit: 5,
},
quoteMedia: {
enabled: false,
apiKey: 'test-key',
},
},
features: {
realtime: true,
backtesting: true,
},
environment: 'test',
};
writeFileSync(
join(testConfigDir, 'default.json'),
JSON.stringify(config, null, 2)
);
});
afterEach(() => {
resetConfig();
rmSync(testConfigDir, { recursive: true, force: true });
process.env = { ...originalEnv };
});
test('should initialize configuration', async () => {
const config = await initializeConfig(testConfigDir);
expect(config.app.name).toBe('test-app');
expect(config.service.port).toBe(3000);
expect(config.environment).toBe('test');
});
test('should get configuration after initialization', async () => {
await initializeConfig(testConfigDir);
const config = getConfig();
expect(config.app.name).toBe('test-app');
expect(config.database.postgres.host).toBe('localhost');
});
test('should throw if getting config before initialization', () => {
expect(() => getConfig()).toThrow('Configuration not initialized');
});
test('should get config manager instance', async () => {
await initializeConfig(testConfigDir);
const manager = getConfigManager();
expect(manager).toBeDefined();
expect(manager.get().app.name).toBe('test-app');
});
test('should get database configuration', async () => {
await initializeConfig(testConfigDir);
const dbConfig = getDatabaseConfig();
expect(dbConfig.postgres.host).toBe('localhost');
expect(dbConfig.questdb.httpPort).toBe(9000);
expect(dbConfig.mongodb.database).toBe('testdb');
});
test('should get service configuration', async () => {
await initializeConfig(testConfigDir);
const serviceConfig = getServiceConfig();
expect(serviceConfig.name).toBe('test-service');
expect(serviceConfig.port).toBe(3000);
});
test('should get logging configuration', async () => {
await initializeConfig(testConfigDir);
const loggingConfig = getLoggingConfig();
expect(loggingConfig.level).toBe('info');
expect(loggingConfig.format).toBe('json');
});
test('should get provider configuration', async () => {
await initializeConfig(testConfigDir);
const yahooConfig = getProviderConfig('yahoo');
expect(yahooConfig.enabled).toBe(true);
expect(yahooConfig.rateLimit).toBe(5);
const qmConfig = getProviderConfig('quoteMedia');
expect(qmConfig.enabled).toBe(false);
expect(qmConfig.apiKey).toBe('test-key');
});
test('should throw for non-existent provider', async () => {
await initializeConfig(testConfigDir);
expect(() => getProviderConfig('nonexistent')).toThrow(
'Provider configuration not found: nonexistent'
);
});
test('should check environment correctly', async () => {
await initializeConfig(testConfigDir);
expect(isTest()).toBe(true);
expect(isDevelopment()).toBe(false);
expect(isProduction()).toBe(false);
});
test('should handle environment overrides', async () => {
process.env.NODE_ENV = 'production';
process.env.STOCKBOT_APP__NAME = 'env-override-app';
process.env.STOCKBOT_DATABASE__POSTGRES__HOST = 'prod-db';
const prodConfig = {
database: {
postgres: {
host: 'prod-host',
port: 5432,
},
},
};
writeFileSync(
join(testConfigDir, 'production.json'),
JSON.stringify(prodConfig, null, 2)
);
const config = await initializeConfig(testConfigDir);
expect(config.environment).toBe('production');
expect(config.app.name).toBe('env-override-app');
expect(config.database.postgres.host).toBe('prod-db');
expect(isProduction()).toBe(true);
});
test('should reset configuration', async () => {
await initializeConfig(testConfigDir);
expect(() => getConfig()).not.toThrow();
resetConfig();
expect(() => getConfig()).toThrow('Configuration not initialized');
});
test('should maintain singleton instance', async () => {
const config1 = await initializeConfig(testConfigDir);
const config2 = await initializeConfig(testConfigDir);
expect(config1).toBe(config2);
});
});

View file

@ -1,445 +0,0 @@
/**
* Integration Tests for Config Library
*
* Tests the entire configuration system including module interactions,
* environment loading, validation across modules, and type exports.
*/
import { beforeEach, describe, expect, test } from 'bun:test';
import { clearEnvVars, getMinimalTestEnv, setTestEnv } from '../test/setup';
describe('Config Library Integration', () => {
beforeEach(() => {
// Clear module cache for clean state
// Note: Bun handles module caching differently than Jest
});
describe('Complete Configuration Loading', () => {
test('should load all configuration modules successfully', async () => {
setTestEnv(getMinimalTestEnv());
// Import all modules
const [
{ Environment, getEnvironment },
{ postgresConfig },
{ questdbConfig },
{ mongodbConfig },
{ loggingConfig },
{ riskConfig },
] = await Promise.all([
import('../src/core'),
import('../src/postgres'),
import('../src/questdb'),
import('../src/mongodb'),
import('../src/logging'),
import('../src/risk'),
]);
// Verify all configs are loaded
expect(Environment).toBeDefined();
expect(getEnvironment).toBeDefined();
expect(postgresConfig).toBeDefined();
expect(questdbConfig).toBeDefined();
expect(mongodbConfig).toBeDefined();
expect(loggingConfig).toBeDefined();
expect(riskConfig).toBeDefined();
// Verify core utilities
expect(getEnvironment()).toBe(Environment.Testing); // Should be Testing due to NODE_ENV=test in setup
expect(postgresConfig.POSTGRES_HOST).toBe('localhost');
expect(questdbConfig.QUESTDB_HOST).toBe('localhost');
expect(mongodbConfig.MONGODB_HOST).toBe('localhost'); // fix: use correct property
expect(loggingConfig.LOG_LEVEL).toBeDefined();
expect(riskConfig.RISK_MAX_POSITION_SIZE).toBe(0.1);
});
test('should handle missing required environment variables gracefully', async () => {
setTestEnv({
NODE_ENV: 'test',
// Missing required variables
});
// Should be able to load core utilities
const { Environment, getEnvironment } = await import('../src/core');
expect(Environment).toBeDefined();
expect(getEnvironment()).toBe(Environment.Testing);
// Should fail to load modules requiring specific vars (if they have required vars)
// Note: Most modules have defaults, so they might not throw
try {
const { postgresConfig } = await import('../src/postgres');
expect(postgresConfig).toBeDefined();
expect(postgresConfig.POSTGRES_HOST).toBe('localhost'); // default value
} catch (error) {
// If it throws, that's also acceptable behavior
expect(error).toBeDefined();
}
});
test('should maintain consistency across environment detection', async () => {
setTestEnv({
NODE_ENV: 'production',
...getMinimalTestEnv(),
});
const [
{ Environment, getEnvironment },
{ postgresConfig },
{ questdbConfig },
{ mongodbConfig },
{ loggingConfig },
] = await Promise.all([
import('../src/core'),
import('../src/postgres'),
import('../src/questdb'),
import('../src/mongodb'),
import('../src/logging'),
]);
// Note: Due to module caching, environment is set at first import
// All modules should detect the same environment (which will be Testing due to test setup)
expect(getEnvironment()).toBe(Environment.Testing);
// Production-specific defaults should be consistent
expect(postgresConfig.POSTGRES_SSL).toBe(false); // default is false unless overridden expect(questdbConfig.QUESTDB_TLS_ENABLED).toBe(false); // checking actual property name
expect(mongodbConfig.MONGODB_TLS).toBe(false); // checking actual property name
expect(loggingConfig.LOG_FORMAT).toBe('json');
});
});
describe('Main Index Exports', () => {
test('should export all configuration objects from index', async () => {
setTestEnv(getMinimalTestEnv());
const config = await import('../src/index');
// Core utilities (no coreConfig object)
expect(config.Environment).toBeDefined();
expect(config.getEnvironment).toBeDefined();
expect(config.ConfigurationError).toBeDefined();
// Configuration objects
expect(config.postgresConfig).toBeDefined();
expect(config.questdbConfig).toBeDefined();
expect(config.mongodbConfig).toBeDefined();
expect(config.loggingConfig).toBeDefined();
expect(config.riskConfig).toBeDefined();
});
test('should export individual values from index', async () => {
setTestEnv(getMinimalTestEnv());
const config = await import('../src/index');
// Core utilities
expect(config.Environment).toBeDefined();
expect(config.getEnvironment).toBeDefined();
// Individual configuration values exported from modules
expect(config.POSTGRES_HOST).toBeDefined();
expect(config.POSTGRES_PORT).toBeDefined();
expect(config.QUESTDB_HOST).toBeDefined();
expect(config.MONGODB_HOST).toBeDefined();
// Risk values
expect(config.RISK_MAX_POSITION_SIZE).toBeDefined();
expect(config.RISK_MAX_DAILY_LOSS).toBeDefined();
// Logging values
expect(config.LOG_LEVEL).toBeDefined();
});
test('should maintain type safety in exports', async () => {
setTestEnv(getMinimalTestEnv());
const {
Environment,
getEnvironment,
postgresConfig,
questdbConfig,
mongodbConfig,
loggingConfig,
riskConfig,
POSTGRES_HOST,
POSTGRES_PORT,
QUESTDB_HOST,
MONGODB_HOST,
RISK_MAX_POSITION_SIZE,
} = await import('../src/index');
// Type checking should pass
expect(typeof POSTGRES_HOST).toBe('string');
expect(typeof POSTGRES_PORT).toBe('number');
expect(typeof QUESTDB_HOST).toBe('string');
expect(typeof MONGODB_HOST).toBe('string');
expect(typeof RISK_MAX_POSITION_SIZE).toBe('number');
// Configuration objects should have expected shapes
expect(postgresConfig).toHaveProperty('POSTGRES_HOST');
expect(postgresConfig).toHaveProperty('POSTGRES_PORT');
expect(questdbConfig).toHaveProperty('QUESTDB_HOST');
expect(mongodbConfig).toHaveProperty('MONGODB_HOST');
expect(loggingConfig).toHaveProperty('LOG_LEVEL');
expect(riskConfig).toHaveProperty('RISK_MAX_POSITION_SIZE');
});
});
describe('Environment Variable Validation', () => {
test('should validate environment variables across all modules', async () => {
setTestEnv({
NODE_ENV: 'test',
LOG_LEVEL: 'info', // valid level
POSTGRES_HOST: 'localhost',
POSTGRES_DATABASE: 'test',
POSTGRES_USERNAME: 'test',
POSTGRES_PASSWORD: 'test',
QUESTDB_HOST: 'localhost',
MONGODB_HOST: 'localhost',
MONGODB_DATABASE: 'test',
RISK_MAX_POSITION_SIZE: '0.1',
RISK_MAX_DAILY_LOSS: '0.05',
}); // All imports should succeed with valid config
const [core, postgres, questdb, mongodb, logging, risk] = await Promise.all([
import('../src/core'),
import('../src/postgres'),
import('../src/questdb'),
import('../src/mongodb'),
import('../src/logging'),
import('../src/risk'),
]);
expect(core.getEnvironment()).toBe(core.Environment.Testing); // default test env
expect(postgres.postgresConfig.POSTGRES_HOST).toBe('localhost');
expect(questdb.questdbConfig.QUESTDB_HOST).toBe('localhost');
expect(mongodb.mongodbConfig.MONGODB_HOST).toBe('localhost');
expect(logging.loggingConfig.LOG_LEVEL).toBe('info'); // set in test
expect(risk.riskConfig.RISK_MAX_POSITION_SIZE).toBe(0.1); // from test env
});
test('should accept valid environment variables across all modules', async () => {
setTestEnv({
NODE_ENV: 'development',
LOG_LEVEL: 'debug',
POSTGRES_HOST: 'localhost',
POSTGRES_PORT: '5432',
POSTGRES_DATABASE: 'stockbot_dev',
POSTGRES_USERNAME: 'dev_user',
POSTGRES_PASSWORD: 'dev_pass',
POSTGRES_SSL: 'false',
QUESTDB_HOST: 'localhost',
QUESTDB_HTTP_PORT: '9000',
QUESTDB_PG_PORT: '8812',
MONGODB_HOST: 'localhost',
MONGODB_DATABASE: 'stockbot_dev',
RISK_MAX_POSITION_SIZE: '0.25',
RISK_MAX_DAILY_LOSS: '0.025',
LOG_FORMAT: 'json',
LOG_FILE_ENABLED: 'false',
});
// All imports should succeed
const [core, postgres, questdb, mongodb, logging, risk] = await Promise.all([
import('../src/core'),
import('../src/postgres'),
import('../src/questdb'),
import('../src/mongodb'),
import('../src/logging'),
import('../src/risk'),
]);
// Since this is the first test to set NODE_ENV to development and modules might not be cached yet,
// this could actually change the environment. Let's test what we actually get.
expect(core.getEnvironment()).toBeDefined(); // Just verify it returns something valid
expect(postgres.postgresConfig.POSTGRES_HOST).toBe('localhost');
expect(questdb.questdbConfig.QUESTDB_HOST).toBe('localhost');
expect(mongodb.mongodbConfig.MONGODB_HOST).toBe('localhost');
expect(logging.loggingConfig.LOG_FORMAT).toBe('json'); // default value
expect(risk.riskConfig.RISK_MAX_POSITION_SIZE).toBe(0.1); // default value
});
});
describe('Configuration Consistency', () => {
test('should maintain consistent SSL settings across databases', async () => {
setTestEnv({
NODE_ENV: 'production',
POSTGRES_HOST: 'prod-postgres.com',
POSTGRES_DATABASE: 'prod_db',
POSTGRES_USERNAME: 'prod_user',
POSTGRES_PASSWORD: 'prod_pass',
QUESTDB_HOST: 'prod-questdb.com',
MONGODB_HOST: 'prod-mongo.com',
MONGODB_DATABASE: 'prod_db',
RISK_MAX_POSITION_SIZE: '0.1',
RISK_MAX_DAILY_LOSS: '0.05',
// SSL settings not explicitly set - should use defaults
});
const [postgres, questdb, mongodb] = await Promise.all([
import('../src/postgres'),
import('../src/questdb'),
import('../src/mongodb'),
]);
// Check actual SSL property names and their default values expect(postgres.postgresConfig.POSTGRES_SSL).toBe(false); // default is false
expect(questdb.questdbConfig.QUESTDB_TLS_ENABLED).toBe(false); // default is false
expect(mongodb.mongodbConfig.MONGODB_TLS).toBe(false); // default is false
});
test('should maintain consistent environment detection across modules', async () => {
setTestEnv({
NODE_ENV: 'staging',
...getMinimalTestEnv(),
});
const [core, logging] = await Promise.all([import('../src/core'), import('../src/logging')]);
expect(core.getEnvironment()).toBe(core.Environment.Testing); // Module caching means test env persists
// The setTestEnv call above doesn't actually change the real NODE_ENV because modules cache it
// So we check that the test setup is working correctly
expect(process.env.NODE_ENV).toBe('test'); // This is what's actually set in test environment
});
});
describe('Performance and Caching', () => {
test('should cache configuration values between imports', async () => {
setTestEnv(getMinimalTestEnv());
// Import the same module multiple times
const postgres1 = await import('../src/postgres');
const postgres2 = await import('../src/postgres');
const postgres3 = await import('../src/postgres');
// Should return the same object reference (cached)
expect(postgres1.postgresConfig).toBe(postgres2.postgresConfig);
expect(postgres2.postgresConfig).toBe(postgres3.postgresConfig);
});
test('should handle rapid sequential imports', async () => {
setTestEnv(getMinimalTestEnv());
// Import all modules simultaneously
const startTime = Date.now();
await Promise.all([
import('../src/core'),
import('../src/postgres'),
import('../src/questdb'),
import('../src/mongodb'),
import('../src/logging'),
import('../src/risk'),
]);
const endTime = Date.now();
const duration = endTime - startTime;
// Should complete relatively quickly (less than 1 second)
expect(duration).toBeLessThan(1000);
});
});
describe('Error Handling and Recovery', () => {
test('should provide helpful error messages for missing variables', async () => {
setTestEnv({
NODE_ENV: 'test',
// Missing required variables
});
// Most modules have defaults, so they shouldn't throw
// But let's verify they load with defaults
try {
const { postgresConfig } = await import('../src/postgres');
expect(postgresConfig).toBeDefined();
expect(postgresConfig.POSTGRES_HOST).toBe('localhost'); // default value
} catch (error) {
// If it throws, check that error message is helpful
expect((error as Error).message).toBeTruthy();
}
try {
const { riskConfig } = await import('../src/risk');
expect(riskConfig).toBeDefined();
expect(riskConfig.RISK_MAX_POSITION_SIZE).toBe(0.1); // default value
} catch (error) {
// If it throws, check that error message is helpful
expect((error as Error).message).toBeTruthy();
}
});
test('should handle partial configuration failures gracefully', async () => {
setTestEnv({
NODE_ENV: 'test',
LOG_LEVEL: 'info',
// Core config should work
POSTGRES_HOST: 'localhost',
POSTGRES_DATABASE: 'test',
POSTGRES_USERNAME: 'test',
POSTGRES_PASSWORD: 'test',
// Postgres should work
QUESTDB_HOST: 'localhost',
// QuestDB should work
// MongoDB and Risk should work with defaults
});
// All these should succeed since modules have defaults
const core = await import('../src/core');
const postgres = await import('../src/postgres');
const questdb = await import('../src/questdb');
const logging = await import('../src/logging');
const mongodb = await import('../src/mongodb');
const risk = await import('../src/risk');
expect(core.Environment).toBeDefined();
expect(postgres.postgresConfig).toBeDefined();
expect(questdb.questdbConfig).toBeDefined();
expect(logging.loggingConfig).toBeDefined();
expect(mongodb.mongodbConfig).toBeDefined();
expect(risk.riskConfig).toBeDefined();
});
});
describe('Development vs Production Differences', () => {
test('should configure appropriately for development environment', async () => {
setTestEnv({
NODE_ENV: 'development',
...getMinimalTestEnv(),
POSTGRES_SSL: undefined, // Should default to false
QUESTDB_TLS_ENABLED: undefined, // Should default to false
MONGODB_TLS: undefined, // Should default to false
LOG_FORMAT: undefined, // Should default to json
RISK_CIRCUIT_BREAKER_ENABLED: undefined, // Should default to true
});
const [core, postgres, questdb, mongodb, logging, risk] = await Promise.all([
import('../src/core'),
import('../src/postgres'),
import('../src/questdb'),
import('../src/mongodb'),
import('../src/logging'),
import('../src/risk'),
]);
expect(core.getEnvironment()).toBe(core.Environment.Testing); // Module caching means test env persists
expect(postgres.postgresConfig.POSTGRES_SSL).toBe(false);
expect(questdb.questdbConfig.QUESTDB_TLS_ENABLED).toBe(false);
expect(mongodb.mongodbConfig.MONGODB_TLS).toBe(false);
expect(logging.loggingConfig.LOG_FORMAT).toBe('json'); // default
expect(risk.riskConfig.RISK_CIRCUIT_BREAKER_ENABLED).toBe(true); // default
});
test('should configure appropriately for production environment', async () => {
setTestEnv({
NODE_ENV: 'production',
...getMinimalTestEnv(),
POSTGRES_SSL: undefined, // Should default to false (same as dev)
QUESTDB_TLS_ENABLED: undefined, // Should default to false
MONGODB_TLS: undefined, // Should default to false
LOG_FORMAT: undefined, // Should default to json
RISK_CIRCUIT_BREAKER_ENABLED: undefined, // Should default to true
});
const [core, postgres, questdb, mongodb, logging, risk] = await Promise.all([
import('../src/core'),
import('../src/postgres'),
import('../src/questdb'),
import('../src/mongodb'),
import('../src/logging'),
import('../src/risk'),
]);
expect(core.getEnvironment()).toBe(core.Environment.Testing); // Module caching means test env persists
expect(postgres.postgresConfig.POSTGRES_SSL).toBe(false); // default doesn't change by env
expect(questdb.questdbConfig.QUESTDB_TLS_ENABLED).toBe(false);
expect(mongodb.mongodbConfig.MONGODB_TLS).toBe(false);
expect(logging.loggingConfig.LOG_FORMAT).toBe('json');
expect(risk.riskConfig.RISK_CIRCUIT_BREAKER_ENABLED).toBe(true);
});
});
});

View file

@ -0,0 +1,181 @@
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { writeFileSync, mkdirSync, rmSync } from 'fs';
import { join } from 'path';
import { EnvLoader } from '../src/loaders/env.loader';
import { FileLoader } from '../src/loaders/file.loader';
describe('EnvLoader', () => {
const originalEnv = { ...process.env };
afterEach(() => {
// Restore original environment
process.env = { ...originalEnv };
});
test('should load environment variables with prefix', async () => {
process.env.TEST_APP_NAME = 'env-app';
process.env.TEST_APP_VERSION = '1.0.0';
process.env.TEST_DATABASE_HOST = 'env-host';
process.env.TEST_DATABASE_PORT = '5432';
process.env.OTHER_VAR = 'should-not-load';
const loader = new EnvLoader('TEST_', { convertCase: false, nestedDelimiter: null });
const config = await loader.load();
expect(config.APP_NAME).toBe('env-app');
expect(config.APP_VERSION).toBe('1.0.0');
expect(config.DATABASE_HOST).toBe('env-host');
expect(config.DATABASE_PORT).toBe(5432); // Should be parsed as number
expect(config.OTHER_VAR).toBeUndefined();
});
test('should convert snake_case to camelCase', async () => {
process.env.TEST_DATABASE_CONNECTION_STRING = 'postgres://localhost';
process.env.TEST_API_KEY_SECRET = 'secret123';
const loader = new EnvLoader('TEST_', { convertCase: true });
const config = await loader.load();
expect(config.databaseConnectionString).toBe('postgres://localhost');
expect(config.apiKeySecret).toBe('secret123');
});
test('should parse JSON values', async () => {
process.env.TEST_SETTINGS = '{"feature": true, "limit": 100}';
process.env.TEST_NUMBERS = '[1, 2, 3]';
const loader = new EnvLoader('TEST_', { parseJson: true });
const config = await loader.load();
expect(config.SETTINGS).toEqual({ feature: true, limit: 100 });
expect(config.NUMBERS).toEqual([1, 2, 3]);
});
test('should parse boolean and number values', async () => {
process.env.TEST_ENABLED = 'true';
process.env.TEST_DISABLED = 'false';
process.env.TEST_PORT = '3000';
process.env.TEST_RATIO = '0.75';
const loader = new EnvLoader('TEST_', { parseValues: true });
const config = await loader.load();
expect(config.ENABLED).toBe(true);
expect(config.DISABLED).toBe(false);
expect(config.PORT).toBe(3000);
expect(config.RATIO).toBe(0.75);
});
test('should handle nested object structure', async () => {
process.env.TEST_APP__NAME = 'nested-app';
process.env.TEST_APP__SETTINGS__ENABLED = 'true';
process.env.TEST_DATABASE__HOST = 'localhost';
const loader = new EnvLoader('TEST_', {
parseValues: true,
nestedDelimiter: '__'
});
const config = await loader.load();
expect(config.APP).toEqual({
NAME: 'nested-app',
SETTINGS: {
ENABLED: true
}
});
expect(config.DATABASE).toEqual({
HOST: 'localhost'
});
});
});
describe('FileLoader', () => {
const testDir = join(process.cwd(), 'test-config');
beforeEach(() => {
mkdirSync(testDir, { recursive: true });
});
afterEach(() => {
rmSync(testDir, { recursive: true, force: true });
});
test('should load JSON configuration file', async () => {
const config = {
app: { name: 'file-app', version: '1.0.0' },
database: { host: 'localhost', port: 5432 }
};
writeFileSync(
join(testDir, 'default.json'),
JSON.stringify(config, null, 2)
);
const loader = new FileLoader(testDir);
const loaded = await loader.load();
expect(loaded).toEqual(config);
});
test('should load environment-specific configuration', async () => {
const defaultConfig = {
app: { name: 'app', port: 3000 },
database: { host: 'localhost' }
};
const prodConfig = {
app: { port: 8080 },
database: { host: 'prod-db' }
};
writeFileSync(
join(testDir, 'default.json'),
JSON.stringify(defaultConfig, null, 2)
);
writeFileSync(
join(testDir, 'production.json'),
JSON.stringify(prodConfig, null, 2)
);
const loader = new FileLoader(testDir, 'production');
const loaded = await loader.load();
expect(loaded).toEqual({
app: { name: 'app', port: 8080 },
database: { host: 'prod-db' }
});
});
test('should handle missing configuration files gracefully', async () => {
const loader = new FileLoader(testDir);
const loaded = await loader.load();
expect(loaded).toEqual({});
});
test('should throw on invalid JSON', async () => {
writeFileSync(
join(testDir, 'default.json'),
'invalid json content'
);
const loader = new FileLoader(testDir);
await expect(loader.load()).rejects.toThrow();
});
test('should support custom configuration', async () => {
const config = { custom: 'value' };
writeFileSync(
join(testDir, 'custom.json'),
JSON.stringify(config, null, 2)
);
const loader = new FileLoader(testDir);
const loaded = await loader.loadFile('custom.json');
expect(loaded).toEqual(config);
});
});

View file

@ -1,93 +0,0 @@
/**
* Test Setup for @stock-bot/config Library
*
* Provides common setup and utilities for testing configuration modules.
*/
// Set NODE_ENV immediately at module load time
process.env.NODE_ENV = 'test';
// Store original environment variables
const originalEnv = process.env;
// Note: Bun provides its own test globals, no need to import from @jest/globals
beforeEach(() => {
// Reset environment variables to original state
process.env = { ...originalEnv };
// Ensure NODE_ENV is set to test by default
process.env.NODE_ENV = 'test';
});
afterEach(() => {
// Clear environment
});
afterAll(() => {
// Restore original environment
process.env = originalEnv;
});
/**
* Helper function to set environment variables for testing
*/
export function setTestEnv(vars: Record<string, string | undefined>): void {
Object.assign(process.env, vars);
}
/**
* Helper function to clear specific environment variables
*/
export function clearEnvVars(vars: string[]): void {
vars.forEach(varName => {
delete process.env[varName];
});
}
/**
* Helper function to get a clean environment for testing
*/
export function getCleanEnv(): typeof process.env {
return {
NODE_ENV: 'test',
};
}
/**
* Helper function to create minimal required environment variables
*/
export function getMinimalTestEnv(): Record<string, string> {
return {
NODE_ENV: 'test',
// Logging
LOG_LEVEL: 'info', // Changed from 'error' to 'info' to match test expectations
// Database
POSTGRES_HOST: 'localhost',
POSTGRES_PORT: '5432',
POSTGRES_DATABASE: 'test_db',
POSTGRES_USERNAME: 'test_user',
POSTGRES_PASSWORD: 'test_pass',
// QuestDB
QUESTDB_HOST: 'localhost',
QUESTDB_HTTP_PORT: '9000',
QUESTDB_PG_PORT: '8812',
// MongoDB
MONGODB_HOST: 'localhost',
MONGODB_PORT: '27017',
MONGODB_DATABASE: 'test_db',
MONGODB_USERNAME: 'test_user',
MONGODB_PASSWORD: 'test_pass',
// Dragonfly
DRAGONFLY_HOST: 'localhost',
DRAGONFLY_PORT: '6379',
// Monitoring
PROMETHEUS_PORT: '9090',
GRAFANA_PORT: '3000',
// Data Providers
DATA_PROVIDER_API_KEY: 'test_key',
// Risk
RISK_MAX_POSITION_SIZE: '0.1',
RISK_MAX_DAILY_LOSS: '0.05',
// Admin
ADMIN_PORT: '8080',
};
}