- 更新管理员控制器和数据库管理功能 - 完善管理员操作日志系统 - 添加全面的属性测试覆盖 - 优化用户管理和用户档案服务 - 更新代码检查规范文档 功能改进: - 增强管理员权限验证 - 完善操作日志记录 - 优化数据库管理接口 - 提升系统安全性和可维护性
255 lines
8.0 KiB
TypeScript
255 lines
8.0 KiB
TypeScript
/**
|
||
* 管理员系统属性测试基础框架
|
||
*
|
||
* 功能描述:
|
||
* - 提供属性测试的基础工具和断言
|
||
* - 实现通用的测试数据生成器
|
||
* - 支持随机化测试和边界条件验证
|
||
*
|
||
* 属性测试原理:
|
||
* - 验证系统在各种输入条件下的通用正确性属性
|
||
* - 通过大量随机测试用例发现边界问题
|
||
* - 确保系统行为的一致性和可靠性
|
||
*
|
||
* 最近修改:
|
||
* - 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<T>(
|
||
testName: string,
|
||
generator: () => T,
|
||
testFunction: (input: T) => Promise<void>,
|
||
config: PropertyTestConfig = DEFAULT_PROPERTY_CONFIG
|
||
): Promise<void> {
|
||
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`);
|
||
}
|
||
} |