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: "synthetic-calendar@example.invalid", 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(); }); });