/** * API响应格式一致性属性测试 * * Property 7: API响应格式一致性 * Validates: Requirements 4.1, 4.2, 4.3 * * 测试目标: * - 验证所有API端点返回统一的响应格式 * - 确保成功和失败响应都符合规范 * - 验证响应字段类型和必需性 * * 最近修改: * - 2026-01-08: 注释规范优化 - 修正@author字段,更新版本号和修改记录 (修改者: moyin) * - 2026-01-08: 功能新增 - 创建API响应格式属性测试 (修改者: 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 './admin_database.controller'; import { DatabaseManagementService } from './database_management.service'; import { AdminOperationLogService } from './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 '../user_mgmt/user_status.enum'; import { PropertyTestRunner, PropertyTestGenerators, PropertyTestAssertions, DEFAULT_PROPERTY_CONFIG } from './admin_property_test.base'; describe('Property Test: API响应格式一致性', () => { let app: INestApplication; let module: TestingModule; let controller: AdminDatabaseController; let mockDatabaseService: any; beforeAll(async () => { mockDatabaseService = { getUserList: jest.fn().mockImplementation((limit, offset) => { return Promise.resolve({ success: true, data: { items: [], total: 0, limit: limit || 20, offset: offset || 0, has_more: false }, message: '获取用户列表成功', timestamp: new Date().toISOString(), request_id: 'test_' + Date.now() }); }), getUserById: jest.fn().mockImplementation((id) => { const user = PropertyTestGenerators.generateUser(); return Promise.resolve({ success: true, data: { ...user, id: id.toString() }, message: '获取用户详情成功', timestamp: new Date().toISOString(), request_id: 'test_' + Date.now() }); }), createUser: jest.fn().mockImplementation((userData) => { return Promise.resolve({ success: true, data: { ...userData, id: '1' }, message: '创建用户成功', timestamp: new Date().toISOString(), request_id: 'test_' + Date.now() }); }), updateUser: jest.fn().mockImplementation((id, updateData) => { const user = PropertyTestGenerators.generateUser(); return Promise.resolve({ success: true, data: { ...user, ...updateData, id: id.toString() }, message: '更新用户成功', timestamp: new Date().toISOString(), request_id: 'test_' + Date.now() }); }), deleteUser: jest.fn().mockImplementation((id) => { return Promise.resolve({ success: true, data: { deleted: true, id: id.toString() }, message: '删除用户成功', timestamp: new Date().toISOString(), request_id: 'test_' + Date.now() }); }), searchUsers: jest.fn().mockImplementation((searchTerm, limit) => { return Promise.resolve({ success: true, data: { items: [], total: 0, limit: limit || 20, offset: 0, has_more: false }, message: '搜索用户成功', timestamp: new Date().toISOString(), request_id: 'test_' + Date.now() }); }), getUserProfileList: jest.fn().mockImplementation((limit, offset) => { return Promise.resolve({ success: true, data: { items: [], total: 0, limit: limit || 20, offset: offset || 0, has_more: false }, message: '获取用户档案列表成功', timestamp: new Date().toISOString(), request_id: 'test_' + Date.now() }); }), getUserProfileById: jest.fn().mockImplementation((id) => { const profile = PropertyTestGenerators.generateUserProfile(); return Promise.resolve({ success: true, data: { ...profile, id: id.toString() }, message: '获取用户档案详情成功', timestamp: new Date().toISOString(), request_id: 'test_' + Date.now() }); }), getUserProfilesByMap: jest.fn().mockImplementation((map, limit, offset) => { return Promise.resolve({ success: true, data: { items: [], total: 0, limit: limit || 20, offset: offset || 0, has_more: false }, message: '按地图获取用户档案成功', timestamp: new Date().toISOString(), request_id: 'test_' + Date.now() }); }), getZulipAccountList: jest.fn().mockImplementation((limit, offset) => { return Promise.resolve({ success: true, data: { items: [], total: 0, limit: limit || 20, offset: offset || 0, has_more: false }, message: '获取Zulip账号列表成功', timestamp: new Date().toISOString(), request_id: 'test_' + Date.now() }); }), getZulipAccountById: jest.fn().mockImplementation((id) => { const account = PropertyTestGenerators.generateZulipAccount(); return Promise.resolve({ success: true, data: { ...account, id: id.toString() }, message: '获取Zulip账号详情成功', timestamp: new Date().toISOString(), request_id: 'test_' + Date.now() }); }), getZulipAccountStatistics: jest.fn().mockImplementation(() => { return Promise.resolve({ success: true, data: { active: 0, inactive: 0, suspended: 0, error: 0, total: 0 }, message: '获取Zulip账号统计成功', timestamp: new Date().toISOString(), request_id: 'test_' + Date.now() }); }) }; module = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: ['.env.test', '.env'] }) ], controllers: [AdminDatabaseController], providers: [ { provide: DatabaseManagementService, useValue: mockDatabaseService }, { 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()) } } ] }) .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 7: API响应格式一致性', () => { it('所有成功响应应该有统一的格式', async () => { await PropertyTestRunner.runPropertyTest( 'API成功响应格式一致性', () => PropertyTestGenerators.generateUser(), async (userData) => { // 测试用户管理端点 const userListResponse = await controller.getUserList(20, 0); PropertyTestAssertions.assertListResponseFormat(userListResponse); const userDetailResponse = await controller.getUserById('1'); PropertyTestAssertions.assertApiResponseFormat(userDetailResponse, true); const createUserResponse = await controller.createUser({ ...userData, status: UserStatus.ACTIVE }); PropertyTestAssertions.assertApiResponseFormat(createUserResponse, true); // 测试用户档案管理端点 const profileListResponse = await controller.getUserProfileList(20, 0); PropertyTestAssertions.assertListResponseFormat(profileListResponse); const profileDetailResponse = await controller.getUserProfileById('1'); PropertyTestAssertions.assertApiResponseFormat(profileDetailResponse, true); // 测试Zulip账号管理端点 const zulipListResponse = await controller.getZulipAccountList(20, 0); PropertyTestAssertions.assertListResponseFormat(zulipListResponse); const zulipDetailResponse = await controller.getZulipAccountById('1'); PropertyTestAssertions.assertApiResponseFormat(zulipDetailResponse, true); const zulipStatsResponse = await controller.getZulipAccountStatistics(); PropertyTestAssertions.assertApiResponseFormat(zulipStatsResponse, true); // 测试系统端点 const healthResponse = await controller.healthCheck(); PropertyTestAssertions.assertApiResponseFormat(healthResponse, true); }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 50 } ); }); it('所有列表响应应该有正确的分页信息', async () => { await PropertyTestRunner.runPropertyTest( '列表响应分页格式一致性', () => PropertyTestGenerators.generatePaginationParams(), async (paginationParams) => { const { limit, offset } = paginationParams; // 限制参数范围以避免无效请求 const safeLimit = Math.min(Math.max(limit, 1), 100); const safeOffset = Math.max(offset, 0); // 测试所有列表端点 const userListResponse = await controller.getUserList(safeLimit, safeOffset); PropertyTestAssertions.assertPaginationLogic(userListResponse, safeLimit, safeOffset); const profileListResponse = await controller.getUserProfileList(safeLimit, safeOffset); PropertyTestAssertions.assertPaginationLogic(profileListResponse, safeLimit, safeOffset); const zulipListResponse = await controller.getZulipAccountList(safeLimit, safeOffset); PropertyTestAssertions.assertPaginationLogic(zulipListResponse, safeLimit, safeOffset); const mapProfilesResponse = await controller.getUserProfilesByMap('plaza', safeLimit, safeOffset); PropertyTestAssertions.assertPaginationLogic(mapProfilesResponse, safeLimit, safeOffset); }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 30 } ); }); it('响应时间戳应该是有效的ISO格式', async () => { await PropertyTestRunner.runPropertyTest( '响应时间戳格式验证', () => ({}), async () => { const response = await controller.healthCheck(); expect(response.timestamp).toBeDefined(); expect(typeof response.timestamp).toBe('string'); // 验证ISO 8601格式 const timestamp = new Date(response.timestamp); expect(timestamp.toISOString()).toBe(response.timestamp); // 验证时间戳是最近的(在过去1分钟内) const now = new Date(); const timeDiff = now.getTime() - timestamp.getTime(); expect(timeDiff).toBeLessThan(60000); // 1分钟 }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 20 } ); }); it('请求ID应该是唯一的', async () => { const requestIds = new Set(); await PropertyTestRunner.runPropertyTest( '请求ID唯一性验证', () => ({}), async () => { const response = await controller.healthCheck(); expect(response.request_id).toBeDefined(); expect(typeof response.request_id).toBe('string'); expect(response.request_id.length).toBeGreaterThan(0); // 验证请求ID唯一性 expect(requestIds.has(response.request_id)).toBe(false); requestIds.add(response.request_id); }, { ...DEFAULT_PROPERTY_CONFIG, iterations: 100 } ); }); }); });