文件
hyb-oa/server/tests/unit/wecomHrMaster.calendar.test.js
T
wangsli 1f8d5b658a 完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)
汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。
统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。
纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。

关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。
预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。
完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
2026-09-19 13:11:04 +08:00

130 行
6.9 KiB
JavaScript

const { normalizeWecomHrProfile, runWecomHrMasterSync } = require("../../src/services/integrations/wecomHrMaster");
function profileFor(value, userid = "synthetic-calendar") {
return normalizeWecomHrProfile({
contact: { userid, name: "合成员工", email: `${userid}@example.invalid`, department: [1] },
departmentMap: new Map([["1", { name: "合成部门" }]]),
staff: { staff_info: value === undefined ? [] : [{ fieldid: "入职日期", value }] },
});
}
function fixture(existing = null) {
const items = [];
let run;
const prisma = {
integrationBinding: { findFirst: vi.fn().mockResolvedValue(null), create: vi.fn().mockResolvedValue({}) },
department: {
findUnique: vi.fn().mockResolvedValue(null), findMany: vi.fn().mockResolvedValue([]),
create: vi.fn(async ({ data }) => ({ ...data, id: "synthetic-department" })), update: vi.fn(),
},
wecomIdentityBinding: { findUnique: vi.fn().mockResolvedValue(existing ? { employee: existing } : null), upsert: vi.fn().mockResolvedValue({}) },
employee: {
findUnique: vi.fn().mockResolvedValue(null), findMany: vi.fn().mockResolvedValue([]),
create: vi.fn(async ({ data }) => ({ ...data, id: "synthetic-created" })),
update: vi.fn(async ({ data }) => ({ ...data, id: existing?.id })),
},
employeeRole: { createMany: vi.fn().mockResolvedValue({ count: 1 }) },
wecomHrMasterSyncRun: {
create: vi.fn(async ({ data }) => (run = { ...data, id: "synthetic-run" })),
update: vi.fn(async ({ data }) => (run = { ...run, ...data })),
},
wecomHrMasterSyncItem: {
create: vi.fn(async ({ data }) => { const item = { ...data, id: `synthetic-item-${items.length}` }; items.push(item); return item; }),
findMany: vi.fn(async () => items), update: vi.fn(),
},
};
prisma.$transaction = vi.fn(async (callback) => callback(prisma));
const fetchImpl = vi.fn(() => { throw new Error("offline test must not fetch"); });
return {
prisma, fetchImpl,
run: (profiles, mode = "apply") => runWecomHrMasterSync({ prisma, config: {}, actorId: "synthetic-actor", mode, sourceProfiles: profiles, fetchImpl }),
};
}
const invalidInputs = ["2026-02-29", "2024/2/30", "2026.4.31", "20260230", "2026-13-01", "2026-01-00", "not-a-date", "0", "-1", "253402300800000", "999999999999999999999"];
const validInputs = [
["2024-02-29", "2024-02-29"], ["2024/2/29", "2024-02-29"], ["2024.2.29", "2024-02-29"],
["20240229", "2024-02-29"], ["2026-4-30", "2026-04-30"],
["1709164800", "2024-02-29"], [1709164800000, "2024-02-29"],
];
function expectNoBusinessCalls(prisma) {
for (const name of ["integrationBinding", "department", "employee", "employeeRole", "wecomIdentityBinding"]) {
for (const fn of Object.values(prisma[name])) expect(fn).not.toHaveBeenCalled();
}
expect(prisma.wecomHrMasterSyncItem.update).not.toHaveBeenCalled();
}
describe("offline HR hire-date calendar contract", () => {
afterEach(() => vi.useRealTimers());
it.each(invalidInputs.flatMap((value) => ["apply", "dry_run"].map((mode) => [value, mode])))("skips invalid %s in %s before business access", async (value, mode) => {
const f = fixture();
const profile = { ...profileFor(value), managerExternalUserId: "synthetic-manager" };
expect(profile.hireDate).toBeNull();
expect(profile.hireDateInvalid).toBe(true);
const result = await f.run([profile], mode);
expect(result.status).toBe("partial");
expect(result.items[0]).toMatchObject({ action: "skip", status: "skipped", errorCode: "invalid_hire_date", employeeId: null,
errorMessage: "入职日期无效,本条未同步;请修正来源日期后重试。" });
expect(result.items[0].fieldChanges).toBeNull();
expect(result.items[0].sourceSummary).not.toHaveProperty("hireDate");
expectNoBusinessCalls(f.prisma);
expect(f.prisma.wecomHrMasterSyncRun.create).toHaveBeenCalledOnce();
expect(f.prisma.wecomHrMasterSyncItem.create).toHaveBeenCalledOnce();
expect(f.fetchImpl).not.toHaveBeenCalled();
});
it.each(validInputs)("creates exact UTC date for %s", async (input, canonical) => {
const f = fixture();
const profile = profileFor(input);
expect(profile.hireDate).toBe(canonical);
expect(profile.hireDateInvalid).toBe(false);
const result = await f.run([profile]);
expect(result.status).toBe("success");
expect(f.prisma.employee.create.mock.calls[0][0].data.hire_date).toEqual(new Date(`${canonical}T00:00:00.000Z`));
expect(f.prisma.employeeRole.createMany).toHaveBeenCalledOnce();
expect(f.prisma.wecomIdentityBinding.upsert).toHaveBeenCalledOnce();
expect(f.fetchImpl).not.toHaveBeenCalled();
});
it.each([undefined, null, "", " "])("keeps missing %s defaulted to the run time", async (value) => {
vi.useFakeTimers({ toFake: ["Date"] });
vi.setSystemTime(new Date("2026-09-17T01:02:03.000Z"));
const f = fixture();
const result = await f.run([profileFor(value)]);
expect(result.status).toBe("success");
expect(f.prisma.employee.create.mock.calls[0][0].data.hire_date).toEqual(new Date("2026-09-17T01:02:03.000Z"));
expect(f.fetchImpl).not.toHaveBeenCalled();
});
it.each(["apply", "dry_run"])("rejects invalid directly supplied normalized profiles in %s", async (mode) => {
const f = fixture({ id: "synthetic-existing" });
const result = await f.run([{ ...profileFor(undefined), hireDate: "2026-02-30", managerExternalUserId: "synthetic-manager" }], mode);
expect(result.items[0].errorCode).toBe("invalid_hire_date");
expectNoBusinessCalls(f.prisma);
expect(f.fetchImpl).not.toHaveBeenCalled();
});
it("never adds hire_date to existing employee updates", async () => {
const f = fixture({ id: "synthetic-existing", name: "合成员工", email: "[email protected]", hire_date: new Date("2020-01-01T00:00:00Z") });
const result = await f.run([profileFor("2024-02-29")]);
expect(result.status).toBe("success");
expect(f.prisma.employee.create).not.toHaveBeenCalled();
expect(f.prisma.employee.update).toHaveBeenCalledOnce();
expect(f.prisma.employee.update.mock.calls[0][0].data).not.toHaveProperty("hire_date");
});
it.each(["apply", "dry_run"])("continues valid profiles after an invalid one in %s", async (mode) => {
const f = fixture();
const result = await f.run([profileFor("2026-02-30", "synthetic-invalid"), profileFor("2024-02-29", "synthetic-valid")], mode);
expect(result.status).toBe("partial");
expect(result.summary).toMatchObject({ total: 2, skipped: 1, failed: 0, created: 1 });
expect(result.items.map((item) => item.status)).toEqual(["skipped", mode === "apply" ? "success" : "planned"]);
for (const fn of [f.prisma.department.create, f.prisma.integrationBinding.create, f.prisma.employee.create, f.prisma.employeeRole.createMany, f.prisma.wecomIdentityBinding.upsert]) {
expect(fn).toHaveBeenCalledTimes(mode === "apply" ? 1 : 0);
}
expect(f.fetchImpl).not.toHaveBeenCalled();
});
});