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,407 @@
/**
* AdminOperationLogService 单元测试
*
* 功能描述:
* - 测试管理员操作日志服务的所有方法
* - 验证日志记录和查询的正确性
* - 测试统计功能和清理功能
*
* 职责分离:
* - 业务逻辑测试不涉及HTTP层
* - Mock数据库操作专注服务逻辑
* - 验证日志处理的正确性
*
* 最近修改:
* - 2026-01-09: 功能新增 - 创建AdminOperationLogService单元测试 (修改者: moyin)
*
* @author moyin
* @version 1.0.0
* @since 2026-01-09
* @lastModified 2026-01-09
*/
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AdminOperationLogService, CreateLogParams, LogQueryParams } from './admin_operation_log.service';
import { AdminOperationLog } from './admin_operation_log.entity';
describe('AdminOperationLogService', () => {
let service: AdminOperationLogService;
let repository: jest.Mocked<Repository<AdminOperationLog>>;
const mockRepository = {
create: jest.fn(),
save: jest.fn(),
createQueryBuilder: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
findAndCount: jest.fn(),
};
const mockQueryBuilder = {
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
offset: jest.fn().mockReturnThis(),
getManyAndCount: jest.fn(),
getCount: jest.fn(),
clone: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
addSelect: jest.fn().mockReturnThis(),
groupBy: jest.fn().mockReturnThis(),
getRawMany: jest.fn(),
getRawOne: jest.fn(),
delete: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
execute: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AdminOperationLogService,
{
provide: getRepositoryToken(AdminOperationLog),
useValue: mockRepository,
},
],
}).compile();
service = module.get<AdminOperationLogService>(AdminOperationLogService);
repository = module.get(getRepositoryToken(AdminOperationLog));
// Setup default mock behavior
mockRepository.createQueryBuilder.mockReturnValue(mockQueryBuilder);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('createLog', () => {
it('should create log successfully', async () => {
const logParams: CreateLogParams = {
adminUserId: 'admin1',
adminUsername: 'admin',
operationType: 'CREATE',
targetType: 'users',
targetId: '1',
operationDescription: 'Create user',
httpMethodPath: 'POST /admin/users',
operationResult: 'SUCCESS',
durationMs: 100,
requestId: 'req_123',
};
const mockLog = {
id: 'log1',
admin_user_id: logParams.adminUserId,
admin_username: logParams.adminUsername,
operation_type: logParams.operationType,
target_type: logParams.targetType,
target_id: logParams.targetId,
operation_description: logParams.operationDescription,
http_method_path: logParams.httpMethodPath,
operation_result: logParams.operationResult,
duration_ms: logParams.durationMs,
request_id: logParams.requestId,
is_sensitive: false,
affected_records: 0,
created_at: new Date(),
updated_at: new Date()
} as AdminOperationLog;
mockRepository.create.mockReturnValue(mockLog);
mockRepository.save.mockResolvedValue(mockLog);
const result = await service.createLog(logParams);
expect(mockRepository.create).toHaveBeenCalledWith({
admin_user_id: logParams.adminUserId,
admin_username: logParams.adminUsername,
operation_type: logParams.operationType,
target_type: logParams.targetType,
target_id: logParams.targetId,
operation_description: logParams.operationDescription,
http_method_path: logParams.httpMethodPath,
request_params: logParams.requestParams,
before_data: logParams.beforeData,
after_data: logParams.afterData,
operation_result: logParams.operationResult,
error_message: logParams.errorMessage,
error_code: logParams.errorCode,
duration_ms: logParams.durationMs,
client_ip: logParams.clientIp,
user_agent: logParams.userAgent,
request_id: logParams.requestId,
context: logParams.context,
is_sensitive: false,
affected_records: 0,
batch_id: logParams.batchId,
});
expect(mockRepository.save).toHaveBeenCalledWith(mockLog);
expect(result).toEqual(mockLog);
});
it('should handle creation error', async () => {
const logParams: CreateLogParams = {
adminUserId: 'admin1',
adminUsername: 'admin',
operationType: 'CREATE',
targetType: 'users',
operationDescription: 'Create user',
httpMethodPath: 'POST /admin/users',
operationResult: 'SUCCESS',
durationMs: 100,
requestId: 'req_123',
};
mockRepository.create.mockReturnValue({} as AdminOperationLog);
mockRepository.save.mockRejectedValue(new Error('Database error'));
await expect(service.createLog(logParams)).rejects.toThrow('Database error');
});
});
describe('queryLogs', () => {
it('should query logs successfully', async () => {
const queryParams: LogQueryParams = {
adminUserId: 'admin1',
operationType: 'CREATE',
limit: 10,
offset: 0,
};
const mockLogs = [
{ id: 'log1', admin_user_id: 'admin1' },
{ id: 'log2', admin_user_id: 'admin1' },
] as AdminOperationLog[];
mockQueryBuilder.getManyAndCount.mockResolvedValue([mockLogs, 2]);
const result = await service.queryLogs(queryParams);
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('log.admin_user_id = :adminUserId', { adminUserId: 'admin1' });
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('log.operation_type = :operationType', { operationType: 'CREATE' });
expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith('log.created_at', 'DESC');
expect(mockQueryBuilder.limit).toHaveBeenCalledWith(10);
expect(mockQueryBuilder.offset).toHaveBeenCalledWith(0);
expect(result.logs).toEqual(mockLogs);
expect(result.total).toBe(2);
});
it('should query logs with date range', async () => {
const startDate = new Date('2026-01-01');
const endDate = new Date('2026-01-31');
const queryParams: LogQueryParams = {
startDate,
endDate,
isSensitive: true,
};
const mockLogs = [] as AdminOperationLog[];
mockQueryBuilder.getManyAndCount.mockResolvedValue([mockLogs, 0]);
const result = await service.queryLogs(queryParams);
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('log.created_at BETWEEN :startDate AND :endDate', {
startDate,
endDate
});
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('log.is_sensitive = :isSensitive', { isSensitive: true });
});
it('should handle query error', async () => {
const queryParams: LogQueryParams = {};
mockQueryBuilder.getManyAndCount.mockRejectedValue(new Error('Query error'));
await expect(service.queryLogs(queryParams)).rejects.toThrow('Query error');
});
});
describe('getLogById', () => {
it('should get log by id successfully', async () => {
const mockLog = { id: 'log1', admin_user_id: 'admin1' } as AdminOperationLog;
mockRepository.findOne.mockResolvedValue(mockLog);
const result = await service.getLogById('log1');
expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: 'log1' } });
expect(result).toEqual(mockLog);
});
it('should return null when log not found', async () => {
mockRepository.findOne.mockResolvedValue(null);
const result = await service.getLogById('nonexistent');
expect(result).toBeNull();
});
it('should handle get error', async () => {
mockRepository.findOne.mockRejectedValue(new Error('Database error'));
await expect(service.getLogById('log1')).rejects.toThrow('Database error');
});
});
describe('getStatistics', () => {
it('should get statistics successfully', async () => {
// Mock basic statistics
mockQueryBuilder.getCount
.mockResolvedValueOnce(100) // total
.mockResolvedValueOnce(80) // successful
.mockResolvedValueOnce(10); // sensitive
// Mock operation type statistics
mockQueryBuilder.getRawMany.mockResolvedValueOnce([
{ type: 'CREATE', count: '50' },
{ type: 'UPDATE', count: '30' },
{ type: 'DELETE', count: '20' },
]);
// Mock target type statistics
mockQueryBuilder.getRawMany.mockResolvedValueOnce([
{ type: 'users', count: '60' },
{ type: 'profiles', count: '40' },
]);
// Mock performance statistics
mockQueryBuilder.getRawOne
.mockResolvedValueOnce({ avgDuration: '150.5' }) // average duration
.mockResolvedValueOnce({ uniqueAdmins: '5' }); // unique admins
const result = await service.getStatistics();
expect(result.totalOperations).toBe(100);
expect(result.successfulOperations).toBe(80);
expect(result.failedOperations).toBe(20);
expect(result.sensitiveOperations).toBe(10);
expect(result.operationsByType).toEqual({
CREATE: 50,
UPDATE: 30,
DELETE: 20,
});
expect(result.operationsByTarget).toEqual({
users: 60,
profiles: 40,
});
expect(result.averageDuration).toBe(150.5);
expect(result.uniqueAdmins).toBe(5);
});
it('should get statistics with date range', async () => {
const startDate = new Date('2026-01-01');
const endDate = new Date('2026-01-31');
mockQueryBuilder.getCount.mockResolvedValue(50);
mockQueryBuilder.getRawMany.mockResolvedValue([]);
mockQueryBuilder.getRawOne.mockResolvedValue({ avgDuration: '100', uniqueAdmins: '3' });
const result = await service.getStatistics(startDate, endDate);
expect(mockQueryBuilder.where).toHaveBeenCalledWith('log.created_at BETWEEN :startDate AND :endDate', {
startDate,
endDate
});
expect(result.totalOperations).toBe(50);
});
});
describe('cleanupExpiredLogs', () => {
it('should cleanup expired logs successfully', async () => {
mockQueryBuilder.execute.mockResolvedValue({ affected: 25 });
const result = await service.cleanupExpiredLogs(30);
expect(mockQueryBuilder.delete).toHaveBeenCalled();
expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith('is_sensitive = :sensitive', { sensitive: false });
expect(result).toBe(25);
});
it('should use default retention days', async () => {
mockQueryBuilder.execute.mockResolvedValue({ affected: 10 });
const result = await service.cleanupExpiredLogs();
expect(result).toBe(10);
});
it('should handle cleanup error', async () => {
mockQueryBuilder.execute.mockRejectedValue(new Error('Cleanup error'));
await expect(service.cleanupExpiredLogs(30)).rejects.toThrow('Cleanup error');
});
});
describe('getAdminOperationHistory', () => {
it('should get admin operation history successfully', async () => {
const mockLogs = [
{ id: 'log1', admin_user_id: 'admin1' },
{ id: 'log2', admin_user_id: 'admin1' },
] as AdminOperationLog[];
mockRepository.find.mockResolvedValue(mockLogs);
const result = await service.getAdminOperationHistory('admin1', 10);
expect(mockRepository.find).toHaveBeenCalledWith({
where: { admin_user_id: 'admin1' },
order: { created_at: 'DESC' },
take: 10
});
expect(result).toEqual(mockLogs);
});
it('should use default limit', async () => {
mockRepository.find.mockResolvedValue([]);
const result = await service.getAdminOperationHistory('admin1');
expect(mockRepository.find).toHaveBeenCalledWith({
where: { admin_user_id: 'admin1' },
order: { created_at: 'DESC' },
take: 20 // DEFAULT_LIMIT
});
});
});
describe('getSensitiveOperations', () => {
it('should get sensitive operations successfully', async () => {
const mockLogs = [
{ id: 'log1', is_sensitive: true },
] as AdminOperationLog[];
mockRepository.findAndCount.mockResolvedValue([mockLogs, 1]);
const result = await service.getSensitiveOperations(10, 0);
expect(mockRepository.findAndCount).toHaveBeenCalledWith({
where: { is_sensitive: true },
order: { created_at: 'DESC' },
take: 10,
skip: 0
});
expect(result.logs).toEqual(mockLogs);
expect(result.total).toBe(1);
});
it('should use default pagination', async () => {
mockRepository.findAndCount.mockResolvedValue([[], 0]);
const result = await service.getSensitiveOperations();
expect(mockRepository.findAndCount).toHaveBeenCalledWith({
where: { is_sensitive: true },
order: { created_at: 'DESC' },
take: 50, // DEFAULT_LIMIT
skip: 0
});
});
});
});