forked from wangziqi/gongxue-base
- 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
124 lines
3.2 KiB
TypeScript
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();
|
|
});
|
|
});
|