/** * 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>; 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); 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 }); }); }); });