WAPC2 实现企微审批只读 POC
这个提交包含在:
@@ -0,0 +1,408 @@
|
||||
const {
|
||||
buildInputs,
|
||||
buildSecretCheck,
|
||||
evaluateConclusion,
|
||||
executeApprovalPoc,
|
||||
parseArgs,
|
||||
} = require("../../../src/scripts/check-wecom-approval-poc");
|
||||
const {
|
||||
fetchWecomApprovalDetail,
|
||||
fetchWecomApprovalTemplateDetail,
|
||||
maskApprovalIdentifier,
|
||||
summarizeApprovalPayload,
|
||||
summarizeTemplatePayload,
|
||||
} = require("../../../src/services/integrations/wecomApproval");
|
||||
const { clearWecomAppAccessTokenCache } = require("../../../src/services/integrations/wecomAccessToken");
|
||||
|
||||
const SENSITIVE_AGENT_CREDENTIAL = ["approval", "agent", "credential", "should", "never", "leak"].join("-");
|
||||
const SENSITIVE_CONTACT_CREDENTIAL = ["approval", "contact", "credential", "unused"].join("-");
|
||||
const SENSITIVE_CHECKIN_CREDENTIAL = ["approval", "checkin", "credential", "unused"].join("-");
|
||||
const SENSITIVE_HR_CREDENTIAL = ["approval", "hr", "credential", "unused"].join("-");
|
||||
const SENSITIVE_TEMPLATE_ID = ["tpl", "leave", "full", "identifier", "should", "not", "leak"].join("-");
|
||||
const SENSITIVE_SP_NO = ["sp", "leave", "full", "identifier", "should", "not", "leak"].join("-");
|
||||
const SENSITIVE_USER_ID = ["approval", "userid", "full", "should", "not", "leak"].join("_");
|
||||
const SENSITIVE_FORM_VALUE = "这里是低风险测试单中的表单值但报告不得输出";
|
||||
|
||||
function baseConfig(overrides = {}) {
|
||||
return {
|
||||
wecomCorpId: "corp-approval",
|
||||
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: true,
|
||||
format: "json",
|
||||
reportDir: "/tmp/wecom-approval-poc-test",
|
||||
templates: {},
|
||||
spNos: {},
|
||||
help: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function templatePayload(overrides = {}) {
|
||||
return {
|
||||
errcode: 0,
|
||||
errmsg: "ok",
|
||||
template_id: SENSITIVE_TEMPLATE_ID,
|
||||
template_names: [{ text: "请假" }],
|
||||
template_content: {
|
||||
controls: [
|
||||
{ property: { control: "Selector" } },
|
||||
{ property: { control: "Textarea" } },
|
||||
],
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function approvalPayload(overrides = {}) {
|
||||
return {
|
||||
errcode: 0,
|
||||
errmsg: "ok",
|
||||
info: {
|
||||
sp_no: SENSITIVE_SP_NO,
|
||||
template_id: SENSITIVE_TEMPLATE_ID,
|
||||
sp_status: 2,
|
||||
applyer: { userid: SENSITIVE_USER_ID },
|
||||
apply_data: {
|
||||
contents: [
|
||||
{ control: "Textarea", value: { text: SENSITIVE_FORM_VALUE } },
|
||||
{ property: { control: "Vacation" }, value: { text: "年假" } },
|
||||
],
|
||||
},
|
||||
sp_record: [{ userid: SENSITIVE_USER_ID }],
|
||||
notifyer: [{ userid: SENSITIVE_USER_ID }],
|
||||
comments: [{ comment: SENSITIVE_FORM_VALUE }],
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function jsonResponse(payload, status = 200) {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => payload,
|
||||
};
|
||||
}
|
||||
|
||||
describe("check-wecom-approval-poc", () => {
|
||||
beforeEach(() => {
|
||||
clearWecomAppAccessTokenCache();
|
||||
});
|
||||
|
||||
it("parses CLI template and sp_no pairs", () => {
|
||||
const options = parseArgs([
|
||||
"--live",
|
||||
"--format=json",
|
||||
"--template=leave:tpl-leave",
|
||||
"--template=missed_punch:tpl-missed",
|
||||
"--sp-no=leave:sp-leave",
|
||||
"--sp-no=missed_punch:sp-missed",
|
||||
]);
|
||||
|
||||
expect(options.live).toBe(true);
|
||||
expect(options.format).toBe("json");
|
||||
expect(options.templates).toEqual({
|
||||
leave: "tpl-leave",
|
||||
missed_punch: "tpl-missed",
|
||||
});
|
||||
expect(options.spNos).toEqual({
|
||||
leave: "sp-leave",
|
||||
missed_punch: "sp-missed",
|
||||
});
|
||||
expect(() => parseArgs(["--template=outing:tpl"])).toThrow(/unsupported target/);
|
||||
});
|
||||
|
||||
it("builds inputs from CLI first, then env, with masked previews", () => {
|
||||
const inputs = buildInputs(baseOptions({
|
||||
templates: { leave: SENSITIVE_TEMPLATE_ID },
|
||||
spNos: { leave: SENSITIVE_SP_NO },
|
||||
}), {
|
||||
WECOM_APPROVAL_LEAVE_TEMPLATE_ID: "env-should-not-win",
|
||||
WECOM_APPROVAL_MISSED_PUNCH_TEMPLATE_ID: "env-missed-template",
|
||||
WECOM_APPROVAL_MISSED_PUNCH_TEST_SP_NO: "env-missed-sp",
|
||||
});
|
||||
|
||||
expect(inputs.find((item) => item.key === "leave")).toMatchObject({
|
||||
templateSource: "cli",
|
||||
spNoSource: "cli",
|
||||
templateIdPrefix: maskApprovalIdentifier(SENSITIVE_TEMPLATE_ID),
|
||||
spNoPrefix: maskApprovalIdentifier(SENSITIVE_SP_NO),
|
||||
});
|
||||
expect(inputs.find((item) => item.key === "missed_punch")).toMatchObject({
|
||||
templateSource: "WECOM_APPROVAL_MISSED_PUNCH_TEMPLATE_ID",
|
||||
spNoSource: "WECOM_APPROVAL_MISSED_PUNCH_TEST_SP_NO",
|
||||
});
|
||||
});
|
||||
|
||||
it("documents WECOM_AGENT_SECRET as the only approval Secret source", () => {
|
||||
const check = buildSecretCheck(baseConfig());
|
||||
expect(check).toMatchObject({
|
||||
status: "OK",
|
||||
source: "WECOM_AGENT_SECRET",
|
||||
endpoints: [
|
||||
"POST /cgi-bin/oa/gettemplatedetail",
|
||||
"POST /cgi-bin/oa/getapprovaldetail",
|
||||
],
|
||||
});
|
||||
expect(check.noFallback).toEqual([
|
||||
"WECOM_CONTACT_SECRET",
|
||||
"WECOM_CHECKIN_SECRET",
|
||||
"WECOM_HR_ASSISTANT_SECRET",
|
||||
]);
|
||||
expect(buildSecretCheck(baseConfig({ wecomAgentSecret: "" }))).toMatchObject({
|
||||
status: "BLOCKED",
|
||||
missing: ["WECOM_AGENT_SECRET"],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns PARTIAL_APPROVAL_POC_NEEDS_CONFIG when template IDs and sp_no are missing", async () => {
|
||||
const fetchTemplateDetailImpl = vi.fn();
|
||||
const fetchApprovalDetailImpl = vi.fn();
|
||||
const report = await executeApprovalPoc(baseOptions(), {
|
||||
config: baseConfig(),
|
||||
env: {},
|
||||
fetchTemplateDetailImpl,
|
||||
fetchApprovalDetailImpl,
|
||||
generatedAt: "2026-05-19T09:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(report.status).toBe("PARTIAL_APPROVAL_POC_NEEDS_CONFIG");
|
||||
expect(report.conclusion).toBe("PARTIAL_APPROVAL_POC_NEEDS_CONFIG");
|
||||
expect(report.configGaps).toHaveLength(4);
|
||||
expect(report.mainBlockers).toEqual([]);
|
||||
expect(fetchTemplateDetailImpl).not.toHaveBeenCalled();
|
||||
expect(fetchApprovalDetailImpl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reads leave and missed-punch template/detail in live mode and returns PASS_APPROVAL_POC", async () => {
|
||||
const options = baseOptions({
|
||||
templates: {
|
||||
leave: "tpl-leave",
|
||||
missed_punch: "tpl-missed",
|
||||
},
|
||||
spNos: {
|
||||
leave: "sp-leave",
|
||||
missed_punch: "sp-missed",
|
||||
},
|
||||
});
|
||||
const fetchTemplateDetailImpl = vi.fn(async ({ config, templateId }) => {
|
||||
expect(config.wecomAgentSecret).toBe(SENSITIVE_AGENT_CREDENTIAL);
|
||||
expect([SENSITIVE_CONTACT_CREDENTIAL, SENSITIVE_CHECKIN_CREDENTIAL, SENSITIVE_HR_CREDENTIAL]).not.toContain(config.wecomAgentSecret);
|
||||
return {
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
summary: summarizeTemplatePayload(templatePayload({ template_id: templateId })),
|
||||
response: { errcode: 0, fieldCount: 2, fieldTypeCounts: { Selector: 1, Textarea: 1 } },
|
||||
};
|
||||
});
|
||||
const fetchApprovalDetailImpl = vi.fn(async ({ spNo }) => ({
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
summary: summarizeApprovalPayload(approvalPayload({ info: { ...approvalPayload().info, sp_no: spNo } })),
|
||||
response: { errcode: 0, spStatus: 2, applyFieldCount: 2, approvalNodeCount: 1 },
|
||||
}));
|
||||
|
||||
const report = await executeApprovalPoc(options, {
|
||||
config: baseConfig(),
|
||||
env: {},
|
||||
fetchTemplateDetailImpl,
|
||||
fetchApprovalDetailImpl,
|
||||
generatedAt: "2026-05-19T09:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(report.status).toBe("PASS_APPROVAL_POC");
|
||||
expect(report.configGaps).toEqual([]);
|
||||
expect(report.mainBlockers).toEqual([]);
|
||||
expect(fetchTemplateDetailImpl).toHaveBeenCalledTimes(2);
|
||||
expect(fetchApprovalDetailImpl).toHaveBeenCalledTimes(2);
|
||||
expect(report.checks.targets.every((item) => item.template.status === "OK" && item.approval.status === "OK")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps WeCom API errors as BLOCKED_BY_EXTERNAL_CONFIG", async () => {
|
||||
const report = await executeApprovalPoc(baseOptions({
|
||||
templates: { leave: "tpl-leave", missed_punch: "tpl-missed" },
|
||||
spNos: { leave: "sp-leave", missed_punch: "sp-missed" },
|
||||
}), {
|
||||
config: baseConfig(),
|
||||
env: {},
|
||||
fetchTemplateDetailImpl: vi.fn(async ({ templateId }) => ({
|
||||
ok: templateId !== "tpl-missed",
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
code: "wecom_approval_api_error",
|
||||
message: "WeCom approval API errcode 48002",
|
||||
response: { errcode: 48002, errmsg: "api forbidden" },
|
||||
summary: templateId === "tpl-missed" ? null : summarizeTemplatePayload(templatePayload({ template_id: templateId })),
|
||||
})),
|
||||
fetchApprovalDetailImpl: vi.fn(async ({ spNo }) => ({
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
summary: summarizeApprovalPayload(approvalPayload({ info: { ...approvalPayload().info, sp_no: spNo } })),
|
||||
response: { errcode: 0, applyFieldCount: 2, approvalNodeCount: 1 },
|
||||
})),
|
||||
generatedAt: "2026-05-19T09:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(report.status).toBe("SKIP");
|
||||
expect(report.conclusion).toBe("BLOCKED_BY_EXTERNAL_CONFIG");
|
||||
expect(report.mainBlockers).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "approval_template_detail",
|
||||
target: "missed_punch",
|
||||
reason: "wecom_approval_api_error",
|
||||
}),
|
||||
]));
|
||||
});
|
||||
|
||||
it("uses WECOM_AGENT_SECRET for template/detail API calls 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: "approval-token", expires_in: 7200 });
|
||||
}
|
||||
if (String(url).includes("/cgi-bin/oa/gettemplatedetail")) {
|
||||
return jsonResponse(templatePayload());
|
||||
}
|
||||
if (String(url).includes("/cgi-bin/oa/getapprovaldetail")) {
|
||||
return jsonResponse(approvalPayload());
|
||||
}
|
||||
throw new Error(`unexpected url ${url}`);
|
||||
});
|
||||
|
||||
const template = await fetchWecomApprovalTemplateDetail({
|
||||
config: baseConfig(),
|
||||
templateId: SENSITIVE_TEMPLATE_ID,
|
||||
fetchImpl,
|
||||
});
|
||||
const detail = await fetchWecomApprovalDetail({
|
||||
config: baseConfig(),
|
||||
spNo: SENSITIVE_SP_NO,
|
||||
fetchImpl,
|
||||
});
|
||||
|
||||
expect(template).toMatchObject({
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
summary: {
|
||||
fieldCount: 2,
|
||||
},
|
||||
});
|
||||
expect(detail).toMatchObject({
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
summary: {
|
||||
applyFieldCount: 2,
|
||||
approvalNodeCount: 1,
|
||||
},
|
||||
});
|
||||
expect(calls.some((item) => item.url.includes("/cgi-bin/oa/gettemplatedetail") && item.body.template_id === SENSITIVE_TEMPLATE_ID)).toBe(true);
|
||||
expect(calls.some((item) => item.url.includes("/cgi-bin/oa/getapprovaldetail") && item.body.sp_no === SENSITIVE_SP_NO)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not treat non-json approval API responses as success", async () => {
|
||||
const fetchImpl = vi.fn(async (url) => {
|
||||
if (String(url).includes("/cgi-bin/gettoken")) {
|
||||
return jsonResponse({ errcode: 0, access_token: "approval-token", expires_in: 7200 });
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => {
|
||||
throw new Error("invalid json");
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const template = await fetchWecomApprovalTemplateDetail({
|
||||
config: baseConfig(),
|
||||
templateId: SENSITIVE_TEMPLATE_ID,
|
||||
fetchImpl,
|
||||
});
|
||||
|
||||
expect(template).toMatchObject({
|
||||
ok: false,
|
||||
code: "wecom_approval_http_200",
|
||||
summary: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps report output redacted", async () => {
|
||||
const report = await executeApprovalPoc(baseOptions({
|
||||
templates: { leave: SENSITIVE_TEMPLATE_ID, missed_punch: "tpl-missed" },
|
||||
spNos: { leave: SENSITIVE_SP_NO, missed_punch: "sp-missed" },
|
||||
}), {
|
||||
config: baseConfig(),
|
||||
env: {},
|
||||
fetchTemplateDetailImpl: vi.fn(async ({ templateId }) => ({
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
summary: summarizeTemplatePayload(templatePayload({ template_id: templateId })),
|
||||
response: { errcode: 0, fieldCount: 2, fieldTypeCounts: { Selector: 1, Textarea: 1 } },
|
||||
})),
|
||||
fetchApprovalDetailImpl: vi.fn(async ({ spNo }) => ({
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
summary: summarizeApprovalPayload(approvalPayload({ info: { ...approvalPayload().info, sp_no: spNo } })),
|
||||
response: { errcode: 0, spStatus: 2, applyFieldCount: 2, approvalNodeCount: 1 },
|
||||
})),
|
||||
generatedAt: "2026-05-19T09: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_TEMPLATE_ID);
|
||||
expect(serialized).not.toContain(SENSITIVE_SP_NO);
|
||||
expect(serialized).not.toContain(SENSITIVE_USER_ID);
|
||||
expect(serialized).not.toContain(SENSITIVE_FORM_VALUE);
|
||||
expect(serialized).toContain(maskApprovalIdentifier(SENSITIVE_TEMPLATE_ID));
|
||||
expect(serialized).toContain(maskApprovalIdentifier(SENSITIVE_SP_NO));
|
||||
});
|
||||
|
||||
it("evaluates conclusion categories", () => {
|
||||
expect(evaluateConclusion({
|
||||
mainBlockers: [{ id: "approval_secret_matrix" }],
|
||||
configGaps: [],
|
||||
verificationGaps: [],
|
||||
})).toEqual({
|
||||
status: "SKIP",
|
||||
conclusion: "BLOCKED_BY_EXTERNAL_CONFIG",
|
||||
});
|
||||
expect(evaluateConclusion({
|
||||
mainBlockers: [],
|
||||
configGaps: [{ id: "approval_detail" }],
|
||||
verificationGaps: [],
|
||||
})).toEqual({
|
||||
status: "PARTIAL_APPROVAL_POC_NEEDS_CONFIG",
|
||||
conclusion: "PARTIAL_APPROVAL_POC_NEEDS_CONFIG",
|
||||
});
|
||||
expect(evaluateConclusion({
|
||||
mainBlockers: [],
|
||||
configGaps: [],
|
||||
verificationGaps: [],
|
||||
})).toEqual({
|
||||
status: "PASS_APPROVAL_POC",
|
||||
conclusion: "PASS_APPROVAL_POC",
|
||||
});
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户