import { BadRequestException, NotFoundException } from '@nestjs/common'; import { ArchiveService } from './archive.service'; function createService(repos: Partial>> = {}) { return new ArchiveService( (repos.student ?? {}) as never, (repos.profile ?? {}) as never, (repos.enrollment ?? {}) as never, (repos.exam ?? {}) as never, (repos.learning ?? {}) as never, (repos.result ?? {}) as never, (repos.attachment ?? {}) as never, (repos.attendance ?? {}) as never, {} as never, ); } describe('ArchiveService — resource and relationship boundaries', () => { it('rejects adding archive records for a missing student', async () => { const student = { findOne: jest.fn().mockResolvedValue(null) }; const service = createService({ student }); await expect( service.addEnrollment(404, { courseCategory: '文化', classType: '冲刺' }), ).rejects.toBeInstanceOf(NotFoundException); await expect( service.addLearningRecord(404, { recordDate: '2026-07-14', recordType: '沟通', content: '内容', }), ).rejects.toBeInstanceOf(NotFoundException); }); it('rejects linking an exam score to another student enrollment', async () => { const exam = { create: jest.fn(), save: jest.fn() }; const service = createService({ student: { findOne: jest.fn().mockResolvedValue({ id: 7 }) }, enrollment: { findOne: jest.fn().mockResolvedValue(null) }, exam, }); await expect( service.addExamScore(7, { examType: '月考', subject: '语文', score: 90, enrollmentId: 99, }), ).rejects.toBeInstanceOf(BadRequestException); expect(exam.save).not.toHaveBeenCalled(); }); it('rejects moving an existing exam score to another student enrollment', async () => { const exam = { findOne: jest.fn().mockResolvedValue({ id: 3, studentId: 7, enrollmentId: 1 }), save: jest.fn(), }; const service = createService({ enrollment: { findOne: jest.fn().mockResolvedValue(null) }, exam, }); await expect(service.updateExamScore(3, { enrollmentId: 99 })).rejects.toBeInstanceOf( BadRequestException, ); expect(exam.save).not.toHaveBeenCalled(); }); it('keeps exam-management scores read-only in the student archive', async () => { const exam = { findOne: jest.fn().mockResolvedValue({ id: 3, studentId: 7, examId: 8 }), save: jest.fn(), update: jest.fn(), }; const service = createService({ exam }); await expect(service.updateExamScore(3, { score: 95 })).rejects.toBeInstanceOf( BadRequestException, ); await expect(service.deleteExamScore(3)).rejects.toBeInstanceOf(BadRequestException); expect(exam.save).not.toHaveBeenCalled(); expect(exam.update).not.toHaveBeenCalled(); }); it('rejects a missing attachment upload before writing to disk', async () => { const service = createService({ student: { findOne: jest.fn() } }); await expect(service.addAttachment(7, undefined as never, 'other')).rejects.toBeInstanceOf( BadRequestException, ); }); it('rejects attachment path traversal', async () => { const service = createService({ attachment: { findOne: jest.fn().mockResolvedValue({ id: 1, studentId: 7, filePath: '../../etc/passwd', }), }, }); await expect(service.getAttachmentFile(7, 1)).rejects.toBeInstanceOf(BadRequestException); }); it('returns not found for update/delete of absent child records', async () => { const service = createService({ enrollment: { findOne: jest.fn().mockResolvedValue(null) }, exam: { findOne: jest.fn().mockResolvedValue(null) }, learning: { findOne: jest.fn().mockResolvedValue(null) }, attachment: { findOne: jest.fn().mockResolvedValue(null) }, }); await expect(service.updateEnrollment(1, {})).rejects.toBeInstanceOf(NotFoundException); await expect(service.deleteExamScore(1)).rejects.toBeInstanceOf(NotFoundException); await expect(service.deleteLearningRecord(1)).rejects.toBeInstanceOf(NotFoundException); await expect(service.deleteAttachment(1)).rejects.toBeInstanceOf(NotFoundException); }); });