import { BadRequestException } from '@nestjs/common'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { ArchiveService } from './archive.service'; describe('ArchiveService purge sub-records', () => { const createService = (overrides?: { enrollment?: Record; examScore?: Record; learningRecord?: Record; attachment?: Record; scoreCount?: number; }) => { const enrollmentRepo = { findOne: jest.fn().mockResolvedValue({ id: 1, status: 'archived', ...overrides?.enrollment, }), delete: jest.fn().mockResolvedValue({ affected: 1 }), }; const examScoreRepo = { findOne: jest.fn().mockResolvedValue({ id: 2, status: 'archived', ...overrides?.examScore, }), count: jest.fn().mockResolvedValue(overrides?.scoreCount ?? 0), delete: jest.fn().mockResolvedValue({ affected: 1 }), }; const learningRecordRepo = { findOne: jest.fn().mockResolvedValue({ id: 3, status: 'archived', ...overrides?.learningRecord, }), delete: jest.fn().mockResolvedValue({ affected: 1 }), }; const attachmentRepo = { findOne: jest.fn().mockResolvedValue({ id: 4, status: 'archived', filePath: 'x.pdf', ...overrides?.attachment, }), delete: jest.fn().mockResolvedValue({ affected: 1 }), }; const service = new ArchiveService( {} as never, {} as never, enrollmentRepo as never, examScoreRepo as never, learningRecordRepo as never, {} as never, attachmentRepo as never, {} as never, {} as never, ); return { service, enrollmentRepo, examScoreRepo, learningRecordRepo, attachmentRepo }; }; it('rejects non-archived sub-records', async () => { const { service, enrollmentRepo } = createService({ enrollment: { status: 'active' } }); await expect(service.purgeEnrollment(1)).rejects.toThrow( new BadRequestException('仅已归档报名记录可以永久删除,请先归档'), ); expect(enrollmentRepo.delete).not.toHaveBeenCalled(); }); it('rejects enrollments referenced by exam scores', async () => { const { service, enrollmentRepo } = createService({ scoreCount: 1 }); await expect(service.purgeEnrollment(1)).rejects.toThrow( new BadRequestException('该报名记录已被考试成绩引用,无法永久删除'), ); expect(enrollmentRepo.delete).not.toHaveBeenCalled(); }); it('deletes archived enrollment, exam score, and learning record', async () => { const { service, enrollmentRepo, examScoreRepo, learningRecordRepo } = createService(); await expect(service.purgeEnrollment(1)).resolves.toEqual({ message: '已永久删除报名记录(不可恢复)', }); await expect(service.purgeExamScore(2)).resolves.toEqual({ message: '已永久删除考试成绩(不可恢复)', }); await expect(service.purgeLearningRecord(3)).resolves.toEqual({ message: '已永久删除学习记录(不可恢复)', }); expect(enrollmentRepo.delete).toHaveBeenCalledWith(1); expect(examScoreRepo.delete).toHaveBeenCalledWith(2); expect(learningRecordRepo.delete).toHaveBeenCalledWith(3); }); it('deletes the attachment row and removes the disk file', async () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'archive-purge-')); process.env.UPLOAD_DIR = tmpDir; const filePath = 'x.pdf'; const fullPath = path.join(tmpDir, 'archive', filePath); fs.mkdirSync(path.dirname(fullPath), { recursive: true }); fs.writeFileSync(fullPath, 'data'); try { const { service, attachmentRepo } = createService({ attachment: { filePath } }); await expect(service.purgeAttachment(4)).resolves.toEqual({ message: '已永久删除附件(不可恢复)', }); expect(fs.existsSync(fullPath)).toBe(false); expect(attachmentRepo.delete).toHaveBeenCalledWith(4); } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); delete process.env.UPLOAD_DIR; } }); });