291 lines
9.8 KiB
TypeScript
291 lines
9.8 KiB
TypeScript
/**
|
||
* 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');
|
||
});
|
||
});
|
||
});
|