From 62a29259b9caa2653e73375aee6d64502283218b Mon Sep 17 00:00:00 2001 From: Boki Date: Fri, 20 Jun 2025 16:03:27 -0400 Subject: [PATCH] upgraded configs and added lots of tests --- libs/config/src/loaders/env.loader.ts | 4 + libs/config/src/schemas/index.ts | 83 ++++- libs/config/test/dynamic-location.test.ts | 386 +++++++++++++++++++++ libs/config/test/edge-cases.test.ts | 384 ++++++++++++++++++++ libs/config/test/index.test.ts | 17 +- libs/config/test/provider-config.test.ts | 320 +++++++++++++++++ libs/config/test/real-usage.test.ts | 405 ++++++++++++++++++++++ 7 files changed, 1583 insertions(+), 16 deletions(-) create mode 100644 libs/config/test/dynamic-location.test.ts create mode 100644 libs/config/test/edge-cases.test.ts create mode 100644 libs/config/test/provider-config.test.ts create mode 100644 libs/config/test/real-usage.test.ts diff --git a/libs/config/src/loaders/env.loader.ts b/libs/config/src/loaders/env.loader.ts index 3008396..51699f8 100644 --- a/libs/config/src/loaders/env.loader.ts +++ b/libs/config/src/loaders/env.loader.ts @@ -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'], }; diff --git a/libs/config/src/schemas/index.ts b/libs/config/src/schemas/index.ts index 177039a..1c69766 100644 --- a/libs/config/src/schemas/index.ts +++ b/libs/config/src/schemas/index.ts @@ -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(), diff --git a/libs/config/test/dynamic-location.test.ts b/libs/config/test/dynamic-location.test.ts new file mode 100644 index 0000000..b632d3d --- /dev/null +++ b/libs/config/test/dynamic-location.test.ts @@ -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 +` + ); +} \ No newline at end of file diff --git a/libs/config/test/edge-cases.test.ts b/libs/config/test/edge-cases.test.ts new file mode 100644 index 0000000..bae771a --- /dev/null +++ b/libs/config/test/edge-cases.test.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/libs/config/test/index.test.ts b/libs/config/test/index.test.ts index 7ff6b8c..215bb64 100644 --- a/libs/config/test/index.test.ts +++ b/libs/config/test/index.test.ts @@ -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', }; diff --git a/libs/config/test/provider-config.test.ts b/libs/config/test/provider-config.test.ts new file mode 100644 index 0000000..444aeec --- /dev/null +++ b/libs/config/test/provider-config.test.ts @@ -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); + } + }); +}); \ No newline at end of file diff --git a/libs/config/test/real-usage.test.ts b/libs/config/test/real-usage.test.ts new file mode 100644 index 0000000..1111fa0 --- /dev/null +++ b/libs/config/test/real-usage.test.ts @@ -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('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 +` + ); +} \ No newline at end of file