/** * 操作日志属性测试 * * Property 11: 操作日志完整性 * * Validates: Requirements 7.1, 7.2, 7.3, 7.4, 7.5 * * 测试目标: * - 验证操作日志记录的完整性和准确性 * - 确保敏感操作被正确记录 * - 验证日志查询和统计功能 * * 最近修改: * - 2026-01-08: 注释规范优化 - 修正@author字段,更新版本号和修改记录 (修改者: moyin) * - 2026-01-08: 功能新增 - 创建操作日志属性测试 (修改者: assistant) * * @author moyin * @version 1.0.1 * @since 2026-01-08 * @lastModified 2026-01-08 */ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AdminDatabaseController } from '../../controllers/admin_database.controller'; import { AdminOperationLogController } from '../../controllers/admin_operation_log.controller'; import { DatabaseManagementService } from '../../services/database_management.service'; import { AdminOperationLogService } from '../../services/admin_operation_log.service'; import { AdminOperationLogInterceptor } from '../../admin_operation_log.interceptor'; import { AdminDatabaseExceptionFilter } from '../../admin_database_exception.filter'; import { AdminGuard } from '../../admin.guard'; import { UserStatus } from '../../../../core/db/users/user_status.enum'; import { PropertyTestRunner, PropertyTestGenerators, PropertyTestAssertions, DEFAULT_PROPERTY_CONFIG } from './admin_property_test.base'; describe('Property Test: 操作日志功能', () => { let app: INestApplication; let module: TestingModule; let databaseController: AdminDatabaseController; let logController: AdminOperationLogController; let mockLogService: any; let logEntries: any[] = []; beforeAll(async () => { logEntries = []; mockLogService = { createLog: jest.fn().mockImplementation((logData) => { const logEntry = { id: `log_${logEntries.length + 1}`, ...logData, created_at: new Date().toISOString() }; logEntries.push(logEntry); return Promise.resolve(logEntry); }), queryLogs: jest.fn().mockImplementation((filters, limit, offset) => { let filteredLogs = [...logEntries]; if (filters.operation_type) { filteredLogs = filteredLogs.filter(log => log.operation_type === filters.operation_type); } if (filters.admin_id) { filteredLogs = filteredLogs.filter(log => log.admin_id === filters.admin_id); } if (filters.entity_type) { filteredLogs = filteredLogs.filter(log => log.entity_type === filters.entity_type); } const total = filteredLogs.length; const paginatedLogs = filteredLogs.slice(offset, offset + limit); return Promise.resolve({ logs: paginatedLogs, total }); }), getLogById: jest.fn().mockImplementation((id) => { const log = logEntries.find(entry => entry.id === id); return Promise.resolve(log || null); }), getStatistics: jest.fn().mockImplementation(() => { const stats = { totalOperations: logEntries.length, operationsByType: {}, operationsByAdmin: {}, recentActivity: logEntries.slice(-10) }; logEntries.forEach(log => { stats.operationsByType[log.operation_type] = (stats.operationsByType[log.operation_type] || 0) + 1; stats.operationsByAdmin[log.admin_id] = (stats.operationsByAdmin[log.admin_id] || 0) + 1; }); return Promise.resolve(stats); }), cleanupExpiredLogs: jest.fn().mockResolvedValue(0), getAdminOperationHistory: jest.fn().mockImplementation((adminId) => { const adminLogs = logEntries.filter(log => log.admin_id === adminId); return Promise.resolve(adminLogs); }), getSensitiveOperations: jest.fn().mockImplementation((limit, offset) => { const sensitiveOps = ['DELETE', 'BATCH_UPDATE', 'ADMIN_CREATE']; const sensitiveLogs = logEntries.filter(log => sensitiveOps.includes(log.operation_type) ); const total = sensitiveLogs.length; const paginatedLogs = sensitiveLogs.slice(offset, offset + limit); return Promise.resolve({ logs: paginatedLogs, total }); }) }; module = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: ['.env.test', '.env'] }) ], controllers: [AdminDatabaseController, AdminOperationLogController], providers: [ DatabaseManagementService, { provide: AdminOperationLogService, useValue: mockLogService }, { provide: AdminOperationLogInterceptor, useValue: { intercept: jest.fn().mockImplementation((context, next) => next.handle()) } }, { provide: 'UsersService', useValue: { findAll: jest.fn().mockResolvedValue([]), findOne: jest.fn().mockImplementation(() => { const user = PropertyTestGenerators.generateUser(); return Promise.resolve({ ...user, id: BigInt(1) }); }), create: jest.fn().mockImplementation((userData) => { return Promise.resolve({ ...userData, id: BigInt(1) }); }), update: jest.fn().mockImplementation((id, updateData) => { const user = PropertyTestGenerators.generateUser(); return Promise.resolve({ ...user, ...updateData, id }); }), remove: jest.fn().mockResolvedValue(undefined), search: jest.fn().mockResolvedValue([]), count: jest.fn().mockResolvedValue(0) } }, { provide: 'IUserProfilesService', useValue: { findAll: jest.fn().mockResolvedValue([]), findOne: jest.fn().mockResolvedValue({ id: BigInt(1) }), create: jest.fn().mockResolvedValue({ id: BigInt(1) }), update: jest.fn().mockResolvedValue({ id: BigInt(1) }), remove: jest.fn().mockResolvedValue(undefined), findByMap: jest.fn().mockResolvedValue([]), count: jest.fn().mockResolvedValue(0) } }, { provide: 'ZulipAccountsService', useValue: { findMany: jest.fn().mockResolvedValue({ accounts: [] }), findById: jest.fn().mockResolvedValue({ id: '1' }), create: jest.fn().mockResolvedValue({ id: '1' }), update: jest.fn().mockResolvedValue({ id: '1' }), delete: jest.fn().mockResolvedValue(undefined), getStatusStatistics: jest.fn().mockResolvedValue({ active: 0, inactive: 0, suspended: 0, error: 0, total: 0 }) } } ] }) .overrideGuard(AdminGuard) .useValue({ canActivate: () => true }) .compile(); app = module.createNestApplication(); app.useGlobalFilters(new AdminDatabaseExceptionFilter()); await app.init(); databaseController = module.get(AdminDatabaseController); logController = module.get(AdminOperationLogController); }); afterAll(async () => { await app.close(); }); beforeEach(() => { logEntries.length = 0; // 清空日志记录 }); describe('Property 11: 操作日志完整性', () => { it('所有CRUD操作都应该生成日志记录', async () => { await PropertyTestRunner.runPropertyTest( 'CRUD操作日志记录完整性', () => PropertyTestGenerators.generateUser(), async (userData) => { const userWithStatus = { ...userData, status: UserStatus.ACTIVE }; // 执行创建操作 await databaseController.createUser(userWithStatus); // 执行读取操作 await databaseController.getUserById('1'); // 执行更新操作 await databaseController.updateUser('1', { nickname: 'Updated Name' }); // 执行删除操作 await databaseController.deleteUser('1'); // 验证日志记录 expect(mockLogService.createLog).toHaveBeenCalledTimes(4); // 验证日志内容包含必要信息 const createLogCall = mockLogService.createLog.mock.calls.find(call => call[0].operation_type === 'CREATE' ); const updateLogCall = mockLogService.createLog.mock.calls.find(call => call[0].operation_type === 'UPDATE' ); const deleteLogCall = mockLogService.createLog.mock.calls.find(call => call[0].operation_type === 'DELETE' ); expect(createLogCall).toBeDefined(); expect(updateLogCall).toBeDefined(); expect(deleteLogCall).toBeDefined(); // 验证日志包含实体信息 expect(createLogCall[0].entity_type).toBe('User'); expect(updateLogCall[0].entity_type).toBe('User'); expect(deleteLogCall[0].entity_type).toBe('User'); }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 20 } ); }); it('日志记录应该包含完整的操作上下文', async () => { await PropertyTestRunner.runPropertyTest( '日志上下文完整性', () => ({ user: PropertyTestGenerators.generateUser(), adminId: `admin_${Math.floor(Math.random() * 1000)}`, ipAddress: `192.168.1.${Math.floor(Math.random() * 255)}`, userAgent: 'Test-Agent/1.0' }), async ({ user, adminId, ipAddress, userAgent }) => { const userWithStatus = { ...user, status: UserStatus.ACTIVE }; // 模拟带上下文的操作 await databaseController.createUser(userWithStatus); // 验证日志记录包含上下文信息 expect(mockLogService.createLog).toHaveBeenCalled(); const logCall = mockLogService.createLog.mock.calls[0][0]; expect(logCall).toHaveProperty('operation_type'); expect(logCall).toHaveProperty('entity_type'); expect(logCall).toHaveProperty('entity_id'); expect(logCall).toHaveProperty('admin_id'); expect(logCall).toHaveProperty('operation_details'); expect(logCall).toHaveProperty('timestamp'); // 验证时间戳格式 expect(new Date(logCall.timestamp)).toBeInstanceOf(Date); }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 25 } ); }); it('敏感操作应该记录详细的前后状态', async () => { await PropertyTestRunner.runPropertyTest( '敏感操作详细日志', () => ({ accounts: Array.from({ length: Math.floor(Math.random() * 5) + 2 }, () => PropertyTestGenerators.generateZulipAccount()), targetStatus: ['active', 'inactive', 'suspended'][Math.floor(Math.random() * 3)] }), async ({ accounts, targetStatus }) => { const accountIds = accounts.map((_, i) => `account_${i + 1}`); // 执行批量更新操作(敏感操作) await databaseController.batchUpdateZulipAccountStatus({ ids: accountIds, status: targetStatus as any, reason: '测试批量更新' }); // 验证敏感操作日志 expect(mockLogService.createLog).toHaveBeenCalled(); const logCall = mockLogService.createLog.mock.calls[0][0]; expect(logCall.operation_type).toBe('BATCH_UPDATE'); expect(logCall.entity_type).toBe('ZulipAccount'); expect(logCall.operation_details).toContain('reason'); expect(logCall.operation_details).toContain(targetStatus); }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 15 } ); }); it('日志查询应该支持多种过滤条件', async () => { await PropertyTestRunner.runPropertyTest( '日志查询过滤功能', () => { // 预先创建一些日志记录 const operations = ['CREATE', 'UPDATE', 'DELETE', 'BATCH_UPDATE']; const entities = ['User', 'UserProfile', 'ZulipAccount']; const adminIds = ['admin1', 'admin2', 'admin3']; return { operation_type: operations[Math.floor(Math.random() * operations.length)], entity_type: entities[Math.floor(Math.random() * entities.length)], admin_id: adminIds[Math.floor(Math.random() * adminIds.length)] }; }, async (filters) => { // 预先添加一些测试日志 await mockLogService.createLog({ operation_type: filters.operation_type, entity_type: filters.entity_type, admin_id: filters.admin_id, entity_id: '1', operation_details: JSON.stringify({ test: true }), timestamp: new Date().toISOString() }); // 查询日志 const response = await logController.queryLogs( filters.operation_type, filters.entity_type, filters.admin_id, undefined, undefined, '20', // 修复:传递字符串而不是数字 0 ); expect(response.success).toBe(true); PropertyTestAssertions.assertListResponseFormat(response); // 验证过滤结果 response.data.items.forEach((log: any) => { expect(log.operation_type).toBe(filters.operation_type); expect(log.entity_type).toBe(filters.entity_type); expect(log.admin_id).toBe(filters.admin_id); }); }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 20 } ); }); it('日志统计应该准确反映操作情况', async () => { await PropertyTestRunner.runPropertyTest( '日志统计准确性', () => { const operations = Array.from({ length: Math.floor(Math.random() * 10) + 5 }, () => ({ operation_type: ['CREATE', 'UPDATE', 'DELETE'][Math.floor(Math.random() * 3)], entity_type: ['User', 'UserProfile'][Math.floor(Math.random() * 2)], admin_id: `admin_${Math.floor(Math.random() * 3) + 1}` })); return { operations }; }, async ({ operations }) => { // 创建测试日志 for (const op of operations) { await mockLogService.createLog({ ...op, entity_id: '1', operation_details: JSON.stringify({}), timestamp: new Date().toISOString() }); } // 获取统计信息 const response = await logController.getStatistics(); expect(response.success).toBe(true); expect(response.data.totalOperations).toBe(operations.length); expect(response.data.operationsByType).toBeDefined(); expect(response.data.operationsByAdmin).toBeDefined(); // 验证统计数据准确性 const expectedByType = {}; const expectedByAdmin = {}; operations.forEach(op => { expectedByType[op.operation_type] = (expectedByType[op.operation_type] || 0) + 1; expectedByAdmin[op.admin_id] = (expectedByAdmin[op.admin_id] || 0) + 1; }); expect(response.data.operationsByType).toEqual(expectedByType); expect(response.data.operationsByAdmin).toEqual(expectedByAdmin); }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 15 } ); }); it('敏感操作查询应该正确识别和过滤', async () => { await PropertyTestRunner.runPropertyTest( '敏感操作识别准确性', () => { const allOperations = ['CREATE', 'READ', 'UPDATE', 'DELETE', 'BATCH_UPDATE', 'ADMIN_CREATE']; const operations = Array.from({ length: Math.floor(Math.random() * 8) + 3 }, () => allOperations[Math.floor(Math.random() * allOperations.length)] ); return { operations }; }, async ({ operations }) => { // 创建测试日志 for (const op of operations) { await mockLogService.createLog({ operation_type: op, entity_type: 'User', admin_id: 'admin1', entity_id: '1', operation_details: JSON.stringify({}), timestamp: new Date().toISOString() }); } // 查询敏感操作 const response = await logController.getSensitiveOperations(20, 0); expect(response.success).toBe(true); PropertyTestAssertions.assertListResponseFormat(response); // 验证只返回敏感操作 const sensitiveOps = ['DELETE', 'BATCH_UPDATE', 'ADMIN_CREATE']; const expectedSensitiveCount = operations.filter(op => sensitiveOps.includes(op) ).length; expect(response.data.total).toBe(expectedSensitiveCount); response.data.items.forEach((log: any) => { expect(sensitiveOps).toContain(log.operation_type); }); }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 20 } ); }); it('管理员操作历史应该完整记录', async () => { await PropertyTestRunner.runPropertyTest( '管理员操作历史完整性', () => { const adminId = `admin_${Math.floor(Math.random() * 100)}`; const operations = Array.from({ length: Math.floor(Math.random() * 5) + 2 }, () => ({ operation_type: ['CREATE', 'UPDATE', 'DELETE'][Math.floor(Math.random() * 3)], entity_type: 'User', admin_id: adminId })); return { adminId, operations }; }, async ({ adminId, operations }) => { // 创建该管理员的操作日志 for (const op of operations) { await mockLogService.createLog({ ...op, entity_id: '1', operation_details: JSON.stringify({}), timestamp: new Date().toISOString() }); } // 创建其他管理员的操作日志(干扰数据) await mockLogService.createLog({ operation_type: 'CREATE', entity_type: 'User', admin_id: 'other_admin', entity_id: '2', operation_details: JSON.stringify({}), timestamp: new Date().toISOString() }); // 查询特定管理员的操作历史 const response = await logController.getAdminOperationHistory(adminId); expect(response.success).toBe(true); expect(response.data).toHaveLength(operations.length); // 验证所有返回的日志都属于指定管理员 response.data.forEach((log: any) => { expect(log.admin_id).toBe(adminId); }); }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 15 } ); }); }); });