/** * AdminService 单元测试 * * 功能描述: * - 测试管理员业务服务的所有方法 * - 验证业务逻辑的正确性 * - 测试异常处理和边界情况 * * 职责分离: * - 业务逻辑测试,不涉及HTTP层 * - Mock核心服务,专注业务服务逻辑 * - 验证数据处理和格式化的正确性 * * 最近修改: * - 2026-01-08: 注释规范优化 - 添加文件头注释,完善测试文档说明 (修改者: moyin) * * @author moyin * @version 1.0.1 * @since 2025-12-19 * @lastModified 2026-01-08 */ import { NotFoundException, BadRequestException } from '@nestjs/common'; import { AdminService } from './admin.service'; import { AdminCoreService } from '../../core/admin_core/admin_core.service'; import { LogManagementService } from '../../core/utils/logger/log_management.service'; import { Users } from '../../core/db/users/users.entity'; import { UserStatus } from '../user_mgmt/user_status.enum'; describe('AdminService', () => { let service: AdminService; const adminCoreServiceMock: Pick = { login: jest.fn(), resetUserPassword: jest.fn(), }; const usersServiceMock = { findAll: jest.fn(), findOne: jest.fn(), update: jest.fn(), }; const logManagementServiceMock: Pick = { getRuntimeLogTail: jest.fn(), getLogDirAbsolutePath: jest.fn(), }; beforeEach(() => { jest.resetAllMocks(); service = new AdminService( adminCoreServiceMock as unknown as AdminCoreService, usersServiceMock as any, logManagementServiceMock as unknown as LogManagementService, ); }); it('should login admin successfully', async () => { (adminCoreServiceMock.login as jest.Mock).mockResolvedValue({ admin: { id: '1', username: 'admin', nickname: '管理员', role: 9 }, access_token: 'token', expires_at: 123, }); const res = await service.login('admin', 'Admin123456'); expect(adminCoreServiceMock.login).toHaveBeenCalledWith({ identifier: 'admin', password: 'Admin123456' }); expect(res.success).toBe(true); expect(res.data?.admin?.role).toBe(9); expect(res.message).toBe('管理员登录成功'); }); it('should handle login failure', async () => { (adminCoreServiceMock.login as jest.Mock).mockRejectedValue(new Error('密码错误')); const res = await service.login('admin', 'bad'); expect(res.success).toBe(false); expect(res.error_code).toBe('ADMIN_LOGIN_FAILED'); expect(res.message).toBe('密码错误'); }); it('should handle non-Error login failure', async () => { (adminCoreServiceMock.login as jest.Mock).mockRejectedValue('boom'); const res = await service.login('admin', 'bad'); expect(res.success).toBe(false); expect(res.error_code).toBe('ADMIN_LOGIN_FAILED'); expect(res.message).toBe('管理员登录失败'); }); it('should list users with pagination', async () => { const user = { id: BigInt(1), username: 'u1', nickname: 'U1', email: 'u1@test.com', email_verified: true, phone: null, avatar_url: null, role: 1, created_at: new Date('2025-01-01T00:00:00Z'), updated_at: new Date('2025-01-02T00:00:00Z'), } as unknown as Users; usersServiceMock.findAll.mockResolvedValue([user]); const res = await service.listUsers(100, 0); expect(usersServiceMock.findAll).toHaveBeenCalledWith(100, 0); expect(res.success).toBe(true); expect(res.data?.users).toHaveLength(1); expect(res.data?.users[0]).toMatchObject({ id: '1', username: 'u1', nickname: 'U1', role: 1, }); }); it('should get user by id', async () => { const user = { id: BigInt(3), username: 'u3', nickname: 'U3', email: null, email_verified: false, phone: '123', avatar_url: null, role: 1, created_at: new Date('2025-01-01T00:00:00Z'), updated_at: new Date('2025-01-02T00:00:00Z'), } as unknown as Users; usersServiceMock.findOne.mockResolvedValue(user); const res = await service.getUser(BigInt(3)); expect(usersServiceMock.findOne).toHaveBeenCalledWith(BigInt(3)); expect(res.success).toBe(true); expect(res.data?.user).toMatchObject({ id: '3', username: 'u3', nickname: 'U3' }); }); it('should reset user password', async () => { usersServiceMock.findOne.mockResolvedValue({ id: BigInt(2) } as unknown as Users); (adminCoreServiceMock.resetUserPassword as jest.Mock).mockResolvedValue(undefined); const res = await service.resetPassword(BigInt(2), 'NewPass1234'); expect(usersServiceMock.findOne).toHaveBeenCalledWith(BigInt(2)); expect(adminCoreServiceMock.resetUserPassword).toHaveBeenCalledWith(BigInt(2), 'NewPass1234'); expect(res).toEqual({ success: true, message: '密码重置成功' }); }); it('should throw NotFoundException when resetting password for missing user', async () => { usersServiceMock.findOne.mockRejectedValue(new Error('not found')); await expect(service.resetPassword(BigInt(999), 'NewPass1234')).rejects.toBeInstanceOf(NotFoundException); }); it('should get runtime logs', async () => { (logManagementServiceMock.getRuntimeLogTail as jest.Mock).mockResolvedValue({ file: 'dev.log', updated_at: '2025-01-01T00:00:00.000Z', lines: ['a', 'b'], }); const res = await service.getRuntimeLogs(2); expect(logManagementServiceMock.getRuntimeLogTail).toHaveBeenCalledWith({ lines: 2 }); expect(res.success).toBe(true); expect(res.data?.file).toBe('dev.log'); expect(res.data?.lines).toEqual(['a', 'b']); }); it('should expose log dir absolute path', () => { (logManagementServiceMock.getLogDirAbsolutePath as jest.Mock).mockReturnValue('/abs/logs'); expect(service.getLogDirAbsolutePath()).toBe('/abs/logs'); expect(logManagementServiceMock.getLogDirAbsolutePath).toHaveBeenCalled(); }); // 测试新增的用户状态管理方法 describe('updateUserStatus', () => { const mockUser = { id: BigInt(1), username: 'testuser', status: UserStatus.ACTIVE } as unknown as Users; it('should update user status successfully', async () => { usersServiceMock.findOne.mockResolvedValue(mockUser); usersServiceMock.update.mockResolvedValue({ ...mockUser, status: UserStatus.INACTIVE }); const result = await service.updateUserStatus(BigInt(1), { status: UserStatus.INACTIVE, reason: 'test' }); expect(result.success).toBe(true); expect(result.message).toBe('用户状态修改成功'); }); it('should throw NotFoundException when user not found', async () => { usersServiceMock.findOne.mockResolvedValue(null); await expect(service.updateUserStatus(BigInt(999), { status: UserStatus.INACTIVE, reason: 'test' })) .rejects.toThrow(NotFoundException); }); it('should return error when status unchanged', async () => { usersServiceMock.findOne.mockResolvedValue({ ...mockUser, status: UserStatus.INACTIVE }); await expect(service.updateUserStatus(BigInt(1), { status: UserStatus.INACTIVE, reason: 'test' })) .rejects.toThrow(BadRequestException); }); }); describe('batchUpdateUserStatus', () => { it('should batch update user status successfully', async () => { const mockUsers = [ { id: BigInt(1), username: 'user1', status: UserStatus.ACTIVE }, { id: BigInt(2), username: 'user2', status: UserStatus.ACTIVE } ] as unknown as Users[]; usersServiceMock.findOne .mockResolvedValueOnce(mockUsers[0]) .mockResolvedValueOnce(mockUsers[1]); usersServiceMock.update .mockResolvedValueOnce({ ...mockUsers[0], status: UserStatus.INACTIVE }) .mockResolvedValueOnce({ ...mockUsers[1], status: UserStatus.INACTIVE }); const result = await service.batchUpdateUserStatus({ userIds: ['1', '2'], status: UserStatus.INACTIVE, reason: 'batch test' }); expect(result.success).toBe(true); expect(result.data?.result.success_count).toBe(2); expect(result.data?.result.failed_count).toBe(0); }); it('should handle mixed success and failure', async () => { usersServiceMock.findOne .mockResolvedValueOnce({ id: BigInt(1), status: UserStatus.ACTIVE }) .mockResolvedValueOnce(null); // User not found usersServiceMock.update.mockResolvedValueOnce({ id: BigInt(1), status: UserStatus.INACTIVE }); const result = await service.batchUpdateUserStatus({ userIds: ['1', '999'], status: UserStatus.INACTIVE, reason: 'mixed test' }); expect(result.success).toBe(true); expect(result.data?.result.success_count).toBe(1); expect(result.data?.result.failed_count).toBe(1); }); }); describe('getUserStatusStats', () => { it('should return user status statistics', async () => { const mockUsers = [ { status: UserStatus.ACTIVE }, { status: UserStatus.ACTIVE }, { status: UserStatus.INACTIVE }, { status: null } // Should default to active ] as unknown as Users[]; usersServiceMock.findAll.mockResolvedValue(mockUsers); const result = await service.getUserStatusStats(); expect(result.success).toBe(true); expect(result.data?.stats.active).toBe(3); // 2 active + 1 null (defaults to active) expect(result.data?.stats.inactive).toBe(1); expect(result.data?.stats.total).toBe(4); }); it('should handle error when getting stats', async () => { usersServiceMock.findAll.mockRejectedValue(new Error('Database error')); const result = await service.getUserStatusStats(); expect(result.success).toBe(false); expect(result.error_code).toBe('USER_STATUS_STATS_FAILED'); }); }); });