401 行
14 KiB
JavaScript
401 行
14 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",
|
|
targets: ["leave", "missed_punch"],
|
|
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("supports WEXT1 outing and overtime dry-run targets without changing defaults", async () => {
|
|
const options = parseArgs(["--format=json", "--target=outing", "--targets=overtime"]);
|
|
expect(options.targets).toEqual(["outing", "overtime"]);
|
|
|
|
const createTemplateImpl = vi.fn();
|
|
const applyEventImpl = vi.fn();
|
|
const report = await executeApprovalFixtures(options, {
|
|
config: baseConfig(),
|
|
env: {},
|
|
actorRows: actorRows(),
|
|
createTemplateImpl,
|
|
applyEventImpl,
|
|
generatedAt: "2026-05-20T10:00:00.000Z",
|
|
});
|
|
|
|
expect(report.status).toBe("PASS_FIXTURE_DRY_RUN_READY");
|
|
expect(report.scope.targets).toEqual(["outing", "overtime"]);
|
|
expect(report.checks.fixturePayloads.map((item) => item.target)).toEqual(["outing", "overtime"]);
|
|
expect(createTemplateImpl).not.toHaveBeenCalled();
|
|
expect(applyEventImpl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
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",
|
|
]);
|
|
});
|
|
});
|