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", targets: ["leave", "missed_punch"], 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("supports WEXT1 outing and overtime dry-run targets without changing defaults", async () => { const options = parseArgs(["--format=json", "--target=outing", "--targets=overtime"]); expect(options.targets).toEqual(["outing", "overtime"]); const createTemplateImpl = vi.fn(); const applyEventImpl = vi.fn(); const report = await executeApprovalFixtures(options, { config: baseConfig(), env: {}, actorRows: actorRows(), createTemplateImpl, applyEventImpl, generatedAt: "2026-05-20T10:00:00.000Z", }); expect(report.status).toBe("PASS_FIXTURE_DRY_RUN_READY"); expect(report.scope.targets).toEqual(["outing", "overtime"]); expect(report.checks.fixturePayloads.map((item) => item.target)).toEqual(["outing", "overtime"]); expect(createTemplateImpl).not.toHaveBeenCalled(); expect(applyEventImpl).not.toHaveBeenCalled(); }); 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("builds create_template payload with official template_name and property controls", () => { const payload = buildTemplateFixturePayload( { key: "leave", label: "请假" }, "2026-05-19T10:00:00.000Z", ); expect(payload.template_names).toBeUndefined(); expect(payload.template_name).toEqual([{ text: "OA-POC-请假-20260519T100000Z", lang: "zh_CN" }]); expect(payload.template_content.controls).toHaveLength(3); expect(payload.template_content.controls[0]).toMatchObject({ property: { control: "Text", id: "Text-01", title: [{ text: "类型", lang: "zh_CN" }], placeholder: [{ text: "类型", lang: "zh_CN" }], require: 1, un_print: 1, }, config: {}, }); expect(payload.template_content.controls[1]).toMatchObject({ property: { control: "Date", id: "Date-01", }, config: { date: { type: "day", }, }, }); }); it("builds applyevent payload with template control ids and non-self approver", () => { const 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", }); expect(payload.creator_userid).toBe(SENSITIVE_APPLICANT_USERID); expect(payload.template_id).toBe(SENSITIVE_TEMPLATE_ID); expect(payload.use_template_approver).toBe(0); expect(payload.approver).toEqual([{ attr: 1, userid: [SENSITIVE_APPROVER_USERID] }]); expect(payload.notifyer).toEqual([SENSITIVE_APPROVER_USERID]); expect(payload.apply_data.contents.map((item) => item.id)).toEqual(["Text-01", "Date-01", "Textarea-01"]); expect(payload.apply_data.contents[1]).toMatchObject({ control: "Date", value: { date: { type: "day", s_timestamp: "1779148800", }, }, }); }); 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", ]); }); });