/** * 用户实体、DTO和服务的完整测试套件 * * 功能: * - 测试Users实体的结构和装饰器 * - 测试CreateUserDto的验证规则 * - 测试UsersService的所有CRUD操作 * - 验证数据类型和约束条件 * * @author moyin * @version 1.0.0 * @since 2025-12-17 */ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository, DataSource } from 'typeorm'; import { ConflictException, NotFoundException, BadRequestException } from '@nestjs/common'; import { validate } from 'class-validator'; import { plainToClass } from 'class-transformer'; import { Users } from './users.entity'; import { CreateUserDto } from './users.dto'; import { UsersService } from './users.service'; describe('Users Entity, DTO and Service Tests', () => { let service: UsersService; let repository: Repository; let module: TestingModule; // 模拟的Repository方法 const mockRepository = { save: jest.fn(), find: jest.fn(), findOne: jest.fn(), delete: jest.fn(), softRemove: jest.fn(), count: jest.fn(), createQueryBuilder: jest.fn(), }; // 测试数据 const mockUser: Users = { id: BigInt(1), username: 'testuser', email: 'test@example.com', phone: '+8613800138000', password_hash: 'hashed_password', nickname: '测试用户', github_id: 'github_123', avatar_url: 'https://example.com/avatar.jpg', role: 1, email_verified: false, created_at: new Date(), updated_at: new Date(), }; const createUserDto: CreateUserDto = { username: 'testuser', email: 'test@example.com', phone: '+8613800138000', password_hash: 'hashed_password', nickname: '测试用户', github_id: 'github_123', avatar_url: 'https://example.com/avatar.jpg', role: 1 }; beforeEach(async () => { module = await Test.createTestingModule({ providers: [ UsersService, { provide: getRepositoryToken(Users), useValue: mockRepository, }, ], }).compile(); service = module.get(UsersService); repository = module.get>(getRepositoryToken(Users)); // 清理所有mock jest.clearAllMocks(); }); afterEach(async () => { await module.close(); }); describe('Users Entity Tests', () => { it('应该正确创建用户实体实例', () => { const user = new Users(); user.username = 'testuser'; user.email = 'test@example.com'; user.phone = '+8613800138000'; user.password_hash = 'hashed_password'; user.nickname = '测试用户'; user.github_id = 'github_123'; user.avatar_url = 'https://example.com/avatar.jpg'; user.role = 1; user.created_at = new Date(); user.updated_at = new Date(); expect(user.username).toBe('testuser'); expect(user.email).toBe('test@example.com'); expect(user.nickname).toBe('测试用户'); expect(user.role).toBe(1); }); it('应该能够设置和获取所有属性', () => { const user = new Users(); // 验证可以设置和获取所有属性 user.username = 'testuser'; user.email = 'test@example.com'; user.phone = '+8613800138000'; user.password_hash = 'hashed_password'; user.nickname = '测试用户'; user.github_id = 'github_123'; user.avatar_url = 'https://example.com/avatar.jpg'; user.role = 1; user.created_at = new Date(); user.updated_at = new Date(); expect(user.username).toBe('testuser'); expect(user.email).toBe('test@example.com'); expect(user.phone).toBe('+8613800138000'); expect(user.password_hash).toBe('hashed_password'); expect(user.nickname).toBe('测试用户'); expect(user.github_id).toBe('github_123'); expect(user.avatar_url).toBe('https://example.com/avatar.jpg'); expect(user.role).toBe(1); expect(user.created_at).toBeInstanceOf(Date); expect(user.updated_at).toBeInstanceOf(Date); }); }); describe('CreateUserDto Validation Tests', () => { it('应该通过有效数据的验证', async () => { const validData = { username: 'testuser', email: 'test@example.com', phone: '+8613800138000', password_hash: 'hashed_password', nickname: '测试用户', github_id: 'github_123', avatar_url: 'https://example.com/avatar.jpg', role: 1 }; const dto = plainToClass(CreateUserDto, validData); const errors = await validate(dto); expect(errors).toHaveLength(0); }); it('应该拒绝缺少必填字段的数据', async () => { const invalidData = { email: 'test@example.com' // 缺少 username 和 nickname }; const dto = plainToClass(CreateUserDto, invalidData); const errors = await validate(dto); expect(errors.length).toBeGreaterThan(0); const usernameError = errors.find(error => error.property === 'username'); const nicknameError = errors.find(error => error.property === 'nickname'); expect(usernameError).toBeDefined(); expect(nicknameError).toBeDefined(); }); it('应该拒绝无效的邮箱格式', async () => { const invalidData = { username: 'testuser', nickname: '测试用户', email: 'invalid-email' }; const dto = plainToClass(CreateUserDto, invalidData); const errors = await validate(dto); const emailError = errors.find(error => error.property === 'email'); expect(emailError).toBeDefined(); }); it('应该拒绝超长的用户名', async () => { const invalidData = { username: 'a'.repeat(51), // 超过50字符 nickname: '测试用户' }; const dto = plainToClass(CreateUserDto, invalidData); const errors = await validate(dto); const usernameError = errors.find(error => error.property === 'username'); expect(usernameError).toBeDefined(); }); it('应该拒绝无效的角色值', async () => { const invalidData = { username: 'testuser', nickname: '测试用户', role: 10 // 超出1-9范围 }; const dto = plainToClass(CreateUserDto, invalidData); const errors = await validate(dto); const roleError = errors.find(error => error.property === 'role'); expect(roleError).toBeDefined(); }); }); describe('UsersService CRUD Tests', () => { describe('create()', () => { it('应该成功创建新用户', async () => { mockRepository.findOne.mockResolvedValue(null); // 没有重复用户 mockRepository.save.mockResolvedValue(mockUser); const result = await service.create(createUserDto); expect(mockRepository.save).toHaveBeenCalled(); expect(result).toEqual(mockUser); }); it('应该在用户名重复时抛出ConflictException', async () => { mockRepository.findOne.mockResolvedValue(mockUser); // 模拟用户名已存在 await expect(service.createWithDuplicateCheck(createUserDto)).rejects.toThrow(ConflictException); expect(mockRepository.save).not.toHaveBeenCalled(); }); it('应该在数据验证失败时抛出BadRequestException', async () => { const invalidDto = { username: '', nickname: '' }; // 无效数据 await expect(service.create(invalidDto as CreateUserDto)).rejects.toThrow(BadRequestException); }); }); describe('findAll()', () => { it('应该返回用户列表', async () => { const mockUsers = [mockUser]; mockRepository.find.mockResolvedValue(mockUsers); const result = await service.findAll(); expect(mockRepository.find).toHaveBeenCalledWith({ take: 100, skip: 0, order: { created_at: 'DESC' } }); expect(result).toEqual(mockUsers); }); it('应该支持分页参数', async () => { mockRepository.find.mockResolvedValue([]); await service.findAll(50, 10); expect(mockRepository.find).toHaveBeenCalledWith({ take: 50, skip: 10, order: { created_at: 'DESC' } }); }); }); describe('findOne()', () => { it('应该根据ID返回用户', async () => { mockRepository.findOne.mockResolvedValue(mockUser); const result = await service.findOne(BigInt(1)); expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: BigInt(1) } }); expect(result).toEqual(mockUser); }); it('应该在用户不存在时抛出NotFoundException', async () => { mockRepository.findOne.mockResolvedValue(null); await expect(service.findOne(BigInt(999))).rejects.toThrow(NotFoundException); }); }); describe('findByUsername()', () => { it('应该根据用户名返回用户', async () => { mockRepository.findOne.mockResolvedValue(mockUser); const result = await service.findByUsername('testuser'); expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { username: 'testuser' } }); expect(result).toEqual(mockUser); }); it('应该在用户不存在时返回null', async () => { mockRepository.findOne.mockResolvedValue(null); const result = await service.findByUsername('nonexistent'); expect(result).toBeNull(); }); }); describe('findByEmail()', () => { it('应该根据邮箱返回用户', async () => { mockRepository.findOne.mockResolvedValue(mockUser); const result = await service.findByEmail('test@example.com'); expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { email: 'test@example.com' } }); expect(result).toEqual(mockUser); }); }); describe('update()', () => { it('应该成功更新用户信息', async () => { const updatedUser = { ...mockUser, nickname: '更新后的昵称' }; mockRepository.findOne .mockResolvedValueOnce(mockUser) // findOne in update method .mockResolvedValueOnce(null); // 检查昵称是否重复 mockRepository.save.mockResolvedValue(updatedUser); const result = await service.update(BigInt(1), { nickname: '更新后的昵称' }); expect(mockRepository.save).toHaveBeenCalled(); expect(result.nickname).toBe('更新后的昵称'); }); it('应该在更新不存在的用户时抛出NotFoundException', async () => { mockRepository.findOne.mockResolvedValue(null); await expect(service.update(BigInt(999), { nickname: '新昵称' })).rejects.toThrow(NotFoundException); }); it('应该在更新数据冲突时抛出ConflictException', async () => { mockRepository.findOne .mockResolvedValueOnce(mockUser) // 找到要更新的用户 .mockResolvedValueOnce(mockUser); // 发现用户名冲突 await expect(service.update(BigInt(1), { username: 'conflictuser' })).rejects.toThrow(ConflictException); }); }); describe('remove()', () => { it('应该成功删除用户', async () => { mockRepository.findOne.mockResolvedValue(mockUser); mockRepository.delete.mockResolvedValue({ affected: 1 }); const result = await service.remove(BigInt(1)); expect(mockRepository.delete).toHaveBeenCalledWith({ id: BigInt(1) }); expect(result.affected).toBe(1); expect(result.message).toContain('成功删除'); }); it('应该在删除不存在的用户时抛出NotFoundException', async () => { mockRepository.findOne.mockResolvedValue(null); await expect(service.remove(BigInt(999))).rejects.toThrow(NotFoundException); }); }); describe('count()', () => { it('应该返回用户总数', async () => { mockRepository.count.mockResolvedValue(10); const result = await service.count(); expect(mockRepository.count).toHaveBeenCalled(); expect(result).toBe(10); }); }); describe('exists()', () => { it('应该在用户存在时返回true', async () => { mockRepository.count.mockResolvedValue(1); const result = await service.exists(BigInt(1)); expect(result).toBe(true); }); it('应该在用户不存在时返回false', async () => { mockRepository.count.mockResolvedValue(0); const result = await service.exists(BigInt(999)); expect(result).toBe(false); }); }); describe('search()', () => { it('应该根据关键词搜索用户', async () => { const mockQueryBuilder = { where: jest.fn().mockReturnThis(), orderBy: jest.fn().mockReturnThis(), limit: jest.fn().mockReturnThis(), getMany: jest.fn().mockResolvedValue([mockUser]), }; mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder); const result = await service.search('test'); expect(mockRepository.createQueryBuilder).toHaveBeenCalledWith('user'); expect(mockQueryBuilder.where).toHaveBeenCalled(); expect(result).toEqual([mockUser]); }); }); describe('findByRole()', () => { it('应该根据角色查询用户', async () => { mockRepository.find.mockResolvedValue([mockUser]); const result = await service.findByRole(1); expect(mockRepository.find).toHaveBeenCalledWith({ where: { role: 1 }, order: { created_at: 'DESC' } }); expect(result).toEqual([mockUser]); }); }); }); describe('Integration Tests', () => { it('应该完成从DTO到Entity的完整流程', async () => { // 1. 验证DTO const dto = plainToClass(CreateUserDto, createUserDto); const validationErrors = await validate(dto); expect(validationErrors).toHaveLength(0); // 2. 创建匹配的mock用户数据 const expectedUser = { ...mockUser, nickname: dto.nickname }; // 3. 模拟服务创建用户 mockRepository.findOne.mockResolvedValue(null); mockRepository.save.mockResolvedValue(expectedUser); const result = await service.create(dto); // 4. 验证结果 expect(result).toBeDefined(); expect(result.username).toBe(dto.username); expect(result.nickname).toBe(dto.nickname); }); it('应该正确处理可选字段', async () => { const minimalDto: CreateUserDto = { username: 'minimaluser', nickname: '最小用户' }; const dto = plainToClass(CreateUserDto, minimalDto); const validationErrors = await validate(dto); expect(validationErrors).toHaveLength(0); // 验证可选字段的默认值处理 expect(dto.role).toBe(1); // 默认角色 expect(dto.email).toBeUndefined(); expect(dto.phone).toBeUndefined(); }); }); describe('Error Handling Tests', () => { it('应该正确处理数据库连接错误', async () => { mockRepository.save.mockRejectedValue(new Error('Database connection failed')); await expect(service.create(createUserDto)).rejects.toThrow('Database connection failed'); }); it('应该正确处理并发创建冲突', async () => { // 模拟并发情况:检查时不存在,保存时出现唯一约束错误 mockRepository.findOne.mockResolvedValue(null); mockRepository.save.mockRejectedValue(new Error('Duplicate entry')); await expect(service.create(createUserDto)).rejects.toThrow('Duplicate entry'); }); }); });