Files
whale-town-end/src/business/admin/admin.service.spec.ts
moyin 6924416bbd feat:实现管理员系统核心功能
- 添加管理员数据库管理控制器和服务
- 实现管理员操作日志记录系统
- 添加数据库异常处理过滤器
- 完善管理员权限验证和响应格式
- 添加全面的属性测试覆盖
2026-01-08 23:05:34 +08:00

291 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<AdminCoreService, 'login' | 'resetUserPassword'> = {
login: jest.fn(),
resetUserPassword: jest.fn(),
};
const usersServiceMock = {
findAll: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
};
const logManagementServiceMock: Pick<LogManagementService, 'getRuntimeLogTail' | 'getLogDirAbsolutePath'> = {
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');
});
});
});