feat:实现管理员系统核心功能

- 添加管理员数据库管理控制器和服务
- 实现管理员操作日志记录系统
- 添加数据库异常处理过滤器
- 完善管理员权限验证和响应格式
- 添加全面的属性测试覆盖
This commit is contained in:
moyin
2026-01-08 23:05:34 +08:00
parent 0f37130832
commit 6924416bbd
34 changed files with 9481 additions and 199 deletions

View File

@@ -0,0 +1,258 @@
/**
* 管理员系统属性测试基础框架
*
* 功能描述:
* - 提供属性测试的基础工具和断言
* - 实现通用的测试数据生成器
* - 支持随机化测试和边界条件验证
*
* 属性测试原理:
* - 验证系统在各种输入条件下的通用正确性属性
* - 通过大量随机测试用例发现边界问题
* - 确保系统行为的一致性和可靠性
*
* 最近修改:
* - 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 { faker } from '@faker-js/faker';
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 {
private static setupFaker(seed?: number) {
if (seed) {
faker.seed(seed);
}
}
/**
* 生成随机用户数据
*/
static generateUser(seed?: number) {
this.setupFaker(seed);
return {
username: faker.internet.username(),
nickname: faker.person.fullName(),
email: faker.internet.email(),
phone: faker.phone.number(),
role: faker.number.int({ min: 0, max: 9 }),
status: faker.helpers.enumValue(UserStatus),
avatar_url: faker.image.avatar(),
github_id: faker.string.alphanumeric(10)
};
}
/**
* 生成随机用户档案数据
*/
static generateUserProfile(seed?: number) {
this.setupFaker(seed);
return {
user_id: faker.string.numeric(10),
bio: faker.lorem.paragraph(),
resume_content: faker.lorem.paragraphs(3),
tags: JSON.stringify(faker.helpers.arrayElements(['developer', 'designer', 'manager'], { min: 1, max: 3 })),
social_links: JSON.stringify({
github: faker.internet.url(),
linkedin: faker.internet.url()
}),
skin_id: faker.string.alphanumeric(8),
current_map: faker.helpers.arrayElement(['plaza', 'forest', 'beach', 'mountain']),
pos_x: faker.number.float({ min: 0, max: 1000 }),
pos_y: faker.number.float({ min: 0, max: 1000 }),
status: faker.number.int({ min: 0, max: 2 })
};
}
/**
* 生成随机Zulip账号数据
*/
static generateZulipAccount(seed?: number) {
this.setupFaker(seed);
return {
gameUserId: faker.string.numeric(10),
zulipUserId: faker.number.int({ min: 1, max: 999999 }),
zulipEmail: faker.internet.email(),
zulipFullName: faker.person.fullName(),
zulipApiKeyEncrypted: faker.string.alphanumeric(32),
status: faker.helpers.arrayElement(['active', 'inactive', 'suspended', 'error'] as const)
};
}
/**
* 生成随机分页参数
*/
static generatePaginationParams(seed?: number) {
this.setupFaker(seed);
return {
limit: faker.number.int({ min: 1, max: 100 }),
offset: faker.number.int({ min: 0, max: 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`);
}
}