forked from datawhale/whale-town-end
feat:实现完整的用户管理系统
- 添加Users实体定义,包含完整的字段映射和约束 - 实现CreateUserDto数据验证,支持所有字段验证规则 - 创建UsersService服务,提供完整的CRUD操作 - 添加UsersModule模块配置 - 支持用户搜索、统计、批量操作等高级功能
This commit is contained in:
491
src/core/db/users/users.service.spec.ts
Normal file
491
src/core/db/users/users.service.spec.ts
Normal file
@@ -0,0 +1,491 @@
|
||||
/**
|
||||
* 用户实体、DTO和服务的完整测试套件
|
||||
*
|
||||
* 功能:
|
||||
* - 测试Users实体的结构和装饰器
|
||||
* - 测试CreateUserDto的验证规则
|
||||
* - 测试UsersService的所有CRUD操作
|
||||
* - 验证数据类型和约束条件
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2024-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<Users>;
|
||||
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,
|
||||
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>(UsersService);
|
||||
repository = module.get<Repository<Users>>(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.create(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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user