Files
bun-app-template/tests/server/config.test.ts
lanyuanxiaoyao 60d50afad1 feat: 引入运行时日志体系和存储配置,配置文件改为必填
- 新增 pino/pino-pretty/pino-roll 依赖,实现结构化日志(console pretty + file JSONL rolling)
- 新增 Logger 接口及 PinoLoggerWrapper/ConsoleFallbackLogger/NoopLogger/MemoryLogger 实现
- 新增 src/pino-roll.d.ts 类型声明
- 新增 server.storage.dataDir 配置(默认 ./data,相对路径基于配置文件目录)
- 新增 server.logging 配置(level/console/file/rotation,支持变量引用)
- 配置文件从可选改为必填,parseRuntimeArgs 无参数时抛错
- bootstrap 创建 logger、确保 dataDir、shutdown flush、失败路径 fallback
- startServer 接收 logger 并输出结构化监听日志
- ESLint 新增 no-restricted-syntax 禁止 src/server 直接 console.*(排除 logger.ts)
- 更新 config.example.yaml、README.md、DEVELOPMENT.md 同步配置和日志文档
- 完善测试覆盖:logger、config、schema、bootstrap 共 150 个测试通过
2026-05-25 14:44:37 +08:00

293 lines
9.1 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { loadServerConfig, parseRuntimeArgs, parseSize } from "../../src/server/config";
import { APP } from "../../src/shared/app";
describe("parseRuntimeArgs", () => {
test("无参数抛出需要配置文件路径错误", () => {
try {
parseRuntimeArgs([]);
expect.unreachable();
} catch (error) {
expect((error as Error).message).toContain("需要指定 YAML 配置文件路径");
}
});
test("有参数返回 configPath", () => {
const result = parseRuntimeArgs(["config.yaml"]);
expect(result).toEqual({ configPath: "config.yaml" });
});
test("--help 抛出错误", () => {
try {
parseRuntimeArgs(["--help"]);
expect.unreachable();
} catch (error) {
expect((error as Error).message).toContain("用法");
}
});
test("-h 抛出错误", () => {
try {
parseRuntimeArgs(["-h"]);
expect.unreachable();
} catch (error) {
expect((error as Error).message).toContain("用法");
}
});
});
describe("parseSize", () => {
test("解析数字字节值", () => {
expect(parseSize(1024)).toBe(1024);
});
test("解析字符串大小", () => {
expect(parseSize("1KB")).toBe(1024);
expect(parseSize("50MB")).toBe(52428800);
expect(parseSize("1GB")).toBe(1073741824);
expect(parseSize("1024B")).toBe(1024);
});
test("非法格式抛出错误", () => {
try {
parseSize("invalid");
expect.unreachable();
} catch (error) {
expect((error as Error).message).toContain("无效的 size 格式");
}
});
});
describe("loadServerConfig", () => {
test("YAML 配置文件不存在时报错", async () => {
try {
await loadServerConfig("/nonexistent/path/config.yaml");
expect.unreachable();
} catch (error) {
expect((error as Error).message).toContain("配置文件不存在");
}
});
test("最简配置解析成功", async () => {
const temp = tmpdir();
const yamlPath = join(temp, "minimal.yaml");
await writeFile(yamlPath, 'server:\n listen:\n host: "0.0.0.0"\n port: 9999\n');
try {
const result = await loadServerConfig(yamlPath);
expect(result.host).toBe("0.0.0.0");
expect(result.port).toBe(9999);
expect(result.configDir).toBe(temp);
expect(result.dataDir).toBe(join(temp, "data"));
expect(result.logging.filePath).toBe(join(temp, "data", "logs", `${APP.name}.log`));
expect(result.logging.consoleLevel).toBe("info");
expect(result.logging.fileLevel).toBe("info");
} 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");
await writeFile(yamlPath, "server:\n listen:\n port: 99999\n");
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");
await writeFile(yamlPath, 'server:\n listen:\n host: "${HOST}"\n port: ${PORT}\n');
try {
const result = await loadServerConfig(yamlPath);
expect(result.host).toBe("10.0.0.1");
expect(result.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");
await writeFile(yamlPath, 'server:\n listen:\n host: "${MY_HOST|0.0.0.0}"\n port: ${MY_PORT|5000}\n');
try {
const result = await loadServerConfig(yamlPath);
expect(result.host).toBe("0.0.0.0");
expect(result.port).toBe(5000);
} finally {
await rm(yamlPath, { force: true });
}
});
test("绝对 dataDir 保持不变", async () => {
const temp = tmpdir();
const dataDir = join(temp, "absolute-data");
await mkdir(dataDir, { recursive: true });
const yamlPath = join(temp, "absolute-dir.yaml");
await writeFile(yamlPath, `server:\n storage:\n dataDir: ${JSON.stringify(dataDir)}\n`);
try {
const result = await loadServerConfig(yamlPath);
expect(result.dataDir).toBe(dataDir);
} finally {
await rm(yamlPath, { force: true });
}
});
test("相对 dataDir 基于 configDir", async () => {
const temp = tmpdir();
const yamlPath = join(temp, "rel-dir.yaml");
await writeFile(yamlPath, 'server:\n storage:\n dataDir: "./my-data"\n');
try {
const result = await loadServerConfig(yamlPath);
expect(result.dataDir).toBe(join(temp, "my-data"));
} finally {
await rm(yamlPath, { force: true });
}
});
test("显式相对日志路径基于 configDir", async () => {
const temp = tmpdir();
const yamlPath = join(temp, "log-path.yaml");
await writeFile(yamlPath, 'server:\n logging:\n file:\n path: "./logs/app.log"\n');
try {
const result = await loadServerConfig(yamlPath);
expect(result.logging.filePath).toBe(join(temp, "logs", "app.log"));
} finally {
await rm(yamlPath, { force: true });
}
});
test("绝对日志路径保持不变", async () => {
const temp = tmpdir();
const logPath = join(temp, "my-app.log");
const yamlPath = join(temp, "abs-log.yaml");
await writeFile(yamlPath, `server:\n logging:\n file:\n path: ${JSON.stringify(logPath)}\n`);
try {
const result = await loadServerConfig(yamlPath);
expect(result.logging.filePath).toBe(logPath);
} finally {
await rm(yamlPath, { force: true });
}
});
test("非法 logging.level 抛出错误", async () => {
const temp = tmpdir();
const yamlPath = join(temp, "bad-level.yaml");
await writeFile(yamlPath, 'server:\n logging:\n level: "invalid"\n');
try {
await loadServerConfig(yamlPath);
expect.unreachable();
} catch (error) {
expect((error as Error).message).toContain("日志等级");
} finally {
await rm(yamlPath, { force: true });
}
});
test("空白 logging.file.path 抛出错误", async () => {
const temp = tmpdir();
const yamlPath = join(temp, "blank-path.yaml");
await writeFile(yamlPath, 'server:\n logging:\n file:\n path: " "\n');
try {
await loadServerConfig(yamlPath);
expect.unreachable();
} catch (error) {
expect((error as Error).message).toContain("日志路径不能为空字符串或空白字符串");
} finally {
await rm(yamlPath, { force: true });
}
});
test("非法 rotation.size 抛出错误", async () => {
const temp = tmpdir();
const yamlPath = join(temp, "bad-size.yaml");
await writeFile(yamlPath, 'server:\n logging:\n file:\n rotation:\n size: "99XX"\n');
try {
await loadServerConfig(yamlPath);
expect.unreachable();
} catch (error) {
expect((error as Error).message).toContain("无效的 size 格式");
} finally {
await rm(yamlPath, { force: true });
}
});
test("非法 rotation.frequency 抛出错误", async () => {
const temp = tmpdir();
const yamlPath = join(temp, "bad-freq.yaml");
await writeFile(yamlPath, 'server:\n logging:\n file:\n rotation:\n frequency: "yearly"\n');
try {
await loadServerConfig(yamlPath);
expect.unreachable();
} catch (error) {
expect((error as Error).message).toContain("rotation.frequency");
} finally {
await rm(yamlPath, { force: true });
}
});
test("非法 rotation.maxFiles 抛出错误", async () => {
const temp = tmpdir();
const yamlPath = join(temp, "bad-max.yaml");
await writeFile(yamlPath, "server:\n logging:\n file:\n rotation:\n maxFiles: 0\n");
try {
await loadServerConfig(yamlPath);
expect.unreachable();
} catch (error) {
expect((error as Error).message).toContain("maxFiles");
} finally {
await rm(yamlPath, { force: true });
}
});
});