226 行
8.3 KiB
JavaScript
226 行
8.3 KiB
JavaScript
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: [],
|
|
oaOnlySmokeExempted: [],
|
|
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("exempts source-classified OA-only smoke accounts from archive plans", () => {
|
|
const plan = buildFixPlan(auditReport({
|
|
findings: {
|
|
...auditReport().findings,
|
|
oaOnlySmokeExempted: [
|
|
{ employeeNo: "TEST001", status: "active", roles: ["admin", "employee"] },
|
|
],
|
|
},
|
|
}));
|
|
|
|
expect(plan.archiveOaTestEmployees.map((item) => item.employeeNo)).toEqual(["TEST002"]);
|
|
expect(plan.oaOnlySmokeExempted.map((item) => item.employeeNo)).toEqual(["TEST001"]);
|
|
});
|
|
|
|
it("supports explicit OA-only smoke employee-no exemption for legacy reports", () => {
|
|
const plan = buildFixPlan(auditReport(), { oaOnlySmokeEmployeeNos: ["TEST002"] });
|
|
|
|
expect(plan.archiveOaTestEmployees.map((item) => item.employeeNo)).toEqual(["TEST001"]);
|
|
expect(plan.oaOnlySmokeExempted.map((item) => item.employeeNo)).toEqual(["TEST002"]);
|
|
});
|
|
|
|
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",
|
|
"--oa-only-smoke-employee-no=TEST003",
|
|
"--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.oaOnlySmokeEmployeeNos).toEqual(["TEST003"]);
|
|
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);
|
|
});
|
|
});
|