P0-1/P0-2/P1-1: dimensions form + E2E tests + PDF export

P0-1 Backend: dimensions column on template entity + validation
P0-1 Frontend: dimensions edit UI in TemplateManager
P0-2: routeAfterGrading unit tests (10 cases), service spec fix + certificate tests, jest-e2e.json
P1-1: proper PDF generation with embedded CJK font via pdf-lib low-level API
This commit is contained in:
Developer
2026-05-19 08:42:03 +08:00
parent 0b0a060967
commit 68371922ca
18 changed files with 663 additions and 72 deletions
@@ -1,10 +1,12 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { AssessmentService } from './assessment.service';
import { AssessmentSession } from './entities/assessment-session.entity';
import { AssessmentSession, AssessmentStatus } from './entities/assessment-session.entity';
import { AssessmentQuestion } from './entities/assessment-question.entity';
import { AssessmentAnswer } from './entities/assessment-answer.entity';
import { AssessmentCertificate } from './entities/assessment-certificate.entity';
import { QuestionBank } from './entities/question-bank.entity';
import { QuestionBankItem } from './entities/question-bank-item.entity';
import { KnowledgeBaseService } from '../knowledge-base/knowledge-base.service';
import { KnowledgeGroupService } from '../knowledge-group/knowledge-group.service';
import { ModelConfigService } from '../model-config/model-config.service';
@@ -22,16 +24,21 @@ import { NotFoundException } from '@nestjs/common';
describe('AssessmentService', () => {
let service: AssessmentService;
let sessionRepository: any;
let certificateRepository: any;
const mockRepository = () => ({
delete: jest.fn(),
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
create: jest.fn(),
});
const mockService = () => ({});
const regularUser = { id: 'user-1', role: 'user' };
const adminUser = { id: 'admin-1', role: 'admin' };
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
@@ -40,6 +47,8 @@ describe('AssessmentService', () => {
{ provide: getRepositoryToken(AssessmentQuestion), useFactory: mockRepository },
{ provide: getRepositoryToken(AssessmentAnswer), useFactory: mockRepository },
{ provide: getRepositoryToken(AssessmentCertificate), useFactory: mockRepository },
{ provide: getRepositoryToken(QuestionBank), useFactory: mockRepository },
{ provide: getRepositoryToken(QuestionBankItem), useFactory: mockRepository },
{ provide: KnowledgeBaseService, useFactory: mockService },
{ provide: KnowledgeGroupService, useFactory: mockService },
{ provide: ModelConfigService, useFactory: mockService },
@@ -57,6 +66,7 @@ describe('AssessmentService', () => {
service = module.get<AssessmentService>(AssessmentService);
sessionRepository = module.get(getRepositoryToken(AssessmentSession));
certificateRepository = module.get(getRepositoryToken(AssessmentCertificate));
});
it('should be defined', () => {
@@ -64,15 +74,96 @@ describe('AssessmentService', () => {
});
describe('deleteSession', () => {
it('should delete a session if it exists and belongs to the user', async () => {
it('should delete a session when non-admin user owns it', async () => {
sessionRepository.delete.mockResolvedValue({ affected: 1 });
await expect(service.deleteSession('session-id', 'user-id')).resolves.not.toThrow();
expect(sessionRepository.delete).toHaveBeenCalledWith({ id: 'session-id', userId: 'user-id' });
await expect(service.deleteSession('session-id', regularUser)).resolves.not.toThrow();
expect(sessionRepository.delete).toHaveBeenCalledWith({ id: 'session-id', userId: 'user-1' });
});
it('should delete any session when admin user', async () => {
sessionRepository.delete.mockResolvedValue({ affected: 1 });
await expect(service.deleteSession('other-session', adminUser)).resolves.not.toThrow();
expect(sessionRepository.delete).toHaveBeenCalledWith({ id: 'other-session' });
});
it('should throw NotFoundException if no session was affected', async () => {
sessionRepository.delete.mockResolvedValue({ affected: 0 });
await expect(service.deleteSession('non-existent', 'user-id')).rejects.toThrow(NotFoundException);
await expect(service.deleteSession('non-existent', regularUser)).rejects.toThrow(NotFoundException);
});
});
describe('generateCertificate', () => {
const completedSession = {
id: 'session-1',
userId: 'user-1',
status: AssessmentStatus.COMPLETED,
finalScore: 85,
templateId: 'template-1',
};
it('should throw NotFoundException when session does not exist', async () => {
sessionRepository.findOne.mockResolvedValue(null);
await expect(
service.generateCertificate('no-session', 'user-1', 'tenant-1'),
).rejects.toThrow(NotFoundException);
});
it('should throw Error when session is not completed', async () => {
sessionRepository.findOne.mockResolvedValue({
...completedSession,
status: AssessmentStatus.IN_PROGRESS,
});
await expect(
service.generateCertificate('session-1', 'user-1', 'tenant-1'),
).rejects.toThrow('Session not completed');
});
it('should return existing certificate if already generated (idempotent)', async () => {
const existingCert = { id: 'cert-1', sessionId: 'session-1' };
sessionRepository.findOne.mockResolvedValue(completedSession);
certificateRepository.findOne.mockResolvedValue(existingCert);
const result = await service.generateCertificate('session-1', 'user-1', 'tenant-1');
expect(result).toEqual(existingCert);
expect(certificateRepository.create).not.toHaveBeenCalled();
});
it('should create a new certificate with correct level for score >= 90 (Expert)', async () => {
sessionRepository.findOne.mockResolvedValue({ ...completedSession, finalScore: 95 });
certificateRepository.findOne.mockResolvedValue(null);
certificateRepository.create.mockReturnValue({ id: 'cert-new' });
certificateRepository.save.mockResolvedValue({ id: 'cert-new', level: 'Expert' });
const result = await service.generateCertificate('session-1', 'user-1', 'tenant-1');
expect(result).toBeDefined();
expect(certificateRepository.create).toHaveBeenCalledWith(
expect.objectContaining({ level: 'Expert', totalScore: 95 }),
);
});
it('should create a new certificate with Advanced level for score 75-89', async () => {
sessionRepository.findOne.mockResolvedValue(completedSession);
certificateRepository.findOne.mockResolvedValue(null);
certificateRepository.create.mockReturnValue({ id: 'cert-new' });
certificateRepository.save.mockResolvedValue({ id: 'cert-new', level: 'Advanced' });
const result = await service.generateCertificate('session-1', 'user-1', 'tenant-1');
expect(result).toBeDefined();
expect(certificateRepository.create).toHaveBeenCalledWith(
expect.objectContaining({ level: 'Advanced', totalScore: 85 }),
);
});
it('should create a new certificate with Novice level for score < 60', async () => {
sessionRepository.findOne.mockResolvedValue({ ...completedSession, finalScore: 45 });
certificateRepository.findOne.mockResolvedValue(null);
certificateRepository.create.mockReturnValue({ id: 'cert-new' });
certificateRepository.save.mockResolvedValue({ id: 'cert-new', level: 'Novice' });
const result = await service.generateCertificate('session-1', 'user-1', 'tenant-1');
expect(result).toBeDefined();
expect(certificateRepository.create).toHaveBeenCalledWith(
expect.objectContaining({ level: 'Novice', totalScore: 45 }),
);
});
});
});