WAPC2F 实现企微审批样件 dry-run

这个提交包含在:
2026-05-19 13:07:46 +08:00
父节点 45caee97ba
当前提交 5b47755241
共修改 14 个文件,包含 1287 行新增和 46 行删除
@@ -0,0 +1,318 @@
const {
buildActorPrecheck,
buildApplyFixturePayload,
buildSecretCheck,
buildTemplateFixturePayload,
executeApprovalFixtures,
liveWriteAllowed,
parseArgs,
} = require("../../../src/scripts/check-wecom-approval-fixtures");
const {
fetchWecomApprovalApplyEvent,
fetchWecomApprovalTemplateCreate,
} = require("../../../src/services/integrations/wecomApproval");
const { clearWecomAppAccessTokenCache } = require("../../../src/services/integrations/wecomAccessToken");
const SENSITIVE_AGENT_CREDENTIAL = ["fixture", "agent", "credential", "should", "never", "leak"].join("-");
const SENSITIVE_CONTACT_CREDENTIAL = ["fixture", "contact", "credential", "unused"].join("-");
const SENSITIVE_CHECKIN_CREDENTIAL = ["fixture", "checkin", "credential", "unused"].join("-");
const SENSITIVE_HR_CREDENTIAL = ["fixture", "hr", "credential", "unused"].join("-");
const SENSITIVE_APPLICANT_USERID = ["fixture", "applicant", "userid", "full", "secret"].join("_");
const SENSITIVE_APPROVER_USERID = ["fixture", "approver", "userid", "full", "secret"].join("_");
const SENSITIVE_TEMPLATE_ID = ["tpl", "fixture", "full", "identifier", "must", "not", "leak"].join("-");
const SENSITIVE_SP_NO = ["sp", "fixture", "full", "identifier", "must", "not", "leak"].join("-");
function baseConfig(overrides = {}) {
return {
wecomCorpId: "corp-fixture",
wecomAgentId: "1000002",
wecomAgentSecret: SENSITIVE_AGENT_CREDENTIAL,
wecomContactSecret: SENSITIVE_CONTACT_CREDENTIAL,
wecomCheckinSecret: SENSITIVE_CHECKIN_CREDENTIAL,
wecomHrAssistantSecret: SENSITIVE_HR_CREDENTIAL,
wecomRequestTimeoutMs: 1000,
...overrides,
};
}
function baseOptions(overrides = {}) {
return {
live: false,
createTemplates: false,
submitTestApprovals: false,
applicantEmployeeNo: "SMOKE002",
approverEmployeeNo: "SMOKE101",
format: "json",
reportDir: "/tmp/wecom-approval-fixtures-test",
help: false,
...overrides,
};
}
function actorRows(overrides = {}) {
return [
{
id: "applicant-id",
employeeNo: "SMOKE002",
status: "active",
archived: false,
wecomExternalUserId: SENSITIVE_APPLICANT_USERID,
},
{
id: "approver-id",
employeeNo: "SMOKE101",
status: "active",
archived: false,
wecomExternalUserId: SENSITIVE_APPROVER_USERID,
},
].map((row) => ({ ...row, ...(overrides[row.employeeNo] || {}) }));
}
function jsonResponse(payload, status = 200) {
return {
ok: status >= 200 && status < 300,
status,
json: async () => payload,
};
}
describe("check-wecom-approval-fixtures", () => {
beforeEach(() => {
clearWecomAppAccessTokenCache();
});
it("parses defaults and live write flags", () => {
expect(parseArgs(["--format=json"])).toMatchObject({
live: false,
applicantEmployeeNo: "SMOKE002",
approverEmployeeNo: "SMOKE101",
format: "json",
});
const live = parseArgs([
"--live",
"--create-templates",
"--submit-test-approvals",
"--applicant-employee-no=SMOKE002",
"--approver-employee-no",
"SMOKE101",
]);
expect(live).toMatchObject({
live: true,
createTemplates: true,
submitTestApprovals: true,
applicantEmployeeNo: "SMOKE002",
approverEmployeeNo: "SMOKE101",
});
});
it("passes dry-run without calling WeCom write APIs", async () => {
const createTemplateImpl = vi.fn();
const applyEventImpl = vi.fn();
const report = await executeApprovalFixtures(baseOptions(), {
config: baseConfig(),
env: {},
actorRows: actorRows(),
createTemplateImpl,
applyEventImpl,
generatedAt: "2026-05-19T10:00:00.000Z",
});
expect(report.status).toBe("PASS_FIXTURE_DRY_RUN_READY");
expect(report.scope.writeBehavior).toContain("dry-run only");
expect(createTemplateImpl).not.toHaveBeenCalled();
expect(applyEventImpl).not.toHaveBeenCalled();
expect(report.checks.fixturePayloads).toHaveLength(2);
});
it("does not write in live mode without the explicit env switch", async () => {
const createTemplateImpl = vi.fn();
const report = await executeApprovalFixtures(baseOptions({
live: true,
createTemplates: true,
submitTestApprovals: true,
}), {
config: baseConfig(),
env: {},
actorRows: actorRows(),
createTemplateImpl,
generatedAt: "2026-05-19T10:00:00.000Z",
});
expect(liveWriteAllowed(baseOptions({
live: true,
createTemplates: true,
submitTestApprovals: true,
}), {})).toBe(false);
expect(report.status).toBe("PARTIAL_FIXTURE_NEEDS_CONFIG");
expect(report.configGaps).toEqual(expect.arrayContaining([
expect.objectContaining({ id: "approval_fixture_write_guard" }),
]));
expect(createTemplateImpl).not.toHaveBeenCalled();
});
it("blocks self-approval by OA employee or WeCom userid", () => {
const sameEmployee = buildActorPrecheck(actorRows({
SMOKE101: { id: "applicant-id" },
}), baseOptions());
expect(sameEmployee.status).toBe("NEEDS_CONFIG");
expect(sameEmployee.gaps.map((item) => item.id)).toContain("self_approval_same_oa_employee");
const sameWecom = buildActorPrecheck(actorRows({
SMOKE101: { wecomExternalUserId: SENSITIVE_APPLICANT_USERID },
}), baseOptions());
expect(sameWecom.gaps.map((item) => item.id)).toContain("self_approval_same_wecom_userid");
});
it("keeps SMOKE101 unavailable as a config gap and does not auto-replace", async () => {
const report = await executeApprovalFixtures(baseOptions(), {
config: baseConfig(),
env: {},
actorRows: actorRows({
SMOKE101: { status: "inactive" },
}),
generatedAt: "2026-05-19T10:00:00.000Z",
});
expect(report.status).toBe("PARTIAL_FIXTURE_NEEDS_CONFIG");
expect(report.configGaps).toEqual(expect.arrayContaining([
expect.objectContaining({
id: "approver_inactive",
employeeNo: "SMOKE101",
}),
]));
expect(JSON.stringify(report)).not.toContain("HYD000007");
});
it("uses WECOM_AGENT_SECRET for create_template and applyevent without fallback", async () => {
const calls = [];
const fetchImpl = vi.fn(async (url, options = {}) => {
calls.push({
url: String(url),
body: options.body ? JSON.parse(options.body) : null,
});
if (String(url).includes("/cgi-bin/gettoken")) {
expect(String(url)).toContain(`corpsecret=${encodeURIComponent(SENSITIVE_AGENT_CREDENTIAL)}`);
expect(String(url)).not.toContain(SENSITIVE_CONTACT_CREDENTIAL);
expect(String(url)).not.toContain(SENSITIVE_CHECKIN_CREDENTIAL);
expect(String(url)).not.toContain(SENSITIVE_HR_CREDENTIAL);
return jsonResponse({ errcode: 0, access_token: "fixture-token", expires_in: 7200 });
}
if (String(url).includes("/cgi-bin/oa/approval/create_template")) {
return jsonResponse({ errcode: 0, errmsg: "ok", template_id: SENSITIVE_TEMPLATE_ID });
}
if (String(url).includes("/cgi-bin/oa/applyevent")) {
return jsonResponse({ errcode: 0, errmsg: "ok", sp_no: SENSITIVE_SP_NO });
}
throw new Error(`unexpected url ${url}`);
});
const template = await fetchWecomApprovalTemplateCreate({
config: baseConfig(),
payload: buildTemplateFixturePayload({ key: "leave", label: "请假" }, "2026-05-19T10:00:00.000Z"),
fetchImpl,
});
const approval = await fetchWecomApprovalApplyEvent({
config: baseConfig(),
payload: buildApplyFixturePayload({
target: { key: "leave", label: "请假" },
templateId: SENSITIVE_TEMPLATE_ID,
applicantUserId: SENSITIVE_APPLICANT_USERID,
approverUserId: SENSITIVE_APPROVER_USERID,
generatedAt: "2026-05-19T10:00:00.000Z",
}),
fetchImpl,
});
expect(template).toMatchObject({
ok: true,
tokenSource: "WECOM_AGENT_SECRET",
templateId: SENSITIVE_TEMPLATE_ID,
});
expect(approval).toMatchObject({
ok: true,
tokenSource: "WECOM_AGENT_SECRET",
spNo: SENSITIVE_SP_NO,
});
expect(calls.some((item) => item.url.includes("/cgi-bin/oa/approval/create_template"))).toBe(true);
expect(calls.some((item) => item.url.includes("/cgi-bin/oa/applyevent"))).toBe(true);
});
it("passes generated template IDs and sp_no into WAPC2 auto rerun without reporting full IDs", async () => {
const executeApprovalPocImpl = vi.fn(async (options) => {
expect(options.live).toBe(true);
expect(options.templates).toEqual({
leave: `${SENSITIVE_TEMPLATE_ID}-leave`,
missed_punch: `${SENSITIVE_TEMPLATE_ID}-missed_punch`,
});
expect(options.spNos).toEqual({
leave: `${SENSITIVE_SP_NO}-leave`,
missed_punch: `${SENSITIVE_SP_NO}-missed_punch`,
});
return { status: "PASS_APPROVAL_POC", conclusion: "PASS_APPROVAL_POC" };
});
const report = await executeApprovalFixtures(baseOptions({
live: true,
createTemplates: true,
submitTestApprovals: true,
}), {
config: baseConfig(),
env: { WECOM_APPROVAL_FIXTURE_WRITE: "1" },
actorRows: actorRows(),
createTemplateImpl: vi.fn(async ({ payload }) => ({
ok: true,
tokenSource: "WECOM_AGENT_SECRET",
templateId: `${SENSITIVE_TEMPLATE_ID}-${payload.fixture_marker.target}`,
response: { errcode: 0 },
})),
applyEventImpl: vi.fn(async ({ payload }) => ({
ok: true,
tokenSource: "WECOM_AGENT_SECRET",
spNo: `${SENSITIVE_SP_NO}-${payload.template_id.split("-").pop()}`,
response: { errcode: 0 },
})),
executeApprovalPocImpl,
generatedAt: "2026-05-19T10:00:00.000Z",
});
const serialized = JSON.stringify(report);
expect(report.status).toBe("PASS_FIXTURE_CREATED");
expect(executeApprovalPocImpl).toHaveBeenCalledTimes(1);
expect(serialized).not.toContain(`${SENSITIVE_TEMPLATE_ID}-leave`);
expect(serialized).not.toContain(`${SENSITIVE_SP_NO}-leave`);
expect(serialized).toContain("tpl-fixt***");
expect(serialized).toContain("sp-fixtu***");
});
it("keeps report output redacted", async () => {
const report = await executeApprovalFixtures(baseOptions(), {
config: baseConfig(),
env: {},
actorRows: actorRows(),
generatedAt: "2026-05-19T10:00:00.000Z",
});
const serialized = JSON.stringify(report);
expect(serialized).not.toContain(SENSITIVE_AGENT_CREDENTIAL);
expect(serialized).not.toContain(SENSITIVE_CONTACT_CREDENTIAL);
expect(serialized).not.toContain(SENSITIVE_CHECKIN_CREDENTIAL);
expect(serialized).not.toContain(SENSITIVE_HR_CREDENTIAL);
expect(serialized).not.toContain(SENSITIVE_APPLICANT_USERID);
expect(serialized).not.toContain(SENSITIVE_APPROVER_USERID);
expect(serialized).not.toContain("low-risk dry-run fixture");
expect(serialized).toContain("fixture_***");
});
it("documents missing WECOM_AGENT_SECRET as a config gap", () => {
const check = buildSecretCheck(baseConfig({ wecomAgentSecret: "" }));
expect(check).toMatchObject({
status: "NEEDS_CONFIG",
missing: ["WECOM_AGENT_SECRET"],
});
expect(check.noFallback).toEqual([
"WECOM_CONTACT_SECRET",
"WECOM_CHECKIN_SECRET",
"WECOM_HR_ASSISTANT_SECRET",
]);
});
});