feat: 将 demo 项目转化为 HTTP 拨测监控工具
新增 YAML 配置解析(Bun 内置 YAML)、SQLite 数据存储(bun:sqlite)、按 interval 分组并发拨测引擎、REST API(/api/summary、/api/targets、/api/targets/:id/history、/api/targets/:id/trend)、React 前端 Dashboard(统计卡片、目标表格、可展开详情面板、recharts 趋势图)。CLI 简化为仅接受配置文件路径。移除 /api/demo 路由和相关 demo 代码。保留 /health、静态资源服务和 SPA fallback。
This commit is contained in:
230
tests/server/checker/config-loader.test.ts
Normal file
230
tests/server/checker/config-loader.test.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import { beforeAll, afterAll, describe, expect, test } from "bun:test";
|
||||
import { loadConfig, parseDuration } from "../../../src/server/checker/config-loader";
|
||||
import { readRuntimeConfig } from "../../../src/server/config";
|
||||
import { mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
describe("parseDuration", () => {
|
||||
test("解析秒", () => {
|
||||
expect(parseDuration("30s")).toBe(30000);
|
||||
expect(parseDuration("1s")).toBe(1000);
|
||||
});
|
||||
|
||||
test("解析分钟", () => {
|
||||
expect(parseDuration("5m")).toBe(300000);
|
||||
expect(parseDuration("1m")).toBe(60000);
|
||||
});
|
||||
|
||||
test("解析毫秒", () => {
|
||||
expect(parseDuration("500ms")).toBe(500);
|
||||
expect(parseDuration("100ms")).toBe(100);
|
||||
});
|
||||
|
||||
test("解析小数", () => {
|
||||
expect(parseDuration("1.5s")).toBe(1500);
|
||||
});
|
||||
|
||||
test("无效格式抛出错误", () => {
|
||||
expect(() => parseDuration("30")).toThrow("无效的时长格式");
|
||||
expect(() => parseDuration("abc")).toThrow("无效的时长格式");
|
||||
expect(() => parseDuration("30x")).toThrow("无效的时长格式");
|
||||
expect(() => parseDuration("")).toThrow("无效的时长格式");
|
||||
});
|
||||
});
|
||||
|
||||
describe("readRuntimeConfig", () => {
|
||||
test("返回配置文件路径", () => {
|
||||
expect(readRuntimeConfig(["./probes.yaml"])).toEqual({ configPath: "./probes.yaml" });
|
||||
});
|
||||
|
||||
test("未提供参数抛出错误", () => {
|
||||
expect(() => readRuntimeConfig([])).toThrow("需要指定 YAML 配置文件路径");
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadConfig", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tempDir = join(tmpdir(), `gc-test-${Date.now()}`);
|
||||
await mkdir(tempDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("解析完整配置", async () => {
|
||||
const configPath = join(tempDir, "full.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`server:
|
||||
host: "0.0.0.0"
|
||||
port: 8080
|
||||
dataDir: "./my-data"
|
||||
defaults:
|
||||
interval: "15s"
|
||||
timeout: "5s"
|
||||
method: "POST"
|
||||
targets:
|
||||
- name: "test"
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
|
||||
const config = await loadConfig(configPath);
|
||||
expect(config.host).toBe("0.0.0.0");
|
||||
expect(config.port).toBe(8080);
|
||||
expect(config.dataDir).toBe("./my-data");
|
||||
expect(config.targets).toHaveLength(1);
|
||||
expect(config.targets[0]).toEqual({
|
||||
name: "test",
|
||||
url: "http://example.com",
|
||||
method: "POST",
|
||||
headers: {},
|
||||
body: undefined,
|
||||
intervalMs: 15000,
|
||||
timeoutMs: 5000,
|
||||
expect: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test("解析最简配置(只有 targets)", async () => {
|
||||
const configPath = join(tempDir, "minimal.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- name: "t1"
|
||||
url: "http://a.com"
|
||||
- name: "t2"
|
||||
url: "http://b.com"
|
||||
interval: "1m"
|
||||
`,
|
||||
);
|
||||
|
||||
const config = await loadConfig(configPath);
|
||||
expect(config.host).toBe("127.0.0.1");
|
||||
expect(config.port).toBe(3000);
|
||||
expect(config.dataDir).toBe("./data");
|
||||
expect(config.targets).toHaveLength(2);
|
||||
expect(config.targets[0]!.intervalMs).toBe(30000);
|
||||
expect(config.targets[1]!.intervalMs).toBe(60000);
|
||||
});
|
||||
|
||||
test("per-target 覆盖 defaults", async () => {
|
||||
const configPath = join(tempDir, "override.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`defaults:
|
||||
interval: "30s"
|
||||
timeout: "10s"
|
||||
method: "GET"
|
||||
headers:
|
||||
Authorization: "Bearer token"
|
||||
targets:
|
||||
- name: "override-all"
|
||||
url: "http://example.com"
|
||||
method: "POST"
|
||||
interval: "5m"
|
||||
timeout: "30s"
|
||||
headers:
|
||||
X-Custom: "value"
|
||||
`,
|
||||
);
|
||||
|
||||
const config = await loadConfig(configPath);
|
||||
const target = config.targets[0]!;
|
||||
expect(target.method).toBe("POST");
|
||||
expect(target.intervalMs).toBe(300000);
|
||||
expect(target.timeoutMs).toBe(30000);
|
||||
expect(target.headers).toEqual({ Authorization: "Bearer token", "X-Custom": "value" });
|
||||
});
|
||||
|
||||
test("配置文件不存在抛出错误", async () => {
|
||||
await expect(loadConfig("/nonexistent/file.yaml")).rejects.toThrow("配置文件不存在");
|
||||
});
|
||||
|
||||
test("target 缺少 name 抛出错误", async () => {
|
||||
const configPath = join(tempDir, "no-name.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("缺少 name 字段");
|
||||
});
|
||||
|
||||
test("target 缺少 url 抛出错误", async () => {
|
||||
const configPath = join(tempDir, "no-url.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- name: "test"
|
||||
`,
|
||||
);
|
||||
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("缺少 url 字段");
|
||||
});
|
||||
|
||||
test("target name 重复抛出错误", async () => {
|
||||
const configPath = join(tempDir, "dup-name.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- name: "dup"
|
||||
url: "http://a.com"
|
||||
- name: "dup"
|
||||
url: "http://b.com"
|
||||
`,
|
||||
);
|
||||
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("target name 重复");
|
||||
});
|
||||
|
||||
test("targets 为空数组抛出错误", async () => {
|
||||
const configPath = join(tempDir, "empty-targets.yaml");
|
||||
await writeFile(configPath, `targets: []`);
|
||||
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("至少一个 target");
|
||||
});
|
||||
|
||||
test("无效端口号抛出错误", async () => {
|
||||
const configPath = join(tempDir, "bad-port.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`server:
|
||||
port: 99999
|
||||
targets:
|
||||
- name: "t"
|
||||
url: "http://a.com"
|
||||
`,
|
||||
);
|
||||
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("无效端口号");
|
||||
});
|
||||
|
||||
test("解析 expect 配置", async () => {
|
||||
const configPath = join(tempDir, "expect.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- name: "with-expect"
|
||||
url: "http://example.com"
|
||||
expect:
|
||||
status: [200, 201]
|
||||
bodyContains: "ok"
|
||||
maxLatencyMs: 3000
|
||||
`,
|
||||
);
|
||||
|
||||
const config = await loadConfig(configPath);
|
||||
expect(config.targets[0]!.expect).toEqual({
|
||||
status: [200, 201],
|
||||
bodyContains: "ok",
|
||||
maxLatencyMs: 3000,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user