import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { configuration, isAdminPortRequest, isSamePort, parsePort, resolveAdminPort, validateEnv, } from '../src/config/configuration'; /** * 管理端口与软件 API 端口必须分开: * PORT 只提供接口,ADMIN_PORT(默认 PORT + 1)提供管理页面 + 全部接口, * 同时允许 ADMIN_PORT === PORT 的单端口兼容模式。 */ const PORT_KEYS = ['PORT', 'ADMIN_PORT', 'HOST', 'ADMIN_HOST'] as const; let savedEnv: Record; beforeEach(() => { savedEnv = {}; for (const key of PORT_KEYS) { savedEnv[key] = process.env[key]; delete process.env[key]; } }); afterEach(() => { for (const key of PORT_KEYS) { if (savedEnv[key] === undefined) delete process.env[key]; else process.env[key] = savedEnv[key]; } }); /** 构造一份除端口外都合法的环境,便于单独验证端口校验。 */ function validEnv(overrides: Record = {}): Record { return { nodeEnv: 'development', port: 3000, adminPort: 3001, db: { host: '127.0.0.1', user: 'u', name: 'n', synchronize: false }, ...overrides, }; } describe('端口解析与默认值', () => { it('parsePort:未设置或空白用 fallback,设置了就取数字', () => { expect(parsePort(undefined, 3000)).toBe(3000); expect(parsePort('', 3000)).toBe(3000); expect(parsePort(' ', 3000)).toBe(3000); expect(parsePort('3111', 3000)).toBe(3111); expect(Number.isNaN(parsePort('abc', 3000))).toBe(true); }); it('resolveAdminPort:未设置 ADMIN_PORT 时默认 PORT + 1', () => { expect(resolveAdminPort(3000, undefined)).toBe(3001); expect(resolveAdminPort(3000, '')).toBe(3001); expect(resolveAdminPort(3111, ' ')).toBe(3112); expect(resolveAdminPort(3000, '4000')).toBe(4000); }); it('configuration:默认端口 3000 / 管理端口 3001,管理端监听地址默认取 HOST', () => { const env = configuration(); expect(env.port).toBe(3000); expect(env.adminPort).toBe(3001); expect(env.bindHost).toBe('0.0.0.0'); expect(env.adminHost).toBe('0.0.0.0'); }); it('configuration:PORT 改动后 ADMIN_PORT 跟着变成 PORT + 1', () => { process.env.PORT = '3111'; const env = configuration(); expect(env.port).toBe(3111); expect(env.adminPort).toBe(3112); }); it('configuration:显式 ADMIN_PORT 优先于默认值', () => { process.env.PORT = '3111'; process.env.ADMIN_PORT = '4111'; expect(configuration().adminPort).toBe(4111); }); it('需求约定:未设置 HOST 时默认监听 0.0.0.0,而不是 127.0.0.1', () => { delete process.env.HOST; delete process.env.ADMIN_HOST; const env = configuration(); expect(env.bindHost).toBe('0.0.0.0'); expect(env.adminHost).toBe('0.0.0.0'); expect(env.bindHost).not.toBe('127.0.0.1'); }); it('两份 .env 模板都写着 HOST=0.0.0.0(防止默认值被改回去)', () => { for (const file of [join(__dirname, '..', '..', 'deploy', '.env.example'), join(__dirname, '..', '.env.example')]) { const content = readFileSync(file, 'utf8'); expect(content, file).toMatch(/^HOST=0\.0\.0\.0$/m); expect(content, file).not.toMatch(/^HOST=127\.0\.0\.1$/m); } }); it('configuration:ADMIN_HOST 显式设置时不再跟随 HOST', () => { process.env.HOST = '127.0.0.1'; expect(configuration().adminHost).toBe('127.0.0.1'); process.env.ADMIN_HOST = '0.0.0.0'; expect(configuration().adminHost).toBe('0.0.0.0'); expect(configuration().bindHost).toBe('127.0.0.1'); }); }); describe('端口配置校验', () => { it('合法端口不报任何问题', () => { const { errors, warnings } = validateEnv(validEnv()); expect(errors).toEqual([]); expect(warnings).toEqual([]); }); it('PORT 非法(0、越界、非整数、NaN)都要报问题', () => { for (const port of [0, 65536, 3000.5, Number.NaN]) { const { errors } = validateEnv(validEnv({ port, adminPort: 3001 })); expect(errors.some((error) => error.includes('PORT'))).toBe(true); } }); it('ADMIN_PORT 非法也要报问题', () => { const { errors } = validateEnv(validEnv({ adminPort: 70000 })); expect(errors.some((error) => error.includes('ADMIN_PORT'))).toBe(true); }); it('ADMIN_PORT === PORT 只给 warn 级提示,不算错误(生产环境也不阻断)', () => { const { errors, warnings } = validateEnv( validEnv({ nodeEnv: 'production', port: 3000, adminPort: 3000 }), ); expect(errors).toEqual([]); expect(warnings).toHaveLength(1); expect(warnings[0]).toContain('未分端口'); }); it('原有的数据库校验保持不变', () => { const { errors } = validateEnv({ nodeEnv: 'production', port: 3000, adminPort: 3000, db: { host: '', user: '', name: '', synchronize: true }, }); expect(errors).toContain('DB_HOST 未设置'); expect(errors).toContain('DB_USER 未设置'); expect(errors).toContain('DB_NAME 未设置'); expect(errors.some((error) => error.includes('DB_SYNCHRONIZE'))).toBe(true); }); }); describe('按请求端口区分管理端行为', () => { it('isSamePort 只在两个端口相等且合法时为 true', () => { expect(isSamePort(3000, 3000)).toBe(true); expect(isSamePort(3000, 3001)).toBe(false); expect(isSamePort(Number.NaN, Number.NaN)).toBe(false); }); it('分端口时:管理端口是管理端,API 端口不是', () => { expect(isAdminPortRequest(3112, 3111, 3112)).toBe(true); expect(isAdminPortRequest(3111, 3111, 3112)).toBe(false); expect(isAdminPortRequest(undefined, 3111, 3112)).toBe(false); }); it('ADMIN_PORT === PORT 的兼容模式下所有请求都按管理端处理', () => { expect(isAdminPortRequest(3111, 3111, 3111)).toBe(true); expect(isAdminPortRequest(undefined, 3111, 3111)).toBe(true); }); });