Files
gongxue-base/apps/server/src/attendance/lesson-attendance-sync.service.spec.ts
wangziqi 90cc321221 fix: UI improvements and DingTalk attendance refresh for completed sessions
- DatePicker editors open immediately on double-click
- Student archive drawer hides empty studentNo parentheses
- Student add/edit form uses 2-column grid layout
- Table horizontal scrollbars show only on hover/focus
- Remove fake 近7日趋势 stats from attendance student detail
- Allow DingTalk refresh to update completed attendance sessions
  (late-arriving punches can now change absent → present)
- Switch dev command from concurrently to turbo run dev
- Remove concurrently/wait-on dependencies
2026-07-22 17:50:59 +08:00

124 lines
3.2 KiB
TypeScript

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();
});
});