feat: 完善管理员系统和用户管理模块

- 更新管理员控制器和数据库管理功能
- 完善管理员操作日志系统
- 添加全面的属性测试覆盖
- 优化用户管理和用户档案服务
- 更新代码检查规范文档

功能改进:
- 增强管理员权限验证
- 完善操作日志记录
- 优化数据库管理接口
- 提升系统安全性和可维护性
This commit is contained in:
moyin
2026-01-09 17:05:08 +08:00
parent 8816b29b0a
commit 5f662ef091
30 changed files with 3881 additions and 599 deletions

View File

@@ -0,0 +1,284 @@
/**
* AdminOperationLogController 单元测试
*
* 功能描述:
* - 测试管理员操作日志控制器的所有HTTP端点
* - 验证请求参数处理和响应格式
* - 测试权限验证和异常处理
*
* 职责分离:
* - HTTP层测试不涉及业务逻辑实现
* - Mock业务服务专注控制器逻辑
* - 验证请求响应的正确性
*
* 最近修改:
* - 2026-01-09: 功能新增 - 创建AdminOperationLogController单元测试 (修改者: moyin)
*
* @author moyin
* @version 1.0.0
* @since 2026-01-09
* @lastModified 2026-01-09
*/
import { Test, TestingModule } from '@nestjs/testing';
import { AdminOperationLogController } from './admin_operation_log.controller';
import { AdminOperationLogService } from './admin_operation_log.service';
import { AdminGuard } from './admin.guard';
import { AdminOperationLog } from './admin_operation_log.entity';
describe('AdminOperationLogController', () => {
let controller: AdminOperationLogController;
let logService: jest.Mocked<AdminOperationLogService>;
const mockLogService = {
queryLogs: jest.fn(),
getLogById: jest.fn(),
getStatistics: jest.fn(),
getSensitiveOperations: jest.fn(),
getAdminOperationHistory: jest.fn(),
cleanupExpiredLogs: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AdminOperationLogController],
providers: [
{
provide: AdminOperationLogService,
useValue: mockLogService,
},
],
})
.overrideGuard(AdminGuard)
.useValue({ canActivate: () => true })
.compile();
controller = module.get<AdminOperationLogController>(AdminOperationLogController);
logService = module.get(AdminOperationLogService);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('getOperationLogs', () => {
it('should query logs with default parameters', async () => {
const mockLogs = [
{ id: 'log1', operation_type: 'CREATE' },
{ id: 'log2', operation_type: 'UPDATE' },
] as AdminOperationLog[];
logService.queryLogs.mockResolvedValue({ logs: mockLogs, total: 2 });
const result = await controller.getOperationLogs(50, 0);
expect(logService.queryLogs).toHaveBeenCalledWith({
limit: 50,
offset: 0
});
expect(result.success).toBe(true);
expect(result.data.items).toEqual(mockLogs);
expect(result.data.total).toBe(2);
});
it('should query logs with custom parameters', async () => {
const mockLogs = [] as AdminOperationLog[];
logService.queryLogs.mockResolvedValue({ logs: mockLogs, total: 0 });
const result = await controller.getOperationLogs(
20,
10,
'admin1',
'CREATE',
'users',
'SUCCESS',
'2026-01-01',
'2026-01-31',
'true'
);
expect(logService.queryLogs).toHaveBeenCalledWith({
adminUserId: 'admin1',
operationType: 'CREATE',
targetType: 'users',
operationResult: 'SUCCESS',
startDate: new Date('2026-01-01'),
endDate: new Date('2026-01-31'),
isSensitive: true,
limit: 20,
offset: 10
});
expect(result.success).toBe(true);
});
it('should handle invalid date parameters', async () => {
await expect(controller.getOperationLogs(
50,
0,
undefined,
undefined,
undefined,
undefined,
'invalid',
'invalid'
)).rejects.toThrow('日期格式无效请使用ISO格式');
});
it('should handle service error', async () => {
logService.queryLogs.mockRejectedValue(new Error('Database error'));
await expect(controller.getOperationLogs(50, 0)).rejects.toThrow('Database error');
});
});
describe('getOperationLogById', () => {
it('should get log by id successfully', async () => {
const mockLog = {
id: 'log1',
operation_type: 'CREATE',
target_type: 'users'
} as AdminOperationLog;
logService.getLogById.mockResolvedValue(mockLog);
const result = await controller.getOperationLogById('log1');
expect(logService.getLogById).toHaveBeenCalledWith('log1');
expect(result.success).toBe(true);
expect(result.data).toEqual(mockLog);
});
it('should handle log not found', async () => {
logService.getLogById.mockResolvedValue(null);
await expect(controller.getOperationLogById('nonexistent')).rejects.toThrow('操作日志不存在');
});
it('should handle service error', async () => {
logService.getLogById.mockRejectedValue(new Error('Database error'));
await expect(controller.getOperationLogById('log1')).rejects.toThrow('Database error');
});
});
describe('getOperationStatistics', () => {
it('should get statistics successfully', async () => {
const mockStats = {
totalOperations: 100,
successfulOperations: 80,
failedOperations: 20,
operationsByType: { CREATE: 50, UPDATE: 30, DELETE: 20 },
operationsByTarget: { users: 60, profiles: 40 },
operationsByAdmin: { admin1: 60, admin2: 40 },
averageDuration: 150.5,
sensitiveOperations: 10,
uniqueAdmins: 5
};
logService.getStatistics.mockResolvedValue(mockStats);
const result = await controller.getOperationStatistics();
expect(logService.getStatistics).toHaveBeenCalledWith(undefined, undefined);
expect(result.success).toBe(true);
expect(result.data).toEqual(mockStats);
});
it('should get statistics with date range', async () => {
const mockStats = {
totalOperations: 50,
successfulOperations: 40,
failedOperations: 10,
operationsByType: {},
operationsByTarget: {},
operationsByAdmin: {},
averageDuration: 100,
sensitiveOperations: 5,
uniqueAdmins: 3
};
logService.getStatistics.mockResolvedValue(mockStats);
const result = await controller.getOperationStatistics('2026-01-01', '2026-01-31');
expect(logService.getStatistics).toHaveBeenCalledWith(
new Date('2026-01-01'),
new Date('2026-01-31')
);
expect(result.success).toBe(true);
});
it('should handle invalid dates', async () => {
await expect(controller.getOperationStatistics('invalid', 'invalid')).rejects.toThrow('日期格式无效请使用ISO格式');
});
it('should handle service error', async () => {
logService.getStatistics.mockRejectedValue(new Error('Statistics error'));
await expect(controller.getOperationStatistics()).rejects.toThrow('Statistics error');
});
});
describe('getSensitiveOperations', () => {
it('should get sensitive operations successfully', async () => {
const mockLogs = [
{ id: 'log1', is_sensitive: true }
] as AdminOperationLog[];
logService.getSensitiveOperations.mockResolvedValue({ logs: mockLogs, total: 1 });
const result = await controller.getSensitiveOperations(50, 0);
expect(logService.getSensitiveOperations).toHaveBeenCalledWith(50, 0);
expect(result.success).toBe(true);
expect(result.data.items).toEqual(mockLogs);
expect(result.data.total).toBe(1);
});
it('should get sensitive operations with pagination', async () => {
logService.getSensitiveOperations.mockResolvedValue({ logs: [], total: 0 });
const result = await controller.getSensitiveOperations(20, 10);
expect(logService.getSensitiveOperations).toHaveBeenCalledWith(20, 10);
});
it('should handle service error', async () => {
logService.getSensitiveOperations.mockRejectedValue(new Error('Query error'));
await expect(controller.getSensitiveOperations(50, 0)).rejects.toThrow('Query error');
});
});
describe('cleanupExpiredLogs', () => {
it('should cleanup logs successfully', async () => {
logService.cleanupExpiredLogs.mockResolvedValue(25);
const result = await controller.cleanupExpiredLogs(90);
expect(logService.cleanupExpiredLogs).toHaveBeenCalledWith(90);
expect(result.success).toBe(true);
expect(result.data.deleted_count).toBe(25);
});
it('should cleanup logs with custom retention days', async () => {
logService.cleanupExpiredLogs.mockResolvedValue(10);
const result = await controller.cleanupExpiredLogs(30);
expect(logService.cleanupExpiredLogs).toHaveBeenCalledWith(30);
expect(result.data.deleted_count).toBe(10);
});
it('should handle invalid retention days', async () => {
await expect(controller.cleanupExpiredLogs(5)).rejects.toThrow('保留天数必须在7-365天之间');
});
it('should handle service error', async () => {
logService.cleanupExpiredLogs.mockRejectedValue(new Error('Cleanup error'));
await expect(controller.cleanupExpiredLogs(90)).rejects.toThrow('Cleanup error');
});
});
});