1
0

feat: 增强 HTTP checker 鲁棒性 — 严格配置校验、完整耗时、流式body、重定向与编码完善

启动期校验: 新增 validate.ts 对 HTTP config/expect/body rule/operator 全方位严格校验
执行语义: body 改为 Web Stream 流式超限中止,durationMs 覆盖完整执行
错误归属: status/header 失败不读 body,phase 分层 request/body,early duration skip body
重定向: 跟随前释放 body,POST/303 改 GET 清理 header,跨 origin 剥离敏感 header
编码: 支持 quoted charset,未知编码返回结构化解码错误
文档: README match→regex+durationMs,DEVELOPMENT 执行流程与错误归属
测试: +63 测试覆盖全部新增场景,325 pass 0 fail
规格: 同步 probe-config/probe-engine/expect-body-checkers 3 个 delta spec
This commit is contained in:
2026-05-13 08:00:05 +08:00
parent 2fd0f206be
commit bce0f8e7a8
14 changed files with 1543 additions and 104 deletions

View File

@@ -625,4 +625,370 @@ targets:
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("group 字段必须为字符串");
});
test("HTTP headers 非字符串值抛出错误", async () => {
const configPath = join(tempDir, "bad-headers-val.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
headers:
X-Custom: 123
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("http.headers");
});
test("HTTP body 非字符串抛出错误", async () => {
const configPath = join(tempDir, "bad-body.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
body: 123
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("http.body 必须为字符串");
});
test("maxBodyBytes 负数抛出错误", async () => {
const configPath = join(tempDir, "neg-bodybytes.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
maxBodyBytes: -1
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("非负安全整数");
});
test("maxBodyBytes 非整数抛出错误", async () => {
const configPath = join(tempDir, "float-bodybytes.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
maxBodyBytes: 1.5
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("非负安全整数");
});
test("expect.status 数字不在 100-599 范围抛出错误", async () => {
const configPath = join(tempDir, "bad-status-num.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
expect:
status: [999]
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("100-599");
});
test("expect.status 范围 6xx 抛出错误", async () => {
const configPath = join(tempDir, "bad-status-6xx.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
expect:
status: ["6xx"]
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("5xx");
});
test("expect.maxDurationMs 负数抛出错误", async () => {
const configPath = join(tempDir, "neg-duration.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
expect:
maxDurationMs: -100
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("maxDurationMs 必须为非负有限数字");
});
test("expect.body 非数组抛出错误", async () => {
const configPath = join(tempDir, "bad-expect-body.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
expect:
body: "not-array"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("expect.body 必须为数组");
});
test("body rule 缺少支持字段抛出错误", async () => {
const configPath = join(tempDir, "bad-body-rule-nofield.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
expect:
body:
- foo: "bar"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("缺少支持的规则类型");
});
test("body rule 使用 match 字段(非支持)抛出错误", async () => {
const configPath = join(tempDir, "bad-body-rule-match.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
expect:
body:
- match: "ok"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("缺少支持的规则类型");
});
test("body rule 多个支持字段抛出错误", async () => {
const configPath = join(tempDir, "bad-body-rule-multi.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
expect:
body:
- contains: "ok"
regex: "ok"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("只能配置一种规则类型");
});
test("body regex 非法正则抛出错误", async () => {
const configPath = join(tempDir, "bad-body-regex.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
expect:
body:
- regex: "[invalid"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("regex 正则不合法");
});
test("body json path 不以 $. 开头抛出错误", async () => {
const configPath = join(tempDir, "bad-json-path.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
expect:
body:
- json:
path: "status"
equals: "ok"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("json.path");
});
test("body css selector 为空抛出错误", async () => {
const configPath = join(tempDir, "bad-css-sel.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
expect:
body:
- css:
selector: ""
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("css.selector 必须为非空字符串");
});
test("operator match 非法正则抛出错误", async () => {
const configPath = join(tempDir, "bad-op-match.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
expect:
headers:
X-Test:
match: "[invalid"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("match 正则不合法");
});
test("operator gte 非数字抛出错误", async () => {
const configPath = join(tempDir, "bad-op-gte.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
expect:
body:
- json:
path: "$.count"
gte: "abc"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("gte 必须为有限数字");
});
test("operator exists 非布尔值抛出错误", async () => {
const configPath = join(tempDir, "bad-op-exists.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
expect:
body:
- json:
path: "$.status"
exists: "yes"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("exists 必须为布尔值");
});
test("未知字段忽略不影响启动", async () => {
const configPath = join(tempDir, "unknown-fields.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
unknownHttpField: "value"
expect:
status: [200]
unknownExpectField: "value"
body:
- contains: "ok"
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]);
}
});
test("xpath path 非空字符串校验", async () => {
const configPath = join(tempDir, "bad-xpath-path.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
expect:
body:
- xpath:
path: ""
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("xpath.path 必须为非空字符串");
});
test("expect headers 非对象抛出错误", async () => {
const configPath = join(tempDir, "bad-expect-headers.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
http:
url: "http://example.com"
expect:
headers: "invalid"
`,
);
// eslint-disable-next-line @typescript-eslint/await-thenable
await expect(loadConfig(configPath)).rejects.toThrow("expect.headers 必须为对象");
});
});