Files
bun-app-template/tests/server/config.test.ts
lanyuanxiaoyao c592f2b97c feat: 引入分层配置生命周期,支持变量引用和 JSON Schema 校验
- 新增 src/server/config/ 模块(types、issues、variables、normalizer、schema)
- 配置布局从 server.host/server.port 切换为 server.listen.host/server.listen.port
- 移除 HOST/PORT 隐式环境变量覆盖,改为 YAML 显式 ${KEY} 变量引用
- 支持 ${KEY}、${KEY|default}、${KEY|}、$${KEY} 变量语法
- 使用 @sinclair/typebox + ajv 实现运行时严格契约校验和 JSON Schema 导出
- 新增 scripts/generate-config-schema.ts 和 config.schema.json
- 新增 bun run schema / schema:check 命令,check 先执行 schema:check
- 更新 README.md 和 DEVELOPMENT.md 匹配新配置体系
- 新增变量解析、schema 校验和 schema 同步测试
2026-05-25 12:17:40 +08:00

205 lines
6.1 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { loadServerConfig, parseRuntimeArgs } from "../../src/server/config";
describe("parseRuntimeArgs", () => {
test("无参数返回空对象", () => {
const result = parseRuntimeArgs([]);
expect(result).toEqual({});
});
test("有参数返回 configPath", () => {
const result = parseRuntimeArgs(["config.yaml"]);
expect(result).toEqual({ configPath: "config.yaml" });
});
test("--help 输出用法并退出", () => {
const logs: string[] = [];
let exitCode: number | undefined;
const originalLog = console.log;
console.log = (...args: unknown[]) => logs.push(args.join(" "));
Object.defineProperty(process, "exit", {
configurable: true,
value: (code: number) => {
exitCode = code;
},
writable: true,
});
try {
parseRuntimeArgs(["--help"]);
} finally {
Object.defineProperty(process, "exit", {
configurable: true,
value: process.exit.bind(process),
writable: true,
});
console.log = originalLog;
}
expect(exitCode).toBe(0);
expect(logs.some((l) => l.includes("用法"))).toBe(true);
});
test("-h 输出用法并退出", () => {
const logs: string[] = [];
let exitCode: number | undefined;
const originalLog = console.log;
console.log = (...args: unknown[]) => logs.push(args.join(" "));
Object.defineProperty(process, "exit", {
configurable: true,
value: (code: number) => {
exitCode = code;
},
writable: true,
});
try {
parseRuntimeArgs(["-h"]);
} finally {
Object.defineProperty(process, "exit", {
configurable: true,
value: process.exit.bind(process),
writable: true,
});
console.log = originalLog;
}
expect(exitCode).toBe(0);
expect(logs.some((l) => l.includes("用法"))).toBe(true);
});
});
describe("loadServerConfig", () => {
test("无 configPath 使用默认值", async () => {
const config = await loadServerConfig();
expect(config.host).toBe("127.0.0.1");
expect(config.port).toBe(3000);
});
test("环境变量 HOST 不影响默认值(无隐式覆盖)", async () => {
const prev = process.env["HOST"];
process.env["HOST"] = "0.0.0.0";
try {
const config = await loadServerConfig();
expect(config.host).toBe("127.0.0.1");
} finally {
if (prev === undefined) {
delete process.env["HOST"];
} else {
process.env["HOST"] = prev;
}
}
});
test("环境变量 PORT 不影响默认值(无隐式覆盖)", async () => {
const prev = process.env["PORT"];
process.env["PORT"] = "8080";
try {
const config = await loadServerConfig();
expect(config.port).toBe(3000);
} finally {
if (prev === undefined) {
delete process.env["PORT"];
} else {
process.env["PORT"] = prev;
}
}
});
test("YAML 配置文件不存在时报错", async () => {
try {
await loadServerConfig("/nonexistent/path/config.yaml");
expect.unreachable();
} catch (error) {
expect((error as Error).message).toContain("配置文件不存在");
}
});
test("新布局 server.listen 加载成功", async () => {
const temp = tmpdir();
const yamlPath = join(temp, "test-listen.yaml");
const yamlContent = 'server:\n listen:\n host: "0.0.0.0"\n port: 9999\n';
await writeFile(yamlPath, yamlContent);
try {
const config = await loadServerConfig(yamlPath);
expect(config.host).toBe("0.0.0.0");
expect(config.port).toBe(9999);
} finally {
await rm(yamlPath, { force: true });
}
});
test("旧布局 server.host/server.port 被拒绝", async () => {
const temp = tmpdir();
const yamlPath = join(temp, "test-old-layout.yaml");
const yamlContent = 'server:\n host: "0.0.0.0"\n port: 9999\n';
await writeFile(yamlPath, yamlContent);
try {
await loadServerConfig(yamlPath);
expect.unreachable();
} catch (error) {
expect((error as Error).message).toContain("未知字段");
} finally {
await rm(yamlPath, { force: true });
}
});
test("非法端口被拒绝", async () => {
const temp = tmpdir();
const yamlPath = join(temp, "test-bad-port.yaml");
const yamlContent = "server:\n listen:\n port: 99999\n";
await writeFile(yamlPath, yamlContent);
try {
await loadServerConfig(yamlPath);
expect.unreachable();
} catch (error) {
expect((error as Error).message).toBeTruthy();
} finally {
await rm(yamlPath, { force: true });
}
});
test("显式变量引用环境变量生效", async () => {
const prevHost = process.env["HOST"];
const prevPort = process.env["PORT"];
process.env["HOST"] = "10.0.0.1";
process.env["PORT"] = "4000";
const temp = tmpdir();
const yamlPath = join(temp, "test-env-var.yaml");
const yamlContent = 'server:\n listen:\n host: "${HOST}"\n port: ${PORT}\n';
await writeFile(yamlPath, yamlContent);
try {
const config = await loadServerConfig(yamlPath);
expect(config.host).toBe("10.0.0.1");
expect(config.port).toBe(4000);
} finally {
await rm(yamlPath, { force: true });
if (prevHost === undefined) delete process.env["HOST"];
else process.env["HOST"] = prevHost;
if (prevPort === undefined) delete process.env["PORT"];
else process.env["PORT"] = prevPort;
}
});
test("变量带默认值生效", async () => {
delete process.env["MY_HOST"];
const temp = tmpdir();
const yamlPath = join(temp, "test-default.yaml");
const yamlContent = 'server:\n listen:\n host: "${MY_HOST|0.0.0.0}"\n port: ${MY_PORT|5000}\n';
await writeFile(yamlPath, yamlContent);
try {
const config = await loadServerConfig(yamlPath);
expect(config.host).toBe("0.0.0.0");
expect(config.port).toBe(5000);
} finally {
await rm(yamlPath, { force: true });
}
});
});