import { BadRequestException } from '@nestjs/common'; import { ExamScore } from '../entities'; import { ExamsService } from './exams.service'; function createService(transaction: (run: (manager: any) => Promise) => Promise) { return new ExamsService( {} as never, {} as never, {} as never, {} as never, { findOne: jest.fn().mockResolvedValue({ id: 1 }) } as never, { transaction } as never, ); } describe('ExamsService', () => { it('creates score rows from the active class roster snapshot', async () => { const members = [ { studentId: 11, status: 'active' }, { studentId: 12, status: 'active' }, ]; const manager = { findOne: jest.fn().mockResolvedValue({ id: 3, isArchived: false }), find: jest.fn().mockResolvedValue(members), create: jest.fn((_entity, value) => value), save: jest.fn(async (entity, value) => entity.name === 'Exam' ? { ...value, id: 9 } : value, ), }; const service = createService(async (run) => run(manager)); await service.create( { examType: '月考', examName: '七月月考', subject: '数学', examDate: '2026-07-21', classId: 3, }, 1, true, ); expect(manager.find).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ where: { classId: 3, status: 'active' } }), ); expect(manager.save).toHaveBeenCalledWith( ExamScore, expect.arrayContaining([ expect.objectContaining({ examId: 9, studentId: 11, score: null }), expect.objectContaining({ examId: 9, studentId: 12, score: null }), ]), ); }); it('rejects creating an exam for an empty class', async () => { const manager = { findOne: jest.fn().mockResolvedValue({ id: 3, isArchived: false }), find: jest.fn().mockResolvedValue([]), }; const service = createService(async (run) => run(manager)); await expect( service.create( { examType: '月考', examName: '七月月考', subject: '数学', examDate: '2026-07-21', classId: 3, }, 1, true, ), ).rejects.toBeInstanceOf(BadRequestException); }); it('counts zero, ignores empty scores, and uses competition ranking', async () => { const rows = [ { id: 1, score: 90, classAvg: null, rank: null }, { id: 2, score: 90, classAvg: null, rank: null }, { id: 3, score: 60, classAvg: null, rank: null }, { id: 4, score: 0, classAvg: null, rank: null }, { id: 5, score: null, classAvg: null, rank: null }, ]; const manager = { findOne: jest .fn() .mockResolvedValueOnce({ id: 8, classId: 3, status: 'active' }) .mockResolvedValueOnce(rows[0]) .mockResolvedValueOnce(rows[0]), find: jest.fn().mockResolvedValue(rows), save: jest.fn(async (_entity, value) => value), }; const service = createService(async (run) => run(manager)); await service.updateScore(8, 1, 90, 1, true); expect(rows.map((row) => row.rank)).toEqual([1, 1, 3, 4, null]); expect(rows.map((row) => row.classAvg)).toEqual([60, 60, 60, 60, 60]); }); it('clears a score and recalculates the remaining rows', async () => { const rows = [ { id: 1, score: 90, classAvg: 80, rank: 1 }, { id: 2, score: 70, classAvg: 80, rank: 2 }, ]; const manager = { findOne: jest .fn() .mockResolvedValueOnce({ id: 8, classId: 3, status: 'active' }) .mockResolvedValueOnce(rows[0]) .mockResolvedValueOnce(rows[0]), find: jest.fn().mockResolvedValue(rows), save: jest.fn(async (_entity, value) => value), }; const service = createService(async (run) => run(manager)); await service.updateScore(8, 1, null, 1, true); expect(rows).toEqual([ expect.objectContaining({ score: null, classAvg: 70, rank: null }), expect.objectContaining({ score: 70, classAvg: 70, rank: 1 }), ]); }); });