/** * 错误处理属性测试 * * Property 9: 错误处理标准化 * * Validates: Requirements 4.6 * * 测试目标: * - 验证错误处理的标准化和一致性 * - 确保错误响应格式统一 * - 验证不同类型错误的正确处理 * * 最近修改: * - 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 { 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 controller: AdminDatabaseController; let mockUsersService: any; let mockUserProfilesService: any; let mockZulipAccountsService: any; beforeAll(async () => { mockUsersService = { findAll: jest.fn(), findOne: jest.fn(), create: jest.fn(), update: jest.fn(), remove: jest.fn(), search: jest.fn(), count: jest.fn() }; mockUserProfilesService = { findAll: jest.fn(), findOne: jest.fn(), create: jest.fn(), update: jest.fn(), remove: jest.fn(), findByMap: jest.fn(), count: jest.fn() }; mockZulipAccountsService = { findMany: jest.fn(), findById: jest.fn(), create: jest.fn(), update: jest.fn(), delete: jest.fn(), getStatusStatistics: jest.fn() }; module = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: ['.env.test', '.env'] }) ], controllers: [AdminDatabaseController], providers: [ DatabaseManagementService, { provide: AdminOperationLogService, useValue: { createLog: jest.fn().mockResolvedValue({}), queryLogs: jest.fn().mockResolvedValue({ logs: [], total: 0 }), getLogById: jest.fn().mockResolvedValue(null), getStatistics: jest.fn().mockResolvedValue({}), cleanupExpiredLogs: jest.fn().mockResolvedValue(0), getAdminOperationHistory: jest.fn().mockResolvedValue([]), getSensitiveOperations: jest.fn().mockResolvedValue({ logs: [], total: 0 }) } }, { provide: AdminOperationLogInterceptor, useValue: { intercept: jest.fn().mockImplementation((context, next) => next.handle()) } }, { provide: 'UsersService', useValue: mockUsersService }, { provide: 'IUserProfilesService', useValue: mockUserProfilesService }, { provide: 'ZulipAccountsService', useValue: mockZulipAccountsService } ] }) .overrideGuard(AdminGuard) .useValue({ canActivate: () => true }) .compile(); app = module.createNestApplication(); app.useGlobalFilters(new AdminDatabaseExceptionFilter()); await app.init(); controller = module.get(AdminDatabaseController); }); afterAll(async () => { await app.close(); }); describe('Property 9: 错误处理标准化', () => { it('数据库连接错误应该返回标准化错误响应', async () => { await PropertyTestRunner.runPropertyTest( '数据库连接错误标准化', () => PropertyTestGenerators.generateUser(), async (userData) => { // 模拟数据库连接错误 mockUsersService.create.mockRejectedValueOnce( new Error('Connection timeout') ); try { const response = await controller.createUser({ ...userData, status: UserStatus.ACTIVE }); // 如果没有抛出异常,验证错误响应格式 if (!response.success) { expect(response).toHaveProperty('success', false); expect(response).toHaveProperty('message'); expect(response).toHaveProperty('error_code'); expect(response).toHaveProperty('timestamp'); expect(response).toHaveProperty('request_id'); expect(typeof response.message).toBe('string'); expect(typeof response.error_code).toBe('string'); expect(typeof response.timestamp).toBe('string'); expect(typeof response.request_id).toBe('string'); } } catch (error) { // 如果抛出异常,验证异常被正确处理 expect(error).toBeDefined(); } }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 20 } ); }); it('资源不存在错误应该返回一致的404响应', async () => { await PropertyTestRunner.runPropertyTest( '资源不存在错误一致性', () => ({ entityType: ['User', 'UserProfile', 'ZulipAccount'][Math.floor(Math.random() * 3)], entityId: `nonexistent_${Math.floor(Math.random() * 1000)}` }), async ({ entityType, entityId }) => { // 模拟资源不存在 if (entityType === 'User') { mockUsersService.findOne.mockResolvedValueOnce(null); } else if (entityType === 'UserProfile') { mockUserProfilesService.findOne.mockResolvedValueOnce(null); } else { mockZulipAccountsService.findById.mockResolvedValueOnce(null); } try { let response; if (entityType === 'User') { response = await controller.getUserById(entityId); } else if (entityType === 'UserProfile') { response = await controller.getUserProfileById(entityId); } else { response = await controller.getZulipAccountById(entityId); } // 验证404错误响应格式 if (!response.success) { expect(response.success).toBe(false); expect(response.error_code).toContain('NOT_FOUND'); expect(response.message).toContain('not found'); PropertyTestAssertions.assertApiResponseFormat(response, false); } } catch (error: any) { // 验证异常包含正确信息 expect(error.message).toContain('not found'); } }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 25 } ); }); it('数据验证错误应该返回详细的错误信息', async () => { await PropertyTestRunner.runPropertyTest( '数据验证错误详细信息', () => { const invalidData = { username: '', // 空用户名 email: 'invalid-email', // 无效邮箱格式 role: -1, // 无效角色 status: 'INVALID_STATUS' as any // 无效状态 }; return invalidData; }, async (invalidData) => { // 模拟验证错误 mockUsersService.create.mockRejectedValueOnce( new Error('Validation failed: username is required, email format invalid') ); try { const response = await controller.createUser({ ...invalidData, nickname: 'Test Nickname' // 添加必需的nickname字段 }); if (!response.success) { expect(response.success).toBe(false); expect(response.error_code).toContain('VALIDATION'); expect(response.message).toContain('validation'); PropertyTestAssertions.assertApiResponseFormat(response, false); // 验证错误信息包含具体字段 expect(response.message.toLowerCase()).toMatch(/(username|email|role|status)/); } } catch (error: any) { expect(error.message).toContain('validation'); } }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 20 } ); }); it('权限不足错误应该返回标准化403响应', async () => { await PropertyTestRunner.runPropertyTest( '权限不足错误标准化', () => PropertyTestGenerators.generateUser(), async (userData) => { // 模拟权限不足错误 mockUsersService.create.mockRejectedValueOnce( new Error('Insufficient permissions') ); try { const response = await controller.createUser({ ...userData, status: UserStatus.ACTIVE }); if (!response.success) { expect(response.success).toBe(false); expect(response.error_code).toContain('FORBIDDEN'); expect(response.message).toContain('permission'); PropertyTestAssertions.assertApiResponseFormat(response, false); } } catch (error: any) { expect(error.message).toContain('permission'); } }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 15 } ); }); it('并发冲突错误应该返回适当的错误响应', async () => { await PropertyTestRunner.runPropertyTest( '并发冲突错误处理', () => ({ user: PropertyTestGenerators.generateUser(), conflictType: ['duplicate_key', 'version_conflict', 'resource_locked'][ Math.floor(Math.random() * 3) ] }), async ({ user, conflictType }) => { // 模拟不同类型的并发冲突 let errorMessage; switch (conflictType) { case 'duplicate_key': errorMessage = 'Duplicate key violation: username already exists'; break; case 'version_conflict': errorMessage = 'Version conflict: resource was modified by another user'; break; case 'resource_locked': errorMessage = 'Resource is locked by another operation'; break; } mockUsersService.create.mockRejectedValueOnce(new Error(errorMessage)); try { const response = await controller.createUser({ ...user, status: UserStatus.ACTIVE }); if (!response.success) { expect(response.success).toBe(false); expect(response.error_code).toContain('CONFLICT'); PropertyTestAssertions.assertApiResponseFormat(response, false); // 验证错误信息反映冲突类型 if (conflictType === 'duplicate_key') { expect(response.message).toContain('duplicate'); } else if (conflictType === 'version_conflict') { expect(response.message).toContain('conflict'); } else { expect(response.message).toContain('locked'); } } } catch (error: any) { expect(error.message).toBe(errorMessage); } }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 20 } ); }); it('系统内部错误应该返回通用错误响应', async () => { await PropertyTestRunner.runPropertyTest( '系统内部错误处理', () => PropertyTestGenerators.generateUser(), async (userData) => { // 模拟系统内部错误 mockUsersService.create.mockRejectedValueOnce( new Error('Internal system error: unexpected null pointer') ); try { const response = await controller.createUser({ ...userData, status: UserStatus.ACTIVE }); if (!response.success) { expect(response.success).toBe(false); expect(response.error_code).toContain('INTERNAL_ERROR'); expect(response.message).toContain('internal error'); PropertyTestAssertions.assertApiResponseFormat(response, false); // 内部错误不应该暴露敏感信息 expect(response.message).not.toContain('null pointer'); expect(response.message).not.toContain('stack trace'); } } catch (error: any) { // 如果抛出异常,验证异常被适当处理 expect(error).toBeDefined(); } }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 15 } ); }); it('网络超时错误应该返回适当的错误响应', async () => { await PropertyTestRunner.runPropertyTest( '网络超时错误处理', () => PropertyTestGenerators.generateUser(), async (userData) => { // 模拟网络超时错误 const timeoutError = new Error('Request timeout'); timeoutError.name = 'TimeoutError'; mockUsersService.create.mockRejectedValueOnce(timeoutError); try { const response = await controller.createUser({ ...userData, status: UserStatus.ACTIVE }); if (!response.success) { expect(response.success).toBe(false); expect(response.error_code).toContain('TIMEOUT'); expect(response.message).toContain('timeout'); PropertyTestAssertions.assertApiResponseFormat(response, false); } } catch (error: any) { expect(error.message).toContain('timeout'); } }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 15 } ); }); it('错误响应应该包含有用的调试信息', async () => { await PropertyTestRunner.runPropertyTest( '错误调试信息完整性', () => PropertyTestGenerators.generateUser(), async (userData) => { // 模拟带详细信息的错误 mockUsersService.create.mockRejectedValueOnce( new Error('Database constraint violation: unique_username_constraint') ); try { const response = await controller.createUser({ ...userData, status: UserStatus.ACTIVE }); if (!response.success) { PropertyTestAssertions.assertApiResponseFormat(response, false); // 验证调试信息 expect(response.timestamp).toBeDefined(); expect(response.request_id).toBeDefined(); expect(response.error_code).toBeDefined(); // 验证时间戳格式 const timestamp = new Date(response.timestamp); expect(timestamp.toISOString()).toBe(response.timestamp); // 验证请求ID格式 expect(response.request_id).toMatch(/^[a-zA-Z0-9_-]+$/); // 验证错误码格式 expect(response.error_code).toMatch(/^[A-Z_]+$/); } } catch (error: any) { expect(error).toBeDefined(); } }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 20 } ); }); it('批量操作中的部分错误应该被正确处理', async () => { await PropertyTestRunner.runPropertyTest( '批量操作部分错误处理', () => { const accountIds = Array.from({ length: Math.floor(Math.random() * 5) + 3 }, (_, i) => `account_${i + 1}`); const targetStatus = 'active' as const; return { accountIds, targetStatus }; }, async ({ accountIds, targetStatus }) => { // 模拟部分成功,部分失败的批量操作 accountIds.forEach((id, index) => { if (index === 0) { // 第一个操作失败 mockZulipAccountsService.update.mockRejectedValueOnce( new Error(`Failed to update account ${id}: validation error`) ); } else { // 其他操作成功 mockZulipAccountsService.update.mockResolvedValueOnce({ id, status: targetStatus, ...PropertyTestGenerators.generateZulipAccount() }); } }); const response = await controller.batchUpdateZulipAccountStatus({ ids: accountIds, status: targetStatus, reason: '测试批量更新' }); expect(response.success).toBe(true); // 批量操作本身成功 expect(response.data.failed).toBe(1); // 一个失败 expect(response.data.success).toBe(accountIds.length - 1); // 其他成功 // 验证错误信息格式 expect(response.data.errors).toHaveLength(1); expect(response.data.errors[0]).toHaveProperty('id'); expect(response.data.errors[0]).toHaveProperty('success', false); expect(response.data.errors[0]).toHaveProperty('error'); PropertyTestAssertions.assertApiResponseFormat(response, true); }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 15 } ); }); }); });