style: 完善代码规范和测试覆盖
- 新增多个模块的单元测试文件,提升测试覆盖率 - 完善AI-Reading文档系统,包含7步代码检查流程 - 新增集成测试和属性测试框架 - 优化项目结构和配置文件 - 清理过时的规范文档,统一使用新的检查标准
This commit is contained in:
234
test/integration/zulip_accounts_database.spec.ts
Normal file
234
test/integration/zulip_accounts_database.spec.ts
Normal 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');
|
||||
}
|
||||
161
test/integration/zulip_accounts_integration.spec.ts
Normal file
161
test/integration/zulip_accounts_integration.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
469
test/integration/zulip_message_integration.spec.ts
Normal file
469
test/integration/zulip_message_integration.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user