- 更新管理员控制器和数据库管理功能 - 完善管理员操作日志系统 - 添加全面的属性测试覆盖 - 优化用户管理和用户档案服务 - 更新代码检查规范文档 功能改进: - 增强管理员权限验证 - 完善操作日志记录 - 优化数据库管理接口 - 提升系统安全性和可维护性
407 lines
13 KiB
TypeScript
407 lines
13 KiB
TypeScript
/**
|
||
* 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
|
||
});
|
||
});
|
||
});
|
||
}); |