forked from wangziqi/gongxue-base
test: harden business boundary conditions
This commit is contained in:
@@ -60,3 +60,30 @@ it('removes the retired departmentId field from create requests', async () => {
|
||||
|
||||
expect(dto).not.toHaveProperty('departmentId');
|
||||
});
|
||||
|
||||
describe('schedule boundary validation', () => {
|
||||
it.each(['24:00', '09:60', '99:99', '9:00'])('rejects invalid time %s', async (time) => {
|
||||
const dto = createSchedule('');
|
||||
dto.startTime = time;
|
||||
expect((await validate(dto)).some((error) => error.property === 'startTime')).toBe(true);
|
||||
});
|
||||
|
||||
it.each(['2026-02-31', '2026-07-13T00:00:00Z', '2026-7-13'])(
|
||||
'rejects invalid or non-date-only value %s',
|
||||
async (date) => {
|
||||
const dto = createSchedule('');
|
||||
dto.startDate = date;
|
||||
expect((await validate(dto)).some((error) => error.property === 'startDate')).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it('accepts inclusive attendance-window limits and a leap-day date', async () => {
|
||||
const zero = createSchedule('');
|
||||
zero.attendanceAdvanceMinutes = 0;
|
||||
zero.startDate = '2028-02-29';
|
||||
const fullDay = createSchedule('');
|
||||
fullDay.attendanceAdvanceMinutes = 1440;
|
||||
expect(await validate(zero)).toEqual([]);
|
||||
expect(await validate(fullDay)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,8 @@ import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsInt,
|
||||
IsDateString,
|
||||
IsISO8601,
|
||||
IsMilitaryTime,
|
||||
Matches,
|
||||
Min,
|
||||
Max,
|
||||
@@ -26,11 +27,11 @@ export class CreateScheduleDto {
|
||||
@IsNotEmpty()
|
||||
weekDay: number;
|
||||
|
||||
@Matches(/^\d{2}:\d{2}$/)
|
||||
@IsMilitaryTime()
|
||||
@IsNotEmpty()
|
||||
startTime: string;
|
||||
|
||||
@Matches(/^\d{2}:\d{2}$/)
|
||||
@IsMilitaryTime()
|
||||
@IsNotEmpty()
|
||||
endTime: string;
|
||||
|
||||
@@ -40,11 +41,13 @@ export class CreateScheduleDto {
|
||||
@Max(1440)
|
||||
attendanceAdvanceMinutes?: number;
|
||||
|
||||
@IsDateString()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
@IsISO8601({ strict: true })
|
||||
@IsNotEmpty()
|
||||
startDate: string;
|
||||
|
||||
@IsDateString()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
@IsISO8601({ strict: true })
|
||||
@IsNotEmpty()
|
||||
endDate: string;
|
||||
|
||||
@@ -87,11 +90,11 @@ export class UpdateScheduleDto {
|
||||
weekDay?: number;
|
||||
|
||||
@IsOptional()
|
||||
@Matches(/^\d{2}:\d{2}$/)
|
||||
@IsMilitaryTime()
|
||||
startTime?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Matches(/^\d{2}:\d{2}$/)
|
||||
@IsMilitaryTime()
|
||||
endTime?: string;
|
||||
|
||||
@IsOptional()
|
||||
@@ -101,11 +104,13 @@ export class UpdateScheduleDto {
|
||||
attendanceAdvanceMinutes?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
@IsISO8601({ strict: true })
|
||||
startDate?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
@IsISO8601({ strict: true })
|
||||
endDate?: string;
|
||||
|
||||
@IsOptional()
|
||||
@@ -149,21 +154,25 @@ export class QueryScheduleDto {
|
||||
weekDay?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
@IsISO8601({ strict: true })
|
||||
startDate?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
@IsISO8601({ strict: true })
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
export class WeeklyViewQueryDto {
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
@IsISO8601({ strict: true })
|
||||
startDate?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
@IsISO8601({ strict: true })
|
||||
endDate?: string;
|
||||
|
||||
@IsOptional()
|
||||
|
||||
@@ -339,3 +339,41 @@ describe('SchedulesService — remove', () => {
|
||||
expect(scheduleRepo.remove).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SchedulesService — range boundaries', () => {
|
||||
const makeService = () => {
|
||||
const scheduleRepo = { create: jest.fn() };
|
||||
return {
|
||||
service: new SchedulesService(
|
||||
scheduleRepo as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
),
|
||||
scheduleRepo,
|
||||
};
|
||||
};
|
||||
const valid = {
|
||||
classId: 1, classroomId: 2, weekDay: 1,
|
||||
startTime: '09:00', endTime: '10:00',
|
||||
startDate: '2026-07-01', endDate: '2026-07-31', subject: '数学',
|
||||
};
|
||||
|
||||
it('rejects zero-duration schedules before repository access', async () => {
|
||||
const { service, scheduleRepo } = makeService();
|
||||
await expect(service.create({ ...valid, endTime: '09:00' })).rejects.toThrow(
|
||||
'上课时间和下课时间不能相同',
|
||||
);
|
||||
expect(scheduleRepo.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects reversed date ranges before repository access', async () => {
|
||||
const { service, scheduleRepo } = makeService();
|
||||
await expect(
|
||||
service.create({ ...valid, startDate: '2026-08-01', endDate: '2026-07-31' }),
|
||||
).rejects.toThrow('排课结束日期不能早于开始日期');
|
||||
expect(scheduleRepo.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -179,7 +179,22 @@ export class SchedulesService {
|
||||
}
|
||||
}
|
||||
|
||||
private assertValidScheduleRange(
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
) {
|
||||
if (startTime === endTime) {
|
||||
throw new BadRequestException('上课时间和下课时间不能相同');
|
||||
}
|
||||
if (startDate > endDate) {
|
||||
throw new BadRequestException('排课结束日期不能早于开始日期');
|
||||
}
|
||||
}
|
||||
|
||||
async create(dto: CreateScheduleDto) {
|
||||
this.assertValidScheduleRange(dto.startTime, dto.endTime, dto.startDate, dto.endDate);
|
||||
await this.assertClassroomAvailable(dto.classroomId);
|
||||
await this.normalizeTeacherForSchedule(dto);
|
||||
await this.assertTeacherAssignedToClass(dto.classId, dto.teacherId);
|
||||
@@ -211,6 +226,7 @@ export class SchedulesService {
|
||||
const endTime = dto.endTime ?? existing.endTime;
|
||||
const startDate = dto.startDate ?? existing.startDate;
|
||||
const endDate = dto.endDate ?? existing.endDate;
|
||||
this.assertValidScheduleRange(startTime, endTime, startDate, endDate);
|
||||
|
||||
const normalized = await this.normalizeTeacherForSchedule({
|
||||
...dto,
|
||||
|
||||
Reference in New Issue
Block a user