127 lines
4.3 KiB
TypeScript
127 lines
4.3 KiB
TypeScript
import { ForbiddenException } from '@nestjs/common';
|
|
import { SchedulesController } from './schedules.controller';
|
|
import { SchedulesService } from './schedules.service';
|
|
import { OperationLogsService } from '../operation-logs/operation-logs.service';
|
|
import { NotificationsService } from '../notifications/notifications.service';
|
|
|
|
const teacherRequest = {
|
|
user: { id: 21, username: 'teacher', permissions: [], isSuperAdmin: false },
|
|
headers: {},
|
|
};
|
|
|
|
const scheduleDto = {
|
|
classId: 8,
|
|
classroomId: 3,
|
|
weekDay: 1,
|
|
startTime: '09:00',
|
|
endTime: '10:00',
|
|
startDate: '2026-07-01',
|
|
endDate: '2026-07-31',
|
|
subject: '数学',
|
|
};
|
|
|
|
describe('SchedulesController — class data scope', () => {
|
|
const service = {
|
|
getAccessibleClassIds: jest.fn(),
|
|
assertClassAccess: jest.fn(),
|
|
findOne: jest.fn(),
|
|
create: jest.fn(),
|
|
update: jest.fn(),
|
|
remove: jest.fn(),
|
|
getClassroomOccupancy: jest.fn(),
|
|
maskScheduleOccupancy: jest.fn((schedule) => ({
|
|
...schedule,
|
|
id: null,
|
|
classId: null,
|
|
subject: '已占用',
|
|
teacherId: null,
|
|
notes: null,
|
|
canViewDetails: false,
|
|
})),
|
|
checkConflict: jest.fn(),
|
|
};
|
|
const logService = { log: jest.fn().mockResolvedValue(undefined) };
|
|
const notificationsService = { create: jest.fn() };
|
|
const ability = { can: jest.fn().mockReturnValue(false) };
|
|
const authzService = { abilityForRequest: jest.fn().mockReturnValue(ability) };
|
|
let controller: SchedulesController;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
ability.can.mockReturnValue(false);
|
|
service.getAccessibleClassIds.mockResolvedValue([8]);
|
|
controller = new SchedulesController(
|
|
service as unknown as SchedulesService,
|
|
logService as unknown as OperationLogsService,
|
|
notificationsService as unknown as NotificationsService,
|
|
authzService as never,
|
|
);
|
|
});
|
|
|
|
it('checks the requested class before creating a schedule', async () => {
|
|
service.create.mockResolvedValue({ id: 1, ...scheduleDto });
|
|
|
|
await controller.create(scheduleDto, teacherRequest);
|
|
|
|
expect(service.assertClassAccess).toHaveBeenCalledWith(21, 8, false);
|
|
expect(service.create).toHaveBeenCalledWith(scheduleDto);
|
|
});
|
|
|
|
it('checks both the current and destination class before moving a schedule', async () => {
|
|
service.findOne.mockResolvedValue({ id: 4, ...scheduleDto });
|
|
service.update.mockResolvedValue({ id: 4, ...scheduleDto, classId: 9 });
|
|
|
|
await controller.update('4', { classId: 9 }, teacherRequest);
|
|
|
|
expect(service.assertClassAccess).toHaveBeenNthCalledWith(1, 21, 8, false);
|
|
expect(service.assertClassAccess).toHaveBeenNthCalledWith(2, 21, 9, false);
|
|
});
|
|
|
|
it('checks the owning class before returning full schedule details', async () => {
|
|
service.findOne.mockResolvedValue({ id: 4, ...scheduleDto });
|
|
|
|
await controller.findOne('4', teacherRequest);
|
|
|
|
expect(service.assertClassAccess).toHaveBeenCalledWith(21, 8, false);
|
|
});
|
|
|
|
it('checks the owning class before deleting a schedule', async () => {
|
|
service.findOne.mockResolvedValue({ id: 4, ...scheduleDto });
|
|
service.remove.mockResolvedValue({ success: true });
|
|
|
|
await controller.remove('4', teacherRequest);
|
|
|
|
expect(service.assertClassAccess).toHaveBeenCalledWith(21, 8, false);
|
|
expect(service.remove).toHaveBeenCalledWith(4);
|
|
});
|
|
|
|
it('returns only masked occupancy blocks from the classroom occupancy endpoint', async () => {
|
|
service.getClassroomOccupancy.mockResolvedValue([
|
|
{ id: 2, classId: 99, subject: '英语', teacherId: 7, notes: '隐私', classroomId: 3 },
|
|
]);
|
|
|
|
const result = await controller.getClassroomOccupancy('3', undefined, teacherRequest);
|
|
|
|
expect(result).toEqual([
|
|
expect.objectContaining({
|
|
id: null,
|
|
classId: null,
|
|
subject: '已占用',
|
|
teacherId: null,
|
|
notes: null,
|
|
canViewDetails: false,
|
|
}),
|
|
]);
|
|
expect(JSON.stringify(result)).not.toContain('英语');
|
|
expect(JSON.stringify(result)).not.toContain('隐私');
|
|
});
|
|
|
|
it('rejects records without a class instead of exposing full details to a scoped teacher', async () => {
|
|
service.findOne.mockResolvedValue({ id: 4, ...scheduleDto, classId: null });
|
|
|
|
await expect(controller.findOne('4', teacherRequest)).rejects.toBeInstanceOf(
|
|
ForbiddenException,
|
|
);
|
|
});
|
|
});
|