This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { BadRequestException, ForbiddenException, ValidationPipe } from '@nestjs/common';
|
||||
import { BadRequestException, ForbiddenException, NotFoundException, ValidationPipe } from '@nestjs/common';
|
||||
import { ExamScore } from '../entities';
|
||||
import { QueryExamDto } from './dto/exam.dto';
|
||||
import { ExamsService } from './exams.service';
|
||||
@@ -23,6 +23,19 @@ function createService(
|
||||
);
|
||||
}
|
||||
|
||||
function updateQb(affected = 1) {
|
||||
const qb = {
|
||||
update: jest.fn(),
|
||||
set: jest.fn(),
|
||||
where: jest.fn(),
|
||||
execute: jest.fn().mockResolvedValue({ affected }),
|
||||
};
|
||||
qb.update.mockReturnValue(qb);
|
||||
qb.set.mockReturnValue(qb);
|
||||
qb.where.mockReturnValue(qb);
|
||||
return qb;
|
||||
}
|
||||
|
||||
describe('ExamsService', () => {
|
||||
it('creates score rows from the active class roster snapshot', async () => {
|
||||
const members = [
|
||||
@@ -224,6 +237,102 @@ describe('ExamsService', () => {
|
||||
expect(examRepo.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects empty and invalid ids for batch archive and restore', async () => {
|
||||
const service = createService(async () => undefined, { examRepo: { find: jest.fn() } });
|
||||
|
||||
for (const call of [
|
||||
(ids: number[]) => service.batchArchive(ids, 1, true),
|
||||
(ids: number[]) => service.batchRestore(ids, 1, true),
|
||||
]) {
|
||||
await expect(call([])).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(call([0])).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(call([1.5])).rejects.toBeInstanceOf(BadRequestException);
|
||||
}
|
||||
});
|
||||
|
||||
it('deduplicates ids, archives active exams, skips archived exams, and leaves scores unchanged', async () => {
|
||||
const qb = updateQb();
|
||||
const examRepo = {
|
||||
find: jest.fn().mockResolvedValue([
|
||||
{ id: 8, classId: 3, status: 'active' },
|
||||
{ id: 9, classId: 4, status: 'archived' },
|
||||
]),
|
||||
createQueryBuilder: jest.fn(() => qb),
|
||||
};
|
||||
const scoreRepo = { update: jest.fn(), save: jest.fn() };
|
||||
const service = createService(async () => undefined, { examRepo, scoreRepo });
|
||||
|
||||
await expect(service.batchArchive([8, 8, 9], 1, true)).resolves.toEqual({
|
||||
message: '已批量归档 1 场考试',
|
||||
archived: 1,
|
||||
skipped: 1,
|
||||
});
|
||||
expect(examRepo.find).toHaveBeenCalledWith({ where: { id: expect.anything() } });
|
||||
expect(qb.set).toHaveBeenCalledWith({ status: 'archived' });
|
||||
expect(qb.where).toHaveBeenCalledWith('id IN (:...ids)', { ids: [8] });
|
||||
expect(scoreRepo.update).not.toHaveBeenCalled();
|
||||
expect(scoreRepo.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('restores archived exams and skips active exams', async () => {
|
||||
const qb = updateQb();
|
||||
const examRepo = {
|
||||
find: jest.fn().mockResolvedValue([
|
||||
{ id: 8, classId: 3, status: 'archived' },
|
||||
{ id: 9, classId: 4, status: 'active' },
|
||||
]),
|
||||
createQueryBuilder: jest.fn(() => qb),
|
||||
};
|
||||
const service = createService(async () => undefined, { examRepo });
|
||||
|
||||
await expect(service.batchRestore([8, 9], 1, true)).resolves.toEqual({
|
||||
message: '已批量恢复 1 场考试',
|
||||
restored: 1,
|
||||
skipped: 1,
|
||||
});
|
||||
expect(qb.set).toHaveBeenCalledWith({ status: 'active' });
|
||||
expect(qb.where).toHaveBeenCalledWith('id IN (:...ids)', { ids: [8] });
|
||||
});
|
||||
|
||||
it('rejects a batch when any exam is missing before updating', async () => {
|
||||
const examRepo = {
|
||||
find: jest.fn().mockResolvedValue([{ id: 8, classId: 3, status: 'active' }]),
|
||||
createQueryBuilder: jest.fn(),
|
||||
};
|
||||
const service = createService(async () => undefined, { examRepo });
|
||||
|
||||
await expect(service.batchArchive([8, 9], 1, true)).rejects.toBeInstanceOf(NotFoundException);
|
||||
expect(examRepo.createQueryBuilder).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('checks access for every selected exam before a batch update', async () => {
|
||||
const examRepo = {
|
||||
find: jest.fn().mockResolvedValue([
|
||||
{ id: 8, classId: 3, status: 'active' },
|
||||
{ id: 9, classId: 4, status: 'active' },
|
||||
]),
|
||||
createQueryBuilder: jest.fn(),
|
||||
};
|
||||
const classTeacherRepo = {
|
||||
findOne: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ id: 1 })
|
||||
.mockResolvedValueOnce(null),
|
||||
};
|
||||
const service = createService(async () => undefined, { examRepo, classTeacherRepo });
|
||||
|
||||
await expect(service.batchArchive([8, 9], 21, false)).rejects.toBeInstanceOf(
|
||||
ForbiddenException,
|
||||
);
|
||||
expect(classTeacherRepo.findOne).toHaveBeenNthCalledWith(1, {
|
||||
where: { userId: 21, classId: 3 },
|
||||
});
|
||||
expect(classTeacherRepo.findOne).toHaveBeenNthCalledWith(2, {
|
||||
where: { userId: 21, classId: 4 },
|
||||
});
|
||||
expect(examRepo.createQueryBuilder).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects score updates for archived exams', async () => {
|
||||
const manager = {
|
||||
findOne: jest.fn().mockResolvedValue({ id: 8, classId: 3, status: 'archived' }),
|
||||
|
||||
Reference in New Issue
Block a user