import { BadRequestException } from '@nestjs/common'; import { LessonAttendanceSyncService } from './lesson-attendance-sync.service'; const lesson = { schedule: { id: 4, classId: 8, startTime: '22:00', endTime: '01:00', attendanceAdvanceMinutes: 30, }, session: null, records: [], }; const createService = () => { const attendanceService = { getLessonAttendance: jest.fn().mockResolvedValue(lesson), getTeacherClassDingUserIds: jest.fn().mockResolvedValue(['ding-1', 'ding-2']), getLessonAttendanceImportDateRange: jest.fn().mockReturnValue({ startDate: '2026-07-11', endDate: '2026-07-12', }), createLessonAttendanceFromDingTalk: jest.fn().mockResolvedValue({ ...lesson, session: { id: 90, status: 'in_progress' }, }), }; const importService = { importFromDingTalk: jest.fn().mockResolvedValue({ success: true, imported: 3, skipped: 0, matched: 2, errors: [], duration: 10, }), }; return { service: new LessonAttendanceSyncService(attendanceService as never, importService as never), attendanceService, importService, }; }; describe('LessonAttendanceSyncService', () => { it('owns the complete refresh recipe and returns attendance with import statistics', async () => { const { service, attendanceService, importService } = createService(); const result = await service.syncLesson({ scheduleId: 4, lessonDate: '2026-07-11', actorId: 21, canManageAll: false, mode: 'refresh', }); expect(attendanceService.getTeacherClassDingUserIds).toHaveBeenCalledWith( 21, 8, false, '2026-07-11', ); expect(importService.importFromDingTalk).toHaveBeenCalledWith({ startDate: '2026-07-11', endDate: '2026-07-12', userIds: ['ding-1', 'ding-2'], autoMatch: true, userId: 21, }); expect(attendanceService.createLessonAttendanceFromDingTalk).toHaveBeenCalledWith( 4, '2026-07-11', 21, false, ); expect(result).toMatchObject({ attendance: { session: { id: 90 } }, imported: 3, matched: 2, }); }); it('finalizes only after a complete import', async () => { const { service, attendanceService } = createService(); await service.syncLesson({ scheduleId: 4, lessonDate: '2026-07-11', actorId: 21, canManageAll: true, mode: 'finalize', }); expect(attendanceService.createLessonAttendanceFromDingTalk).toHaveBeenCalledWith( 4, '2026-07-11', 21, true, ); }); it('rejects partial imports before changing lesson attendance', async () => { const { service, attendanceService, importService } = createService(); importService.importFromDingTalk.mockResolvedValue({ success: true, imported: 1, matched: 1, errors: ['one batch failed'], }); await expect( service.syncLesson({ scheduleId: 4, lessonDate: '2026-07-11', actorId: 21, canManageAll: false, mode: 'refresh', }), ).rejects.toBeInstanceOf(BadRequestException); expect(attendanceService.createLessonAttendanceFromDingTalk).not.toHaveBeenCalled(); }); });