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