文件
hyb-oa/server/tests/unit/scripts/import-wecom-directory-export.test.js
T
wangsli 4886f9b081
ci-e2e / e2e (push) Waiting to run
ci-minimal / check-minimal (push) Waiting to run
修复企微通讯录生产导入兼容性(07-26-internal-ops-recovery-baseline)
2026-07-27 18:31:04 +08:00

183 行
6.3 KiB
JavaScript

const fs = require("node:fs");
const ExcelJS = require("exceljs");
const {
DirectoryExportError,
buildSafeReport,
parseArgs,
parseDirectoryExport,
readStdinBuffer,
} = require("../../../src/scripts/import-wecom-directory-export");
async function buildDirectoryWorkbook(rows) {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("通讯录");
worksheet.addRow(["填写须知:本行为企微导出说明"]);
worksheet.addRow([]);
worksheet.addRow([
"姓名", "账号", "别名", "职务", "部门", "性别", "手机", "座机",
"邮箱", "地址", "企业邮箱", "视频号", "激活状态", "禁用状态", "微信插件",
]);
for (const row of rows) {
worksheet.addRow([
row.name,
row.account,
"",
row.title ?? "员工",
row.department ?? "总部",
"",
row.phone ?? "",
"",
row.email ?? "",
"",
row.businessEmail,
"",
"已激活",
"",
"未关注",
]);
}
return Buffer.from(await workbook.xlsx.writeBuffer());
}
describe("WeCom directory export importer", () => {
it("reads stdin through the fd-compatible synchronous API", () => {
const expected = Buffer.from("synthetic-xlsx");
const readFileSync = vi.spyOn(fs, "readFileSync").mockReturnValue(expected);
try {
expect(readStdinBuffer()).toBe(expected);
expect(readFileSync).toHaveBeenCalledWith(0);
} finally {
readFileSync.mockRestore();
}
});
it("requires stdin, the apply environment gate, and an actor for apply", () => {
const previous = process.env.WECOM_DIRECTORY_EXPORT_APPLY;
delete process.env.WECOM_DIRECTORY_EXPORT_APPLY;
try {
expect(() => parseArgs(["--input=directory.xlsx"])).toThrowError(
expect.objectContaining({ code: "stdin_required" }),
);
expect(() => parseArgs(["--apply", "--actor-employee-no=HYD000001"])).toThrowError(
expect.objectContaining({ code: "apply_confirmation_required" }),
);
process.env.WECOM_DIRECTORY_EXPORT_APPLY = "1";
expect(() => parseArgs(["--apply"])).toThrowError(
expect.objectContaining({ code: "actor_required" }),
);
expect(parseArgs(["--apply", "--actor-employee-no=HYD000001"])).toEqual(
expect.objectContaining({ input: "-", mode: "apply", actorEmployeeNo: "HYD000001" }),
);
} finally {
if (previous == null) delete process.env.WECOM_DIRECTORY_EXPORT_APPLY;
else process.env.WECOM_DIRECTORY_EXPORT_APPLY = previous;
}
});
it("finds the official Chinese header after instruction rows and accepts one missing phone", async () => {
const rows = Array.from({ length: 6 }, (_, index) => ({
name: `合成员${index + 1}`,
account: `member-${index + 1}`,
phone: index === 5 ? "" : `1990000000${index + 1}`,
businessEmail: `member-${index + 1}@example.test`,
}));
const parsed = await parseDirectoryExport(await buildDirectoryWorkbook(rows));
expect(parsed.sourceSummary).toEqual({
memberCount: 6,
activeCount: 6,
phonePresentCount: 5,
phoneMissingCount: 1,
emailPresentCount: 6,
incompleteCount: 0,
});
expect(parsed.profiles[5].phone).toBeNull();
expect(parsed.profiles[5].missingRequiredFields).toEqual([]);
const report = buildSafeReport(
{ mode: "dry_run" },
parsed,
{ summary: { created: 5, updated: 1, planned: 6 } },
);
const serialized = JSON.stringify(report);
expect(report.status).toBe("PASS_DIRECTORY_EXPORT_DRY_RUN");
for (const row of rows) {
expect(serialized).not.toContain(row.name);
expect(serialized).not.toContain(row.account);
expect(serialized).not.toContain(row.businessEmail);
if (row.phone) expect(serialized).not.toContain(row.phone);
}
});
it.each([
["account", { account: "member-1", phone: "19900000002", businessEmail: "[email protected]" }],
["phone", { account: "member-2", phone: "19900000001", businessEmail: "[email protected]" }],
["email", { account: "member-2", phone: "19900000002", businessEmail: "[email protected]" }],
])("rejects duplicate %s values without returning the duplicated value", async (_field, duplicateRow) => {
const buffer = await buildDirectoryWorkbook([
{
name: "合成员一",
account: "member-1",
phone: "19900000001",
businessEmail: "[email protected]",
},
{ name: "合成员二", ...duplicateRow },
]);
let caught;
try {
await parseDirectoryExport(buffer);
} catch (error) {
caught = error;
}
expect(caught).toBeInstanceOf(DirectoryExportError);
expect(caught.code).toBe("duplicate_values");
const serialized = JSON.stringify(caught);
expect(serialized).not.toContain("[email protected]");
expect(serialized).not.toContain("19900000001");
});
it("requires a valid enterprise email even when a personal email is present", async () => {
const parsed = await parseDirectoryExport(await buildDirectoryWorkbook([{
name: "合成员一",
account: "member-1",
phone: "19900000001",
email: "[email protected]",
businessEmail: "",
}]));
expect(parsed.sourceSummary.incompleteCount).toBe(1);
expect(parsed.profiles[0].missingRequiredFields).toContain("email");
});
it("blocks a supplied invalid phone instead of treating it as an absent phone", async () => {
const parsed = await parseDirectoryExport(await buildDirectoryWorkbook([{
name: "合成员一",
account: "member-1",
phone: "not-a-phone",
businessEmail: "[email protected]",
}]));
expect(parsed.sourceSummary.phoneMissingCount).toBe(1);
expect(parsed.sourceSummary.incompleteCount).toBe(1);
expect(parsed.profiles[0].missingRequiredFields).toContain("phone_invalid");
});
it("uses a distinct success status for an apply report", async () => {
const parsed = await parseDirectoryExport(await buildDirectoryWorkbook([{
name: "合成员一",
account: "member-1",
phone: "",
businessEmail: "[email protected]",
}]));
const report = buildSafeReport(
{ mode: "apply" },
parsed,
{ summary: { created: 1, success: 1 } },
);
expect(report.status).toBe("PASS_DIRECTORY_EXPORT_APPLY");
});
});