WOUAFIX 收口 OA 与企微用户一致性差异
这个提交包含在:
@@ -0,0 +1,201 @@
|
||||
const {
|
||||
applyPlanWithClient,
|
||||
assertApplyAllowed,
|
||||
buildDryRunResult,
|
||||
buildFixPlan,
|
||||
normalizeEmployeeNoList,
|
||||
parseArgs,
|
||||
} = require("../../../src/scripts/resolve-wecom-oa-user-consistency");
|
||||
|
||||
function auditReport(overrides = {}) {
|
||||
return {
|
||||
status: "PASS_WITH_FINDINGS",
|
||||
generatedAt: "2026-05-18T14:25:44.000Z",
|
||||
findings: {
|
||||
oaActiveMissingInWecom: [
|
||||
{ employeeNo: "TEST001", status: "active", roles: ["employee"] },
|
||||
{ employeeNo: "TEST002", status: "active", roles: ["employee"] },
|
||||
],
|
||||
missingRequiredFields: [
|
||||
{
|
||||
employee: { employeeNo: "REAL001", status: "active", roles: ["employee"] },
|
||||
wecom: { externalUserIdPrefix: "wecom_re***" },
|
||||
fields: ["email", "phone", "department"],
|
||||
},
|
||||
],
|
||||
inactiveWithBinding: [
|
||||
{ employeeNo: "HRWDTEST-20260517214548", status: "archived", archived: true, roles: ["employee"] },
|
||||
],
|
||||
matchedActive: [],
|
||||
wecomActiveMissingInOa: [],
|
||||
oaActiveMissingBinding: [],
|
||||
statusMismatch: [],
|
||||
matchConflict: [],
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function employeeRow(overrides = {}) {
|
||||
return {
|
||||
id: overrides.id ?? "11111111-1111-4111-8111-111111111111",
|
||||
employee_no: overrides.employeeNo ?? "TEST001",
|
||||
status: overrides.status ?? "active",
|
||||
employment_status: overrides.employmentStatus ?? "active",
|
||||
archived: overrides.archived ?? false,
|
||||
token_version: overrides.tokenVersion ?? 1,
|
||||
roles: overrides.roles ?? ["employee"],
|
||||
has_wecom_binding: overrides.hasWecomBinding ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
function buildClient(rows) {
|
||||
const calls = [];
|
||||
return {
|
||||
calls,
|
||||
async query(sql, params) {
|
||||
const text = String(sql);
|
||||
calls.push({ sql: text, params });
|
||||
if (text.includes("FROM employees e")) {
|
||||
return { rows };
|
||||
}
|
||||
return { rows: [], rowCount: 1 };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function sqlCalls(client, pattern) {
|
||||
return client.calls.filter((call) => call.sql.includes(pattern));
|
||||
}
|
||||
|
||||
describe("resolve-wecom-oa-user-consistency", () => {
|
||||
afterEach(() => {
|
||||
delete process.env.WOUA_FIX_APPLY;
|
||||
});
|
||||
|
||||
it("builds a default plan that archives missing OA test employees and accepts missing WeCom fields", () => {
|
||||
const plan = buildFixPlan(auditReport());
|
||||
const dryRun = buildDryRunResult(plan);
|
||||
const serialized = JSON.stringify(dryRun);
|
||||
|
||||
expect(plan.archiveOaTestEmployees.map((item) => item.employeeNo)).toEqual(["TEST001", "TEST002"]);
|
||||
expect(plan.acceptedMissingRequiredFields).toEqual([
|
||||
expect.objectContaining({
|
||||
employeeNo: "REAL001",
|
||||
action: "accepted_no_write",
|
||||
fields: ["department", "email", "phone"],
|
||||
}),
|
||||
]);
|
||||
expect(plan.inactiveWithBinding[0]).toEqual(expect.objectContaining({
|
||||
employeeNo: "HRWDTEST-20260517214548",
|
||||
action: "accepted_no_write",
|
||||
}));
|
||||
expect(dryRun.totals.wecomWrites).toBe(0);
|
||||
expect(serialized).not.toContain("13800000001");
|
||||
expect(serialized).not.toContain("[email protected]");
|
||||
expect(serialized).not.toContain("wecom_user_001_very_long");
|
||||
});
|
||||
|
||||
it("supports allowlist narrowing to avoid touching unlisted employees", () => {
|
||||
const plan = buildFixPlan(auditReport(), { allowEmployeeNos: ["TEST002"] });
|
||||
|
||||
expect(plan.archiveOaTestEmployees.map((item) => item.employeeNo)).toEqual(["TEST002"]);
|
||||
expect(plan.excludedByAllowlist.map((item) => item.employeeNo)).toEqual(["TEST001"]);
|
||||
});
|
||||
|
||||
it("requires explicit environment approval for apply mode", () => {
|
||||
expect(() => assertApplyAllowed({ mode: "apply" })).toThrow(/WOUA_FIX_APPLY=1/);
|
||||
process.env.WOUA_FIX_APPLY = "1";
|
||||
expect(() => assertApplyAllowed({ mode: "apply" })).not.toThrow();
|
||||
});
|
||||
|
||||
it("parses mode and repeated employee allowlist arguments", () => {
|
||||
const options = parseArgs([
|
||||
"--report=summary.json",
|
||||
"--apply",
|
||||
"--allow-employee-no=TEST001,TEST002",
|
||||
"--allow-employee-no",
|
||||
"TEST001",
|
||||
"--repair-manager-effects",
|
||||
"--manager-fallback-employee-no=HYD000007",
|
||||
"--format=json",
|
||||
]);
|
||||
|
||||
expect(options.mode).toBe("apply");
|
||||
expect(options.reportPath).toBe("summary.json");
|
||||
expect(options.format).toBe("json");
|
||||
expect(options.allowEmployeeNos).toEqual(["TEST001", "TEST002"]);
|
||||
expect(options.repairManagerEffects).toBe(true);
|
||||
expect(options.managerFallbackEmployeeNo).toBe("HYD000007");
|
||||
});
|
||||
|
||||
it("deduplicates normalized employee numbers", () => {
|
||||
expect(normalizeEmployeeNoList([" TEST001 ", "TEST001", "", "TEST002"])).toEqual(["TEST001", "TEST002"]);
|
||||
});
|
||||
|
||||
it("dry-runs inside a rolled-back transaction and does not update employees", async () => {
|
||||
const plan = buildFixPlan(auditReport(), { allowEmployeeNos: ["TEST001"] });
|
||||
const client = buildClient([employeeRow({ employeeNo: "TEST001" })]);
|
||||
const result = await applyPlanWithClient(client, plan, {
|
||||
mode: "dry_run",
|
||||
now: new Date("2026-05-18T00:00:00.000Z"),
|
||||
});
|
||||
|
||||
expect(result.status).toBe("DRY_RUN");
|
||||
expect(result.totals.archivedPlanned).toBe(1);
|
||||
expect(sqlCalls(client, "UPDATE employees")).toHaveLength(0);
|
||||
expect(sqlCalls(client, "ROLLBACK")).toHaveLength(1);
|
||||
expect(sqlCalls(client, "COMMIT")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("archives active test employees in apply mode and increments token_version", async () => {
|
||||
const plan = buildFixPlan(auditReport(), { allowEmployeeNos: ["TEST001"] });
|
||||
const client = buildClient([employeeRow({ employeeNo: "TEST001" })]);
|
||||
const result = await applyPlanWithClient(client, plan, {
|
||||
mode: "apply",
|
||||
now: new Date("2026-05-18T00:00:00.000Z"),
|
||||
});
|
||||
|
||||
expect(result.status).toBe("APPLIED");
|
||||
expect(result.totals.archivedApplied).toBe(1);
|
||||
expect(sqlCalls(client, "UPDATE employees")).toHaveLength(1);
|
||||
expect(sqlCalls(client, "token_version = token_version + 1")).toHaveLength(1);
|
||||
expect(sqlCalls(client, "INSERT INTO employee_lifecycle_events")).toHaveLength(1);
|
||||
expect(sqlCalls(client, "COMMIT")).toHaveLength(1);
|
||||
expect(sqlCalls(client, "ROLLBACK")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("keeps inactive WeCom bindings by default and clears them only with explicit flag", async () => {
|
||||
const plan = buildFixPlan(auditReport(), { clearInactiveBindings: true });
|
||||
const dryClient = buildClient([
|
||||
employeeRow({ employeeNo: "TEST001" }),
|
||||
employeeRow({
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
employeeNo: "HRWDTEST-20260517214548",
|
||||
status: "archived",
|
||||
employmentStatus: "inactive",
|
||||
archived: true,
|
||||
hasWecomBinding: true,
|
||||
}),
|
||||
]);
|
||||
const defaultPlan = buildFixPlan(auditReport());
|
||||
const defaultClient = buildClient([
|
||||
employeeRow({ employeeNo: "TEST001" }),
|
||||
]);
|
||||
|
||||
const defaultResult = await applyPlanWithClient(defaultClient, defaultPlan, {
|
||||
mode: "apply",
|
||||
now: new Date("2026-05-18T00:00:00.000Z"),
|
||||
});
|
||||
const explicitResult = await applyPlanWithClient(dryClient, plan, {
|
||||
mode: "apply",
|
||||
clearInactiveBindings: true,
|
||||
now: new Date("2026-05-18T00:00:00.000Z"),
|
||||
});
|
||||
|
||||
expect(defaultResult.totals.inactiveBindingAccepted).toBe(1);
|
||||
expect(sqlCalls(defaultClient, "DELETE FROM wecom_identity_bindings")).toHaveLength(0);
|
||||
expect(explicitResult.totals.inactiveBindingApplied).toBe(1);
|
||||
expect(sqlCalls(dryClient, "DELETE FROM wecom_identity_bindings")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户