import { describe, expect, it } from 'vitest'; import { loadApiConfig, loadSchedulerConfig, loadWorkerConfig, toSafeRuntimeSummary, } from './runtime-config'; const baseRedisEnv = { REDIS_HOST: '127.0.0.1', REDIS_PORT: '6379', REDIS_USERNAME: '', REDIS_PASSWORD: 'TEST_SENSITIVE_VALUE', REDIS_DATABASE: '0', REDIS_TLS: 'false', }; const disabledMysqlEnv = { MYSQL_ENABLED: 'false', MYSQL_HOST: '127.0.0.1', MYSQL_PORT: '3306', MYSQL_DATABASE: 'a_share_analysis_dev', MYSQL_USERNAME: '', MYSQL_PASSWORD: '', MYSQL_CHARSET: 'utf8mb4', MYSQL_COLLATION: 'utf8mb4_general_ci', MYSQL_TIMEZONE: '+08:00', }; const disabledAiEnv = { AI_ENABLED: 'false', AI_BASE_URL: '', AI_API_KEY: '', AI_MODEL: '', AI_TIMEOUT_MS: '30000', }; const disabledRocketMqEnv = { ROCKETMQ_ENABLED: 'false', ROCKETMQ_NAMESRV_ADDR: '127.0.0.1:9876', ROCKETMQ_ENDPOINT: '127.0.0.1:8081', ROCKETMQ_TOPIC_PREFIX: 'a-share-analysis', }; describe('进程级运行配置', () => { it('API 只返回自己的配置且不要求 Scheduler 或 Worker 字段', () => { const config = loadApiConfig({ APP_ENV: 'development', LOG_LEVEL: 'debug', API_HOST: '127.0.0.1', API_PORT: '3000', TDX_BASE_URL: 'http://127.0.0.1:17710', TDX_TIMEOUT_MS: '5000', ...baseRedisEnv, ...disabledMysqlEnv, ...disabledAiEnv, SCHEDULER_TIMEZONE: 'should-not-leak', MARKET_DATA_API_KEY: 'should-not-leak', }); expect(config).toMatchObject({ processName: 'api', appEnv: 'development', api: { host: '127.0.0.1', port: 3000 }, redis: { database: 0 }, mysql: { enabled: false, timezone: '+08:00' }, }); expect(config).not.toHaveProperty('SCHEDULER_TIMEZONE'); expect(config).not.toHaveProperty('MARKET_DATA_API_KEY'); expect(Object.isFrozen(config)).toBe(true); expect(Object.isFrozen(config.redis)).toBe(true); }); it('Scheduler 不读取 MySQL 密码且不要求 API 端口', () => { const config = loadSchedulerConfig({ APP_ENV: 'development', LOG_LEVEL: 'debug', SCHEDULER_TIMEZONE: 'Asia/Shanghai', SCHEDULER_ENABLED: 'true', ...baseRedisEnv, ...disabledRocketMqEnv, ROCKETMQ_SEND_TIMEOUT_MS: '3000', MYSQL_PASSWORD: 'must-not-enter-scheduler', }); expect(config).toMatchObject({ processName: 'scheduler', scheduler: { enabled: true, timezone: 'Asia/Shanghai' }, rocketMq: { enabled: false, sendTimeoutMs: 3000 }, }); expect(JSON.stringify(config)).not.toContain('must-not-enter-scheduler'); expect(config).not.toHaveProperty('api'); }); it('Worker 不要求 API 端口并保留自己的行情与消费配置', () => { const config = loadWorkerConfig({ APP_ENV: 'development', LOG_LEVEL: 'debug', MARKET_DATA_MODE: 'mock', MARKET_DATA_PROVIDER: 'deterministic-fixture', MARKET_DATA_BASE_URL: '', MARKET_DATA_API_KEY: '', ...baseRedisEnv, REDIS_DATABASE: '2', ...disabledMysqlEnv, ...disabledRocketMqEnv, ROCKETMQ_CONSUMER_GROUP: 'a-share-analysis-worker', ROCKETMQ_CONSUMER_CONCURRENCY: '4', ...disabledAiEnv, }); expect(config).toMatchObject({ processName: 'worker', marketData: { mode: 'mock', provider: 'deterministic-fixture' }, redis: { database: 2 }, rocketMq: { consumerConcurrency: 4 }, }); expect(config).not.toHaveProperty('api'); }); it('MySQL 启用时拒绝缺失密码或错误时区', () => { const enabledMysql = { ...disabledMysqlEnv, MYSQL_ENABLED: 'true', MYSQL_USERNAME: 'dev-user', MYSQL_PASSWORD: 'TEST_SENSITIVE_VALUE', }; const apiEnv = { APP_ENV: 'development', LOG_LEVEL: 'debug', API_HOST: '127.0.0.1', API_PORT: '3000', TDX_BASE_URL: 'http://127.0.0.1:17710', TDX_TIMEOUT_MS: '5000', ...baseRedisEnv, ...enabledMysql, ...disabledAiEnv, }; expect(() => loadApiConfig({ ...apiEnv, MYSQL_PASSWORD: '' })).toThrow(/MYSQL_PASSWORD/); expect(() => loadApiConfig({ ...apiEnv, MYSQL_TIMEZONE: 'SYSTEM' })).toThrow(/MYSQL_TIMEZONE/); }); it('TDX 只允许本机回环地址和固定端口', () => { const apiEnv = { APP_ENV: 'development', LOG_LEVEL: 'debug', API_HOST: '127.0.0.1', API_PORT: '3000', TDX_BASE_URL: 'http://127.0.0.1:17710', TDX_TIMEOUT_MS: '5000', ...baseRedisEnv, ...disabledMysqlEnv, ...disabledAiEnv, }; expect(() => loadApiConfig({ ...apiEnv, TDX_BASE_URL: 'http://192.168.28.106:17709' })).toThrow( /TDX_BASE_URL 必须指向本机回环地址/, ); expect(() => loadApiConfig({ ...apiEnv, TDX_BASE_URL: 'https://127.0.0.1:17710' })).toThrow( /TDX_BASE_URL 必须使用 http 协议/, ); expect(() => loadApiConfig({ ...apiEnv, TDX_BASE_URL: 'http://127.0.0.1:17711' })).toThrow( /TDX_BASE_URL 必须使用 17710 端口/, ); }); it('日志摘要不包含任何连接密码、密钥或未知字段', () => { const config = loadApiConfig({ APP_ENV: 'development', LOG_LEVEL: 'debug', API_HOST: '127.0.0.1', API_PORT: '3000', TDX_BASE_URL: 'http://127.0.0.1:17710', TDX_TIMEOUT_MS: '5000', ...baseRedisEnv, ...disabledMysqlEnv, ...disabledAiEnv, UNKNOWN_TOKEN: 'TEST_UNKNOWN_VALUE', }); const serialized = JSON.stringify(toSafeRuntimeSummary(config)); expect(serialized).toContain('"processName":"api"'); expect(serialized).not.toContain('TEST_SENSITIVE_VALUE'); expect(serialized).not.toContain('TEST_UNKNOWN_VALUE'); expect(serialized).not.toContain('password'); expect(serialized).not.toContain('apiKey'); }); it('RocketMQ 在未落地已验证 Node 客户端前禁止启用', () => { expect(() => loadSchedulerConfig({ APP_ENV: 'development', LOG_LEVEL: 'debug', SCHEDULER_TIMEZONE: 'Asia/Shanghai', SCHEDULER_ENABLED: 'true', ...baseRedisEnv, ...disabledRocketMqEnv, ROCKETMQ_ENABLED: 'true', ROCKETMQ_SEND_TIMEOUT_MS: '3000', }), ).toThrow(/RocketMQ Node 客户端/); }); });