206 lines
6.1 KiB
TypeScript
206 lines
6.1 KiB
TypeScript
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 客户端/);
|
|
});
|
|
});
|