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

564 行
20 KiB
JavaScript

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",
"--discover-template-ids",
"--discover-applicant-employee-no=SMOKE002",
]);
expect(options.live).toBe(true);
expect(options.format).toBe("json");
expect(options.size).toBe(50);
expect(options.discoverTemplateIds).toBe(true);
expect(options.discoverApplicantEmployeeNo).toBe("SMOKE002");
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("auto-discovers missing template IDs by applicant creator without leaking full identifiers", async () => {
const discoveredLeaveTemplateId = "tpllv-auto-discovered-leave-sensitive";
const discoveredMissedTemplateId = "tplmp-auto-discovered-missed-sensitive";
const fetchApprovalInfoListImpl = vi.fn(async ({ filters }) => {
if (filters[0].key === "creator") {
return {
ok: true,
tokenSource: "WECOM_AGENT_SECRET",
response: {
errcode: 0,
spNoCount: 2,
spNoPrefixes: [maskApprovalIdentifier("sp-discovery-leave"), maskApprovalIdentifier("sp-discovery-missed")],
},
spNos: ["sp-discovery-leave", "sp-discovery-missed"],
nextCursor: "",
};
}
return {
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 }) => {
if (spNo === "sp-discovery-leave") {
return {
ok: true,
tokenSource: "WECOM_AGENT_SECRET",
templateId: discoveredLeaveTemplateId,
spName: "OA-POC-请假-2026-05-19",
summary: detailSummary(spNo, { templateIdPrefix: maskApprovalIdentifier(discoveredLeaveTemplateId) }),
response: { errcode: 0, spStatus: "2" },
};
}
if (spNo === "sp-discovery-missed") {
return {
ok: true,
tokenSource: "WECOM_AGENT_SECRET",
templateId: discoveredMissedTemplateId,
spName: "OA-POC-补卡-2026-05-19",
summary: detailSummary(spNo, { templateIdPrefix: maskApprovalIdentifier(discoveredMissedTemplateId) }),
response: { errcode: 0, spStatus: "2" },
};
}
return {
ok: true,
tokenSource: "WECOM_AGENT_SECRET",
summary: detailSummary(spNo, {
templateIdPrefix: spNo.startsWith(discoveredLeaveTemplateId)
? maskApprovalIdentifier(discoveredLeaveTemplateId)
: maskApprovalIdentifier(discoveredMissedTemplateId),
}),
response: { errcode: 0, spStatus: "2" },
};
});
const report = await executeCompensationPoc(baseOptions({
templates: {},
discoverTemplateIds: true,
discoverApplicantEmployeeNo: "SMOKE002",
}), {
config: baseConfig(),
env: {},
generatedAt: "2026-05-20T00:00:00.000Z",
queryApplicantActorImpl: vi.fn(async () => ({
employeeNo: "SMOKE002",
status: "active",
archived: false,
wecomExternalUserId: SENSITIVE_USER_ID,
})),
fetchApprovalInfoListImpl,
fetchApprovalDetailImpl,
readCallbackEvidenceImpl: vi.fn(async () => [
{
callbackKind: "approval_status_change",
spStatus: "2",
statusLabel: "已通过",
spNoPrefix: maskApprovalIdentifier(`${discoveredLeaveTemplateId}-sp`),
},
{
callbackKind: "approval_status_change",
spStatus: "2",
statusLabel: "已通过",
spNoPrefix: maskApprovalIdentifier(`${discoveredMissedTemplateId}-sp`),
},
]),
});
expect(report.status).toBe("PASS_COMPENSATION_POC");
expect(fetchApprovalInfoListImpl.mock.calls[0][0].filters).toEqual([
{ key: "creator", value: SENSITIVE_USER_ID },
]);
expect(fetchApprovalInfoListImpl.mock.calls[1][0].filters).toEqual([
{ key: "template_id", value: discoveredLeaveTemplateId },
]);
expect(fetchApprovalInfoListImpl.mock.calls[2][0].filters).toEqual([
{ key: "template_id", value: discoveredMissedTemplateId },
]);
const serialized = JSON.stringify(report);
expect(serialized).not.toContain(discoveredLeaveTemplateId);
expect(serialized).not.toContain(discoveredMissedTemplateId);
expect(serialized).not.toContain(SENSITIVE_USER_ID);
expect(report.checks.templateDiscovery.discoveredTemplatePrefixes).toEqual({
leave: maskApprovalIdentifier(discoveredLeaveTemplateId),
missed_punch: maskApprovalIdentifier(discoveredMissedTemplateId),
});
});
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("treats missing callback evidence after successful detail pull as findings", async () => {
const report = await executeCompensationPoc(baseOptions({
templates: { leave: SENSITIVE_TEMPLATE_ID, missed_punch: "tpl-missed-punch" },
}), {
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 () => []),
});
expect(report.status).toBe("PASS_WITH_FINDINGS");
expect(report.configGaps.map((item) => item.id)).not.toContain("callback_evidence_missing");
expect(report.checks.callbackCompare.findings.map((item) => item.id)).toContain("callback_missing_for_pulled_detail");
});
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("已通过");
});
});