/** * 管理员系统属性测试基础框架 * * 功能描述: * - 提供属性测试的基础工具和断言 * - 实现通用的测试数据生成器 * - 支持随机化测试和边界条件验证 * * 属性测试原理: * - 验证系统在各种输入条件下的通用正确性属性 * - 通过大量随机测试用例发现边界问题 * - 确保系统行为的一致性和可靠性 * * 最近修改: * - 2026-01-08: 注释规范优化 - 修正@author字段,更新版本号和修改记录 (修改者: moyin) * - 2026-01-08: 注释规范优化 - 为接口添加注释,完善文档说明 (修改者: moyin) * - 2026-01-08: 功能新增 - 创建属性测试基础框架 (修改者: assistant) * * @author moyin * @version 1.0.2 * @since 2026-01-08 * @lastModified 2026-01-08 */ import { Logger } from '@nestjs/common'; import { UserStatus } from '../user_mgmt/user_status.enum'; /** * 属性测试配置接口 * * 功能描述: * 定义属性测试的运行配置参数 * * 使用场景: * - 配置属性测试的迭代次数和超时时间 * - 设置随机种子以确保测试的可重现性 */ export interface PropertyTestConfig { iterations: number; timeout: number; seed?: number; } export const DEFAULT_PROPERTY_CONFIG: PropertyTestConfig = { iterations: 100, timeout: 30000, seed: 12345 }; /** * 属性测试生成器 */ export class PropertyTestGenerators { /** * 生成随机用户数据 */ static generateUser(seed?: number) { const random = seed ? Math.sin(seed) * 10000 - Math.floor(Math.sin(seed) * 10000) : Math.random(); const id = Math.floor(random * 1000000); return { username: `testuser${id}`, nickname: `Test User ${id}`, email: `test${id}@example.com`, phone: `138${String(id).padStart(8, '0').substring(0, 8)}`, role: Math.floor(random * 10), status: ['ACTIVE', 'INACTIVE', 'SUSPENDED'][Math.floor(random * 3)] as any, avatar_url: `https://example.com/avatar${id}.jpg`, github_id: `github${id}` }; } /** * 生成随机用户档案数据 */ static generateUserProfile(seed?: number) { const random = seed ? Math.sin(seed) * 10000 - Math.floor(Math.sin(seed) * 10000) : Math.random(); const id = Math.floor(random * 1000000); return { user_id: String(id), bio: `This is a test bio for user ${id}`, resume_content: `Test resume content for user ${id}. This is a sample resume.`, tags: JSON.stringify(['developer', 'tester']), social_links: JSON.stringify({ github: `https://github.com/user${id}`, linkedin: `https://linkedin.com/in/user${id}` }), skin_id: `skin${id}`, current_map: ['plaza', 'forest', 'beach', 'mountain'][Math.floor(random * 4)], pos_x: random * 1000, pos_y: random * 1000, status: Math.floor(random * 3) }; } /** * 生成随机Zulip账号数据 */ static generateZulipAccount(seed?: number) { const random = seed ? Math.sin(seed) * 10000 - Math.floor(Math.sin(seed) * 10000) : Math.random(); const id = Math.floor(random * 1000000); const statuses = ['active', 'inactive', 'suspended', 'error'] as const; return { gameUserId: String(id), zulipUserId: Math.floor(random * 999999) + 1, zulipEmail: `zulip${id}@example.com`, zulipFullName: `Zulip User ${id}`, zulipApiKeyEncrypted: `encrypted_key_${id}`, status: statuses[Math.floor(random * 4)] }; } /** * 生成随机分页参数 */ static generatePaginationParams(seed?: number) { const random = seed ? Math.sin(seed) * 10000 - Math.floor(Math.sin(seed) * 10000) : Math.random(); return { limit: Math.floor(random * 100) + 1, offset: Math.floor(random * 1000) }; } /** * 生成边界值测试数据 */ static generateBoundaryValues() { return { limits: [0, 1, 50, 100, 101, 999, 1000], offsets: [0, 1, 100, 999, 1000, 9999], strings: ['', 'a', 'x'.repeat(50), 'x'.repeat(255), 'x'.repeat(256)], numbers: [-1, 0, 1, 999, 1000, 9999, 99999] }; } } /** * 属性测试断言工具 */ export class PropertyTestAssertions { /** * 验证API响应格式一致性 */ static assertApiResponseFormat(response: any, shouldHaveData: boolean = true) { expect(response).toHaveProperty('success'); expect(response).toHaveProperty('message'); expect(response).toHaveProperty('timestamp'); expect(response).toHaveProperty('request_id'); expect(typeof response.success).toBe('boolean'); expect(typeof response.message).toBe('string'); expect(typeof response.timestamp).toBe('string'); expect(typeof response.request_id).toBe('string'); if (shouldHaveData && response.success) { expect(response).toHaveProperty('data'); } if (!response.success) { expect(response).toHaveProperty('error_code'); expect(typeof response.error_code).toBe('string'); } } /** * 验证列表响应格式 */ static assertListResponseFormat(response: any) { this.assertApiResponseFormat(response, true); expect(response.data).toHaveProperty('items'); expect(response.data).toHaveProperty('total'); expect(response.data).toHaveProperty('limit'); expect(response.data).toHaveProperty('offset'); expect(response.data).toHaveProperty('has_more'); expect(Array.isArray(response.data.items)).toBe(true); expect(typeof response.data.total).toBe('number'); expect(typeof response.data.limit).toBe('number'); expect(typeof response.data.offset).toBe('number'); expect(typeof response.data.has_more).toBe('boolean'); } /** * 验证分页逻辑正确性 */ static assertPaginationLogic(response: any, requestedLimit: number, requestedOffset: number) { this.assertListResponseFormat(response); const { items, total, limit, offset, has_more } = response.data; // 验证分页参数 expect(limit).toBeLessThanOrEqual(100); // 最大限制 expect(offset).toBeGreaterThanOrEqual(0); // 验证has_more逻辑 const expectedHasMore = offset + items.length < total; expect(has_more).toBe(expectedHasMore); // 验证返回项目数量 expect(items.length).toBeLessThanOrEqual(limit); } /** * 验证CRUD操作一致性 */ static assertCrudConsistency(createResponse: any, readResponse: any, updateResponse: any) { // 创建和读取的数据应该一致 expect(createResponse.success).toBe(true); expect(readResponse.success).toBe(true); expect(createResponse.data.id).toBe(readResponse.data.id); // 更新后的数据应该反映变更 expect(updateResponse.success).toBe(true); expect(updateResponse.data.id).toBe(createResponse.data.id); } } /** * 属性测试运行器 */ export class PropertyTestRunner { static async runPropertyTest( testName: string, generator: () => T, testFunction: (input: T) => Promise, config: PropertyTestConfig = DEFAULT_PROPERTY_CONFIG ): Promise { const logger = new Logger('PropertyTestRunner'); logger.log(`Running property test: ${testName} with ${config.iterations} iterations`); const failures: Array<{ iteration: number; input: T; error: any }> = []; for (let i = 0; i < config.iterations; i++) { try { const input = generator(); await testFunction(input); } catch (error) { failures.push({ iteration: i, input: generator(), // 重新生成用于错误报告 error }); } } if (failures.length > 0) { const failureRate = (failures.length / config.iterations) * 100; logger.error(`Property test failed: ${failures.length}/${config.iterations} iterations failed (${failureRate.toFixed(2)}%)`); logger.error('First failure:', failures[0]); throw new Error(`Property test "${testName}" failed with ${failures.length} failures`); } logger.log(`Property test "${testName}" passed all ${config.iterations} iterations`); } }