183 行
6.3 KiB
JavaScript
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");
|
|
});
|
|
});
|