forked from datawhale/whale-town-end
- 更新管理员控制器和数据库管理功能 - 完善管理员操作日志系统 - 添加全面的属性测试覆盖 - 优化用户管理和用户档案服务 - 更新代码检查规范文档 功能改进: - 增强管理员权限验证 - 完善操作日志记录 - 优化数据库管理接口 - 提升系统安全性和可维护性
586 lines
18 KiB
TypeScript
586 lines
18 KiB
TypeScript
/**
|
|
* 用户状态管理控制器测试
|
|
*
|
|
* 功能描述:
|
|
* - 测试用户状态管理API接口
|
|
* - 测试HTTP请求处理和参数验证
|
|
* - 测试权限控制和频率限制
|
|
* - 测试响应格式和错误处理
|
|
*
|
|
* 职责分离:
|
|
* - 单元测试覆盖所有API端点
|
|
* - 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 { UserStatusController } from './user_status.controller';
|
|
import { UserManagementService } from './user_management.service';
|
|
import { AdminGuard } from '../admin/admin.guard';
|
|
import { UserStatusDto, BatchUserStatusDto } from './user_status.dto';
|
|
import { UserStatus } from './user_status.enum';
|
|
import { BATCH_OPERATION } from './user_mgmt.constants';
|
|
|
|
describe('UserStatusController', () => {
|
|
let controller: UserStatusController;
|
|
let mockUserManagementService: jest.Mocked<UserManagementService>;
|
|
|
|
beforeEach(async () => {
|
|
const mockUserManagementServiceProvider = {
|
|
updateUserStatus: jest.fn(),
|
|
batchUpdateUserStatus: jest.fn(),
|
|
getUserStatusStats: jest.fn(),
|
|
};
|
|
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
controllers: [UserStatusController],
|
|
providers: [
|
|
{
|
|
provide: UserManagementService,
|
|
useValue: mockUserManagementServiceProvider,
|
|
},
|
|
],
|
|
})
|
|
.overrideGuard(AdminGuard)
|
|
.useValue({ canActivate: jest.fn(() => true) })
|
|
.compile();
|
|
|
|
controller = module.get<UserStatusController>(UserStatusController);
|
|
mockUserManagementService = module.get(UserManagementService);
|
|
|
|
// Mock Logger to avoid console output during tests
|
|
jest.spyOn(Logger.prototype, 'log').mockImplementation();
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('updateUserStatus', () => {
|
|
it('should update user status successfully', async () => {
|
|
// Arrange
|
|
const userId = '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: '用户状态修改成功'
|
|
};
|
|
|
|
mockUserManagementService.updateUserStatus.mockResolvedValue(expectedResult);
|
|
|
|
// Act
|
|
const result = await controller.updateUserStatus(userId, userStatusDto);
|
|
|
|
// Assert
|
|
expect(result).toEqual(expectedResult);
|
|
expect(mockUserManagementService.updateUserStatus).toHaveBeenCalledWith(BigInt(123), userStatusDto);
|
|
expect(mockUserManagementService.updateUserStatus).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should handle user not found error', async () => {
|
|
// Arrange
|
|
const userId = '999';
|
|
const userStatusDto: UserStatusDto = {
|
|
status: UserStatus.LOCKED,
|
|
reason: '违规操作'
|
|
};
|
|
const expectedResult = {
|
|
success: false,
|
|
message: '用户不存在',
|
|
error_code: 'USER_NOT_FOUND'
|
|
};
|
|
|
|
mockUserManagementService.updateUserStatus.mockResolvedValue(expectedResult);
|
|
|
|
// Act
|
|
const result = await controller.updateUserStatus(userId, userStatusDto);
|
|
|
|
// Assert
|
|
expect(result).toEqual(expectedResult);
|
|
expect(mockUserManagementService.updateUserStatus).toHaveBeenCalledWith(BigInt(999), userStatusDto);
|
|
});
|
|
|
|
it('should log operation details', async () => {
|
|
// Arrange
|
|
const userId = '456';
|
|
const userStatusDto: UserStatusDto = {
|
|
status: UserStatus.BANNED,
|
|
reason: '严重违规'
|
|
};
|
|
const mockResult = {
|
|
success: true,
|
|
data: {
|
|
user: {
|
|
id: '456',
|
|
username: 'testuser',
|
|
nickname: '测试用户',
|
|
status: UserStatus.BANNED,
|
|
status_description: '已封禁',
|
|
updated_at: new Date()
|
|
},
|
|
reason: '严重违规'
|
|
},
|
|
message: '成功'
|
|
};
|
|
mockUserManagementService.updateUserStatus.mockResolvedValue(mockResult);
|
|
const logSpy = jest.spyOn(Logger.prototype, 'log');
|
|
|
|
// Act
|
|
await controller.updateUserStatus(userId, userStatusDto);
|
|
|
|
// Assert
|
|
expect(logSpy).toHaveBeenCalledWith(
|
|
'管理员修改用户状态',
|
|
expect.objectContaining({
|
|
operation: 'update_user_status',
|
|
userId: '456',
|
|
newStatus: UserStatus.BANNED,
|
|
reason: '严重违规'
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should convert string id to BigInt correctly', async () => {
|
|
// Arrange
|
|
const userId = '9007199254740991'; // Large number as string
|
|
const userStatusDto: UserStatusDto = {
|
|
status: UserStatus.INACTIVE,
|
|
reason: '长期未活跃'
|
|
};
|
|
const mockResult = {
|
|
success: true,
|
|
data: {
|
|
user: {
|
|
id: '9007199254740991',
|
|
username: 'large_id_user',
|
|
nickname: '大ID用户',
|
|
status: UserStatus.INACTIVE,
|
|
status_description: '非活跃',
|
|
updated_at: new Date()
|
|
},
|
|
reason: '长期未活跃'
|
|
},
|
|
message: '成功'
|
|
};
|
|
|
|
mockUserManagementService.updateUserStatus.mockResolvedValue(mockResult);
|
|
|
|
// Act
|
|
await controller.updateUserStatus(userId, userStatusDto);
|
|
|
|
// Assert
|
|
expect(mockUserManagementService.updateUserStatus).toHaveBeenCalledWith(
|
|
BigInt('9007199254740991'),
|
|
userStatusDto
|
|
);
|
|
});
|
|
});
|
|
|
|
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: [
|
|
{ id: '1', username: 'user1', nickname: '用户1', status: UserStatus.LOCKED, status_description: '已锁定', updated_at: new Date() },
|
|
{ id: '2', username: 'user2', nickname: '用户2', status: UserStatus.LOCKED, status_description: '已锁定', updated_at: new Date() },
|
|
{ id: '3', username: 'user3', nickname: '用户3', status: UserStatus.LOCKED, status_description: '已锁定', updated_at: new Date() }
|
|
],
|
|
failed_users: [],
|
|
success_count: 3,
|
|
failed_count: 0,
|
|
total_count: 3
|
|
},
|
|
reason: '批量锁定违规用户'
|
|
},
|
|
message: '批量用户状态修改完成'
|
|
};
|
|
|
|
mockUserManagementService.batchUpdateUserStatus.mockResolvedValue(expectedResult);
|
|
|
|
// Act
|
|
const result = await controller.batchUpdateUserStatus(batchUserStatusDto);
|
|
|
|
// Assert
|
|
expect(result).toEqual(expectedResult);
|
|
expect(mockUserManagementService.batchUpdateUserStatus).toHaveBeenCalledWith(batchUserStatusDto);
|
|
expect(mockUserManagementService.batchUpdateUserStatus).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should handle partial success in batch operation', async () => {
|
|
// Arrange
|
|
const batchUserStatusDto: BatchUserStatusDto = {
|
|
userIds: ['1', '2', '999'],
|
|
status: UserStatus.ACTIVE,
|
|
reason: '批量激活用户'
|
|
};
|
|
const expectedResult = {
|
|
success: true,
|
|
data: {
|
|
result: {
|
|
success_users: [
|
|
{ id: '1', username: 'user1', nickname: '用户1', status: UserStatus.ACTIVE, status_description: '正常', updated_at: new Date() },
|
|
{ id: '2', username: 'user2', nickname: '用户2', status: UserStatus.ACTIVE, status_description: '正常', updated_at: new Date() }
|
|
],
|
|
failed_users: [
|
|
{ user_id: '999', error: '用户不存在' }
|
|
],
|
|
success_count: 2,
|
|
failed_count: 1,
|
|
total_count: 3
|
|
},
|
|
reason: '批量激活用户'
|
|
},
|
|
message: '批量用户状态修改完成'
|
|
};
|
|
|
|
mockUserManagementService.batchUpdateUserStatus.mockResolvedValue(expectedResult);
|
|
|
|
// Act
|
|
const result = await controller.batchUpdateUserStatus(batchUserStatusDto);
|
|
|
|
// Assert
|
|
expect(result).toEqual(expectedResult);
|
|
expect(result.data.result.success_count).toBe(2);
|
|
expect(result.data.result.failed_count).toBe(1);
|
|
});
|
|
|
|
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
|
|
},
|
|
reason: '空列表测试'
|
|
},
|
|
message: '批量操作完成'
|
|
};
|
|
|
|
mockUserManagementService.batchUpdateUserStatus.mockResolvedValue(expectedResult);
|
|
|
|
// Act
|
|
const result = await controller.batchUpdateUserStatus(batchUserStatusDto);
|
|
|
|
// Assert
|
|
expect(result).toEqual(expectedResult);
|
|
expect(result.data.result.total_count).toBe(0);
|
|
});
|
|
|
|
it('should log batch operation details', async () => {
|
|
// Arrange
|
|
const batchUserStatusDto: BatchUserStatusDto = {
|
|
userIds: ['1', '2', '3', '4', '5'],
|
|
status: UserStatus.BANNED,
|
|
reason: '批量封禁违规用户'
|
|
};
|
|
const mockResult = {
|
|
success: true,
|
|
data: {
|
|
result: {
|
|
success_users: [],
|
|
failed_users: [],
|
|
success_count: 0,
|
|
failed_count: 0,
|
|
total_count: 0
|
|
}
|
|
},
|
|
message: '成功'
|
|
};
|
|
|
|
mockUserManagementService.batchUpdateUserStatus.mockResolvedValue(mockResult);
|
|
const logSpy = jest.spyOn(Logger.prototype, 'log');
|
|
|
|
// Act
|
|
await controller.batchUpdateUserStatus(batchUserStatusDto);
|
|
|
|
// Assert
|
|
expect(logSpy).toHaveBeenCalledWith(
|
|
'管理员批量修改用户状态',
|
|
expect.objectContaining({
|
|
operation: 'batch_update_user_status',
|
|
userCount: 5,
|
|
newStatus: UserStatus.BANNED,
|
|
reason: '批量封禁违规用户'
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should handle large user list within limits', async () => {
|
|
// Arrange
|
|
const userIds = Array.from({ length: BATCH_OPERATION.MAX_USER_COUNT }, (_, i) => i.toString());
|
|
const batchUserStatusDto: BatchUserStatusDto = {
|
|
userIds,
|
|
status: UserStatus.INACTIVE,
|
|
reason: '批量设置非活跃'
|
|
};
|
|
const mockResult = {
|
|
success: true,
|
|
data: {
|
|
result: {
|
|
success_users: userIds.map(id => ({
|
|
id,
|
|
username: `user_${id}`,
|
|
nickname: `用户_${id}`,
|
|
status: UserStatus.INACTIVE,
|
|
status_description: '非活跃',
|
|
updated_at: new Date()
|
|
})),
|
|
failed_users: [],
|
|
success_count: userIds.length,
|
|
failed_count: 0,
|
|
total_count: userIds.length
|
|
}
|
|
},
|
|
message: '批量操作完成'
|
|
};
|
|
|
|
mockUserManagementService.batchUpdateUserStatus.mockResolvedValue(mockResult);
|
|
|
|
// Act
|
|
const result = await controller.batchUpdateUserStatus(batchUserStatusDto);
|
|
|
|
// Assert
|
|
expect(result.success).toBe(true);
|
|
expect(result.data.result.total_count).toBe(BATCH_OPERATION.MAX_USER_COUNT);
|
|
expect(mockUserManagementService.batchUpdateUserStatus).toHaveBeenCalledWith(batchUserStatusDto);
|
|
});
|
|
});
|
|
|
|
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: '用户状态统计获取成功'
|
|
};
|
|
|
|
mockUserManagementService.getUserStatusStats.mockResolvedValue(expectedResult);
|
|
|
|
// Act
|
|
const result = await controller.getUserStatusStats();
|
|
|
|
// Assert
|
|
expect(result).toEqual(expectedResult);
|
|
expect(mockUserManagementService.getUserStatusStats).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should handle statistics retrieval failure', async () => {
|
|
// Arrange
|
|
const expectedResult = {
|
|
success: false,
|
|
message: '统计数据获取失败',
|
|
error_code: 'STATS_RETRIEVAL_FAILED'
|
|
};
|
|
|
|
mockUserManagementService.getUserStatusStats.mockResolvedValue(expectedResult);
|
|
|
|
// Act
|
|
const result = await controller.getUserStatusStats();
|
|
|
|
// Assert
|
|
expect(result).toEqual(expectedResult);
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('should log statistics query operation', async () => {
|
|
// Arrange
|
|
const mockResult = {
|
|
success: true,
|
|
data: {
|
|
stats: {
|
|
active: 800,
|
|
inactive: 150,
|
|
locked: 30,
|
|
banned: 15,
|
|
deleted: 5,
|
|
pending: 20,
|
|
total: 1020
|
|
},
|
|
timestamp: '2026-01-07T15:30:00.000Z'
|
|
},
|
|
message: '成功'
|
|
};
|
|
|
|
mockUserManagementService.getUserStatusStats.mockResolvedValue(mockResult);
|
|
const logSpy = jest.spyOn(Logger.prototype, 'log');
|
|
|
|
// Act
|
|
await controller.getUserStatusStats();
|
|
|
|
// Assert
|
|
expect(logSpy).toHaveBeenCalledWith(
|
|
'管理员获取用户状态统计',
|
|
expect.objectContaining({
|
|
operation: 'get_user_status_stats'
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should return detailed statistics breakdown', async () => {
|
|
// Arrange
|
|
const expectedResult = {
|
|
success: true,
|
|
data: {
|
|
stats: {
|
|
active: 800,
|
|
inactive: 150,
|
|
locked: 30,
|
|
banned: 15,
|
|
deleted: 5,
|
|
pending: 20,
|
|
total: 1020
|
|
},
|
|
timestamp: '2026-01-07T15:30:00.000Z',
|
|
metadata: {
|
|
last_updated: '2026-01-07T15:30:00.000Z',
|
|
cache_duration: 300
|
|
}
|
|
},
|
|
message: '用户状态统计获取成功'
|
|
};
|
|
|
|
mockUserManagementService.getUserStatusStats.mockResolvedValue(expectedResult);
|
|
|
|
// Act
|
|
const result = await controller.getUserStatusStats();
|
|
|
|
// Assert
|
|
expect(result.data.stats.total).toBe(1020);
|
|
expect(result.data.stats.active).toBe(800);
|
|
expect(result.data.stats.locked).toBe(30);
|
|
expect(result.data.stats.banned).toBe(15);
|
|
});
|
|
|
|
it('should handle zero statistics gracefully', async () => {
|
|
// Arrange
|
|
const expectedResult = {
|
|
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: '用户状态统计获取成功'
|
|
};
|
|
|
|
mockUserManagementService.getUserStatusStats.mockResolvedValue(expectedResult);
|
|
|
|
// Act
|
|
const result = await controller.getUserStatusStats();
|
|
|
|
// Assert
|
|
expect(result.data.stats.total).toBe(0);
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('AdminGuard Integration', () => {
|
|
it('should be protected by AdminGuard', () => {
|
|
// Verify that AdminGuard is applied to the controller methods
|
|
const updateUserStatusMethod = Reflect.getMetadata('__guards__', UserStatusController.prototype.updateUserStatus);
|
|
const batchUpdateMethod = Reflect.getMetadata('__guards__', UserStatusController.prototype.batchUpdateUserStatus);
|
|
const getStatsMethod = Reflect.getMetadata('__guards__', UserStatusController.prototype.getUserStatusStats);
|
|
|
|
// At least one method should have guards (they are applied via @UseGuards decorator)
|
|
expect(updateUserStatusMethod || batchUpdateMethod || getStatsMethod).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should handle service errors gracefully in updateUserStatus', async () => {
|
|
// Arrange
|
|
const userId = '123';
|
|
const userStatusDto: UserStatusDto = {
|
|
status: UserStatus.ACTIVE,
|
|
reason: '测试错误处理'
|
|
};
|
|
|
|
mockUserManagementService.updateUserStatus.mockRejectedValue(new Error('Service error'));
|
|
|
|
// Act & Assert
|
|
await expect(controller.updateUserStatus(userId, userStatusDto)).rejects.toThrow('Service error');
|
|
});
|
|
|
|
it('should handle service errors gracefully in batchUpdateUserStatus', async () => {
|
|
// Arrange
|
|
const batchUserStatusDto: BatchUserStatusDto = {
|
|
userIds: ['1', '2'],
|
|
status: UserStatus.ACTIVE,
|
|
reason: '测试错误处理'
|
|
};
|
|
|
|
mockUserManagementService.batchUpdateUserStatus.mockRejectedValue(new Error('Batch service error'));
|
|
|
|
// Act & Assert
|
|
await expect(controller.batchUpdateUserStatus(batchUserStatusDto)).rejects.toThrow('Batch service error');
|
|
});
|
|
|
|
it('should handle service errors gracefully in getUserStatusStats', async () => {
|
|
// Arrange
|
|
mockUserManagementService.getUserStatusStats.mockRejectedValue(new Error('Stats service error'));
|
|
|
|
// Act & Assert
|
|
await expect(controller.getUserStatusStats()).rejects.toThrow('Stats service error');
|
|
});
|
|
});
|
|
}); |