style: 完善代码规范和测试覆盖

- 新增多个模块的单元测试文件,提升测试覆盖率
- 完善AI-Reading文档系统,包含7步代码检查流程
- 新增集成测试和属性测试框架
- 优化项目结构和配置文件
- 清理过时的规范文档,统一使用新的检查标准
This commit is contained in:
moyin
2026-01-12 20:09:03 +08:00
parent 59128ea9a6
commit 5af44f95d5
22 changed files with 2595 additions and 2096 deletions

View File

@@ -0,0 +1,230 @@
/**
* Zulip集成功能端到端测试
*
* 功能描述:
* - 测试用户注册时Zulip账号的创建和绑定
* - 验证用户登录时Zulip API Key的验证和更新
* - 确保Zulip账号关联的完整性
* - 测试Zulip集成的完整业务流程
*
* 职责分离:
* - E2E测试测试完整的用户注册和登录流程
* - 集成验证验证Zulip服务与业务逻辑的集成
* - 数据一致性确保Zulip账号关联数据的正确性
*
* 测试策略:
* - 模拟真实用户操作流程进行端到端测试
* - 验证Zulip账号创建和绑定的各种场景
* - 测试异常情况下的错误处理和恢复机制
*
* 使用场景:
* - 验证Zulip集成功能的完整性
* - 确保用户注册登录流程的稳定性
* - 回归测试中验证Zulip相关功能
*
* 最近修改:
* - 2026-01-12: 代码规范优化 - 完善E2E测试文件注释规范添加职责分离和使用场景 (修改者: moyin)
*
* @author moyin
* @version 1.0.1
* @since 2026-01-12
* @lastModified 2026-01-12
*/
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../../src/app.module';
describe('Zulip Integration (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterEach(async () => {
await app.close();
});
describe('用户注册时的Zulip集成', () => {
it('应该在用户注册时创建或绑定Zulip账号', async () => {
const timestamp = Date.now();
const username = `zuliptest${timestamp}`;
const email = `zuliptest${timestamp}@example.com`;
const response = await request(app.getHttpServer())
.post('/auth/register')
.send({
username,
password: 'password123',
nickname: 'Zulip测试用户',
email,
email_verification_code: '123456' // 在测试模式下可能需要
})
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.user.username).toBe(username);
expect(response.body.data.access_token).toBeDefined();
// 检查响应消息是否包含Zulip相关信息
const message = response.body.data.message || response.body.message;
console.log('注册响应消息:', message);
});
it('应该处理邮箱已存在的Zulip账号绑定', async () => {
const timestamp = Date.now();
const username1 = `zulipbind1_${timestamp}`;
const username2 = `zulipbind2_${timestamp}`;
const sharedEmail = `shared${timestamp}@example.com`;
// 第一次注册
await request(app.getHttpServer())
.post('/auth/register')
.send({
username: username1,
password: 'password123',
nickname: 'Zulip绑定测试1',
email: sharedEmail,
})
.expect(201);
// 第二次注册使用不同用户名但相同邮箱模拟Zulip账号已存在的情况
const response = await request(app.getHttpServer())
.post('/auth/register')
.send({
username: username2,
password: 'password123',
nickname: 'Zulip绑定测试2',
email: `different${timestamp}@example.com`, // 使用不同邮箱避免冲突
})
.expect(201);
expect(response.body.success).toBe(true);
});
});
describe('用户登录时的Zulip API Key验证', () => {
let testUser: any;
beforeEach(async () => {
// 创建测试用户
const timestamp = Date.now();
const username = `loginzulip${timestamp}`;
const email = `loginzulip${timestamp}@example.com`;
const registerResponse = await request(app.getHttpServer())
.post('/auth/register')
.send({
username,
password: 'password123',
nickname: 'Zulip登录测试用户',
email,
})
.expect(201);
testUser = {
username,
password: 'password123',
email,
userId: registerResponse.body.data.user.id
};
});
it('应该在登录时验证Zulip API Key', async () => {
const response = await request(app.getHttpServer())
.post('/auth/login')
.send({
identifier: testUser.username,
password: testUser.password
})
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.user.username).toBe(testUser.username);
expect(response.body.data.access_token).toBeDefined();
// 登录成功表示Zulip API Key验证通过或已更新
console.log('登录成功Zulip API Key状态正常');
});
it('应该处理多次登录的API Key验证', async () => {
// 第一次登录
const firstLogin = await request(app.getHttpServer())
.post('/auth/login')
.send({
identifier: testUser.username,
password: testUser.password
})
.expect(200);
expect(firstLogin.body.success).toBe(true);
// 第二次登录测试API Key缓存和验证
const secondLogin = await request(app.getHttpServer())
.post('/auth/login')
.send({
identifier: testUser.username,
password: testUser.password
})
.expect(200);
expect(secondLogin.body.success).toBe(true);
console.log('多次登录API Key验证正常');
});
});
describe('错误处理', () => {
it('应该在Zulip服务不可用时仍能正常注册', async () => {
const timestamp = Date.now();
const username = `errortest${timestamp}`;
// 即使Zulip服务出错用户注册也应该成功
const response = await request(app.getHttpServer())
.post('/auth/register')
.send({
username,
password: 'password123',
nickname: 'Zulip错误测试用户',
// 不提供邮箱跳过Zulip创建
})
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.user.username).toBe(username);
});
it('应该在Zulip API Key验证失败时仍能正常登录', async () => {
// 创建没有邮箱的用户不会创建Zulip账号
const timestamp = Date.now();
const username = `nozulip${timestamp}`;
await request(app.getHttpServer())
.post('/auth/register')
.send({
username,
password: 'password123',
nickname: '无Zulip测试用户',
})
.expect(201);
// 登录应该成功即使没有Zulip账号
const response = await request(app.getHttpServer())
.post('/auth/login')
.send({
identifier: username,
password: 'password123'
})
.expect(200);
expect(response.body.success).toBe(true);
console.log('无Zulip账号用户登录正常');
});
});
});

View File

@@ -0,0 +1,234 @@
/**
* Zulip账号关联服务数据库测试
*
* 功能描述:
* - 专门测试数据库模式下的真实数据库操作
* - 需要配置数据库环境变量才能运行
* - 测试真实的CRUD操作和业务逻辑
*
* 运行条件:
* - 需要设置环境变量DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, DB_NAME
* - 数据库中需要存在 zulip_accounts 表
*
* @author angjustinl
* @version 1.0.0
* @since 2026-01-10
*/
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { CacheModule } from '@nestjs/cache-manager';
import { ZulipAccountsService } from '../../src/core/db/zulip_accounts/zulip_accounts.service';
import { ZulipAccountsRepository } from '../../src/core/db/zulip_accounts/zulip_accounts.repository';
import { ZulipAccounts } from '../../src/core/db/zulip_accounts/zulip_accounts.entity';
import { Users } from '../../src/core/db/users/users.entity';
import { AppLoggerService } from '../../src/core/utils/logger/logger.service';
/**
* 检查是否配置了数据库
*/
function isDatabaseConfigured(): boolean {
const requiredEnvVars = ['DB_HOST', 'DB_PORT', 'DB_USERNAME', 'DB_PASSWORD', 'DB_NAME'];
return requiredEnvVars.every(varName => process.env[varName]);
}
// 只有在配置了数据库时才运行这些测试
const describeDatabase = isDatabaseConfigured() ? describe : describe.skip;
describeDatabase('ZulipAccountsService - Database Mode', () => {
let service: ZulipAccountsService;
let module: TestingModule;
// 只有在数据库配置完整时才输出这些信息
if (isDatabaseConfigured()) {
console.log('🗄️ 运行数据库模式测试');
console.log('📊 使用真实数据库连接进行测试');
}
beforeAll(async () => {
module = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env.test', '.env'],
isGlobal: true,
}),
TypeOrmModule.forRoot({
type: 'mysql',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '3306'),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: [ZulipAccounts, Users],
synchronize: false,
logging: false,
}),
TypeOrmModule.forFeature([ZulipAccounts, Users]),
CacheModule.register({
ttl: 300,
max: 1000,
}),
],
providers: [
ZulipAccountsService,
ZulipAccountsRepository,
AppLoggerService,
],
}).compile();
service = module.get<ZulipAccountsService>(ZulipAccountsService);
}, 30000); // 增加超时时间
afterAll(async () => {
if (module) {
await module.close();
}
});
// 生成唯一的测试数据
const generateTestData = (suffix: string = Date.now().toString()) => {
const timestamp = Date.now();
const uniqueId = timestamp + Math.floor(Math.random() * 1000); // 添加随机数避免冲突
return {
gameUserId: uniqueId.toString(), // 使用纯数字字符串
zulipUserId: parseInt(`8${timestamp.toString().slice(-5)}`),
zulipEmail: `test_db_${timestamp}_${suffix}@example.com`,
zulipFullName: `数据库测试用户_${timestamp}_${suffix}`,
zulipApiKeyEncrypted: 'encrypted_api_key_for_db_test',
status: 'active' as const,
};
};
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('Database CRUD Operations', () => {
it('should create and retrieve account from database', async () => {
const testData = generateTestData('crud');
// 创建账号
const created = await service.create(testData);
expect(created).toBeDefined();
expect(created.gameUserId).toBe(testData.gameUserId);
expect(created.zulipEmail).toBe(testData.zulipEmail);
expect(created.status).toBe('active');
// 根据游戏用户ID查找
const found = await service.findByGameUserId(testData.gameUserId);
expect(found).toBeDefined();
expect(found?.id).toBe(created.id);
expect(found?.zulipUserId).toBe(testData.zulipUserId);
// 清理测试数据
await service.deleteByGameUserId(testData.gameUserId);
}, 15000);
it('should handle duplicate creation properly', async () => {
const testData = generateTestData('duplicate');
// 创建第一个账号
const created = await service.create(testData);
expect(created).toBeDefined();
// 尝试创建重复账号,应该抛出异常
await expect(service.create(testData)).rejects.toThrow();
// 清理测试数据
await service.deleteByGameUserId(testData.gameUserId);
}, 15000);
it('should update account in database', async () => {
const testData = generateTestData('update');
// 创建账号
const created = await service.create(testData);
// 更新账号
const updated = await service.update(created.id, {
zulipFullName: '更新后的用户名',
status: 'inactive',
});
expect(updated.zulipFullName).toBe('更新后的用户名');
expect(updated.status).toBe('inactive');
// 清理测试数据
await service.deleteByGameUserId(testData.gameUserId);
}, 15000);
it('should delete account from database', async () => {
const testData = generateTestData('delete');
// 创建账号
const created = await service.create(testData);
// 删除账号
const deleted = await service.delete(created.id);
expect(deleted).toBe(true);
// 验证账号已被删除
const found = await service.findByGameUserId(testData.gameUserId);
expect(found).toBeNull();
}, 15000);
});
describe('Database Business Logic', () => {
it('should check email existence in database', async () => {
const testData = generateTestData('email_check');
// 邮箱不存在时应该返回false
const notExists = await service.existsByEmail(testData.zulipEmail);
expect(notExists).toBe(false);
// 创建账号
await service.create(testData);
// 邮箱存在时应该返回true
const exists = await service.existsByEmail(testData.zulipEmail);
expect(exists).toBe(true);
// 清理测试数据
await service.deleteByGameUserId(testData.gameUserId);
}, 15000);
it('should get status statistics from database', async () => {
const stats = await service.getStatusStatistics();
expect(typeof stats.active).toBe('number');
expect(typeof stats.inactive).toBe('number');
expect(typeof stats.suspended).toBe('number');
expect(typeof stats.error).toBe('number');
expect(typeof stats.total).toBe('number');
expect(stats.total).toBe(stats.active + stats.inactive + stats.suspended + stats.error);
}, 15000);
it('should verify account in database', async () => {
const testData = generateTestData('verify');
// 创建账号
await service.create(testData);
// 验证账号
const result = await service.verifyAccount(testData.gameUserId);
expect(result.success).toBe(true);
expect(result.isValid).toBe(true);
expect(result.verifiedAt).toBeDefined();
// 清理测试数据
await service.deleteByGameUserId(testData.gameUserId);
}, 15000);
});
});
// 如果没有配置数据库,显示跳过信息
if (!isDatabaseConfigured()) {
console.log('⚠️ 数据库测试已跳过:未检测到数据库配置');
console.log('💡 要运行数据库测试,请设置以下环境变量:');
console.log(' - DB_HOST');
console.log(' - DB_PORT');
console.log(' - DB_USERNAME');
console.log(' - DB_PASSWORD');
console.log(' - DB_NAME');
}

View File

@@ -0,0 +1,161 @@
/**
* Zulip账号关联集成测试
*
* 功能描述:
* - 测试数据库和内存模式的切换
* - 测试完整的业务流程
* - 验证模块配置的正确性
*
* @author angjustinl
* @version 1.0.0
* @since 2025-01-07
*/
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigModule } from '@nestjs/config';
import { ZulipAccountsModule } from '../../src/core/db/zulip_accounts/zulip_accounts.module';
import { ZulipAccountsMemoryService } from '../../src/core/db/zulip_accounts/zulip_accounts_memory.service';
import { CreateZulipAccountDto } from '../../src/core/db/zulip_accounts/zulip_accounts.dto';
describe('ZulipAccountsModule Integration', () => {
let memoryModule: TestingModule;
beforeAll(async () => {
// 测试内存模式
memoryModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env.test', '.env'],
isGlobal: true,
}),
ZulipAccountsModule.forMemory()
],
}).compile();
});
afterAll(async () => {
if (memoryModule) {
await memoryModule.close();
}
});
describe('Memory Mode', () => {
let service: ZulipAccountsMemoryService;
beforeEach(() => {
service = memoryModule.get<ZulipAccountsMemoryService>('ZulipAccountsService');
});
it('should be defined', () => {
expect(service).toBeDefined();
expect(service).toBeInstanceOf(ZulipAccountsMemoryService);
});
it('should create and retrieve account in memory', async () => {
const createDto: CreateZulipAccountDto = {
gameUserId: '77777',
zulipUserId: 88888,
zulipEmail: 'memory@example.com',
zulipFullName: '内存测试用户',
zulipApiKeyEncrypted: 'encrypted_api_key',
status: 'active',
};
// 创建账号关联
const created = await service.create(createDto);
expect(created).toBeDefined();
expect(created.gameUserId).toBe('77777');
expect(created.zulipEmail).toBe('memory@example.com');
// 根据游戏用户ID查找
const found = await service.findByGameUserId('77777');
expect(found).toBeDefined();
expect(found?.id).toBe(created.id);
});
it('should handle batch operations in memory', async () => {
// 创建多个账号
const accounts = [];
for (let i = 1; i <= 3; i++) {
const createDto: CreateZulipAccountDto = {
gameUserId: `${20000 + i}`,
zulipUserId: 30000 + i,
zulipEmail: `batch${i}@example.com`,
zulipFullName: `批量用户${i}`,
zulipApiKeyEncrypted: 'encrypted_api_key',
status: 'active',
};
const account = await service.create(createDto);
accounts.push(account);
}
// 批量更新状态
const ids = accounts.map(a => a.id);
const batchResult = await service.batchUpdateStatus(ids, 'inactive');
expect(batchResult.success).toBe(true);
expect(batchResult.updatedCount).toBe(3);
// 验证状态已更新
for (const account of accounts) {
const updated = await service.findById(account.id);
expect(updated.status).toBe('inactive');
}
});
it('should get statistics in memory', async () => {
// 创建不同状态的账号
const statuses: Array<'active' | 'inactive' | 'suspended' | 'error'> = ['active', 'inactive', 'suspended', 'error'];
for (let i = 0; i < statuses.length; i++) {
const createDto: CreateZulipAccountDto = {
gameUserId: `${40000 + i}`,
zulipUserId: 50000 + i,
zulipEmail: `stats${i}@example.com`,
zulipFullName: `统计用户${i}`,
zulipApiKeyEncrypted: 'encrypted_api_key',
status: statuses[i],
};
await service.create(createDto);
}
// 获取统计信息
const stats = await service.getStatusStatistics();
expect(stats.active).toBeGreaterThanOrEqual(1);
expect(stats.inactive).toBeGreaterThanOrEqual(1);
expect(stats.suspended).toBeGreaterThanOrEqual(1);
expect(stats.error).toBeGreaterThanOrEqual(1);
expect(stats.total).toBeGreaterThanOrEqual(4);
});
});
describe('Cross-Mode Compatibility', () => {
it('should have same interface for both modes', () => {
const memoryService = memoryModule.get<ZulipAccountsMemoryService>('ZulipAccountsService');
// 检查内存服务有所需的方法
const methods = [
'create',
'findByGameUserId',
'findByZulipUserId',
'findByZulipEmail',
'findById',
'update',
'updateByGameUserId',
'delete',
'deleteByGameUserId',
'findMany',
'findAccountsNeedingVerification',
'findErrorAccounts',
'batchUpdateStatus',
'getStatusStatistics',
'verifyAccount',
'existsByEmail',
'existsByZulipUserId',
];
methods.forEach(method => {
expect(typeof memoryService[method]).toBe('function');
});
});
});
});

View File

@@ -0,0 +1,469 @@
/**
* Zulip消息发送集成测试
*
* 功能描述:
* - 测试消息发送到真实Zulip服务器的完整流程
* - 验证HTTP请求、响应处理和错误场景
* - 包含网络异常和API错误的测试
*
* 注意这些测试需要真实的Zulip服务器配置
*
* 最近修改:
* - 2026-01-12: 架构优化 - 从src/core/zulip_core/services/移动到test/integration/,符合测试分离规范 (修改者: moyin)
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
* - 2026-01-10: 测试新增 - 创建Zulip消息发送集成测试 (修改者: moyin)
*
* @author moyin
* @version 1.1.0
* @since 2026-01-10
* @lastModified 2026-01-12
*/
import { Test, TestingModule } from '@nestjs/testing';
import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from '../../src/core/zulip_core/services/zulip_client.service';
import * as nock from 'nock';
describe('ZulipMessageIntegration', () => {
let service: ZulipClientService;
let mockZulipClient: any;
let clientInstance: ZulipClientInstance;
const testConfig: ZulipClientConfig = {
username: 'test-bot@example.com',
apiKey: 'test-api-key-12345',
realm: 'https://test-zulip.example.com',
};
beforeEach(async () => {
// 清理所有HTTP拦截
nock.cleanAll();
const module: TestingModule = await Test.createTestingModule({
providers: [ZulipClientService],
}).compile();
service = module.get<ZulipClientService>(ZulipClientService);
// 创建模拟的zulip-js客户端
mockZulipClient = {
config: testConfig,
users: {
me: {
getProfile: jest.fn(),
},
},
messages: {
send: jest.fn(),
},
queues: {
register: jest.fn(),
deregister: jest.fn(),
},
events: {
retrieve: jest.fn(),
},
};
// 模拟客户端实例
clientInstance = {
userId: 'test-user-123',
config: testConfig,
client: mockZulipClient,
lastEventId: -1,
createdAt: new Date(),
lastActivity: new Date(),
isValid: true,
};
// Mock zulip-js模块加载
jest.spyOn(service as any, 'loadZulipModule').mockResolvedValue(() => mockZulipClient);
});
afterEach(() => {
nock.cleanAll();
jest.clearAllMocks();
});
describe('消息发送到Zulip服务器', () => {
it('应该成功发送消息到Zulip API', async () => {
// 模拟成功的API响应
mockZulipClient.messages.send.mockResolvedValue({
result: 'success',
id: 12345,
msg: '',
});
const result = await service.sendMessage(
clientInstance,
'test-stream',
'test-topic',
'Hello from integration test!'
);
expect(result.success).toBe(true);
expect(result.messageId).toBe(12345);
expect(mockZulipClient.messages.send).toHaveBeenCalledWith({
type: 'stream',
to: 'test-stream',
subject: 'test-topic',
content: 'Hello from integration test!',
});
});
it('应该处理Zulip API错误响应', async () => {
// 模拟API错误响应
mockZulipClient.messages.send.mockResolvedValue({
result: 'error',
msg: 'Stream does not exist',
code: 'STREAM_NOT_FOUND',
});
const result = await service.sendMessage(
clientInstance,
'nonexistent-stream',
'test-topic',
'This should fail'
);
expect(result.success).toBe(false);
expect(result.error).toBe('Stream does not exist');
});
it('应该处理网络连接异常', async () => {
// 模拟网络异常
mockZulipClient.messages.send.mockRejectedValue(new Error('Network timeout'));
const result = await service.sendMessage(
clientInstance,
'test-stream',
'test-topic',
'This will timeout'
);
expect(result.success).toBe(false);
expect(result.error).toBe('Network timeout');
});
it('应该处理认证失败', async () => {
// 模拟认证失败
mockZulipClient.messages.send.mockResolvedValue({
result: 'error',
msg: 'Invalid API key',
code: 'BAD_REQUEST',
});
const result = await service.sendMessage(
clientInstance,
'test-stream',
'test-topic',
'Authentication test'
);
expect(result.success).toBe(false);
expect(result.error).toBe('Invalid API key');
});
it('应该正确处理特殊字符和长消息', async () => {
const longMessage = 'A'.repeat(1000) + '特殊字符测试: 🎮🎯🚀 @#$%^&*()';
mockZulipClient.messages.send.mockResolvedValue({
result: 'success',
id: 67890,
});
const result = await service.sendMessage(
clientInstance,
'test-stream',
'special-chars-topic',
longMessage
);
expect(result.success).toBe(true);
expect(result.messageId).toBe(67890);
expect(mockZulipClient.messages.send).toHaveBeenCalledWith({
type: 'stream',
to: 'test-stream',
subject: 'special-chars-topic',
content: longMessage,
});
});
it('应该更新客户端最后活动时间', async () => {
const initialTime = new Date('2026-01-01T00:00:00Z');
clientInstance.lastActivity = initialTime;
mockZulipClient.messages.send.mockResolvedValue({
result: 'success',
id: 11111,
});
await service.sendMessage(
clientInstance,
'test-stream',
'test-topic',
'Activity test'
);
expect(clientInstance.lastActivity.getTime()).toBeGreaterThan(initialTime.getTime());
});
});
describe('事件队列与Zulip服务器交互', () => {
it('应该成功注册事件队列', async () => {
mockZulipClient.queues.register.mockResolvedValue({
result: 'success',
queue_id: 'test-queue-123',
last_event_id: 42,
});
const result = await service.registerQueue(clientInstance, ['message', 'typing']);
expect(result.success).toBe(true);
expect(result.queueId).toBe('test-queue-123');
expect(result.lastEventId).toBe(42);
expect(clientInstance.queueId).toBe('test-queue-123');
expect(clientInstance.lastEventId).toBe(42);
});
it('应该处理队列注册失败', async () => {
mockZulipClient.queues.register.mockResolvedValue({
result: 'error',
msg: 'Rate limit exceeded',
});
const result = await service.registerQueue(clientInstance);
expect(result.success).toBe(false);
expect(result.error).toBe('Rate limit exceeded');
});
it('应该成功获取事件', async () => {
clientInstance.queueId = 'test-queue-123';
clientInstance.lastEventId = 10;
const mockEvents = [
{
id: 11,
type: 'message',
message: {
id: 98765,
sender_email: 'user@example.com',
content: 'Test message from Zulip',
stream_id: 1,
subject: 'Test Topic',
},
},
{
id: 12,
type: 'typing',
sender: { user_id: 123 },
},
];
mockZulipClient.events.retrieve.mockResolvedValue({
result: 'success',
events: mockEvents,
});
const result = await service.getEvents(clientInstance, true);
expect(result.success).toBe(true);
expect(result.events).toEqual(mockEvents);
expect(clientInstance.lastEventId).toBe(12); // 更新为最后一个事件的ID
});
it('应该处理空事件队列', async () => {
clientInstance.queueId = 'test-queue-123';
mockZulipClient.events.retrieve.mockResolvedValue({
result: 'success',
events: [],
});
const result = await service.getEvents(clientInstance, true);
expect(result.success).toBe(true);
expect(result.events).toEqual([]);
});
it('应该成功注销事件队列', async () => {
clientInstance.queueId = 'test-queue-123';
mockZulipClient.queues.deregister.mockResolvedValue({
result: 'success',
});
const result = await service.deregisterQueue(clientInstance);
expect(result).toBe(true);
expect(clientInstance.queueId).toBeUndefined();
expect(clientInstance.lastEventId).toBe(-1);
});
it('应该处理队列过期情况', async () => {
clientInstance.queueId = 'expired-queue';
// 模拟队列过期的JSON解析错误
mockZulipClient.queues.deregister.mockRejectedValue(
new Error('invalid json response body at https://zulip.example.com/api/v1/events reason: Unexpected token')
);
const result = await service.deregisterQueue(clientInstance);
expect(result).toBe(true); // 应该返回true因为队列已过期
expect(clientInstance.queueId).toBeUndefined();
expect(clientInstance.lastEventId).toBe(-1);
});
});
describe('API Key验证', () => {
it('应该成功验证有效的API Key', async () => {
mockZulipClient.users.me.getProfile.mockResolvedValue({
result: 'success',
email: 'test-bot@example.com',
full_name: 'Test Bot',
user_id: 123,
});
const isValid = await service.validateApiKey(clientInstance);
expect(isValid).toBe(true);
expect(clientInstance.isValid).toBe(true);
});
it('应该拒绝无效的API Key', async () => {
mockZulipClient.users.me.getProfile.mockResolvedValue({
result: 'error',
msg: 'Invalid API key',
});
const isValid = await service.validateApiKey(clientInstance);
expect(isValid).toBe(false);
expect(clientInstance.isValid).toBe(false);
});
it('应该处理API Key验证网络异常', async () => {
mockZulipClient.users.me.getProfile.mockRejectedValue(new Error('Connection refused'));
const isValid = await service.validateApiKey(clientInstance);
expect(isValid).toBe(false);
expect(clientInstance.isValid).toBe(false);
});
});
describe('错误恢复和重试机制', () => {
it('应该在临时网络错误后恢复', async () => {
// 第一次调用失败,第二次成功
mockZulipClient.messages.send
.mockRejectedValueOnce(new Error('Temporary network error'))
.mockResolvedValueOnce({
result: 'success',
id: 99999,
});
// 第一次调用应该失败
const firstResult = await service.sendMessage(
clientInstance,
'test-stream',
'test-topic',
'First attempt'
);
expect(firstResult.success).toBe(false);
// 第二次调用应该成功
const secondResult = await service.sendMessage(
clientInstance,
'test-stream',
'test-topic',
'Second attempt'
);
expect(secondResult.success).toBe(true);
expect(secondResult.messageId).toBe(99999);
});
it('应该处理服务器5xx错误', async () => {
mockZulipClient.messages.send.mockRejectedValue(new Error('Internal Server Error (500)'));
const result = await service.sendMessage(
clientInstance,
'test-stream',
'test-topic',
'Server error test'
);
expect(result.success).toBe(false);
expect(result.error).toBe('Internal Server Error (500)');
});
});
describe('性能和并发测试', () => {
it('应该处理并发消息发送', async () => {
// 模拟多个并发消息 - 设置一次mock让它返回不同的ID
mockZulipClient.messages.send.mockImplementation(() => {
const id = Math.floor(Math.random() * 10000) + 1000;
return Promise.resolve({
result: 'success',
id: id,
});
});
// 创建并发消息发送的Promise数组
const messagePromises: Promise<any>[] = [];
for (let i = 0; i < 10; i++) {
messagePromises.push(
service.sendMessage(
clientInstance,
'test-stream',
'concurrent-topic',
`Concurrent message ${i}`
)
);
}
const results = await Promise.all(messagePromises);
results.forEach((result) => {
expect(result.success).toBe(true);
expect(result.messageId).toBeGreaterThan(999);
});
});
it('应该在大量消息发送时保持性能', async () => {
const startTime = Date.now();
const messageCount = 100;
mockZulipClient.messages.send.mockImplementation(() =>
Promise.resolve({
result: 'success',
id: Math.floor(Math.random() * 100000),
})
);
const promises = Array.from({ length: messageCount }, (_, i) =>
service.sendMessage(
clientInstance,
'performance-stream',
'performance-topic',
`Performance test message ${i}`
)
);
const results = await Promise.all(promises);
const endTime = Date.now();
const duration = endTime - startTime;
// 验证所有消息都成功发送
results.forEach(result => {
expect(result.success).toBe(true);
});
// 性能检查100条消息应该在合理时间内完成这里设为5秒
expect(duration).toBeLessThan(5000);
console.log(`发送${messageCount}条消息耗时: ${duration}ms`);
}, 10000);
});
});

View File

@@ -0,0 +1,340 @@
/**
* 配置验证属性测试
*
* 功能描述:
* - 使用fast-check进行配置验证的属性测试
* - 验证配置验证逻辑的正确性和完整性
* - 测试各种边界情况和随机输入
*
* 职责分离:
* - 属性测试:验证配置验证的数学属性
* - 随机测试:使用随机生成的数据验证逻辑
* - 边界测试:测试各种边界条件
*
* 最近修改:
* - 2026-01-12: 代码规范优化 - 从单元测试中分离属性测试 (修改者: moyin)
*
* @author moyin
* @version 1.0.1
* @since 2026-01-12
* @lastModified 2026-01-12
*/
import { Test, TestingModule } from '@nestjs/testing';
import * as fc from 'fast-check';
import { ConfigManagerService } from '../../src/core/zulip_core/services/config_manager.service';
import { AppLoggerService } from '../../src/core/utils/logger/logger.service';
import * as fs from 'fs';
// Mock fs module
jest.mock('fs');
describe('ConfigManagerService Property Tests', () => {
let service: ConfigManagerService;
let mockLogger: jest.Mocked<AppLoggerService>;
const mockFs = fs as jest.Mocked<typeof fs>;
// 默认有效配置
const validMapConfig = {
maps: [
{
mapId: 'novice_village',
mapName: '新手村',
zulipStream: 'Novice Village',
interactionObjects: [
{
objectId: 'notice_board',
objectName: '公告板',
zulipTopic: 'Notice Board',
position: { x: 100, y: 150 }
}
]
}
]
};
beforeEach(async () => {
jest.clearAllMocks();
// 设置测试环境变量
process.env.NODE_ENV = 'test';
process.env.ZULIP_SERVER_URL = 'https://test-zulip.com';
process.env.ZULIP_BOT_EMAIL = 'test-bot@test.com';
process.env.ZULIP_BOT_API_KEY = 'test-api-key';
process.env.ZULIP_API_KEY_ENCRYPTION_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
mockLogger = {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
} as any;
// 默认mock fs行为
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue(JSON.stringify(validMapConfig));
mockFs.writeFileSync.mockImplementation(() => {});
mockFs.mkdirSync.mockImplementation(() => undefined);
const module: TestingModule = await Test.createTestingModule({
providers: [
ConfigManagerService,
{
provide: AppLoggerService,
useValue: mockLogger,
},
],
}).compile();
service = module.get<ConfigManagerService>(ConfigManagerService);
await service.loadMapConfig();
});
afterEach(() => {
jest.restoreAllMocks();
// 清理环境变量
delete process.env.NODE_ENV;
delete process.env.ZULIP_SERVER_URL;
delete process.env.ZULIP_BOT_EMAIL;
delete process.env.ZULIP_BOT_API_KEY;
delete process.env.ZULIP_API_KEY_ENCRYPTION_KEY;
});
/**
* 属性测试: 配置验证
*
* **Feature: zulip-integration, Property 12: 配置验证**
* **Validates: Requirements 10.5**
*
* 对于任何系统配置,系统应该在启动时验证配置的有效性,
* 并在发现无效配置时报告详细的错误信息
*/
describe('Property 12: 配置验证', () => {
/**
* 属性: 对于任何有效的地图配置验证应该返回valid=true
* 验证需求 10.5: 验证配置时系统应在启动时检查配置的有效性并报告错误
*/
it('对于任何有效的地图配置验证应该返回valid=true', async () => {
await fc.assert(
fc.asyncProperty(
// 生成有效的mapId
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
// 生成有效的mapName
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
// 生成有效的zulipStream
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
// 生成有效的交互对象数组
fc.array(
fc.record({
objectId: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
objectName: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
zulipTopic: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
position: fc.record({
x: fc.integer({ min: 0, max: 10000 }),
y: fc.integer({ min: 0, max: 10000 }),
}),
}),
{ minLength: 0, maxLength: 10 }
),
async (mapId, mapName, zulipStream, interactionObjects) => {
const config = {
mapId: mapId.trim(),
mapName: mapName.trim(),
zulipStream: zulipStream.trim(),
interactionObjects: interactionObjects.map(obj => ({
objectId: obj.objectId.trim(),
objectName: obj.objectName.trim(),
zulipTopic: obj.zulipTopic.trim(),
position: obj.position,
})),
};
const result = service.validateMapConfigDetailed(config);
// 有效配置应该通过验证
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
}
),
{ numRuns: 100 }
);
}, 60000);
/**
* 属性: 对于任何缺少必填字段的配置验证应该返回valid=false并包含错误信息
* 验证需求 10.5: 验证配置时系统应在启动时检查配置的有效性并报告错误
*/
it('对于任何缺少mapId的配置验证应该返回valid=false', async () => {
await fc.assert(
fc.asyncProperty(
// 生成有效的mapName
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
// 生成有效的zulipStream
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
async (mapName, zulipStream) => {
const config = {
// 缺少mapId
mapName: mapName.trim(),
zulipStream: zulipStream.trim(),
interactionObjects: [] as any[],
};
const result = service.validateMapConfigDetailed(config);
// 缺少mapId应该验证失败
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('mapId'))).toBe(true);
}
),
{ numRuns: 100 }
);
}, 60000);
/**
* 属性: 对于任何缺少mapName的配置验证应该返回valid=false
*/
it('对于任何缺少mapName的配置验证应该返回valid=false', async () => {
await fc.assert(
fc.asyncProperty(
// 生成有效的mapId
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
// 生成有效的zulipStream
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
async (mapId, zulipStream) => {
const config = {
mapId: mapId.trim(),
// 缺少mapName
zulipStream: zulipStream.trim(),
interactionObjects: [] as any[],
};
const result = service.validateMapConfigDetailed(config);
// 缺少mapName应该验证失败
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('mapName'))).toBe(true);
}
),
{ numRuns: 100 }
);
}, 60000);
/**
* 属性: 对于任何缺少zulipStream的配置验证应该返回valid=false
*/
it('对于任何缺少zulipStream的配置验证应该返回valid=false', async () => {
await fc.assert(
fc.asyncProperty(
// 生成有效的mapId
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
// 生成有效的mapName
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
async (mapId, mapName) => {
const config = {
mapId: mapId.trim(),
mapName: mapName.trim(),
// 缺少zulipStream
interactionObjects: [] as any[],
};
const result = service.validateMapConfigDetailed(config);
// 缺少zulipStream应该验证失败
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('zulipStream'))).toBe(true);
}
),
{ numRuns: 100 }
);
}, 60000);
/**
* 属性: 验证结果的错误数量应该与实际错误数量一致
*/
it('验证结果的错误数量应该与实际错误数量一致', async () => {
await fc.assert(
fc.asyncProperty(
// 随机决定是否包含各个字段
fc.boolean(),
fc.boolean(),
fc.boolean(),
// 生成字段值
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
async (includeMapId, includeMapName, includeZulipStream, mapId, mapName, zulipStream) => {
const config: any = {
interactionObjects: [] as any[],
};
let expectedErrors = 0;
if (includeMapId) {
config.mapId = mapId.trim();
} else {
expectedErrors++;
}
if (includeMapName) {
config.mapName = mapName.trim();
} else {
expectedErrors++;
}
if (includeZulipStream) {
config.zulipStream = zulipStream.trim();
} else {
expectedErrors++;
}
const result = service.validateMapConfigDetailed(config);
// 错误数量应该与预期一致
expect(result.errors.length).toBe(expectedErrors);
expect(result.valid).toBe(expectedErrors === 0);
}
),
{ numRuns: 100 }
);
}, 60000);
/**
* 属性: 配置验证的幂等性
*/
it('配置验证应该是幂等的', async () => {
await fc.assert(
fc.asyncProperty(
fc.record({
mapId: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
mapName: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
zulipStream: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
interactionObjects: fc.array(
fc.record({
objectId: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
objectName: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
zulipTopic: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
position: fc.record({
x: fc.integer({ min: 0, max: 10000 }),
y: fc.integer({ min: 0, max: 10000 }),
}),
}),
{ maxLength: 5 }
),
}),
async (config) => {
// 多次验证同一个配置应该返回相同结果
const result1 = service.validateMapConfigDetailed(config);
const result2 = service.validateMapConfigDetailed(config);
const result3 = service.validateMapConfigDetailed(config);
expect(result1.valid).toBe(result2.valid);
expect(result2.valid).toBe(result3.valid);
expect(result1.errors).toEqual(result2.errors);
expect(result2.errors).toEqual(result3.errors);
}
),
{ numRuns: 100 }
);
}, 60000);
});
});