Files
gongxue-base/apps/server/src/schedules/schedules.controller.spec.ts

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