feat(p1): complete P1 foundation stage
- monorepo: pnpm workspace + go.work + pyproject.toml + commitlint/husky - infra: docker-compose (minimal + full profiles) + init-sql + prometheus - arch-scan: multi-language scanner skeleton (TS/Go/Python/Proto) - shared-proto: buf v2 + classes.proto (ClassService CRUD contract) - api-gateway: Go/Gin + JWT HS256 auth + reverse proxy + request ID - classes: NestJS golden template (error system + observability + middleware + CRUD + tests) - teacher-portal: Next.js + paper-feel UI design system - CI/CD: 4 workflows (go/ts/py/proto) - docs: migration guide + project_rules + coding-standards + git-workflow + ui-design-system + 004 + 9 module READMEs + known-issues + spec/plan migration + roadmap
This commit is contained in:
104
services/classes/test/unit/classes.service.test.ts
Normal file
104
services/classes/test/unit/classes.service.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { Mock } from 'vitest';
|
||||
import { ClassesService } from '../../src/classes/classes.service.js';
|
||||
import type { ClassesRepository } from '../../src/classes/classes.repository.js';
|
||||
import { ValidationError, NotFoundError } from '../../src/shared/errors/application-error.js';
|
||||
|
||||
describe('ClassesService', () => {
|
||||
let service: ClassesService;
|
||||
let mockRepository: {
|
||||
create: Mock;
|
||||
findById: Mock;
|
||||
list: Mock;
|
||||
update: Mock;
|
||||
delete: Mock;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepository = {
|
||||
create: vi.fn(),
|
||||
findById: vi.fn(),
|
||||
list: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
};
|
||||
// 测试场景:mock 对象通过 unknown 转换为 ClassesRepository(规则允许测试中 as 转换)
|
||||
service = new ClassesService(mockRepository as unknown as ClassesRepository);
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a class successfully', async () => {
|
||||
const dto = { name: 'Class 1A', gradeId: '550e8400-e29b-41d4-a716-446655440000' };
|
||||
const mockClass = {
|
||||
id: 'cls-1',
|
||||
name: 'Class 1A',
|
||||
gradeId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
createdAt: new Date('2026-01-01'),
|
||||
updatedAt: new Date('2026-01-01'),
|
||||
};
|
||||
mockRepository.create.mockResolvedValue(mockClass);
|
||||
|
||||
const result = await service.create(dto);
|
||||
|
||||
expect(result.id).toBe('cls-1');
|
||||
expect(mockRepository.create).toHaveBeenCalledWith(expect.objectContaining(dto));
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should throw ValidationError when no fields provided', async () => {
|
||||
await expect(service.update('cls-1', {})).rejects.toThrow(ValidationError);
|
||||
});
|
||||
|
||||
it('should update class successfully', async () => {
|
||||
const mockClass = {
|
||||
id: 'cls-1',
|
||||
name: 'Updated',
|
||||
gradeId: 'g1',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
mockRepository.update.mockResolvedValue(mockClass);
|
||||
|
||||
const result = await service.update('cls-1', { name: 'Updated' });
|
||||
|
||||
expect(result.name).toBe('Updated');
|
||||
expect(mockRepository.update).toHaveBeenCalledWith('cls-1', { name: 'Updated' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getById', () => {
|
||||
it('should throw NotFoundError when class not found', async () => {
|
||||
mockRepository.findById.mockResolvedValue(undefined);
|
||||
await expect(service.getById('not-exist')).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
it('should list classes without gradeId filter', async () => {
|
||||
mockRepository.list.mockResolvedValue([]);
|
||||
await service.list();
|
||||
expect(mockRepository.list).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it('should list classes with gradeId filter', async () => {
|
||||
mockRepository.list.mockResolvedValue([]);
|
||||
await service.list('grade-1');
|
||||
expect(mockRepository.list).toHaveBeenCalledWith('grade-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should throw NotFoundError when class not found', async () => {
|
||||
mockRepository.findById.mockResolvedValue(undefined);
|
||||
await expect(service.delete('not-exist')).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it('should delete class', async () => {
|
||||
mockRepository.findById.mockResolvedValue({ id: 'cls-1' });
|
||||
mockRepository.delete.mockResolvedValue(undefined);
|
||||
await service.delete('cls-1');
|
||||
expect(mockRepository.delete).toHaveBeenCalledWith('cls-1');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user