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

378 行
13 KiB
JavaScript

const {
buildActorPrecheck,
buildApplyFixturePayload,
buildSecretCheck,
buildTemplateFixturePayload,
executeApprovalFixtures,
liveWriteAllowed,
parseArgs,
} = require("../../../src/scripts/check-wecom-approval-fixtures");
const {
fetchWecomApprovalApplyEvent,
fetchWecomApprovalTemplateCreate,
} = require("../../../src/services/integrations/wecomApproval");
const { clearWecomAppAccessTokenCache } = require("../../../src/services/integrations/wecomAccessToken");
const SENSITIVE_AGENT_CREDENTIAL = ["fixture", "agent", "credential", "should", "never", "leak"].join("-");
const SENSITIVE_CONTACT_CREDENTIAL = ["fixture", "contact", "credential", "unused"].join("-");
const SENSITIVE_CHECKIN_CREDENTIAL = ["fixture", "checkin", "credential", "unused"].join("-");
const SENSITIVE_HR_CREDENTIAL = ["fixture", "hr", "credential", "unused"].join("-");
const SENSITIVE_APPLICANT_USERID = ["fixture", "applicant", "userid", "full", "secret"].join("_");
const SENSITIVE_APPROVER_USERID = ["fixture", "approver", "userid", "full", "secret"].join("_");
const SENSITIVE_TEMPLATE_ID = ["tpl", "fixture", "full", "identifier", "must", "not", "leak"].join("-");
const SENSITIVE_SP_NO = ["sp", "fixture", "full", "identifier", "must", "not", "leak"].join("-");
function baseConfig(overrides = {}) {
return {
wecomCorpId: "corp-fixture",
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: false,
createTemplates: false,
submitTestApprovals: false,
applicantEmployeeNo: "SMOKE002",
approverEmployeeNo: "SMOKE101",
format: "json",
reportDir: "/tmp/wecom-approval-fixtures-test",
help: false,
...overrides,
};
}
function actorRows(overrides = {}) {
return [
{
id: "applicant-id",
employeeNo: "SMOKE002",
status: "active",
archived: false,
wecomExternalUserId: SENSITIVE_APPLICANT_USERID,
},
{
id: "approver-id",
employeeNo: "SMOKE101",
status: "active",
archived: false,
wecomExternalUserId: SENSITIVE_APPROVER_USERID,
},
].map((row) => ({ ...row, ...(overrides[row.employeeNo] || {}) }));
}
function jsonResponse(payload, status = 200) {
return {
ok: status >= 200 && status < 300,
status,
json: async () => payload,
};
}
describe("check-wecom-approval-fixtures", () => {
beforeEach(() => {
clearWecomAppAccessTokenCache();
});
it("parses defaults and live write flags", () => {
expect(parseArgs(["--format=json"])).toMatchObject({
live: false,
applicantEmployeeNo: "SMOKE002",
approverEmployeeNo: "SMOKE101",
format: "json",
});
const live = parseArgs([
"--live",
"--create-templates",
"--submit-test-approvals",
"--applicant-employee-no=SMOKE002",
"--approver-employee-no",
"SMOKE101",
]);
expect(live).toMatchObject({
live: true,
createTemplates: true,
submitTestApprovals: true,
applicantEmployeeNo: "SMOKE002",
approverEmployeeNo: "SMOKE101",
});
});
it("passes dry-run without calling WeCom write APIs", async () => {
const createTemplateImpl = vi.fn();
const applyEventImpl = vi.fn();
const report = await executeApprovalFixtures(baseOptions(), {
config: baseConfig(),
env: {},
actorRows: actorRows(),
createTemplateImpl,
applyEventImpl,
generatedAt: "2026-05-19T10:00:00.000Z",
});
expect(report.status).toBe("PASS_FIXTURE_DRY_RUN_READY");
expect(report.scope.writeBehavior).toContain("dry-run only");
expect(createTemplateImpl).not.toHaveBeenCalled();
expect(applyEventImpl).not.toHaveBeenCalled();
expect(report.checks.fixturePayloads).toHaveLength(2);
});
it("builds create_template payload with official template_name and property controls", () => {
const payload = buildTemplateFixturePayload(
{ key: "leave", label: "请假" },
"2026-05-19T10:00:00.000Z",
);
expect(payload.template_names).toBeUndefined();
expect(payload.template_name).toEqual([{ text: "OA-POC-请假-20260519T100000Z", lang: "zh_CN" }]);
expect(payload.template_content.controls).toHaveLength(3);
expect(payload.template_content.controls[0]).toMatchObject({
property: {
control: "Text",
id: "Text-01",
title: [{ text: "类型", lang: "zh_CN" }],
placeholder: [{ text: "类型", lang: "zh_CN" }],
require: 1,
un_print: 1,
},
config: {},
});
expect(payload.template_content.controls[1]).toMatchObject({
property: {
control: "Date",
id: "Date-01",
},
config: {
date: {
type: "day",
},
},
});
});
it("builds applyevent payload with template control ids and non-self approver", () => {
const payload = buildApplyFixturePayload({
target: { key: "leave", label: "请假" },
templateId: SENSITIVE_TEMPLATE_ID,
applicantUserId: SENSITIVE_APPLICANT_USERID,
approverUserId: SENSITIVE_APPROVER_USERID,
generatedAt: "2026-05-19T10:00:00.000Z",
});
expect(payload.creator_userid).toBe(SENSITIVE_APPLICANT_USERID);
expect(payload.template_id).toBe(SENSITIVE_TEMPLATE_ID);
expect(payload.use_template_approver).toBe(0);
expect(payload.approver).toEqual([{ attr: 1, userid: [SENSITIVE_APPROVER_USERID] }]);
expect(payload.notifyer).toEqual([SENSITIVE_APPROVER_USERID]);
expect(payload.apply_data.contents.map((item) => item.id)).toEqual(["Text-01", "Date-01", "Textarea-01"]);
expect(payload.apply_data.contents[1]).toMatchObject({
control: "Date",
value: {
date: {
type: "day",
s_timestamp: "1779148800",
},
},
});
});
it("does not write in live mode without the explicit env switch", async () => {
const createTemplateImpl = vi.fn();
const report = await executeApprovalFixtures(baseOptions({
live: true,
createTemplates: true,
submitTestApprovals: true,
}), {
config: baseConfig(),
env: {},
actorRows: actorRows(),
createTemplateImpl,
generatedAt: "2026-05-19T10:00:00.000Z",
});
expect(liveWriteAllowed(baseOptions({
live: true,
createTemplates: true,
submitTestApprovals: true,
}), {})).toBe(false);
expect(report.status).toBe("PARTIAL_FIXTURE_NEEDS_CONFIG");
expect(report.configGaps).toEqual(expect.arrayContaining([
expect.objectContaining({ id: "approval_fixture_write_guard" }),
]));
expect(createTemplateImpl).not.toHaveBeenCalled();
});
it("blocks self-approval by OA employee or WeCom userid", () => {
const sameEmployee = buildActorPrecheck(actorRows({
SMOKE101: { id: "applicant-id" },
}), baseOptions());
expect(sameEmployee.status).toBe("NEEDS_CONFIG");
expect(sameEmployee.gaps.map((item) => item.id)).toContain("self_approval_same_oa_employee");
const sameWecom = buildActorPrecheck(actorRows({
SMOKE101: { wecomExternalUserId: SENSITIVE_APPLICANT_USERID },
}), baseOptions());
expect(sameWecom.gaps.map((item) => item.id)).toContain("self_approval_same_wecom_userid");
});
it("keeps SMOKE101 unavailable as a config gap and does not auto-replace", async () => {
const report = await executeApprovalFixtures(baseOptions(), {
config: baseConfig(),
env: {},
actorRows: actorRows({
SMOKE101: { status: "inactive" },
}),
generatedAt: "2026-05-19T10:00:00.000Z",
});
expect(report.status).toBe("PARTIAL_FIXTURE_NEEDS_CONFIG");
expect(report.configGaps).toEqual(expect.arrayContaining([
expect.objectContaining({
id: "approver_inactive",
employeeNo: "SMOKE101",
}),
]));
expect(JSON.stringify(report)).not.toContain("HYD000007");
});
it("uses WECOM_AGENT_SECRET for create_template and applyevent 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: "fixture-token", expires_in: 7200 });
}
if (String(url).includes("/cgi-bin/oa/approval/create_template")) {
return jsonResponse({ errcode: 0, errmsg: "ok", template_id: SENSITIVE_TEMPLATE_ID });
}
if (String(url).includes("/cgi-bin/oa/applyevent")) {
return jsonResponse({ errcode: 0, errmsg: "ok", sp_no: SENSITIVE_SP_NO });
}
throw new Error(`unexpected url ${url}`);
});
const template = await fetchWecomApprovalTemplateCreate({
config: baseConfig(),
payload: buildTemplateFixturePayload({ key: "leave", label: "请假" }, "2026-05-19T10:00:00.000Z"),
fetchImpl,
});
const approval = await fetchWecomApprovalApplyEvent({
config: baseConfig(),
payload: buildApplyFixturePayload({
target: { key: "leave", label: "请假" },
templateId: SENSITIVE_TEMPLATE_ID,
applicantUserId: SENSITIVE_APPLICANT_USERID,
approverUserId: SENSITIVE_APPROVER_USERID,
generatedAt: "2026-05-19T10:00:00.000Z",
}),
fetchImpl,
});
expect(template).toMatchObject({
ok: true,
tokenSource: "WECOM_AGENT_SECRET",
templateId: SENSITIVE_TEMPLATE_ID,
});
expect(approval).toMatchObject({
ok: true,
tokenSource: "WECOM_AGENT_SECRET",
spNo: SENSITIVE_SP_NO,
});
expect(calls.some((item) => item.url.includes("/cgi-bin/oa/approval/create_template"))).toBe(true);
expect(calls.some((item) => item.url.includes("/cgi-bin/oa/applyevent"))).toBe(true);
});
it("passes generated template IDs and sp_no into WAPC2 auto rerun without reporting full IDs", async () => {
const executeApprovalPocImpl = vi.fn(async (options) => {
expect(options.live).toBe(true);
expect(options.templates).toEqual({
leave: `${SENSITIVE_TEMPLATE_ID}-leave`,
missed_punch: `${SENSITIVE_TEMPLATE_ID}-missed_punch`,
});
expect(options.spNos).toEqual({
leave: `${SENSITIVE_SP_NO}-leave`,
missed_punch: `${SENSITIVE_SP_NO}-missed_punch`,
});
return { status: "PASS_APPROVAL_POC", conclusion: "PASS_APPROVAL_POC" };
});
const report = await executeApprovalFixtures(baseOptions({
live: true,
createTemplates: true,
submitTestApprovals: true,
}), {
config: baseConfig(),
env: { WECOM_APPROVAL_FIXTURE_WRITE: "1" },
actorRows: actorRows(),
createTemplateImpl: vi.fn(async ({ payload }) => ({
ok: true,
tokenSource: "WECOM_AGENT_SECRET",
templateId: `${SENSITIVE_TEMPLATE_ID}-${payload.fixture_marker.target}`,
response: { errcode: 0 },
})),
applyEventImpl: vi.fn(async ({ payload }) => ({
ok: true,
tokenSource: "WECOM_AGENT_SECRET",
spNo: `${SENSITIVE_SP_NO}-${payload.template_id.split("-").pop()}`,
response: { errcode: 0 },
})),
executeApprovalPocImpl,
generatedAt: "2026-05-19T10:00:00.000Z",
});
const serialized = JSON.stringify(report);
expect(report.status).toBe("PASS_FIXTURE_CREATED");
expect(executeApprovalPocImpl).toHaveBeenCalledTimes(1);
expect(serialized).not.toContain(`${SENSITIVE_TEMPLATE_ID}-leave`);
expect(serialized).not.toContain(`${SENSITIVE_SP_NO}-leave`);
expect(serialized).toContain("tpl-fixt***");
expect(serialized).toContain("sp-fixtu***");
});
it("keeps report output redacted", async () => {
const report = await executeApprovalFixtures(baseOptions(), {
config: baseConfig(),
env: {},
actorRows: actorRows(),
generatedAt: "2026-05-19T10: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_APPLICANT_USERID);
expect(serialized).not.toContain(SENSITIVE_APPROVER_USERID);
expect(serialized).not.toContain("low-risk dry-run fixture");
expect(serialized).toContain("fixture_***");
});
it("documents missing WECOM_AGENT_SECRET as a config gap", () => {
const check = buildSecretCheck(baseConfig({ wecomAgentSecret: "" }));
expect(check).toMatchObject({
status: "NEEDS_CONFIG",
missing: ["WECOM_AGENT_SECRET"],
});
expect(check.noFallback).toEqual([
"WECOM_CONTACT_SECRET",
"WECOM_CHECKIN_SECRET",
"WECOM_HR_ASSISTANT_SECRET",
]);
});
});