import { afterAll, beforeAll, 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 { loadConfig, parseDuration } from "../../../src/server/checker/config-loader"; import { checkerRegistry } from "../../../src/server/checker/runner"; import { CommandChecker } from "../../../src/server/checker/runner/command/runner"; import { HttpChecker } from "../../../src/server/checker/runner/http/runner"; import { readRuntimeConfig } from "../../../src/server/config"; function ensureRegistered() { if (!checkerRegistry.supportedTypes.includes("http")) { checkerRegistry.register(new HttpChecker()); checkerRegistry.register(new CommandChecker()); } } beforeAll(() => { ensureRegistered(); }); 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, { force: true, recursive: true }); }); test("解析最简 HTTP 配置", async () => { const configPath = join(tempDir, "minimal-http.yaml"); await writeFile( configPath, `targets: - name: "test" type: http http: url: "http://example.com" `, ); 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.maxConcurrentChecks).toBe(20); expect(config.targets).toHaveLength(1); const t = config.targets[0]!; expect(t.type).toBe("http"); if (t.type === "http") { expect(t.name).toBe("test"); expect(t.http.url).toBe("http://example.com"); expect(t.http.method).toBe("GET"); expect(t.http.headers).toEqual({}); expect(t.http.ignoreSSL).toBe(false); expect(t.http.maxBodyBytes).toBe(104857600); expect(t.http.maxRedirects).toBe(0); expect(t.intervalMs).toBe(30000); expect(t.timeoutMs).toBe(10000); } }); test("解析最简 command 配置", async () => { const subdir = join(tempDir, "subdir"); await mkdir(subdir, { recursive: true }); const configPath = join(subdir, "cmd.yaml"); await writeFile( configPath, `targets: - name: "check-nginx" type: command command: exec: "pgrep" args: ["nginx"] `, ); const config = await loadConfig(configPath); expect(config.targets).toHaveLength(1); const t = config.targets[0]!; expect(t.type).toBe("command"); if (t.type === "command") { expect(t.name).toBe("check-nginx"); expect(t.command.exec).toBe("pgrep"); expect(t.command.args).toEqual(["nginx"]); expect(t.command.cwd).toBe(subdir); expect(t.command.maxOutputBytes).toBe(104857600); expect(t.command.env["PATH"]).toBeDefined(); } }); test("解析完整配置", async () => { const configPath = join(tempDir, "full.yaml"); await writeFile( configPath, `server: host: "0.0.0.0" port: 8080 dataDir: "./my-data" runtime: maxConcurrentChecks: 5 defaults: interval: "15s" timeout: "5s" http: method: "POST" headers: Authorization: "Bearer token" maxBodyBytes: "50MB" command: cwd: "/tmp" maxOutputBytes: "10MB" targets: - name: "http-target" type: http interval: "1m" http: url: "http://example.com" ignoreSSL: true maxRedirects: 5 expect: status: ["2xx", 301] body: - contains: "ok" - name: "cmd-target" type: command command: exec: "ls" args: ["/tmp"] expect: exitCode: [0] `, ); 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.maxConcurrentChecks).toBe(5); expect(config.targets).toHaveLength(2); const http = config.targets[0]!; expect(http.type).toBe("http"); if (http.type === "http") { expect(http.http.url).toBe("http://example.com"); expect(http.http.method).toBe("POST"); expect(http.http.headers).toEqual({ Authorization: "Bearer token" }); expect(http.http.ignoreSSL).toBe(true); expect(http.http.maxBodyBytes).toBe(52428800); expect(http.http.maxRedirects).toBe(5); expect(http.expect?.status).toEqual(["2xx", 301]); expect(http.intervalMs).toBe(60000); expect(http.timeoutMs).toBe(5000); } const cmd = config.targets[1]!; expect(cmd.type).toBe("command"); if (cmd.type === "command") { expect(cmd.command.exec).toBe("ls"); expect(cmd.command.args).toEqual(["/tmp"]); expect(cmd.command.maxOutputBytes).toBe(10485760); } }); test("per-target 覆盖 defaults", async () => { const configPath = join(tempDir, "override.yaml"); await writeFile( configPath, `defaults: interval: "30s" timeout: "10s" http: method: "GET" maxBodyBytes: "10MB" targets: - name: "override-all" type: http interval: "5m" timeout: "30s" http: url: "http://example.com" method: "POST" maxBodyBytes: "1MB" `, ); const config = await loadConfig(configPath); const t = config.targets[0]!; if (t.type === "http") { expect(t.http.method).toBe("POST"); expect(t.intervalMs).toBe(300000); expect(t.timeoutMs).toBe(30000); expect(t.http.maxBodyBytes).toBe(1048576); } }); test("配置文件不存在抛出错误", async () => { // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig("/nonexistent/file.yaml")).rejects.toThrow("配置文件不存在"); }); test("target 缺少 name 抛出错误", async () => { const configPath = join(tempDir, "no-name.yaml"); await writeFile( configPath, `targets: - type: http http: url: "http://example.com" `, ); // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("缺少 name 字段"); }); test("target 缺少 type 抛出错误", async () => { const configPath = join(tempDir, "no-type.yaml"); await writeFile( configPath, `targets: - name: "test" http: url: "http://example.com" `, ); // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("缺少 type 字段"); }); test("HTTP target 缺少 url 抛出错误", async () => { const configPath = join(tempDir, "no-url.yaml"); await writeFile( configPath, `targets: - name: "test" type: http http: {} `, ); // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("缺少 http.url 字段"); }); test("HTTP target 缺少 http 分组抛出清晰错误", async () => { const configPath = join(tempDir, "no-http-group.yaml"); await writeFile( configPath, `targets: - name: "test" type: http `, ); // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("缺少 http.url 字段"); }); test("HTTP target ignoreSSL 非布尔值抛出错误", async () => { const configPath = join(tempDir, "bad-ignore-ssl.yaml"); await writeFile( configPath, `targets: - name: "test" type: http http: url: "http://example.com" ignoreSSL: "true" `, ); // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("ignoreSSL 必须为布尔值"); }); test("HTTP target maxRedirects 非负整数校验", async () => { const configPath = join(tempDir, "bad-max-redirects.yaml"); await writeFile( configPath, `targets: - name: "test" type: http http: url: "http://example.com" maxRedirects: 1.5 `, ); // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("maxRedirects 必须为非负整数"); }); test("HTTP target status 模式非法抛出错误", async () => { const configPath = join(tempDir, "bad-status-pattern.yaml"); await writeFile( configPath, `targets: - name: "test" type: http http: url: "http://example.com" expect: status: ["abc"] `, ); // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("status 模式"); }); test("command target 缺少 exec 抛出错误", async () => { const configPath = join(tempDir, "no-exec.yaml"); await writeFile( configPath, `targets: - name: "test" type: command command: {} `, ); // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("缺少 command.exec 字段"); }); test("非法 target type 抛出错误", async () => { const configPath = join(tempDir, "bad-type.yaml"); await writeFile( configPath, `targets: - name: "test" type: dns `, ); // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("不支持的 type"); }); test("target name 重复抛出错误", async () => { const configPath = join(tempDir, "dup-name.yaml"); await writeFile( configPath, `targets: - name: "dup" type: http http: url: "http://a.com" - name: "dup" type: http http: url: "http://b.com" `, ); // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("target name 重复"); }); test("targets 为空数组抛出错误", async () => { const configPath = join(tempDir, "empty-targets.yaml"); await writeFile(configPath, `targets: []`); // eslint-disable-next-line @typescript-eslint/await-thenable 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" type: http http: url: "http://a.com" `, ); // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("无效端口号"); }); test("非法 maxConcurrentChecks 抛出错误", async () => { const configPath = join(tempDir, "bad-concurrency.yaml"); await writeFile( configPath, `runtime: maxConcurrentChecks: -1 targets: - name: "t" type: http http: url: "http://a.com" `, ); // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("maxConcurrentChecks 必须为正整数"); }); test("非法 size 格式抛出错误", async () => { const configPath = join(tempDir, "bad-size.yaml"); await writeFile( configPath, `defaults: http: maxBodyBytes: "100TB" targets: - name: "t" type: http http: url: "http://a.com" `, ); // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("无效的 size 格式"); }); test("非法 interval 格式抛出错误", async () => { const configPath = join(tempDir, "bad-interval.yaml"); await writeFile( configPath, `targets: - name: "t" type: http interval: "30x" http: url: "http://a.com" `, ); // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("无效的时长格式"); }); test("解析 expect 配置", async () => { const configPath = join(tempDir, "expect.yaml"); await writeFile( configPath, `targets: - name: "with-expect" type: http http: url: "http://example.com" expect: status: [200, 201] body: - contains: "ok" - json: path: "$.status" equals: "ok" maxDurationMs: 3000 `, ); const config = await loadConfig(configPath); const t = config.targets[0]!; if (t.type === "http") { expect(t.expect).toEqual({ body: [{ contains: "ok" }, { json: { equals: "ok", path: "$.status" } }], maxDurationMs: 3000, status: [200, 201], }); } }); test("解析 command expect 配置", async () => { const configPath = join(tempDir, "cmd-expect.yaml"); await writeFile( configPath, `targets: - name: "cmd-with-expect" type: command command: exec: "mycheck" expect: exitCode: [0, 2] stdout: - contains: "ok" - match: "done" stderr: - empty: true maxDurationMs: 5000 `, ); const config = await loadConfig(configPath); const t = config.targets[0]!; if (t.type === "command") { expect(t.expect).toEqual({ exitCode: [0, 2], maxDurationMs: 5000, stderr: [{ empty: true }], stdout: [{ contains: "ok" }, { match: "done" }], }); } }); test("command cwd 相对配置文件目录", async () => { const subdir = join(tempDir, "cwd-test"); await mkdir(subdir, { recursive: true }); const configPath = join(subdir, "cwd.yaml"); await writeFile( configPath, `targets: - name: "cwd-test" type: command command: exec: "ls" cwd: "scripts" `, ); const config = await loadConfig(configPath); const t = config.targets[0]!; if (t.type === "command") { expect(t.command.cwd).toBe(join(subdir, "scripts")); } }); test("command env 覆盖", async () => { const configPath = join(tempDir, "env.yaml"); await writeFile( configPath, `targets: - name: "env-test" type: command command: exec: "echo" env: LANG: "C" CUSTOM_VAR: "test" `, ); const config = await loadConfig(configPath); const t = config.targets[0]!; if (t.type === "command") { expect(t.command.env["LANG"]).toBe("C"); expect(t.command.env["CUSTOM_VAR"]).toBe("test"); expect(t.command.env["PATH"]).toBeDefined(); } }); test("解析 group 字段", async () => { const configPath = join(tempDir, "group.yaml"); await writeFile( configPath, `targets: - name: "grouped" type: http group: "搜索引擎" http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.targets[0]!.group).toBe("搜索引擎"); }); test("group 字段默认为 default", async () => { const configPath = join(tempDir, "no-group.yaml"); await writeFile( configPath, `targets: - name: "no-group" type: http http: url: "http://example.com" `, ); const config = await loadConfig(configPath); expect(config.targets[0]!.group).toBe("default"); }); test("非法 group 类型抛出错误", async () => { const configPath = join(tempDir, "bad-group.yaml"); await writeFile( configPath, `targets: - name: "test" type: http group: 123 http: url: "http://example.com" `, ); // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("group 字段必须为字符串"); }); });