test: harden business boundary conditions
This commit is contained in:
@@ -593,3 +593,38 @@ describe('AttendanceService \u2014 DingTalk course attendance', () => {
|
||||
expect(result.source).toBe('manual');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AttendanceService — attendance window boundaries', () => {
|
||||
it('crosses calendar boundaries only when the window requires it', () => {
|
||||
const { service } = createService();
|
||||
expect(service.getLessonAttendanceImportDateRange(
|
||||
{ startTime: '00:30', endTime: '01:30', attendanceAdvanceMinutes: 30 }, '2026-07-13',
|
||||
)).toEqual({ startDate: '2026-07-13', endDate: '2026-07-13' });
|
||||
expect(service.getLessonAttendanceImportDateRange(
|
||||
{ startTime: '00:30', endTime: '01:30', attendanceAdvanceMinutes: 31 }, '2026-07-13',
|
||||
)).toEqual({ startDate: '2026-07-12', endDate: '2026-07-13' });
|
||||
expect(service.getLessonAttendanceImportDateRange(
|
||||
{ startTime: '22:00', endTime: '01:00', attendanceAdvanceMinutes: 30 }, '2026-07-13',
|
||||
)).toEqual({ startDate: '2026-07-13', endDate: '2026-07-14' });
|
||||
});
|
||||
|
||||
it('uses Asia/Shanghai time when deciding whether todays lesson has started', async () => {
|
||||
const originalTz = process.env.TZ;
|
||||
process.env.TZ = 'UTC';
|
||||
jest.useFakeTimers().setSystemTime(new Date('2026-07-13T01:00:00.000Z'));
|
||||
try {
|
||||
const { service, scheduleRepo, sessionRepo, attendanceRepo } = createService();
|
||||
scheduleRepo.findOne.mockResolvedValue({
|
||||
...endedSchedule, weekDay: 1, startTime: '08:30', endTime: '10:00',
|
||||
startDate: '2026-07-13', endDate: '2026-07-13',
|
||||
});
|
||||
sessionRepo.findOne.mockResolvedValue({ id: 90, status: 'completed' });
|
||||
attendanceRepo.find.mockResolvedValue([]);
|
||||
await expect(service.createLessonAttendanceFromDingTalk(4, '2026-07-13', 21))
|
||||
.resolves.toMatchObject({ records: [] });
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
process.env.TZ = originalTz;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -276,16 +276,13 @@ export class AttendanceService {
|
||||
) {
|
||||
const schedule = await this.getScheduleOccurrence(scheduleId, lessonDate);
|
||||
const now = new Date();
|
||||
const today = [
|
||||
now.getFullYear(),
|
||||
String(now.getMonth() + 1).padStart(2, '0'),
|
||||
String(now.getDate()).padStart(2, '0'),
|
||||
].join('-');
|
||||
const courseClock = this.getCourseClock(now);
|
||||
const today = courseClock.date;
|
||||
if (lessonDate > today) throw new BadRequestException('课程尚未开始,不能拉取考勤');
|
||||
if (lessonDate === today) {
|
||||
const [hour, minute] = schedule.startTime.split(':').map(Number);
|
||||
const startMinute = hour * 60 + minute;
|
||||
const currentMinute = now.getHours() * 60 + now.getMinutes();
|
||||
const currentMinute = courseClock.minutes;
|
||||
if (currentMinute < startMinute) {
|
||||
throw new BadRequestException('课程尚未开始,不能拉取考勤');
|
||||
}
|
||||
@@ -676,6 +673,20 @@ export class AttendanceService {
|
||||
return hour * 60 + minute;
|
||||
}
|
||||
|
||||
private getCourseClock(date: Date): { date: string; minutes: number } {
|
||||
const parts = Object.fromEntries(
|
||||
new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
|
||||
}).formatToParts(date).filter((part) => part.type !== 'literal').map((part) => [part.type, part.value]),
|
||||
);
|
||||
return {
|
||||
date: `${parts.year}-${parts.month}-${parts.day}`,
|
||||
minutes: Number(parts.hour) * 60 + Number(parts.minute),
|
||||
};
|
||||
}
|
||||
|
||||
private shiftDate(date: string, days: number): string {
|
||||
const shifted = new Date(`${date}T00:00:00.000Z`);
|
||||
shifted.setUTCDate(shifted.getUTCDate() + days);
|
||||
|
||||
Reference in New Issue
Block a user