文件
hyb-oa/server/tests/unit/scripts/check-wecom-approval-poc.test.js
T

409 行
14 KiB
JavaScript

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",
});
});
});