415 lines
11 KiB
TypeScript
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
|
|
`
|
|
);
|
|
}
|