stock-bot/libs/core/config/test/real-usage.test.ts
2025-06-23 18:14:43 -04:00

415 lines
11 KiB
TypeScript

import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
import { join } from 'path';
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import {
getConfig,
getDatabaseConfig,
getLoggingConfig,
getProviderConfig,
getServiceConfig,
initializeServiceConfig,
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-ingestion usage', async () => {
const dataServiceDir = join(TEST_DIR, 'apps', 'data-ingestion');
process.chdir(dataServiceDir);
// Simulate how data-ingestion would initialize config
const config = await initializeServiceConfig();
// 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);
});
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-ingestion');
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-ingestion');
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-ingestion');
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-ingestion');
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-ingestion');
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');
expect(port).toBe(3001);
});
test('should handle config updates at runtime', async () => {
const dataServiceDir = join(TEST_DIR, 'apps', 'data-ingestion');
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-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
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-ingestion'),
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-ingestion',
},
service: {
name: 'data-ingestion',
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
`
);
}