174 行
8.0 KiB
JavaScript
174 行
8.0 KiB
JavaScript
const path = require("node:path");
|
|
const { pathToFileURL } = require("node:url");
|
|
|
|
let gate;
|
|
|
|
function csvEscape(value) {
|
|
return `"${String(value).replace(/"/g, "\"\"")}"`;
|
|
}
|
|
|
|
function buildCsv(rows) {
|
|
const header = [
|
|
"ID",
|
|
"Title",
|
|
"Description",
|
|
"Acceptance",
|
|
"Test_Method",
|
|
"Tools",
|
|
"Dev_Status",
|
|
"Review1_Status",
|
|
"Regression_Status",
|
|
"Files",
|
|
"Dependencies",
|
|
"Notes",
|
|
];
|
|
return [
|
|
header.join(","),
|
|
...rows.map((row) => header.map((column) => csvEscape(row[column] ?? "")).join(",")),
|
|
].join("\n");
|
|
}
|
|
|
|
function validFixtureRow(overrides = {}) {
|
|
return {
|
|
ID: "WAPC2F-0",
|
|
Title: "合法企微审批样件合同",
|
|
Description: "WAPC2F 自动生成 OA-POC 请假/补卡测试模板;路线:WAPC2F -> WCBK1 -> WACP3 -> WADM1 -> wecom_attendance_approval_mirror_v1。",
|
|
Acceptance: "固定 WECOM_AGENT_SECRET;不尝试其他 Secret;默认 dry-run;live 必须 --live --create-templates --submit-test-approvals 且 WECOM_APPROVAL_FIXTURE_WRITE=1;申请人 SMOKE002 -> 审批人 SMOKE101;禁止自审,不是同一 OA 员工,不是同一企微 userid;缺配置记录 NEEDS_CONFIG 或 SKIP/BLOCKED_BY_EXTERNAL_CONFIG。",
|
|
Test_Method: "受影响流程:WAPC2 缺配置 -> 自动生成 POC 样件 -> WAPC2 自动复跑;验证步骤:node scripts/qa/check-process-contract.mjs --issue fixture.csv --profile wecom-approval-fixture --json。",
|
|
Tools: "node|vitest",
|
|
Dev_Status: "TODO",
|
|
Review1_Status: "TODO",
|
|
Regression_Status: "TODO",
|
|
Files: "scripts/qa/check-wecom-approval-fixtures.js|server/package.json",
|
|
Dependencies: "[]",
|
|
Notes: "回滚路径:撤回代码;企微后台 OA-POC 测试模板或测试单如已生成需隐藏、停用或保留审计;报告不得输出手机号、邮箱、token、secret、完整模板 ID、完整 sp_no、完整 userid 或原始 payload;无 fallback。",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function validCallbackRow(overrides = {}) {
|
|
return {
|
|
ID: "WCBK1-0",
|
|
Title: "合法企微审批回调只读合同",
|
|
Description: "WCBK1 回调只读 POC,路线:WCBK1 -> WACP3 -> WADM1 -> wecom_attendance_approval_mirror_v1;范围只验证企微审批回调。",
|
|
Acceptance: "固定 WECOM_APPROVAL_CALLBACK_URL、WECOM_CALLBACK_TOKEN、WECOM_CALLBACK_ENCODING_AES_KEY;GET /api/wecom/approval/callback 做 URL 验证;POST /api/wecom/approval/callback 接收事件;使用 msg_signature 验签和 EncodingAESKey 解密;只读、幂等,不写 OA 业务镜像,不写企微数据;不使用通讯录 Secret,不尝试其他 Secret,不允许运行时猜 Secret;缺配置记录 NEEDS_CONFIG,外部阻断记录 SKIP/BLOCKED_BY_EXTERNAL_CONFIG。",
|
|
Test_Method: "受影响流程:WAPC2FLIVE 样件 -> WCBK1 回调只读 POC -> WACP3 补偿拉取;验证步骤:node scripts/qa/check-process-contract.mjs --issue callback.csv --profile wecom-callback --json。",
|
|
Tools: "node|vitest",
|
|
Dev_Status: "TODO",
|
|
Review1_Status: "TODO",
|
|
Regression_Status: "TODO",
|
|
Files: "server/src/routes/wecomApprovalCallback.js|server/src/services/integrations/wecomCallbackCrypto.js",
|
|
Dependencies: "[]",
|
|
Notes: "回滚路径:移除回调 URL 或新增路由;报告不得输出原始 XML、密文、完整 sp_no、完整 userid、完整 template_id、Token、EncodingAESKey 或原始 payload。",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function evaluate(rows, profile = "wecom-approval-fixture") {
|
|
return gate.evaluateIssueContract({
|
|
csvText: buildCsv(rows),
|
|
issuePath: "fixture.csv",
|
|
profile,
|
|
});
|
|
}
|
|
|
|
function errorCodes(result) {
|
|
return result.errors.map((item) => item.code);
|
|
}
|
|
|
|
describe("check-process-contract", () => {
|
|
beforeAll(async () => {
|
|
const scriptPath = path.resolve(__dirname, "../../../../scripts/qa/check-process-contract.mjs");
|
|
gate = await import(pathToFileURL(scriptPath).href);
|
|
});
|
|
|
|
it("passes a valid WAPC2F fixture contract", () => {
|
|
const result = evaluate([validFixtureRow()]);
|
|
|
|
expect(result.status).toBe("PASS");
|
|
expect(errorCodes(result)).toEqual([]);
|
|
expect(result.checks.some((item) => item.name === "profile.wecom_approval_fixture")).toBe(true);
|
|
});
|
|
|
|
it("passes a valid WCBK1 callback contract", () => {
|
|
const result = evaluate([validCallbackRow()], "wecom-callback");
|
|
|
|
expect(result.status).toBe("PASS");
|
|
expect(errorCodes(result)).toEqual([]);
|
|
expect(result.checks.some((item) => item.name === "profile.wecom_callback")).toBe(true);
|
|
});
|
|
|
|
it("fails when Dependencies is not a JSON array", () => {
|
|
const result = evaluate([validFixtureRow({ Dependencies: "WAPC2F-0" })]);
|
|
|
|
expect(result.status).toBe("FAIL");
|
|
expect(errorCodes(result)).toContain("csv.dependencies");
|
|
});
|
|
|
|
it("fails when Test_Method lacks affected business flow wording", () => {
|
|
const result = evaluate([validFixtureRow({ Test_Method: "验证步骤:node scripts/qa/check-process-contract.mjs --baseline。" })]);
|
|
|
|
expect(result.status).toBe("FAIL");
|
|
expect(errorCodes(result)).toContain("csv.test_method");
|
|
});
|
|
|
|
it("fails self-approval defaults", () => {
|
|
const result = evaluate([
|
|
validFixtureRow({
|
|
Acceptance: "默认 wangshunli -> wangshunli;固定 WECOM_AGENT_SECRET;默认 dry-run;live 必须 --live --create-templates --submit-test-approvals 且 WECOM_APPROVAL_FIXTURE_WRITE=1;缺配置记录 NEEDS_CONFIG 或 SKIP/BLOCKED_BY_EXTERNAL_CONFIG。",
|
|
}),
|
|
]);
|
|
|
|
expect(result.status).toBe("FAIL");
|
|
expect(errorCodes(result)).toContain("fixture.self_approval");
|
|
});
|
|
|
|
it("fails when live write switch is missing", () => {
|
|
const row = validFixtureRow({
|
|
Acceptance: "固定 WECOM_AGENT_SECRET;不尝试其他 Secret;默认 dry-run;live 必须 --live --create-templates --submit-test-approvals;申请人 SMOKE002 -> 审批人 SMOKE101;禁止自审;缺配置记录 NEEDS_CONFIG 或 SKIP/BLOCKED_BY_EXTERNAL_CONFIG。",
|
|
});
|
|
const result = evaluate([row]);
|
|
|
|
expect(result.status).toBe("FAIL");
|
|
expect(errorCodes(result)).toContain("fixture.required_term");
|
|
expect(result.errors.some((item) => item.message.includes("WECOM_APPROVAL_FIXTURE_WRITE=1"))).toBe(true);
|
|
});
|
|
|
|
it("fails when Secret fallback is not explicitly forbidden", () => {
|
|
const row = validFixtureRow({
|
|
Acceptance: "固定 WECOM_AGENT_SECRET;默认 dry-run;live 必须 --live --create-templates --submit-test-approvals 且 WECOM_APPROVAL_FIXTURE_WRITE=1;申请人 SMOKE002 -> 审批人 SMOKE101;禁止自审;缺配置记录 NEEDS_CONFIG 或 SKIP/BLOCKED_BY_EXTERNAL_CONFIG。",
|
|
Notes: "回滚路径:撤回代码;企微后台 OA-POC 测试模板或测试单如已生成需隐藏、停用或保留审计;报告不得输出手机号、邮箱、token、secret、完整模板 ID、完整 sp_no、完整 userid 或原始 payload。",
|
|
});
|
|
const result = evaluate([row]);
|
|
|
|
expect(result.status).toBe("FAIL");
|
|
expect(errorCodes(result)).toContain("wecom.secret_source");
|
|
});
|
|
|
|
it("fails when privacy report boundary is incomplete", () => {
|
|
const row = validFixtureRow({
|
|
Notes: "回滚路径:撤回代码;企微后台 OA-POC 测试模板或测试单如已生成需隐藏、停用或保留审计;报告不得输出手机号、邮箱、token、secret、完整模板 ID、完整 userid 或原始 payload;无 fallback。",
|
|
});
|
|
const result = evaluate([row]);
|
|
|
|
expect(result.status).toBe("FAIL");
|
|
expect(errorCodes(result)).toContain("fixture.privacy");
|
|
});
|
|
|
|
it("parses CLI options for baseline and issue profiles", () => {
|
|
expect(gate.parseArgs(["--baseline", "--json"])).toMatchObject({
|
|
baseline: true,
|
|
json: true,
|
|
});
|
|
expect(gate.parseArgs(["--issue", "a.csv", "--profile", "wecom"])).toMatchObject({
|
|
issue: "a.csv",
|
|
profile: "wecom",
|
|
});
|
|
expect(gate.parseArgs(["--issue", "a.csv", "--profile", "wecom-callback"])).toMatchObject({
|
|
issue: "a.csv",
|
|
profile: "wecom-callback",
|
|
});
|
|
expect(() => gate.parseArgs(["--issue", "a.csv", "--profile", "unknown"])).toThrow(/unsupported profile/);
|
|
});
|
|
});
|