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:
@@ -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 必须为对象");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user