- 统一文件命名为snake_case格式(kebab-case snake_case) - 重构zulip模块为zulip_core,明确Core层职责 - 重构user-mgmt模块为user_mgmt,统一命名规范 - 调整模块依赖关系,优化架构分层 - 删除过时的文件和目录结构 - 更新相关文档和配置文件 本次重构涉及大量文件重命名和模块重组, 旨在建立更清晰的项目架构和统一的命名规范。
453 lines
13 KiB
TypeScript
453 lines
13 KiB
TypeScript
/**
|
|
* 用户管理业务服务测试
|
|
*
|
|
* 功能描述:
|
|
* - 测试用户状态管理业务逻辑
|
|
* - 测试批量用户操作功能
|
|
* - 测试用户状态统计功能
|
|
* - 测试状态变更审计功能
|
|
*
|
|
* 职责分离:
|
|
* - 单元测试覆盖所有公共方法
|
|
* - 异常情况和边界情况测试
|
|
* - Mock依赖服务的行为验证
|
|
*
|
|
* 最近修改:
|
|
* - 2026-01-07: 代码规范优化 - 创建完整的测试覆盖 (修改者: moyin)
|
|
*
|
|
* @author moyin
|
|
* @version 1.0.1
|
|
* @since 2026-01-07
|
|
* @lastModified 2026-01-07
|
|
*/
|
|
|
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { Logger } from '@nestjs/common';
|
|
import { UserManagementService } from './user_management.service';
|
|
import { AdminService } from '../admin/admin.service';
|
|
import { UserStatusDto, BatchUserStatusDto } from './user_status.dto';
|
|
import { UserStatus } from './user_status.enum';
|
|
import { BATCH_OPERATION, ERROR_CODES, MESSAGES } from './user_mgmt.constants';
|
|
|
|
describe('UserManagementService', () => {
|
|
let service: UserManagementService;
|
|
let mockAdminService: jest.Mocked<AdminService>;
|
|
|
|
beforeEach(async () => {
|
|
const mockAdminServiceProvider = {
|
|
updateUserStatus: jest.fn(),
|
|
batchUpdateUserStatus: jest.fn(),
|
|
getUserStatusStats: jest.fn(),
|
|
};
|
|
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
UserManagementService,
|
|
{
|
|
provide: AdminService,
|
|
useValue: mockAdminServiceProvider,
|
|
},
|
|
],
|
|
}).compile();
|
|
|
|
service = module.get<UserManagementService>(UserManagementService);
|
|
mockAdminService = module.get(AdminService);
|
|
|
|
// Mock Logger to avoid console output during tests
|
|
jest.spyOn(Logger.prototype, 'log').mockImplementation();
|
|
jest.spyOn(Logger.prototype, 'warn').mockImplementation();
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('updateUserStatus', () => {
|
|
it('should update user status successfully', async () => {
|
|
// Arrange
|
|
const userId = BigInt(123);
|
|
const userStatusDto: UserStatusDto = {
|
|
status: UserStatus.ACTIVE,
|
|
reason: '用户申诉通过'
|
|
};
|
|
const expectedResult = {
|
|
success: true,
|
|
data: {
|
|
user: {
|
|
id: '123',
|
|
username: 'testuser',
|
|
nickname: '测试用户',
|
|
status: UserStatus.ACTIVE,
|
|
status_description: '正常',
|
|
updated_at: new Date()
|
|
},
|
|
reason: '用户申诉通过'
|
|
},
|
|
message: '用户状态修改成功'
|
|
};
|
|
|
|
mockAdminService.updateUserStatus.mockResolvedValue(expectedResult);
|
|
|
|
// Act
|
|
const result = await service.updateUserStatus(userId, userStatusDto);
|
|
|
|
// Assert
|
|
expect(result).toEqual(expectedResult);
|
|
expect(mockAdminService.updateUserStatus).toHaveBeenCalledWith(userId, userStatusDto);
|
|
expect(mockAdminService.updateUserStatus).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should handle update failure', async () => {
|
|
// Arrange
|
|
const userId = BigInt(999);
|
|
const userStatusDto: UserStatusDto = {
|
|
status: UserStatus.LOCKED,
|
|
reason: '违规操作'
|
|
};
|
|
const expectedResult = {
|
|
success: false,
|
|
message: '用户不存在',
|
|
error_code: 'USER_NOT_FOUND'
|
|
};
|
|
|
|
mockAdminService.updateUserStatus.mockResolvedValue(expectedResult);
|
|
|
|
// Act
|
|
const result = await service.updateUserStatus(userId, userStatusDto);
|
|
|
|
// Assert
|
|
expect(result).toEqual(expectedResult);
|
|
expect(mockAdminService.updateUserStatus).toHaveBeenCalledWith(userId, userStatusDto);
|
|
});
|
|
|
|
it('should log success when update succeeds', async () => {
|
|
// Arrange
|
|
const userId = BigInt(123);
|
|
const userStatusDto: UserStatusDto = {
|
|
status: UserStatus.ACTIVE,
|
|
reason: '测试'
|
|
};
|
|
const successResult = {
|
|
success: true,
|
|
data: {
|
|
user: {
|
|
id: '123',
|
|
username: 'testuser',
|
|
nickname: '测试用户',
|
|
status: UserStatus.ACTIVE,
|
|
status_description: '正常',
|
|
updated_at: new Date()
|
|
},
|
|
reason: '测试'
|
|
},
|
|
message: '成功'
|
|
};
|
|
|
|
mockAdminService.updateUserStatus.mockResolvedValue(successResult);
|
|
const logSpy = jest.spyOn(Logger.prototype, 'log');
|
|
|
|
// Act
|
|
await service.updateUserStatus(userId, userStatusDto);
|
|
|
|
// Assert
|
|
expect(logSpy).toHaveBeenCalledWith(
|
|
'用户管理:用户状态修改成功',
|
|
expect.objectContaining({
|
|
operation: 'user_mgmt_update_status_success',
|
|
userId: '123',
|
|
newStatus: UserStatus.ACTIVE
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('batchUpdateUserStatus', () => {
|
|
it('should batch update user status successfully', async () => {
|
|
// Arrange
|
|
const batchUserStatusDto: BatchUserStatusDto = {
|
|
userIds: ['1', '2', '3'],
|
|
status: UserStatus.LOCKED,
|
|
reason: '批量锁定违规用户'
|
|
};
|
|
const expectedResult = {
|
|
success: true,
|
|
data: {
|
|
result: {
|
|
success_users: [],
|
|
failed_users: [],
|
|
success_count: 3,
|
|
failed_count: 0,
|
|
total_count: 3
|
|
},
|
|
reason: '批量锁定违规用户'
|
|
},
|
|
message: '批量用户状态修改完成'
|
|
};
|
|
|
|
mockAdminService.batchUpdateUserStatus.mockResolvedValue(expectedResult);
|
|
|
|
// Act
|
|
const result = await service.batchUpdateUserStatus(batchUserStatusDto);
|
|
|
|
// Assert
|
|
expect(result).toEqual(expectedResult);
|
|
expect(mockAdminService.batchUpdateUserStatus).toHaveBeenCalledWith(batchUserStatusDto);
|
|
});
|
|
|
|
it('should reject batch operation when user count exceeds limit', async () => {
|
|
// Arrange
|
|
const userIds = Array.from({ length: BATCH_OPERATION.MAX_USER_COUNT + 1 }, (_, i) => i.toString());
|
|
const batchUserStatusDto: BatchUserStatusDto = {
|
|
userIds,
|
|
status: UserStatus.LOCKED,
|
|
reason: '超限测试'
|
|
};
|
|
|
|
// Act
|
|
const result = await service.batchUpdateUserStatus(batchUserStatusDto);
|
|
|
|
// Assert
|
|
expect(result).toEqual({
|
|
success: false,
|
|
message: MESSAGES.BATCH_OPERATION_LIMIT_ERROR,
|
|
error_code: ERROR_CODES.BATCH_OPERATION_LIMIT_EXCEEDED
|
|
});
|
|
expect(mockAdminService.batchUpdateUserStatus).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle empty user list', async () => {
|
|
// Arrange
|
|
const batchUserStatusDto: BatchUserStatusDto = {
|
|
userIds: [],
|
|
status: UserStatus.ACTIVE,
|
|
reason: '空列表测试'
|
|
};
|
|
const expectedResult = {
|
|
success: true,
|
|
data: {
|
|
result: {
|
|
success_users: [],
|
|
failed_users: [],
|
|
success_count: 0,
|
|
failed_count: 0,
|
|
total_count: 0
|
|
}
|
|
},
|
|
message: '批量操作完成'
|
|
};
|
|
|
|
mockAdminService.batchUpdateUserStatus.mockResolvedValue(expectedResult);
|
|
|
|
// Act
|
|
const result = await service.batchUpdateUserStatus(batchUserStatusDto);
|
|
|
|
// Assert
|
|
expect(result).toEqual(expectedResult);
|
|
});
|
|
|
|
it('should log warning when batch operation exceeds limit', async () => {
|
|
// Arrange
|
|
const userIds = Array.from({ length: BATCH_OPERATION.MAX_USER_COUNT + 1 }, (_, i) => i.toString());
|
|
const batchUserStatusDto: BatchUserStatusDto = {
|
|
userIds,
|
|
status: UserStatus.LOCKED,
|
|
reason: '超限测试'
|
|
};
|
|
const warnSpy = jest.spyOn(Logger.prototype, 'warn');
|
|
|
|
// Act
|
|
await service.batchUpdateUserStatus(batchUserStatusDto);
|
|
|
|
// Assert
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
'用户管理:批量操作数量超限',
|
|
expect.objectContaining({
|
|
operation: 'user_mgmt_batch_update_limit_exceeded',
|
|
requestCount: BATCH_OPERATION.MAX_USER_COUNT + 1,
|
|
maxAllowed: BATCH_OPERATION.MAX_USER_COUNT
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('getUserStatusStats', () => {
|
|
it('should get user status statistics successfully', async () => {
|
|
// Arrange
|
|
const expectedResult = {
|
|
success: true,
|
|
data: {
|
|
stats: {
|
|
active: 1250,
|
|
inactive: 45,
|
|
locked: 12,
|
|
banned: 8,
|
|
deleted: 3,
|
|
pending: 15,
|
|
total: 1333
|
|
},
|
|
timestamp: '2026-01-07T10:00:00.000Z'
|
|
},
|
|
message: '用户状态统计获取成功'
|
|
};
|
|
|
|
mockAdminService.getUserStatusStats.mockResolvedValue(expectedResult);
|
|
|
|
// Act
|
|
const result = await service.getUserStatusStats();
|
|
|
|
// Assert
|
|
expect(result).toEqual(expectedResult);
|
|
expect(mockAdminService.getUserStatusStats).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should handle statistics retrieval failure', async () => {
|
|
// Arrange
|
|
const expectedResult = {
|
|
success: false,
|
|
message: '统计数据获取失败',
|
|
error_code: 'STATS_RETRIEVAL_FAILED'
|
|
};
|
|
|
|
mockAdminService.getUserStatusStats.mockResolvedValue(expectedResult);
|
|
|
|
// Act
|
|
const result = await service.getUserStatusStats();
|
|
|
|
// Assert
|
|
expect(result).toEqual(expectedResult);
|
|
});
|
|
|
|
it('should calculate business metrics when stats are available', async () => {
|
|
// Arrange
|
|
const statsResult = {
|
|
success: true,
|
|
data: {
|
|
stats: {
|
|
active: 80,
|
|
inactive: 10,
|
|
locked: 5,
|
|
banned: 3,
|
|
deleted: 2,
|
|
pending: 0,
|
|
total: 100
|
|
},
|
|
timestamp: '2026-01-07T10:00:00.000Z'
|
|
},
|
|
message: '成功'
|
|
};
|
|
|
|
mockAdminService.getUserStatusStats.mockResolvedValue(statsResult);
|
|
const logSpy = jest.spyOn(Logger.prototype, 'log');
|
|
|
|
// Act
|
|
await service.getUserStatusStats();
|
|
|
|
// Assert
|
|
expect(logSpy).toHaveBeenCalledWith(
|
|
'用户管理:用户状态统计分析',
|
|
expect.objectContaining({
|
|
operation: 'user_mgmt_status_analysis',
|
|
totalUsers: 100,
|
|
activeUsers: 80,
|
|
activeRate: '80.00%',
|
|
problemUsers: 10 // locked + banned + deleted
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should handle zero total users in statistics', async () => {
|
|
// Arrange
|
|
const statsResult = {
|
|
success: true,
|
|
data: {
|
|
stats: {
|
|
active: 0,
|
|
inactive: 0,
|
|
locked: 0,
|
|
banned: 0,
|
|
deleted: 0,
|
|
pending: 0,
|
|
total: 0
|
|
},
|
|
timestamp: '2026-01-07T10:00:00.000Z'
|
|
},
|
|
message: '成功'
|
|
};
|
|
|
|
mockAdminService.getUserStatusStats.mockResolvedValue(statsResult);
|
|
const logSpy = jest.spyOn(Logger.prototype, 'log');
|
|
|
|
// Act
|
|
await service.getUserStatusStats();
|
|
|
|
// Assert
|
|
expect(logSpy).toHaveBeenCalledWith(
|
|
'用户管理:用户状态统计分析',
|
|
expect.objectContaining({
|
|
activeRate: '0%'
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('getUserStatusHistory', () => {
|
|
it('should return mock history data with default limit', async () => {
|
|
// Arrange
|
|
const userId = BigInt(123);
|
|
|
|
// Act
|
|
const result = await service.getUserStatusHistory(userId);
|
|
|
|
// Assert
|
|
expect(result).toEqual({
|
|
success: true,
|
|
data: {
|
|
user_id: '123',
|
|
history: [],
|
|
total_count: 0
|
|
},
|
|
message: '状态变更历史获取成功(当前返回空数据,待实现完整功能)'
|
|
});
|
|
});
|
|
|
|
it('should return mock history data with custom limit', async () => {
|
|
// Arrange
|
|
const userId = BigInt(456);
|
|
const customLimit = 20;
|
|
|
|
// Act
|
|
const result = await service.getUserStatusHistory(userId, customLimit);
|
|
|
|
// Assert
|
|
expect(result).toEqual({
|
|
success: true,
|
|
data: {
|
|
user_id: '456',
|
|
history: [],
|
|
total_count: 0
|
|
},
|
|
message: '状态变更历史获取成功(当前返回空数据,待实现完整功能)'
|
|
});
|
|
});
|
|
|
|
it('should log history query operation', async () => {
|
|
// Arrange
|
|
const userId = BigInt(789);
|
|
const limit = 15;
|
|
const logSpy = jest.spyOn(Logger.prototype, 'log');
|
|
|
|
// Act
|
|
await service.getUserStatusHistory(userId, limit);
|
|
|
|
// Assert
|
|
expect(logSpy).toHaveBeenCalledWith(
|
|
'用户管理:获取用户状态变更历史',
|
|
expect.objectContaining({
|
|
operation: 'user_mgmt_get_status_history',
|
|
userId: '789',
|
|
limit: 15
|
|
})
|
|
);
|
|
});
|
|
});
|
|
}); |