import { AttendanceLeaveSyncService } from './attendance-leave-sync.service'; import { DingTalkService } from '../integration/dingtalk.service'; describe('AttendanceLeaveSyncService', () => { const dingLeaveRawRepo = { find: jest.fn(), findOne: jest.fn(), create: jest.fn((value: Record) => value), save: jest.fn(), }; const studentRepo = { findOne: jest.fn() }; const studentDingMappingRepo = { findOne: jest.fn(), find: jest.fn() }; const dingTalkService = { fetchDailyLeaveStatus: jest.fn(), }; let service: AttendanceLeaveSyncService; beforeEach(() => { jest.clearAllMocks(); service = new AttendanceLeaveSyncService( dingLeaveRawRepo as never, studentRepo as never, studentDingMappingRepo as never, dingTalkService as unknown as DingTalkService, ); }); it('syncs approved DingTalk leaves per user per day and auto-matches them', async () => { dingTalkService.fetchDailyLeaveStatus.mockImplementation( async (userId: string, workDate: string) => [ { userId, workDate, procInstId: `leave-${userId}-${workDate}`, tagName: '请假', leaveType: '事假', beginTime: new Date(`${workDate}T08:00:00+08:00`), endTime: new Date(`${workDate}T12:00:00+08:00`), approvedAt: new Date(`${workDate}T09:00:00+08:00`), duration: '0.5', durationUnit: 'day', }, ], ); dingLeaveRawRepo.findOne.mockResolvedValue(null); dingLeaveRawRepo.save.mockImplementation(async (entities) => entities); studentDingMappingRepo.find.mockResolvedValue([{ dingUserId: 'ding-1', studentId: 7 }]); dingLeaveRawRepo.find.mockResolvedValue([ { dingId: 'leave-ding-1-2026-07-01', dingUserId: 'ding-1', matchStatus: 'unmatched' }, ]); const result = await service.syncLeaveStatusForLesson({ startDate: '2026-07-01', endDate: '2026-07-02', userIds: ['ding-1', 'ding-2'], autoMatch: true, }); expect(dingTalkService.fetchDailyLeaveStatus).toHaveBeenCalledTimes(4); expect(dingTalkService.fetchDailyLeaveStatus).toHaveBeenCalledWith( 'ding-1', '2026-07-01', ); expect(dingTalkService.fetchDailyLeaveStatus).toHaveBeenCalledWith( 'ding-2', '2026-07-02', ); expect(dingLeaveRawRepo.save).toHaveBeenCalled(); expect(result.synced).toBe(4); expect(result.matched).toBe(1); }); it('keeps syncing remaining users when one leave fetch fails', async () => { dingTalkService.fetchDailyLeaveStatus .mockRejectedValueOnce(new Error('DingTalk unavailable')) .mockResolvedValue([]); dingLeaveRawRepo.findOne.mockResolvedValue(null); const result = await service.syncLeaveStatusForLesson({ startDate: '2026-07-01', endDate: '2026-07-01', userIds: ['ding-1', 'ding-2'], autoMatch: false, }); expect(dingTalkService.fetchDailyLeaveStatus).toHaveBeenCalledTimes(2); expect(result.errors).toHaveLength(1); expect(result.synced).toBe(0); }); });