import { describe, expect, it } from "vitest"; import { describeAssertion, evaluateAssertion, exportRunJson, exportRunMarkdown, extractVariables, newTestCase, queryJsonPath, runStats, suggestAssertions, type StepHttpResponse, } from "./testing"; import type { TestAssertion } from "./model"; const res: StepHttpResponse = { status: 200, headers: { "Content-Type": "application/json", "X-Trace-Id": "abc123" }, body: JSON.stringify({ code: 0, data: { id: 42, name: "opsx", tags: ["a", "b"] }, msg: "ok" }), timeMs: 123, }; const as = (over: Partial): TestAssertion => ({ id: "a1", enabled: true, target: "status", locator: "", op: "eq", expected: "", ...over, }); describe("queryJsonPath", () => { it("基本路径", () => { const obj = JSON.parse(res.body); expect(queryJsonPath(obj, "$.code")).toBe(0); expect(queryJsonPath(obj, "$.data.id")).toBe(42); expect(queryJsonPath(obj, "$.data.tags[1]")).toBe("b"); expect(queryJsonPath(obj, "$['data']['name']")).toBe("opsx"); expect(queryJsonPath(obj, "$.data.missing")).toBeUndefined(); expect(queryJsonPath(obj, "$")).toEqual(obj); }); }); describe("断言评估", () => { it("状态码 / 耗时(数字比较)", () => { expect(evaluateAssertion(res, as({ target: "status", op: "eq", expected: "200" })).passed).toBe(true); expect(evaluateAssertion(res, as({ target: "status", op: "lt", expected: "300" })).passed).toBe(true); expect(evaluateAssertion(res, as({ target: "timeMs", op: "gt", expected: "100" })).passed).toBe(true); expect(evaluateAssertion(res, as({ target: "timeMs", op: "lt", expected: "50" })).passed).toBe(false); }); it("Header 大小写不敏感", () => { expect(evaluateAssertion(res, as({ target: "header", locator: "x-trace-id", op: "contains", expected: "abc" })).passed).toBe(true); expect(evaluateAssertion(res, as({ target: "header", locator: "X-Missing", op: "exists" })).passed).toBe(false); }); it("JSONPath 断言", () => { expect(evaluateAssertion(res, as({ target: "body", locator: "$.data.name", op: "jsonpath_eq", expected: "opsx" })).passed).toBe(true); expect(evaluateAssertion(res, as({ target: "body", locator: "$.data.id", op: "jsonpath_eq", expected: "43" })).passed).toBe(false); expect(evaluateAssertion(res, as({ target: "body", locator: "$.msg", op: "jsonpath_contains", expected: "o" })).passed).toBe(true); expect(evaluateAssertion(res, as({ target: "body", locator: "$.data.name", op: "exists" })).passed).toBe(true); expect(evaluateAssertion(res, as({ target: "body", locator: "$.none", op: "not_exists" })).passed).toBe(true); }); it("非 JSON 响应的 jsonpath 断言失败但不抛错", () => { const plain = { ...res, body: "hello world" }; const r = evaluateAssertion(plain, as({ target: "body", locator: "$.a", op: "jsonpath_eq", expected: "1" })); expect(r.passed).toBe(false); expect(r.actual).toContain("非 JSON"); }); it("断言文案", () => { expect(describeAssertion(as({ target: "status", op: "eq", expected: "200" }))).toContain("状态码"); expect(describeAssertion(as({ target: "body", locator: "$.a", op: "jsonpath_contains", expected: "x" }))).toContain("$.a"); }); }); describe("变量提取", () => { it("提取到平面对象", () => { const out = extractVariables(res, [ { id: "e1", jsonpath: "$.data.id", varName: "petId" }, { id: "e2", jsonpath: "$.data.name", varName: "petName" }, { id: "e3", jsonpath: "$.none", varName: "missing" }, { id: "e4", jsonpath: "", varName: "bad" }, ]); expect(out).toEqual({ petId: "42", petName: "opsx" }); }); }); describe("默认断言与统计", () => { it("suggestAssertions 至少含状态码与耗时", () => { const list = suggestAssertions({ method: "GET", url: "https://x" } as never); expect(list.length).toBeGreaterThanOrEqual(2); expect(list[0].target).toBe("status"); }); it("runStats", () => { expect(runStats({ stepResults: [{ ok: true }, { ok: false }, { ok: true }] })).toEqual({ ok: 2, fail: 1 }); }); }); describe("报告导出", () => { it("Markdown 结构", () => { const md = exportRunMarkdown({ caseName: "下单流程", at: Date.now(), trigger: "manual", totalMs: 500, passed: false, stepResults: [ { name: "创建订单", method: "POST", url: "https://api.test/orders", ok: true, httpStatus: 200, durationMs: 200, error: null, assertionResults: [{ label: "状态码 = 200", passed: true, actual: "200", expected: "200" }], }, { name: "查询订单", method: "GET", url: "https://api.test/orders/1", ok: false, httpStatus: 404, durationMs: 300, error: null, assertionResults: [{ label: "状态码 = 200", passed: false, actual: "404", expected: "200" }], }, ], }); expect(md).toContain("# 测试报告 · 下单流程"); expect(md).toContain("1/2 步骤通过"); expect(md).toContain("POST https://api.test/orders"); expect(md).toContain("| 状态码 = 200 | 200 | 404 | ❌ |"); }); }); describe("用例构造", () => { it("newTestCase 默认值", () => { const tc = newTestCase("冒烟"); expect(tc.steps).toEqual([]); expect(tc.continueOnFailure).toBe(false); expect(tc.id).toMatch(/^tc-/); }); }); import { cronMatches, isValidCron, scheduleIsDue } from "./cron"; describe("exportRunJson", () => { it("emits opsxapi schema", () => { const json = JSON.parse( exportRunJson({ caseName: "demo", at: 1, trigger: "manual", totalMs: 10, passed: true, stepResults: [], }), ); expect(json.schema).toBe("opsxapi.test-run.v1"); expect(json.caseName).toBe("demo"); }); }); describe("cron", () => { it("validates 5-field expressions", () => { expect(isValidCron("*/5 * * * *")).toBe(true); expect(isValidCron("* * *")).toBe(false); expect(isValidCron("*/0 * * * *")).toBe(false); expect(isValidCron("61 * * * *")).toBe(false); expect(isValidCron("10-2 * * * *")).toBe(false); expect(cronMatches("*/0 * * * *", new Date(2026, 0, 1, 12, 10))).toBe(false); }); it("matches */5 minutes", () => { const d = new Date(2026, 0, 1, 12, 10, 0); // min=10 expect(cronMatches("*/5 * * * *", d)).toBe(true); expect(cronMatches("*/5 * * * *", new Date(2026, 0, 1, 12, 11, 0))).toBe(false); }); it("scheduleIsDue prefers cron over interval", () => { const d = new Date(2026, 0, 1, 12, 0, 0); expect( scheduleIsDue({ intervalMinutes: 999, cron: "0 * * * *", lastRunAt: null }, d), ).toBe(true); expect( scheduleIsDue( { intervalMinutes: 1, cron: "0 * * * *", lastRunAt: d.getTime() }, d, ), ).toBe(false); }); });