const { evaluateRuleReport, validateApprovalRuleArchive, } = require("../../../src/services/integrations/wecomApprovalRuleValidator"); const SENSITIVE_SP_NO = "SP_RULE_VALIDATOR_FULL_SHOULD_NOT_LEAK"; const SENSITIVE_TEMPLATE_ID = "TPL_RULE_VALIDATOR_FULL_SHOULD_NOT_LEAK"; function employee(id, overrides = {}) { const { roles = ["employee"], ...rest } = overrides; return { id, employee_no: rest.employee_no || `EMP-${id}`, status: rest.status || "active", roles: roles.map((role_code) => ({ role_code })), wecom_identity_binding: rest.bound === false ? null : { external_user_id: `wecom-${id}-secret-userid` }, direct_manager: rest.direct_manager || null, ...rest, }; } function archive(overrides = {}) { const manager = employee("manager", { employee_no: "SMOKE101", roles: ["manager"] }); const applicant = employee("applicant", { employee_no: "SMOKE002", direct_manager: manager, }); return { id: "archive-1", sp_no: SENSITIVE_SP_NO, template_id: SENSITIVE_TEMPLATE_ID, template_type: "leave", normalized_status: "approved", applicant, participants: [{ role: "approver", node_index: 1, employee: manager, }], actions: [], ...overrides, }; } describe("wecomApprovalRuleValidator", () => { it("passes when actual approver matches OA direct manager", () => { const result = validateApprovalRuleArchive(archive(), { manualGates: { leave: "A" } }); expect(result.status).toBe("PASS_RULE_MATCH"); expect(result.ruleMode).toBe("direct_manager"); expect(result.findings).toEqual([]); }); it("fails when applicant is the actual approver", () => { const row = archive(); row.participants = [{ role: "approver", node_index: 1, employee: row.applicant }]; const result = validateApprovalRuleArchive(row, { manualGates: { leave: "A" } }); expect(result.status).toBe("FAIL_SELF_APPROVAL"); expect(result.findings.map((item) => item.type)).toContain("FAIL_SELF_APPROVAL"); }); it("fails when manual WAPPR1 gate says applicant can choose approver", () => { const result = validateApprovalRuleArchive(archive(), { manualGates: { leave: "B" } }); expect(result.status).toBe("FAIL_APPLICANT_CAN_CHOOSE"); expect(result.findings[0]).toMatchObject({ type: "FAIL_APPLICANT_CAN_CHOOSE", reason: "manual_wappr1_approver_can_be_changed", }); }); it("fails when WeCom actual approver does not match OA theoretical approver", () => { const result = validateApprovalRuleArchive(archive({ participants: [{ role: "approver", node_index: 1, employee: employee("other", { employee_no: "OTHER001", roles: ["employee"] }), }], }), { manualGates: { leave: "A" } }); expect(result.status).toBe("FAIL_APPROVER_MISMATCH"); }); it("allows explainable admin fallback when no manager or approval role candidate exists", () => { const fallback = employee("admin", { employee_no: "HYD000006", roles: ["admin"] }); const applicant = employee("applicant", { employee_no: "SMOKE002", direct_manager: null, }); const result = validateApprovalRuleArchive(archive({ applicant, participants: [{ role: "approver", node_index: 1, employee: fallback }], }), { manualGates: { leave: "A" }, fallbackEmployee: fallback, approvalRoleEmployees: [], }); expect(result.status).toBe("PASS_WITH_ADMIN_FALLBACK"); expect(result.ruleMode).toBe("admin_fallback"); }); it("pauses item status before admin fallback when manual gate is missing", () => { const fallback = employee("admin", { employee_no: "HYD000006", roles: ["admin"] }); const applicant = employee("applicant", { employee_no: "SMOKE002", direct_manager: null, }); const result = validateApprovalRuleArchive(archive({ applicant, participants: [{ role: "approver", node_index: 1, employee: fallback }], }), { fallbackEmployee: fallback, approvalRoleEmployees: [], }); expect(result.status).toBe("PAUSED_MANUAL_CONFIRMATION"); expect(result.findings.map((item) => item.type)).toContain("PASS_WITH_ADMIN_FALLBACK"); }); it("uses approval-role approver when direct manager is unavailable", () => { const roleApprover = employee("hr", { employee_no: "HR001", roles: ["hr"] }); const applicant = employee("applicant", { employee_no: "SMOKE002", direct_manager: employee("manager", { employee_no: "SMOKE101", status: "inactive", roles: ["manager"] }), }); const result = validateApprovalRuleArchive(archive({ applicant, participants: [{ role: "approver", node_index: 1, employee: roleApprover }], }), { manualGates: { leave: "A" }, approvalRoleEmployees: [roleApprover], }); expect(result.status).toBe("PASS_RULE_MATCH"); expect(result.ruleMode).toBe("approval_role"); }); it("blocks when no approver candidate or fallback exists", () => { const applicant = employee("applicant", { employee_no: "SMOKE002", direct_manager: null, }); const result = validateApprovalRuleArchive(archive({ applicant, participants: [{ role: "approver", node_index: 1, employee: employee("other") }], }), { manualGates: { leave: "A" } }); expect(result.status).toBe("SKIP/BLOCKED_BY_EXTERNAL_CONFIG"); expect(result.findings.map((item) => item.reason)).toContain("no_valid_direct_manager_role_or_admin_fallback"); }); it("redacts full external identifiers from public result", () => { const result = validateApprovalRuleArchive(archive(), { manualGates: { leave: "A" } }); const serialized = JSON.stringify(result); expect(serialized).not.toContain(SENSITIVE_SP_NO); expect(serialized).not.toContain(SENSITIVE_TEMPLATE_ID); expect(serialized).not.toContain("wecom-manager-secret-userid"); expect(serialized).not.toContain("SMOKE101"); }); it("prioritizes blocker conclusions in report evaluation", () => { expect(evaluateRuleReport([{ status: "PASS_RULE_MATCH" }])).toBe("PASS_RULE_MATCH"); expect(evaluateRuleReport([{ status: "PASS_WITH_ADMIN_FALLBACK" }])).toBe("PASS_WITH_ADMIN_FALLBACK"); expect(evaluateRuleReport([{ status: "FAIL_APPROVER_MISMATCH" }, { status: "PASS_RULE_MATCH" }])) .toBe("FAIL_APPROVER_MISMATCH"); expect(evaluateRuleReport([{ status: "PASS_RULE_MATCH" }], { manualGateFindings: [{ status: "FAIL_APPLICANT_CAN_CHOOSE" }], })).toBe("FAIL_APPLICANT_CAN_CHOOSE"); expect(evaluateRuleReport([{ status: "PASS_RULE_MATCH" }], { templateDataGaps: [{ status: "SKIP/BLOCKED_BY_EXTERNAL_CONFIG" }], })).toBe("SKIP/BLOCKED_BY_EXTERNAL_CONFIG"); expect(evaluateRuleReport([], { noData: true })).toBe("SKIP/BLOCKED_BY_EXTERNAL_CONFIG"); }); });