WACP3 实现企微审批补偿拉取 POC
这个提交包含在:
@@ -0,0 +1,417 @@
|
||||
const {
|
||||
buildInputs,
|
||||
buildSecretCheck,
|
||||
buildTimeWindow,
|
||||
compareCallbackEvidence,
|
||||
executeCompensationPoc,
|
||||
parseArgs,
|
||||
statusLabelFromSpStatus,
|
||||
} = require("../../../src/scripts/check-wecom-approval-compensation-poc");
|
||||
const {
|
||||
fetchWecomApprovalInfoList,
|
||||
maskApprovalIdentifier,
|
||||
} = require("../../../src/services/integrations/wecomApproval");
|
||||
const { clearWecomAppAccessTokenCache } = require("../../../src/services/integrations/wecomAccessToken");
|
||||
|
||||
const SENSITIVE_AGENT_CREDENTIAL = ["compensation", "agent", "credential", "should", "never", "leak"].join("-");
|
||||
const SENSITIVE_CONTACT_CREDENTIAL = ["compensation", "contact", "credential", "unused"].join("-");
|
||||
const SENSITIVE_CHECKIN_CREDENTIAL = ["compensation", "checkin", "credential", "unused"].join("-");
|
||||
const SENSITIVE_HR_CREDENTIAL = ["compensation", "hr", "credential", "unused"].join("-");
|
||||
const SENSITIVE_TEMPLATE_ID = ["tpl", "compensation", "full", "identifier", "must", "not", "leak"].join("-");
|
||||
const SENSITIVE_SP_NO = ["sp", "compensation", "full", "identifier", "must", "not", "leak"].join("-");
|
||||
const SENSITIVE_USER_ID = ["compensation", "userid", "full", "must", "not", "leak"].join("_");
|
||||
const SENSITIVE_FORM_VALUE = "这里是审批表单值,报告不得输出";
|
||||
|
||||
function baseConfig(overrides = {}) {
|
||||
return {
|
||||
wecomCorpId: "corp-compensation",
|
||||
wecomAgentId: "1000002",
|
||||
wecomAgentSecret: SENSITIVE_AGENT_CREDENTIAL,
|
||||
wecomContactSecret: SENSITIVE_CONTACT_CREDENTIAL,
|
||||
wecomCheckinSecret: SENSITIVE_CHECKIN_CREDENTIAL,
|
||||
wecomHrAssistantSecret: SENSITIVE_HR_CREDENTIAL,
|
||||
wecomApprovalCallbackEvidenceFile: "/tmp/wacp3-test-evidence.jsonl",
|
||||
wecomRequestTimeoutMs: 1000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function baseOptions(overrides = {}) {
|
||||
return {
|
||||
live: true,
|
||||
format: "json",
|
||||
reportDir: "/tmp/wecom-approval-compensation-test",
|
||||
startTime: "2026-05-19T00:00:00.000Z",
|
||||
endTime: "2026-05-20T00:00:00.000Z",
|
||||
templates: {
|
||||
leave: SENSITIVE_TEMPLATE_ID,
|
||||
missed_punch: "tpl-missed-punch",
|
||||
},
|
||||
size: 100,
|
||||
maxPages: 10,
|
||||
help: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function jsonResponse(payload, status = 200) {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => payload,
|
||||
};
|
||||
}
|
||||
|
||||
function detailSummary(spNo = SENSITIVE_SP_NO, overrides = {}) {
|
||||
return {
|
||||
spNoPrefix: maskApprovalIdentifier(spNo),
|
||||
templateIdPrefix: maskApprovalIdentifier(SENSITIVE_TEMPLATE_ID),
|
||||
spStatus: "2",
|
||||
applyFieldCount: 3,
|
||||
approvalNodeCount: 1,
|
||||
notifyerCount: 1,
|
||||
commentCount: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("check-wecom-approval-compensation-poc", () => {
|
||||
beforeEach(() => {
|
||||
clearWecomAppAccessTokenCache();
|
||||
});
|
||||
|
||||
it("parses CLI window, size and template filters", () => {
|
||||
const options = parseArgs([
|
||||
"--live",
|
||||
"--format=json",
|
||||
"--start-time=2026-05-19T00:00:00.000Z",
|
||||
"--end-time=2026-05-20T00:00:00.000Z",
|
||||
"--size=50",
|
||||
"--template=leave:tpl-leave",
|
||||
"--template=missed_punch:tpl-missed",
|
||||
]);
|
||||
|
||||
expect(options.live).toBe(true);
|
||||
expect(options.format).toBe("json");
|
||||
expect(options.size).toBe(50);
|
||||
expect(options.templates).toEqual({
|
||||
leave: "tpl-leave",
|
||||
missed_punch: "tpl-missed",
|
||||
});
|
||||
expect(() => parseArgs(["--template=outing:tpl"])).toThrow(/unsupported target/);
|
||||
expect(() => parseArgs(["--size=101"])).toThrow(/--size/);
|
||||
});
|
||||
|
||||
it("builds a default 72-hour time window", () => {
|
||||
const window = buildTimeWindow(baseOptions({ startTime: "", endTime: "" }), "2026-05-20T00:00:00.000Z");
|
||||
|
||||
expect(window.endTime - window.startTime).toBe(72 * 60 * 60);
|
||||
expect(window.defaultWindowHours).toBe(72);
|
||||
});
|
||||
|
||||
it("builds inputs from CLI first, then env, with masked template previews", () => {
|
||||
const inputs = buildInputs(baseOptions({
|
||||
templates: { leave: SENSITIVE_TEMPLATE_ID },
|
||||
}), {
|
||||
WECOM_APPROVAL_LEAVE_TEMPLATE_ID: "env-should-not-win",
|
||||
WECOM_APPROVAL_MISSED_PUNCH_TEMPLATE_ID: "env-missed-template",
|
||||
});
|
||||
|
||||
expect(inputs.find((item) => item.key === "leave")).toMatchObject({
|
||||
templateSource: "cli",
|
||||
templateIdPrefix: maskApprovalIdentifier(SENSITIVE_TEMPLATE_ID),
|
||||
});
|
||||
expect(inputs.find((item) => item.key === "missed_punch")).toMatchObject({
|
||||
templateSource: "WECOM_APPROVAL_MISSED_PUNCH_TEMPLATE_ID",
|
||||
templateIdPrefix: maskApprovalIdentifier("env-missed-template"),
|
||||
});
|
||||
});
|
||||
|
||||
it("documents WECOM_AGENT_SECRET as the only Secret source", () => {
|
||||
const check = buildSecretCheck(baseConfig());
|
||||
expect(check).toMatchObject({
|
||||
status: "OK",
|
||||
source: "WECOM_AGENT_SECRET",
|
||||
endpoints: [
|
||||
"POST /cgi-bin/oa/getapprovalinfo",
|
||||
"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("calls getapprovalinfo with template filter and redacts sp_no list", async () => {
|
||||
const fetchImpl = vi.fn(async (url, init) => {
|
||||
const urlText = String(url);
|
||||
if (urlText.includes("/cgi-bin/gettoken")) {
|
||||
expect(urlText).toContain("corpsecret=");
|
||||
return jsonResponse({ errcode: 0, errmsg: "ok", access_token: "token-compensation", expires_in: 7200 });
|
||||
}
|
||||
expect(urlText).toContain("/cgi-bin/oa/getapprovalinfo");
|
||||
expect(urlText).not.toContain(SENSITIVE_AGENT_CREDENTIAL);
|
||||
const body = JSON.parse(init.body);
|
||||
expect(body).toMatchObject({
|
||||
starttime: 1779148800,
|
||||
endtime: 1779235200,
|
||||
size: 50,
|
||||
filters: [{ key: "template_id", value: SENSITIVE_TEMPLATE_ID }],
|
||||
});
|
||||
return jsonResponse({
|
||||
errcode: 0,
|
||||
errmsg: "ok",
|
||||
sp_no_list: [SENSITIVE_SP_NO],
|
||||
next_cursor: "",
|
||||
});
|
||||
});
|
||||
|
||||
const result = await fetchWecomApprovalInfoList({
|
||||
config: baseConfig(),
|
||||
startTime: 1779148800,
|
||||
endTime: 1779235200,
|
||||
size: 50,
|
||||
filters: [{ key: "template_id", value: SENSITIVE_TEMPLATE_ID }],
|
||||
fetchImpl,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.spNos).toEqual([SENSITIVE_SP_NO]);
|
||||
expect(result.response.spNoPrefixes).toEqual([maskApprovalIdentifier(SENSITIVE_SP_NO)]);
|
||||
expect(JSON.stringify(result.response)).not.toContain(SENSITIVE_SP_NO);
|
||||
});
|
||||
|
||||
it("returns PARTIAL_COMPENSATION_NEEDS_CONFIG when template filters are missing", async () => {
|
||||
const report = await executeCompensationPoc(baseOptions({
|
||||
templates: {},
|
||||
}), {
|
||||
config: baseConfig(),
|
||||
env: {},
|
||||
generatedAt: "2026-05-20T00:00:00.000Z",
|
||||
fetchApprovalInfoListImpl: vi.fn(),
|
||||
fetchApprovalDetailImpl: vi.fn(),
|
||||
readCallbackEvidenceImpl: vi.fn(async () => []),
|
||||
});
|
||||
|
||||
expect(report.status).toBe("PARTIAL_COMPENSATION_NEEDS_CONFIG");
|
||||
expect(report.configGaps.map((item) => item.id)).toContain("approval_compensation_pull");
|
||||
expect(report.configGaps.map((item) => item.id)).not.toContain("approval_compensation_window_empty");
|
||||
expect(report.checks.targets.every((item) => item.status === "NEEDS_CONFIG")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns PARTIAL_COMPENSATION_NEEDS_CONFIG in dry-run even when templates are configured", async () => {
|
||||
const report = await executeCompensationPoc(baseOptions({ live: false }), {
|
||||
config: baseConfig(),
|
||||
env: {},
|
||||
generatedAt: "2026-05-20T00:00:00.000Z",
|
||||
readCallbackEvidenceImpl: vi.fn(async () => []),
|
||||
});
|
||||
|
||||
expect(report.status).toBe("PARTIAL_COMPENSATION_NEEDS_CONFIG");
|
||||
expect(report.configGaps.map((item) => item.id)).toContain("live_read");
|
||||
expect(report.checks.targets.every((item) => item.status === "DRY_RUN_READY")).toBe(true);
|
||||
});
|
||||
|
||||
it("pulls approval numbers and details, then passes when callback evidence matches", async () => {
|
||||
const fetchApprovalInfoListImpl = vi.fn(async ({ filters }) => ({
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
response: {
|
||||
errcode: 0,
|
||||
spNoCount: 1,
|
||||
spNoPrefixes: [maskApprovalIdentifier(`${filters[0].value}-sp`)],
|
||||
},
|
||||
spNos: [`${filters[0].value}-sp`],
|
||||
nextCursor: "",
|
||||
}));
|
||||
const fetchApprovalDetailImpl = vi.fn(async ({ spNo }) => ({
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
summary: detailSummary(spNo, {
|
||||
templateIdPrefix: spNo.startsWith(SENSITIVE_TEMPLATE_ID)
|
||||
? maskApprovalIdentifier(SENSITIVE_TEMPLATE_ID)
|
||||
: maskApprovalIdentifier("tpl-missed-punch"),
|
||||
}),
|
||||
response: {
|
||||
errcode: 0,
|
||||
spStatus: "2",
|
||||
applyFieldCount: 3,
|
||||
approvalNodeCount: 1,
|
||||
commentCount: 0,
|
||||
},
|
||||
}));
|
||||
const evidence = [
|
||||
{
|
||||
callbackKind: "approval_status_change",
|
||||
approvalType: "leave",
|
||||
spStatus: "2",
|
||||
statusLabel: "已通过",
|
||||
spNoPrefix: maskApprovalIdentifier(`${SENSITIVE_TEMPLATE_ID}-sp`),
|
||||
templateIdPrefix: maskApprovalIdentifier(SENSITIVE_TEMPLATE_ID),
|
||||
},
|
||||
{
|
||||
callbackKind: "approval_status_change",
|
||||
approvalType: "missed_punch",
|
||||
spStatus: "2",
|
||||
statusLabel: "已通过",
|
||||
spNoPrefix: maskApprovalIdentifier("tpl-missed-punch-sp"),
|
||||
templateIdPrefix: maskApprovalIdentifier("tpl-missed-punch"),
|
||||
},
|
||||
];
|
||||
|
||||
const report = await executeCompensationPoc(baseOptions(), {
|
||||
config: baseConfig(),
|
||||
env: {},
|
||||
generatedAt: "2026-05-20T00:00:00.000Z",
|
||||
fetchApprovalInfoListImpl,
|
||||
fetchApprovalDetailImpl,
|
||||
readCallbackEvidenceImpl: vi.fn(async () => evidence),
|
||||
});
|
||||
|
||||
expect(report.status).toBe("PASS_COMPENSATION_POC");
|
||||
expect(fetchApprovalInfoListImpl).toHaveBeenCalledTimes(2);
|
||||
expect(fetchApprovalDetailImpl).toHaveBeenCalledTimes(2);
|
||||
expect(report.checks.callbackCompare.findings).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns PASS_WITH_FINDINGS for missing callback and status mismatch", async () => {
|
||||
const report = await executeCompensationPoc(baseOptions(), {
|
||||
config: baseConfig(),
|
||||
env: {},
|
||||
generatedAt: "2026-05-20T00:00:00.000Z",
|
||||
fetchApprovalInfoListImpl: vi.fn(async () => ({
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
response: { errcode: 0, spNoCount: 1 },
|
||||
spNos: [SENSITIVE_SP_NO],
|
||||
nextCursor: "",
|
||||
})),
|
||||
fetchApprovalDetailImpl: vi.fn(async ({ spNo }) => ({
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
summary: detailSummary(spNo, { spStatus: "2" }),
|
||||
response: { errcode: 0, spStatus: "2" },
|
||||
})),
|
||||
readCallbackEvidenceImpl: vi.fn(async () => [{
|
||||
callbackKind: "approval_status_change",
|
||||
spStatus: "3",
|
||||
statusLabel: "已驳回",
|
||||
spNoPrefix: maskApprovalIdentifier(SENSITIVE_SP_NO),
|
||||
}]),
|
||||
});
|
||||
|
||||
expect(report.status).toBe("PASS_WITH_FINDINGS");
|
||||
expect(report.checks.callbackCompare.findings.map((item) => item.id)).toContain("callback_status_mismatch");
|
||||
});
|
||||
|
||||
it("classifies WeCom API errors as BLOCKED_BY_EXTERNAL_CONFIG", async () => {
|
||||
const report = await executeCompensationPoc(baseOptions({
|
||||
templates: { leave: SENSITIVE_TEMPLATE_ID },
|
||||
}), {
|
||||
config: baseConfig(),
|
||||
env: {},
|
||||
generatedAt: "2026-05-20T00:00:00.000Z",
|
||||
fetchApprovalInfoListImpl: vi.fn(async () => ({
|
||||
ok: false,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
code: "wecom_approval_api_error",
|
||||
message: "WeCom approval API errcode 48002",
|
||||
response: { errcode: 48002, errmsg: "api forbidden" },
|
||||
spNos: [],
|
||||
})),
|
||||
fetchApprovalDetailImpl: vi.fn(),
|
||||
readCallbackEvidenceImpl: vi.fn(async () => []),
|
||||
});
|
||||
|
||||
expect(report.status).toBe("SKIP");
|
||||
expect(report.conclusion).toBe("BLOCKED_BY_EXTERNAL_CONFIG");
|
||||
expect(report.mainBlockers[0]).toMatchObject({
|
||||
reason: "wecom_approval_api_error",
|
||||
});
|
||||
});
|
||||
|
||||
it("compares callback evidence using redacted prefixes", () => {
|
||||
const compare = compareCallbackEvidence([
|
||||
{
|
||||
details: [{
|
||||
target: "leave",
|
||||
spNoPrefix: "SPMATCH***",
|
||||
spStatus: "2",
|
||||
statusLabel: "已通过",
|
||||
status: "OK",
|
||||
}],
|
||||
},
|
||||
], {
|
||||
eventCount: 2,
|
||||
bySpNoPrefix: {
|
||||
"SPMATCH***": [
|
||||
{ spNoPrefix: "SPMATCH***", spStatus: "2", statusLabel: "已通过" },
|
||||
{ spNoPrefix: "SPMATCH***", spStatus: "2", statusLabel: "已通过" },
|
||||
],
|
||||
"SPONLYCB***": [
|
||||
{ spNoPrefix: "SPONLYCB***", spStatus: "2", statusLabel: "已通过" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(compare.status).toBe("FINDINGS");
|
||||
expect(compare.findings.map((item) => item.id)).toEqual(expect.arrayContaining([
|
||||
"callback_duplicate_for_pulled_detail",
|
||||
"callback_not_in_compensation_window",
|
||||
]));
|
||||
});
|
||||
|
||||
it("redacts secrets, full identifiers and form values from reports", async () => {
|
||||
const report = await executeCompensationPoc(baseOptions({
|
||||
templates: { leave: SENSITIVE_TEMPLATE_ID },
|
||||
}), {
|
||||
config: baseConfig(),
|
||||
env: {
|
||||
WECOM_AGENT_SECRET: SENSITIVE_AGENT_CREDENTIAL,
|
||||
},
|
||||
generatedAt: "2026-05-20T00:00:00.000Z",
|
||||
fetchApprovalInfoListImpl: vi.fn(async () => ({
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
response: { errcode: 0, spNoCount: 1, spNoPrefixes: [maskApprovalIdentifier(SENSITIVE_SP_NO)] },
|
||||
spNos: [SENSITIVE_SP_NO],
|
||||
nextCursor: "",
|
||||
})),
|
||||
fetchApprovalDetailImpl: vi.fn(async ({ spNo }) => ({
|
||||
ok: true,
|
||||
tokenSource: "WECOM_AGENT_SECRET",
|
||||
summary: detailSummary(spNo),
|
||||
response: {
|
||||
errcode: 0,
|
||||
spStatus: "2",
|
||||
applyFieldCount: 3,
|
||||
approvalNodeCount: 1,
|
||||
},
|
||||
})),
|
||||
readCallbackEvidenceImpl: vi.fn(async () => [{
|
||||
callbackKind: "approval_status_change",
|
||||
spStatus: "2",
|
||||
statusLabel: "已通过",
|
||||
spNoPrefix: maskApprovalIdentifier(SENSITIVE_SP_NO),
|
||||
templateIdPrefix: maskApprovalIdentifier(SENSITIVE_TEMPLATE_ID),
|
||||
}]),
|
||||
});
|
||||
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_SP_NO);
|
||||
expect(serialized).not.toContain(SENSITIVE_TEMPLATE_ID);
|
||||
expect(serialized).not.toContain(SENSITIVE_USER_ID);
|
||||
expect(serialized).not.toContain(SENSITIVE_FORM_VALUE);
|
||||
expect(statusLabelFromSpStatus("2")).toBe("已通过");
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户