WAPC2F 实现企微审批样件 dry-run
这个提交包含在:
@@ -0,0 +1,318 @@
|
||||
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("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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户