forked from datawhale/whale-town-end
refactor:项目架构重构和命名规范化
- 统一文件命名为snake_case格式(kebab-case snake_case) - 重构zulip模块为zulip_core,明确Core层职责 - 重构user-mgmt模块为user_mgmt,统一命名规范 - 调整模块依赖关系,优化架构分层 - 删除过时的文件和目录结构 - 更新相关文档和配置文件 本次重构涉及大量文件重命名和模块重组, 旨在建立更清晰的项目架构和统一的命名规范。
This commit is contained in:
586
src/business/user_mgmt/user_status.controller.spec.ts
Normal file
586
src/business/user_mgmt/user_status.controller.spec.ts
Normal file
@@ -0,0 +1,586 @@
|
||||
/**
|
||||
* 用户状态管理控制器测试
|
||||
*
|
||||
* 功能描述:
|
||||
* - 测试用户状态管理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/guards/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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user