feat: 重构配置校验为 TypeBox + Ajv + semantic validator,严格禁止未知字段
- 新增 config-contract 模块(TypeBox fragments、Ajv 契约校验、ConfigValidationIssue) - CheckerDefinition 扩展为含 configKey、schemas、validate 的完整插件接口 - HTTP/Command 各自维护 contract.ts + validate.ts,校验从 resolve 中分离 - resolve 不再承担校验,只做默认值合并和路径/单位解析 - config-loader 流程: unknown → RawProbeConfig → ValidatedProbeConfig → ResolvedConfig - 导出 probe-config.schema.json,新增 schema/schema:check 脚本 - 更新 DEVELOPMENT.md 新增 1.7 开发新 Checker 完整指引 - 同步更新 4 个 main specs(probe-config、command-checker、expect-body-checkers、checker-runner-abstraction)
This commit is contained in:
@@ -40,6 +40,11 @@ describe("parseDuration", () => {
|
||||
expect(parseDuration("1.5s")).toBe(1500);
|
||||
});
|
||||
|
||||
test("拒绝非正整数毫秒结果", () => {
|
||||
expect(() => parseDuration("0ms")).toThrow("正整数毫秒");
|
||||
expect(() => parseDuration("1.5ms")).toThrow("正整数毫秒");
|
||||
});
|
||||
|
||||
test("无效格式抛出错误", () => {
|
||||
expect(() => parseDuration("30")).toThrow("无效的时长格式");
|
||||
expect(() => parseDuration("abc")).toThrow("无效的时长格式");
|
||||
@@ -70,6 +75,19 @@ describe("loadConfig", () => {
|
||||
await rm(tempDir, { force: true, recursive: true });
|
||||
});
|
||||
|
||||
async function expectConfigError(fileName: string, content: string, message: string): Promise<void> {
|
||||
const configPath = join(tempDir, fileName);
|
||||
await writeFile(configPath, content);
|
||||
let error: unknown;
|
||||
try {
|
||||
await loadConfig(configPath);
|
||||
} catch (caught) {
|
||||
error = caught;
|
||||
}
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as Error).message).toContain(message);
|
||||
}
|
||||
|
||||
test("解析最简 HTTP 配置", async () => {
|
||||
const configPath = join(tempDir, "minimal-http.yaml");
|
||||
await writeFile(
|
||||
@@ -310,7 +328,7 @@ targets:
|
||||
`,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("ignoreSSL 必须为布尔值");
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("http.ignoreSSL 类型不合法");
|
||||
});
|
||||
|
||||
test("HTTP target maxRedirects 非负整数校验", async () => {
|
||||
@@ -326,7 +344,7 @@ targets:
|
||||
`,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("maxRedirects 必须为非负整数");
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("http.maxRedirects 类型不合法");
|
||||
});
|
||||
|
||||
test("HTTP target status 模式非法抛出错误", async () => {
|
||||
@@ -413,7 +431,7 @@ targets:
|
||||
`,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("无效端口号");
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("server.port 数值范围不合法");
|
||||
});
|
||||
|
||||
test("非法 maxConcurrentChecks 抛出错误", async () => {
|
||||
@@ -430,7 +448,7 @@ targets:
|
||||
`,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("maxConcurrentChecks 必须为正整数");
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("runtime.maxConcurrentChecks 数值范围不合法");
|
||||
});
|
||||
|
||||
test("非法 size 格式抛出错误", async () => {
|
||||
@@ -623,7 +641,7 @@ targets:
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("group 字段必须为字符串");
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("group 必须为字符串");
|
||||
});
|
||||
|
||||
test("HTTP headers 非字符串值抛出错误", async () => {
|
||||
@@ -656,7 +674,7 @@ targets:
|
||||
`,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("http.body 必须为字符串");
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("http.body 类型不合法");
|
||||
});
|
||||
|
||||
test("maxBodyBytes 负数抛出错误", async () => {
|
||||
@@ -930,7 +948,7 @@ targets:
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("exists 必须为布尔值");
|
||||
});
|
||||
|
||||
test("未知字段忽略不影响启动", async () => {
|
||||
test("未知字段导致启动失败", async () => {
|
||||
const configPath = join(tempDir, "unknown-fields.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
@@ -948,12 +966,8 @@ targets:
|
||||
note: "ignored"
|
||||
`,
|
||||
);
|
||||
const config = await loadConfig(configPath);
|
||||
expect(config.targets).toHaveLength(1);
|
||||
const t = config.targets[0]!;
|
||||
if (t.type === "http") {
|
||||
expect(t.expect?.status).toEqual([200]);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("unknownHttpField 是未知字段");
|
||||
});
|
||||
|
||||
test("xpath path 非空字符串校验", async () => {
|
||||
@@ -989,6 +1003,228 @@ targets:
|
||||
`,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("expect.headers 必须为对象");
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("expect.headers 类型不合法");
|
||||
});
|
||||
|
||||
test("HTTP method 小写输入失败", async () => {
|
||||
await expectConfigError(
|
||||
"lowercase-method.yaml",
|
||||
`targets:
|
||||
- name: "test"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
method: get
|
||||
`,
|
||||
"http.method 不在允许范围内",
|
||||
);
|
||||
});
|
||||
|
||||
test("defaults.http.method 小写输入失败", async () => {
|
||||
await expectConfigError(
|
||||
"lowercase-default-method.yaml",
|
||||
`defaults:
|
||||
http:
|
||||
method: post
|
||||
targets:
|
||||
- name: "test"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"defaults.http.method 不在允许范围内",
|
||||
);
|
||||
});
|
||||
|
||||
test("HTTP method 大写输入通过", async () => {
|
||||
const configPath = join(tempDir, "uppercase-method.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- name: "test"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
method: POST
|
||||
`,
|
||||
);
|
||||
const config = await loadConfig(configPath);
|
||||
const target = config.targets[0]!;
|
||||
expect(target.type).toBe("http");
|
||||
if (target.type === "http") expect(target.http.method).toBe("POST");
|
||||
});
|
||||
|
||||
test("动态 headers 和 env 允许任意键名", async () => {
|
||||
const configPath = join(tempDir, "dynamic-maps.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`defaults:
|
||||
http:
|
||||
headers:
|
||||
X-Default-Header: "default"
|
||||
targets:
|
||||
- name: "http-test"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
headers:
|
||||
X-Custom-Header: "custom"
|
||||
expect:
|
||||
headers:
|
||||
X-Response-Header:
|
||||
contains: "ok"
|
||||
- name: "cmd-test"
|
||||
type: command
|
||||
command:
|
||||
exec: "true"
|
||||
env:
|
||||
CUSTOM_ENV_NAME: "custom"
|
||||
`,
|
||||
);
|
||||
const config = await loadConfig(configPath);
|
||||
const http = config.targets[0]!;
|
||||
const command = config.targets[1]!;
|
||||
expect(http.type).toBe("http");
|
||||
expect(command.type).toBe("command");
|
||||
if (http.type === "http") {
|
||||
expect(http.http.headers["X-Default-Header"]).toBe("default");
|
||||
expect(http.http.headers["X-Custom-Header"]).toBe("custom");
|
||||
}
|
||||
if (command.type === "command") expect(command.command.env["CUSTOM_ENV_NAME"]).toBe("custom");
|
||||
});
|
||||
|
||||
test("command args 类型非法", async () => {
|
||||
await expectConfigError(
|
||||
"bad-command-args.yaml",
|
||||
`targets:
|
||||
- name: "cmd"
|
||||
type: command
|
||||
command:
|
||||
exec: "echo"
|
||||
args: "hello"
|
||||
`,
|
||||
"command.args 类型不合法",
|
||||
);
|
||||
});
|
||||
|
||||
test("command cwd 类型非法", async () => {
|
||||
await expectConfigError(
|
||||
"bad-command-cwd.yaml",
|
||||
`targets:
|
||||
- name: "cmd"
|
||||
type: command
|
||||
command:
|
||||
exec: "echo"
|
||||
cwd: 123
|
||||
`,
|
||||
"command.cwd 类型不合法",
|
||||
);
|
||||
});
|
||||
|
||||
test("command env 值类型非法", async () => {
|
||||
await expectConfigError(
|
||||
"bad-command-env.yaml",
|
||||
`targets:
|
||||
- name: "cmd"
|
||||
type: command
|
||||
command:
|
||||
exec: "echo"
|
||||
env:
|
||||
COUNT: 123
|
||||
`,
|
||||
"command.env.COUNT 类型不合法",
|
||||
);
|
||||
});
|
||||
|
||||
test("command maxOutputBytes 非法", async () => {
|
||||
await expectConfigError(
|
||||
"bad-command-max-output.yaml",
|
||||
`targets:
|
||||
- name: "cmd"
|
||||
type: command
|
||||
command:
|
||||
exec: "echo"
|
||||
maxOutputBytes: "1TB"
|
||||
`,
|
||||
"maxOutputBytes 无效的 size 格式",
|
||||
);
|
||||
});
|
||||
|
||||
test("command expect exitCode 类型非法", async () => {
|
||||
await expectConfigError(
|
||||
"bad-command-exit-code.yaml",
|
||||
`targets:
|
||||
- name: "cmd"
|
||||
type: command
|
||||
command:
|
||||
exec: "echo"
|
||||
expect:
|
||||
exitCode: [1.5]
|
||||
`,
|
||||
"expect.exitCode[0] 类型不合法",
|
||||
);
|
||||
});
|
||||
|
||||
test("command stdout 空 text rule 非法", async () => {
|
||||
await expectConfigError(
|
||||
"bad-command-stdout-empty.yaml",
|
||||
`targets:
|
||||
- name: "cmd"
|
||||
type: command
|
||||
command:
|
||||
exec: "echo"
|
||||
expect:
|
||||
stdout:
|
||||
- {}
|
||||
`,
|
||||
"stdout[0] 必须包含至少一个合法 operator",
|
||||
);
|
||||
});
|
||||
|
||||
test("command stderr 未知 operator 非法", async () => {
|
||||
await expectConfigError(
|
||||
"bad-command-stderr-operator.yaml",
|
||||
`targets:
|
||||
- name: "cmd"
|
||||
type: command
|
||||
command:
|
||||
exec: "echo"
|
||||
expect:
|
||||
stderr:
|
||||
- foo: "bar"
|
||||
`,
|
||||
"expect.stderr[0].foo 是未知字段",
|
||||
);
|
||||
});
|
||||
|
||||
test("command stdout match 正则非法", async () => {
|
||||
await expectConfigError(
|
||||
"bad-command-stdout-regex.yaml",
|
||||
`targets:
|
||||
- name: "cmd"
|
||||
type: command
|
||||
command:
|
||||
exec: "echo"
|
||||
expect:
|
||||
stdout:
|
||||
- match: "[invalid"
|
||||
`,
|
||||
"stdout[0].match 正则不合法",
|
||||
);
|
||||
});
|
||||
|
||||
test("command expect 未知字段失败", async () => {
|
||||
await expectConfigError(
|
||||
"bad-command-expect-unknown.yaml",
|
||||
`targets:
|
||||
- name: "cmd"
|
||||
type: command
|
||||
command:
|
||||
exec: "echo"
|
||||
expect:
|
||||
status: [200]
|
||||
`,
|
||||
"expect.status 是未知字段",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user