diff --git a/full_diagnosis.js b/full_diagnosis.js deleted file mode 100644 index ec6a85d..0000000 --- a/full_diagnosis.js +++ /dev/null @@ -1,311 +0,0 @@ -const io = require('socket.io-client'); -const https = require('https'); -const http = require('http'); - -console.log('🔍 全面WebSocket连接诊断'); -console.log('='.repeat(60)); - -// 1. 测试基础网络连接 -async function testBasicConnection() { - console.log('\n1️⃣ 测试基础HTTPS连接...'); - - return new Promise((resolve) => { - const options = { - hostname: 'whaletownend.xinghangee.icu', - port: 443, - path: '/', - method: 'GET', - timeout: 10000 - }; - - const req = https.request(options, (res) => { - console.log(`✅ HTTPS连接成功 - 状态码: ${res.statusCode}`); - console.log(`📋 服务器: ${res.headers.server || '未知'}`); - resolve({ success: true, statusCode: res.statusCode }); - }); - - req.on('error', (error) => { - console.log(`❌ HTTPS连接失败: ${error.message}`); - resolve({ success: false, error: error.message }); - }); - - req.on('timeout', () => { - console.log('❌ HTTPS连接超时'); - req.destroy(); - resolve({ success: false, error: 'timeout' }); - }); - - req.end(); - }); -} - -// 2. 测试本地服务器 -async function testLocalServer() { - console.log('\n2️⃣ 测试本地服务器...'); - - const testPaths = [ - 'http://localhost:3000/', - 'http://localhost:3000/socket.io/?EIO=4&transport=polling' - ]; - - for (const url of testPaths) { - console.log(`🧪 测试: ${url}`); - - await new Promise((resolve) => { - const urlObj = new URL(url); - const options = { - hostname: urlObj.hostname, - port: urlObj.port, - path: urlObj.pathname + urlObj.search, - method: 'GET', - timeout: 5000 - }; - - const req = http.request(options, (res) => { - console.log(` 状态码: ${res.statusCode}`); - if (res.statusCode === 200) { - console.log(' ✅ 本地服务器正常'); - } else { - console.log(` ⚠️ 本地服务器响应: ${res.statusCode}`); - } - resolve(); - }); - - req.on('error', (error) => { - console.log(` ❌ 本地服务器连接失败: ${error.message}`); - resolve(); - }); - - req.on('timeout', () => { - console.log(' ❌ 本地服务器超时'); - req.destroy(); - resolve(); - }); - - req.end(); - }); - } -} - -// 3. 测试远程Socket.IO路径 -async function testRemoteSocketIO() { - console.log('\n3️⃣ 测试远程Socket.IO路径...'); - - const testPaths = [ - '/socket.io/?EIO=4&transport=polling', - '/game/socket.io/?EIO=4&transport=polling', - '/socket.io/?transport=polling', - '/api/socket.io/?EIO=4&transport=polling' - ]; - - const results = []; - - for (const path of testPaths) { - console.log(`🧪 测试路径: ${path}`); - - const result = await new Promise((resolve) => { - const options = { - hostname: 'whaletownend.xinghangee.icu', - port: 443, - path: path, - method: 'GET', - timeout: 8000, - headers: { - 'User-Agent': 'socket.io-diagnosis' - } - }; - - const req = https.request(options, (res) => { - console.log(` 状态码: ${res.statusCode}`); - - let data = ''; - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - if (res.statusCode === 200) { - console.log(' ✅ 路径可用'); - console.log(` 📄 响应: ${data.substring(0, 50)}...`); - } else { - console.log(` ❌ 路径不可用: ${res.statusCode}`); - } - resolve({ path, statusCode: res.statusCode, success: res.statusCode === 200 }); - }); - }); - - req.on('error', (error) => { - console.log(` ❌ 请求失败: ${error.message}`); - resolve({ path, error: error.message, success: false }); - }); - - req.on('timeout', () => { - console.log(' ❌ 请求超时'); - req.destroy(); - resolve({ path, error: 'timeout', success: false }); - }); - - req.end(); - }); - - results.push(result); - } - - return results; -} - -// 4. 测试Socket.IO客户端连接 -async function testSocketIOClient() { - console.log('\n4️⃣ 测试Socket.IO客户端连接...'); - - const configs = [ - { - name: 'HTTPS + 所有传输方式', - url: 'https://whaletownend.xinghangee.icu', - options: { transports: ['websocket', 'polling'], timeout: 10000 } - }, - { - name: 'HTTPS + 仅Polling', - url: 'https://whaletownend.xinghangee.icu', - options: { transports: ['polling'], timeout: 10000 } - }, - { - name: 'HTTPS + /game namespace', - url: 'https://whaletownend.xinghangee.icu/game', - options: { transports: ['polling'], timeout: 10000 } - } - ]; - - const results = []; - - for (const config of configs) { - console.log(`🧪 测试: ${config.name}`); - console.log(` URL: ${config.url}`); - - const result = await new Promise((resolve) => { - const socket = io(config.url, config.options); - let resolved = false; - - const timeout = setTimeout(() => { - if (!resolved) { - resolved = true; - socket.disconnect(); - console.log(' ❌ 连接超时'); - resolve({ success: false, error: 'timeout' }); - } - }, config.options.timeout); - - socket.on('connect', () => { - if (!resolved) { - resolved = true; - clearTimeout(timeout); - console.log(' ✅ 连接成功'); - console.log(` 📡 Socket ID: ${socket.id}`); - console.log(` 🚀 传输方式: ${socket.io.engine.transport.name}`); - socket.disconnect(); - resolve({ success: true, transport: socket.io.engine.transport.name }); - } - }); - - socket.on('connect_error', (error) => { - if (!resolved) { - resolved = true; - clearTimeout(timeout); - console.log(` ❌ 连接失败: ${error.message}`); - resolve({ success: false, error: error.message }); - } - }); - }); - - results.push({ config: config.name, ...result }); - - // 等待1秒再测试下一个 - await new Promise(resolve => setTimeout(resolve, 1000)); - } - - return results; -} - -// 5. 检查DNS解析 -async function testDNS() { - console.log('\n5️⃣ 检查DNS解析...'); - - const dns = require('dns'); - - return new Promise((resolve) => { - dns.lookup('whaletownend.xinghangee.icu', (err, address, family) => { - if (err) { - console.log(`❌ DNS解析失败: ${err.message}`); - resolve({ success: false, error: err.message }); - } else { - console.log(`✅ DNS解析成功: ${address} (IPv${family})`); - resolve({ success: true, address, family }); - } - }); - }); -} - -// 主诊断函数 -async function runFullDiagnosis() { - console.log('开始全面诊断...\n'); - - try { - const dnsResult = await testDNS(); - const basicResult = await testBasicConnection(); - await testLocalServer(); - const socketIOPaths = await testRemoteSocketIO(); - const clientResults = await testSocketIOClient(); - - console.log('\n' + '='.repeat(60)); - console.log('📊 诊断结果汇总'); - console.log('='.repeat(60)); - - console.log(`1. DNS解析: ${dnsResult.success ? '✅ 正常' : '❌ 失败'}`); - if (dnsResult.address) { - console.log(` IP地址: ${dnsResult.address}`); - } - - console.log(`2. HTTPS连接: ${basicResult.success ? '✅ 正常' : '❌ 失败'}`); - if (basicResult.error) { - console.log(` 错误: ${basicResult.error}`); - } - - const workingPaths = socketIOPaths.filter(r => r.success); - console.log(`3. Socket.IO路径: ${workingPaths.length}/${socketIOPaths.length} 个可用`); - workingPaths.forEach(p => { - console.log(` ✅ ${p.path}`); - }); - - const workingClients = clientResults.filter(r => r.success); - console.log(`4. Socket.IO客户端: ${workingClients.length}/${clientResults.length} 个成功`); - workingClients.forEach(c => { - console.log(` ✅ ${c.config} (${c.transport})`); - }); - - console.log('\n💡 建议:'); - - if (!dnsResult.success) { - console.log('❌ DNS解析失败 - 检查域名配置'); - } else if (!basicResult.success) { - console.log('❌ 基础HTTPS连接失败 - 检查服务器状态和防火墙'); - } else if (workingPaths.length === 0) { - console.log('❌ 所有Socket.IO路径都不可用 - 检查nginx配置和后端服务'); - } else if (workingClients.length === 0) { - console.log('❌ Socket.IO客户端无法连接 - 可能是CORS或协议问题'); - } else { - console.log('✅ 部分功能正常 - 使用可用的配置继续开发'); - - if (workingClients.length > 0) { - const bestConfig = workingClients.find(c => c.transport === 'websocket') || workingClients[0]; - console.log(`💡 推荐使用: ${bestConfig.config}`); - } - } - - } catch (error) { - console.error('诊断过程中发生错误:', error); - } - - process.exit(0); -} - -runFullDiagnosis(); \ No newline at end of file diff --git a/package.json b/package.json index 3d9fbf6..0c1126b 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,7 @@ "start:prod": "node dist/main.js", "test": "jest", "test:watch": "jest --watch", - "test:cov": "jest --coverage", - "test:e2e": "cross-env RUN_E2E_TESTS=true jest --testPathPattern=e2e.spec.ts", - "test:unit": "jest --testPathPattern=spec.ts --testPathIgnorePatterns=e2e.spec.ts", - "test:all": "cross-env RUN_E2E_TESTS=true jest" + "test:cov": "jest --coverage" }, "keywords": [ "game", @@ -65,7 +62,6 @@ "@types/node": "^20.19.27", "@types/nodemailer": "^6.4.14", "@types/supertest": "^6.0.3", - "cross-env": "^10.1.0", "fast-check": "^4.5.2", "jest": "^29.7.0", "pino-pretty": "^13.1.3", diff --git a/src/business/zulip/zulip.service.ts b/src/business/zulip/zulip.service.ts index 0e18c51..1386948 100644 --- a/src/business/zulip/zulip.service.ts +++ b/src/business/zulip/zulip.service.ts @@ -118,9 +118,6 @@ export class ZulipService { private readonly apiKeySecurityService: ApiKeySecurityService, ) { this.logger.log('ZulipService初始化完成'); - - // 启动事件处理 - this.initializeEventProcessing(); } /** @@ -763,42 +760,5 @@ export class ZulipService { async getSocketsInMap(mapId: string): Promise { return this.sessionManager.getSocketsInMap(mapId); } - - /** - * 获取事件处理器实例 - * - * 功能描述: - * 返回ZulipEventProcessorService实例,用于设置消息分发器 - * - * @returns ZulipEventProcessorService 事件处理器实例 - */ - getEventProcessor(): ZulipEventProcessorService { - return this.eventProcessor; - } - - /** - * 初始化事件处理 - * - * 功能描述: - * 启动Zulip事件处理循环,用于接收和处理从Zulip服务器返回的消息 - * - * @private - */ - private async initializeEventProcessing(): Promise { - try { - this.logger.log('开始初始化Zulip事件处理'); - - // 启动事件处理循环 - await this.eventProcessor.startEventProcessing(); - - this.logger.log('Zulip事件处理初始化完成'); - } catch (error) { - const err = error as Error; - this.logger.error('初始化Zulip事件处理失败', { - operation: 'initializeEventProcessing', - error: err.message, - }, err.stack); - } - } } diff --git a/src/business/zulip/zulip_websocket.gateway.ts b/src/business/zulip/zulip_websocket.gateway.ts index 15bdbee..50e0598 100644 --- a/src/business/zulip/zulip_websocket.gateway.ts +++ b/src/business/zulip/zulip_websocket.gateway.ts @@ -139,9 +139,6 @@ export class ZulipWebSocketGateway implements OnGatewayConnection, OnGatewayDisc namespace: '/game', timestamp: new Date().toISOString(), }); - - // 设置消息分发器,使ZulipEventProcessorService能够向客户端发送消息 - this.setupMessageDistributor(); } /** @@ -376,13 +373,6 @@ export class ZulipWebSocketGateway implements OnGatewayConnection, OnGatewayDisc ): Promise { const clientData = client.data as ClientData | undefined; - console.log('🔍 DEBUG: handleChat 被调用了!', { - socketId: client.id, - data: data, - clientData: clientData, - timestamp: new Date().toISOString(), - }); - this.logger.log('收到聊天消息', { operation: 'handleChat', socketId: client.id, @@ -759,41 +749,5 @@ export class ZulipWebSocketGateway implements OnGatewayConnection, OnGatewayDisc }); } } - - /** - * 设置消息分发器 - * - * 功能描述: - * 将当前WebSocket网关设置为ZulipEventProcessorService的消息分发器, - * 使其能够接收从Zulip返回的消息并转发给游戏客户端 - * - * @private - */ - private setupMessageDistributor(): void { - try { - // 获取ZulipEventProcessorService实例 - const eventProcessor = this.zulipService.getEventProcessor(); - - if (eventProcessor) { - // 设置消息分发器 - eventProcessor.setMessageDistributor(this); - - this.logger.log('消息分发器设置完成', { - operation: 'setupMessageDistributor', - timestamp: new Date().toISOString(), - }); - } else { - this.logger.warn('无法获取ZulipEventProcessorService实例', { - operation: 'setupMessageDistributor', - }); - } - } catch (error) { - const err = error as Error; - this.logger.error('设置消息分发器失败', { - operation: 'setupMessageDistributor', - error: err.message, - }, err.stack); - } - } } diff --git a/src/core/zulip/services/api_key_security.service.spec.ts b/src/core/zulip/services/api_key_security.service.spec.ts index 5d12184..a5698b2 100644 --- a/src/core/zulip/services/api_key_security.service.spec.ts +++ b/src/core/zulip/services/api_key_security.service.spec.ts @@ -5,7 +5,7 @@ * - 测试ApiKeySecurityService的核心功能 * - 包含属性测试验证API Key安全存储 * - * @author angjustinl, moyin + * @author angjustinl * @version 1.0.0 * @since 2025-12-25 */ @@ -548,424 +548,4 @@ describe('ApiKeySecurityService', () => { ); }, 60000); }); - - - // ==================== 补充测试用例 ==================== - - describe('访问频率限制测试', () => { - it('应该在超过频率限制时拒绝访问', async () => { - const userId = 'rate-limit-user'; - const apiKey = 'abcdefghijklmnopqrstuvwxyz123456'; - - // 存储API Key - await service.storeApiKey(userId, apiKey); - - // 模拟已达到频率限制 - const rateLimitKey = `zulip:api_key_access:${userId}`; - memoryStore.set(rateLimitKey, { value: '60' }); - - // 尝试获取API Key应该被拒绝 - const result = await service.getApiKey(userId); - expect(result.success).toBe(false); - expect(result.message).toContain('访问频率过高'); - }); - - it('应该正确处理频率限制计数', async () => { - const userId = 'counter-user'; - const apiKey = 'abcdefghijklmnopqrstuvwxyz123456'; - - await service.storeApiKey(userId, apiKey); - - // 连续访问多次 - for (let i = 0; i < 5; i++) { - const result = await service.getApiKey(userId); - expect(result.success).toBe(true); - } - - // 检查计数器 - const rateLimitKey = `zulip:api_key_access:${userId}`; - const count = await mockRedisService.get(rateLimitKey); - expect(parseInt(count || '0', 10)).toBe(5); - }); - - it('应该在Redis错误时默认允许访问', async () => { - const userId = 'redis-error-user'; - const apiKey = 'abcdefghijklmnopqrstuvwxyz123456'; - - await service.storeApiKey(userId, apiKey); - - // 模拟Redis错误 - mockRedisService.get.mockRejectedValueOnce(new Error('Redis connection failed')); - mockRedisService.incr.mockRejectedValueOnce(new Error('Redis connection failed')); - - // 应该仍然允许访问 - const result = await service.getApiKey(userId); - expect(result.success).toBe(true); - expect(result.apiKey).toBe(apiKey); - }); - }); - - describe('Redis错误处理测试', () => { - it('应该处理存储时的Redis错误', async () => { - mockRedisService.set.mockRejectedValueOnce(new Error('Redis connection failed')); - - const result = await service.storeApiKey('user-123', 'abcdefghijklmnopqrstuvwxyz123456'); - expect(result.success).toBe(false); - expect(result.message).toContain('存储失败'); - }); - - it('应该处理获取时的Redis错误', async () => { - // 模拟所有Redis get调用都失败 - mockRedisService.get.mockRejectedValue(new Error('Redis connection failed')); - - const result = await service.getApiKey('user-123'); - expect(result.success).toBe(false); - expect(result.message).toContain('获取失败'); - }); - - it('应该处理删除时的Redis错误', async () => { - mockRedisService.del.mockRejectedValueOnce(new Error('Redis connection failed')); - - const result = await service.deleteApiKey('user-123'); - expect(result).toBe(false); - }); - - it('应该处理检查存在性时的Redis错误', async () => { - mockRedisService.exists.mockRejectedValueOnce(new Error('Redis connection failed')); - - const result = await service.hasApiKey('user-123'); - expect(result).toBe(false); - }); - - it('应该处理获取统计信息时的Redis错误', async () => { - mockRedisService.get.mockRejectedValueOnce(new Error('Redis connection failed')); - - const stats = await service.getApiKeyStats('user-123'); - expect(stats.exists).toBe(false); - }); - }); - - describe('数据损坏处理测试', () => { - it('应该处理损坏的JSON数据', async () => { - const userId = 'corrupted-data-user'; - const storageKey = `zulip:api_key:${userId}`; - - // 存储损坏的JSON数据 - memoryStore.set(storageKey, { value: 'invalid-json-data' }); - - const result = await service.getApiKey(userId); - expect(result.success).toBe(false); - expect(result.message).toContain('获取失败'); - }); - - it('应该处理缺少必要字段的数据', async () => { - const userId = 'incomplete-data-user'; - const storageKey = `zulip:api_key:${userId}`; - - // 存储不完整的数据 - const incompleteData = { - encryptedKey: 'some-encrypted-data', - // 缺少 iv 和 authTag - }; - memoryStore.set(storageKey, { value: JSON.stringify(incompleteData) }); - - const result = await service.getApiKey(userId); - expect(result.success).toBe(false); - }); - }); - - describe('可疑访问记录测试', () => { - it('应该记录可疑访问事件', async () => { - const userId = 'suspicious-user'; - const reason = 'multiple_failed_attempts'; - const details = { attemptCount: 5, timeWindow: '1min' }; - const metadata = { ipAddress: '192.168.1.100', userAgent: 'TestAgent' }; - - await service.logSuspiciousAccess(userId, reason, details, metadata); - - // 验证安全日志被记录 - expect(mockRedisService.setex).toHaveBeenCalled(); - const setexCalls = mockRedisService.setex.mock.calls; - const securityLogCall = setexCalls.find(call => call[0].includes('security_log')); - expect(securityLogCall).toBeDefined(); - - // 验证日志内容 - const logData = JSON.parse(securityLogCall![2]); - expect(logData.eventType).toBe(SecurityEventType.SUSPICIOUS_ACCESS); - expect(logData.severity).toBe(SecuritySeverity.WARNING); - expect(logData.details.reason).toBe(reason); - expect(logData.ipAddress).toBe(metadata.ipAddress); - }); - }); - - describe('元数据处理测试', () => { - it('应该在安全日志中记录IP地址和User-Agent', async () => { - const userId = 'metadata-user'; - const apiKey = 'abcdefghijklmnopqrstuvwxyz123456'; - const metadata = { - ipAddress: '192.168.1.100', - userAgent: 'Mozilla/5.0 (Test Browser)', - }; - - await service.storeApiKey(userId, apiKey, metadata); - - // 验证元数据被记录在安全日志中 - const setexCalls = mockRedisService.setex.mock.calls; - const securityLogCall = setexCalls.find(call => call[0].includes('security_log')); - expect(securityLogCall).toBeDefined(); - - const logData = JSON.parse(securityLogCall![2]); - expect(logData.ipAddress).toBe(metadata.ipAddress); - expect(logData.userAgent).toBe(metadata.userAgent); - }); - - it('应该处理缺少元数据的情况', async () => { - const userId = 'no-metadata-user'; - const apiKey = 'abcdefghijklmnopqrstuvwxyz123456'; - - // 不提供元数据 - const result = await service.storeApiKey(userId, apiKey); - expect(result.success).toBe(true); - - // 验证安全日志仍然被记录 - const setexCalls = mockRedisService.setex.mock.calls; - const securityLogCall = setexCalls.find(call => call[0].includes('security_log')); - expect(securityLogCall).toBeDefined(); - }); - }); - - describe('边界条件测试', () => { - it('应该处理极长的用户ID', async () => { - const longUserId = 'a'.repeat(1000); - const apiKey = 'abcdefghijklmnopqrstuvwxyz123456'; - - const result = await service.storeApiKey(longUserId, apiKey); - expect(result.success).toBe(true); - - const getResult = await service.getApiKey(longUserId); - expect(getResult.success).toBe(true); - expect(getResult.apiKey).toBe(apiKey); - }); - - it('应该处理最大长度的API Key', async () => { - const userId = 'max-key-user'; - const maxLengthApiKey = 'a'.repeat(128); // 最大允许长度 - - const result = await service.storeApiKey(userId, maxLengthApiKey); - expect(result.success).toBe(true); - - const getResult = await service.getApiKey(userId); - expect(getResult.success).toBe(true); - expect(getResult.apiKey).toBe(maxLengthApiKey); - }); - - it('应该拒绝超长的API Key', async () => { - const userId = 'overlong-key-user'; - const overlongApiKey = 'a'.repeat(129); // 超过最大长度 - - const result = await service.storeApiKey(userId, overlongApiKey); - expect(result.success).toBe(false); - expect(result.message).toContain('格式无效'); - }); - - it('应该处理最小长度的API Key', async () => { - const userId = 'min-key-user'; - const minLengthApiKey = 'a'.repeat(16); // 最小允许长度 - - const result = await service.storeApiKey(userId, minLengthApiKey); - expect(result.success).toBe(true); - - const getResult = await service.getApiKey(userId); - expect(getResult.success).toBe(true); - expect(getResult.apiKey).toBe(minLengthApiKey); - }); - }); - - describe('时间相关测试', () => { - it('应该正确设置创建时间和更新时间', async () => { - const userId = 'time-test-user'; - const apiKey = 'abcdefghijklmnopqrstuvwxyz123456'; - const beforeStore = new Date(); - - await service.storeApiKey(userId, apiKey); - - const stats = await service.getApiKeyStats(userId); - expect(stats.exists).toBe(true); - expect(stats.createdAt).toBeDefined(); - expect(stats.updatedAt).toBeDefined(); - expect(stats.createdAt!.getTime()).toBeGreaterThanOrEqual(beforeStore.getTime()); - expect(stats.updatedAt!.getTime()).toBeGreaterThanOrEqual(beforeStore.getTime()); - }); - - it('应该在访问时更新最后访问时间', async () => { - const userId = 'access-time-user'; - const apiKey = 'abcdefghijklmnopqrstuvwxyz123456'; - - await service.storeApiKey(userId, apiKey); - - // 等待一小段时间 - await new Promise(resolve => setTimeout(resolve, 10)); - - const beforeAccess = new Date(); - await service.getApiKey(userId); - - const stats = await service.getApiKeyStats(userId); - expect(stats.lastAccessedAt).toBeDefined(); - expect(stats.lastAccessedAt!.getTime()).toBeGreaterThanOrEqual(beforeAccess.getTime()); - }); - - it('应该在更新时保持创建时间不变', async () => { - const userId = 'update-time-user'; - const oldApiKey = 'abcdefghijklmnopqrstuvwxyz123456'; - const newApiKey = 'newkeyabcdefghijklmnopqrstuvwx'; - - await service.storeApiKey(userId, oldApiKey); - const statsAfterStore = await service.getApiKeyStats(userId); - const originalCreatedAt = statsAfterStore.createdAt; - - // 等待一小段时间 - await new Promise(resolve => setTimeout(resolve, 10)); - - await service.updateApiKey(userId, newApiKey); - const statsAfterUpdate = await service.getApiKeyStats(userId); - - expect(statsAfterUpdate.createdAt).toEqual(originalCreatedAt); - expect(statsAfterUpdate.updatedAt!.getTime()).toBeGreaterThan(statsAfterStore.updatedAt!.getTime()); - }); - }); - - describe('并发访问测试', () => { - it('应该处理并发的API Key访问', async () => { - const userId = 'concurrent-user'; - const apiKey = 'abcdefghijklmnopqrstuvwxyz123456'; - - await service.storeApiKey(userId, apiKey); - - // 并发访问 - 使用串行方式来确保计数正确 - const results = []; - for (let i = 0; i < 10; i++) { - const result = await service.getApiKey(userId); - results.push(result); - } - - // 所有访问都应该成功 - results.forEach(result => { - expect(result.success).toBe(true); - expect(result.apiKey).toBe(apiKey); - }); - - // 访问计数应该正确 - const stats = await service.getApiKeyStats(userId); - expect(stats.accessCount).toBe(10); - }); - - it('应该处理并发的存储和获取操作', async () => { - const userId = 'concurrent-store-get-user'; - const apiKey = 'abcdefghijklmnopqrstuvwxyz123456'; - - // 并发执行存储和获取操作 - const storePromise = service.storeApiKey(userId, apiKey); - const getPromise = service.getApiKey(userId); - - const [storeResult, getResult] = await Promise.all([storePromise, getPromise]); - - // 存储应该成功 - expect(storeResult.success).toBe(true); - - // 获取可能成功也可能失败(取决于执行顺序) - if (getResult.success) { - expect(getResult.apiKey).toBe(apiKey); - } else { - expect(getResult.message).toContain('不存在'); - } - }); - }); - - describe('安全事件记录错误处理', () => { - it('应该处理记录安全事件时的Redis错误', async () => { - mockRedisService.setex.mockRejectedValueOnce(new Error('Redis connection failed')); - - // 记录安全事件不应该抛出异常 - await expect(service.logSecurityEvent({ - eventType: SecurityEventType.API_KEY_STORED, - severity: SecuritySeverity.INFO, - userId: 'test-user', - details: { action: 'test' }, - timestamp: new Date(), - })).resolves.not.toThrow(); - - // 应该记录错误日志 - expect(Logger.prototype.error).toHaveBeenCalledWith( - '记录安全事件失败', - expect.any(Object) - ); - }); - }); - - describe('环境变量处理测试', () => { - it('应该在没有环境变量时使用默认密钥并记录警告', () => { - // 这个测试需要在服务初始化时进行,当前实现中已经初始化了 - // 验证警告日志被记录 - expect(Logger.prototype.warn).toHaveBeenCalledWith( - expect.stringContaining('使用默认加密密钥') - ); - }); - }); - - describe('属性测试 - 错误处理和边界条件', () => { - /** - * 属性测试: 任何Redis错误都不应该导致服务崩溃 - */ - it('任何Redis错误都不应该导致服务崩溃', async () => { - await fc.assert( - fc.asyncProperty( - fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), - fc.stringMatching(/^[a-zA-Z0-9_-]{16,64}$/), - fc.constantFrom('set', 'get', 'del', 'exists', 'setex', 'incr'), - async (userId, apiKey, failingMethod) => { - // 清理之前的数据 - memoryStore.clear(); - jest.clearAllMocks(); - - // 模拟特定方法失败 - (mockRedisService as any)[failingMethod].mockRejectedValueOnce( - new Error(`${failingMethod} failed`) - ); - - // 执行操作不应该抛出异常 - await expect(service.storeApiKey(userId.trim(), apiKey)).resolves.not.toThrow(); - await expect(service.getApiKey(userId.trim())).resolves.not.toThrow(); - await expect(service.deleteApiKey(userId.trim())).resolves.not.toThrow(); - await expect(service.hasApiKey(userId.trim())).resolves.not.toThrow(); - } - ), - { numRuns: 50 } - ); - }, 30000); - - /** - * 属性测试: 任何输入都不应该导致服务崩溃 - */ - it('任何输入都不应该导致服务崩溃', async () => { - await fc.assert( - fc.asyncProperty( - fc.string({ maxLength: 1000 }), // 任意字符串作为用户ID - fc.string({ maxLength: 1000 }), // 任意字符串作为API Key - async (userId, apiKey) => { - // 清理之前的数据 - memoryStore.clear(); - - // 任何输入都不应该导致崩溃 - await expect(service.storeApiKey(userId, apiKey)).resolves.not.toThrow(); - await expect(service.getApiKey(userId)).resolves.not.toThrow(); - await expect(service.updateApiKey(userId, apiKey)).resolves.not.toThrow(); - await expect(service.deleteApiKey(userId)).resolves.not.toThrow(); - await expect(service.hasApiKey(userId)).resolves.not.toThrow(); - await expect(service.getApiKeyStats(userId)).resolves.not.toThrow(); - } - ), - { numRuns: 100 } - ); - }, 60000); - }); }); diff --git a/src/core/zulip/services/api_key_security.service.ts b/src/core/zulip/services/api_key_security.service.ts index b2c1247..a27ae3a 100644 --- a/src/core/zulip/services/api_key_security.service.ts +++ b/src/core/zulip/services/api_key_security.service.ts @@ -23,7 +23,7 @@ * - AppLoggerService: 日志记录服务 * - IRedisService: Redis缓存服务 * - * @author angjustinl, moyin + * @author angjustinl * @version 1.0.0 * @since 2025-12-25 */ diff --git a/src/core/zulip/services/config_manager.service.spec.ts b/src/core/zulip/services/config_manager.service.spec.ts index b1b6466..39a91bb 100644 --- a/src/core/zulip/services/config_manager.service.spec.ts +++ b/src/core/zulip/services/config_manager.service.spec.ts @@ -5,7 +5,7 @@ * - 测试ConfigManagerService的核心功能 * - 包含属性测试验证配置验证正确性 * - * @author angjustinl, moyin + * @author angjustinl * @version 1.0.0 * @since 2025-12-25 */ @@ -60,13 +60,6 @@ describe('ConfigManagerService', () => { 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(), @@ -95,12 +88,6 @@ describe('ConfigManagerService', () => { 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; }); it('should be defined', () => { @@ -608,504 +595,4 @@ describe('ConfigManagerService', () => { ); }, 60000); }); - - - // ==================== 补充测试用例 ==================== - - describe('hasMap - 检查地图是否存在', () => { - it('应该返回true当地图存在时', () => { - const exists = service.hasMap('novice_village'); - expect(exists).toBe(true); - }); - - it('应该返回false当地图不存在时', () => { - const exists = service.hasMap('nonexistent'); - expect(exists).toBe(false); - }); - - it('应该处理空字符串输入', () => { - const exists = service.hasMap(''); - expect(exists).toBe(false); - }); - - it('应该处理null/undefined输入', () => { - const exists1 = service.hasMap(null as any); - const exists2 = service.hasMap(undefined as any); - expect(exists1).toBe(false); - expect(exists2).toBe(false); - }); - }); - - describe('getAllMapIds - 获取所有地图ID', () => { - it('应该返回所有地图ID列表', () => { - const mapIds = service.getAllMapIds(); - expect(mapIds).toContain('novice_village'); - expect(mapIds).toContain('tavern'); - expect(mapIds.length).toBe(2); - }); - }); - - describe('getMapConfigByStream - 根据Stream获取地图配置', () => { - it('应该返回正确的地图配置', () => { - const config = service.getMapConfigByStream('Novice Village'); - expect(config).toBeDefined(); - expect(config?.mapId).toBe('novice_village'); - }); - - it('应该支持大小写不敏感查询', () => { - const config = service.getMapConfigByStream('novice village'); - expect(config).toBeDefined(); - expect(config?.mapId).toBe('novice_village'); - }); - - it('应该在Stream不存在时返回null', () => { - const config = service.getMapConfigByStream('nonexistent'); - expect(config).toBeNull(); - }); - }); - - describe('getAllStreams - 获取所有Stream名称', () => { - it('应该返回所有Stream名称列表', () => { - const streams = service.getAllStreams(); - expect(streams).toContain('Novice Village'); - expect(streams).toContain('Tavern'); - expect(streams.length).toBe(2); - }); - }); - - describe('hasStream - 检查Stream是否存在', () => { - it('应该返回true当Stream存在时', () => { - const exists = service.hasStream('Novice Village'); - expect(exists).toBe(true); - }); - - it('应该支持大小写不敏感查询', () => { - const exists = service.hasStream('novice village'); - expect(exists).toBe(true); - }); - - it('应该返回false当Stream不存在时', () => { - const exists = service.hasStream('nonexistent'); - expect(exists).toBe(false); - }); - - it('应该处理空字符串输入', () => { - const exists = service.hasStream(''); - expect(exists).toBe(false); - }); - }); - - describe('findObjectByTopic - 根据Topic查找交互对象', () => { - it('应该找到正确的交互对象', () => { - const obj = service.findObjectByTopic('Notice Board'); - expect(obj).toBeDefined(); - expect(obj?.objectId).toBe('notice_board'); - expect(obj?.mapId).toBe('novice_village'); - }); - - it('应该支持大小写不敏感查询', () => { - const obj = service.findObjectByTopic('notice board'); - expect(obj).toBeDefined(); - expect(obj?.objectId).toBe('notice_board'); - }); - - it('应该在Topic不存在时返回null', () => { - const obj = service.findObjectByTopic('nonexistent'); - expect(obj).toBeNull(); - }); - - it('应该处理空字符串输入', () => { - const obj = service.findObjectByTopic(''); - expect(obj).toBeNull(); - }); - }); - - describe('getObjectsInMap - 获取地图中的所有交互对象', () => { - it('应该返回地图中的所有交互对象', () => { - const objects = service.getObjectsInMap('novice_village'); - expect(objects.length).toBe(1); - expect(objects[0].objectId).toBe('notice_board'); - expect(objects[0].mapId).toBe('novice_village'); - }); - - it('应该在地图不存在时返回空数组', () => { - const objects = service.getObjectsInMap('nonexistent'); - expect(objects).toEqual([]); - }); - }); - - describe('getConfigFilePath - 获取配置文件路径', () => { - it('应该返回正确的配置文件路径', () => { - const filePath = service.getConfigFilePath(); - expect(filePath).toContain('map-config.json'); - }); - }); - - describe('configFileExists - 检查配置文件是否存在', () => { - it('应该返回true当配置文件存在时', () => { - mockFs.existsSync.mockReturnValue(true); - const exists = service.configFileExists(); - expect(exists).toBe(true); - }); - - it('应该返回false当配置文件不存在时', () => { - mockFs.existsSync.mockReturnValue(false); - const exists = service.configFileExists(); - expect(exists).toBe(false); - }); - }); - - describe('reloadConfig - 热重载配置', () => { - it('应该成功重载配置', async () => { - await expect(service.reloadConfig()).resolves.not.toThrow(); - }); - - it('应该在配置文件读取失败时抛出错误', async () => { - mockFs.readFileSync.mockImplementation(() => { - throw new Error('File read error'); - }); - - await expect(service.reloadConfig()).rejects.toThrow(); - }); - }); - - describe('getZulipConfig - 获取Zulip配置', () => { - it('应该返回Zulip配置对象', () => { - const config = service.getZulipConfig(); - expect(config).toBeDefined(); - expect(config.zulipServerUrl).toBeDefined(); - expect(config.websocketPort).toBeDefined(); - }); - }); - - describe('getAllMapConfigs - 获取所有地图配置', () => { - it('应该返回所有地图配置列表', () => { - const configs = service.getAllMapConfigs(); - expect(configs.length).toBe(2); - expect(configs.some(c => c.mapId === 'novice_village')).toBe(true); - expect(configs.some(c => c.mapId === 'tavern')).toBe(true); - }); - }); - - describe('配置文件监听功能', () => { - let mockWatcher: any; - - beforeEach(() => { - mockWatcher = { - close: jest.fn(), - }; - (fs.watch as jest.Mock).mockReturnValue(mockWatcher); - }); - - describe('enableConfigWatcher - 启用配置文件监听', () => { - it('应该成功启用配置文件监听', () => { - const result = service.enableConfigWatcher(); - expect(result).toBe(true); - expect(fs.watch).toHaveBeenCalled(); - }); - - it('应该在配置文件不存在时返回false', () => { - mockFs.existsSync.mockReturnValue(false); - const result = service.enableConfigWatcher(); - expect(result).toBe(false); - }); - - it('应该在已启用时跳过重复启用', () => { - service.enableConfigWatcher(); - (fs.watch as jest.Mock).mockClear(); - - const result = service.enableConfigWatcher(); - expect(result).toBe(true); - expect(fs.watch).not.toHaveBeenCalled(); - }); - - it('应该处理fs.watch抛出的错误', () => { - (fs.watch as jest.Mock).mockImplementation(() => { - throw new Error('Watch error'); - }); - - const result = service.enableConfigWatcher(); - expect(result).toBe(false); - }); - }); - - describe('disableConfigWatcher - 禁用配置文件监听', () => { - it('应该成功禁用配置文件监听', () => { - service.enableConfigWatcher(); - service.disableConfigWatcher(); - expect(mockWatcher.close).toHaveBeenCalled(); - }); - - it('应该处理未启用监听的情况', () => { - // 不应该抛出错误 - expect(() => service.disableConfigWatcher()).not.toThrow(); - }); - }); - - describe('isConfigWatcherEnabled - 检查监听状态', () => { - it('应该返回正确的监听状态', () => { - expect(service.isConfigWatcherEnabled()).toBe(false); - - service.enableConfigWatcher(); - expect(service.isConfigWatcherEnabled()).toBe(true); - - service.disableConfigWatcher(); - expect(service.isConfigWatcherEnabled()).toBe(false); - }); - }); - }); - - describe('getFullConfiguration - 获取完整配置', () => { - it('应该返回完整的配置对象', () => { - const config = service.getFullConfiguration(); - expect(config).toBeDefined(); - }); - }); - - describe('updateConfigValue - 更新配置值', () => { - it('应该成功更新有效的配置值', () => { - // 这个测试需要模拟fullConfig存在 - const result = service.updateConfigValue('message.rateLimit', 20); - // 由于测试环境中fullConfig可能未初始化,这里主要测试不抛出异常 - expect(typeof result).toBe('boolean'); - }); - - it('应该在配置键不存在时返回false', () => { - const result = service.updateConfigValue('nonexistent.key', 'value'); - expect(result).toBe(false); - }); - - it('应该处理无效的键路径', () => { - const result = service.updateConfigValue('', 'value'); - expect(result).toBe(false); - }); - }); - - describe('exportMapConfig - 导出地图配置', () => { - it('应该成功导出配置到文件', () => { - const result = service.exportMapConfig(); - expect(result).toBe(true); - expect(mockFs.writeFileSync).toHaveBeenCalled(); - }); - - it('应该处理文件写入错误', () => { - mockFs.writeFileSync.mockImplementation(() => { - throw new Error('Write error'); - }); - - const result = service.exportMapConfig(); - expect(result).toBe(false); - }); - - it('应该支持自定义文件路径', () => { - const customPath = '/custom/path/config.json'; - const result = service.exportMapConfig(customPath); - expect(result).toBe(true); - expect(mockFs.writeFileSync).toHaveBeenCalledWith( - customPath, - expect.any(String), - 'utf-8' - ); - }); - }); - - describe('错误处理测试', () => { - it('应该处理JSON解析错误', async () => { - mockFs.readFileSync.mockReturnValue('invalid json'); - - await expect(service.loadMapConfig()).rejects.toThrow(); - }); - - it('应该处理文件系统错误', async () => { - mockFs.readFileSync.mockImplementation(() => { - throw new Error('File system error'); - }); - - await expect(service.loadMapConfig()).rejects.toThrow(); - }); - - it('应该处理配置验证过程中的错误', async () => { - // 模拟验证过程中抛出异常 - const originalValidateMapConfig = (service as any).validateMapConfig; - (service as any).validateMapConfig = jest.fn().mockImplementation(() => { - throw new Error('Validation error'); - }); - - const result = await service.validateConfig(); - expect(result.valid).toBe(false); - expect(result.errors.some(e => e.includes('验证过程出错'))).toBe(true); - - // 恢复原方法 - (service as any).validateMapConfig = originalValidateMapConfig; - }); - }); - - describe('边界条件测试', () => { - it('应该处理空的地图配置', async () => { - const emptyConfig = { maps: [] }; - mockFs.readFileSync.mockReturnValue(JSON.stringify(emptyConfig)); - - await service.loadMapConfig(); - - const mapIds = service.getAllMapIds(); - expect(mapIds).toEqual([]); - }); - - it('应该处理大量地图配置', async () => { - const largeConfig = { - maps: Array.from({ length: 1000 }, (_, i) => ({ - mapId: `map_${i}`, - mapName: `地图${i}`, - zulipStream: `Stream${i}`, - interactionObjects: [] - })) - }; - mockFs.readFileSync.mockReturnValue(JSON.stringify(largeConfig)); - - await service.loadMapConfig(); - - const mapIds = service.getAllMapIds(); - expect(mapIds.length).toBe(1000); - }); - - it('应该处理极长的字符串输入', () => { - const longString = 'a'.repeat(10000); - const stream = service.getStreamByMap(longString); - expect(stream).toBeNull(); - }); - - it('应该处理特殊字符输入', () => { - const specialChars = '!@#$%^&*()[]{}|;:,.<>?'; - const stream = service.getStreamByMap(specialChars); - expect(stream).toBeNull(); - }); - }); - - describe('并发操作测试', () => { - it('应该处理并发的配置查询', async () => { - const promises = Array.from({ length: 100 }, () => - Promise.resolve(service.getStreamByMap('novice_village')) - ); - - const results = await Promise.all(promises); - results.forEach(result => { - expect(result).toBe('Novice Village'); - }); - }); - - it('应该处理并发的配置重载', async () => { - const promises = Array.from({ length: 10 }, () => service.reloadConfig()); - - // 不应该抛出异常 - await expect(Promise.all(promises)).resolves.not.toThrow(); - }); - }); - - describe('内存管理测试', () => { - it('应该正确清理资源', () => { - service.enableConfigWatcher(); - - // 模拟模块销毁 - service.onModuleDestroy(); - - expect(service.isConfigWatcherEnabled()).toBe(false); - }); - }); - - describe('属性测试 - 配置查询一致性', () => { - /** - * 属性测试: 配置查询的一致性 - * 验证双向查询的一致性(mapId <-> stream) - */ - it('mapId和stream之间的双向查询应该保持一致', async () => { - await fc.assert( - fc.asyncProperty( - // 从现有的mapId中选择 - fc.constantFrom('novice_village', 'tavern'), - async (mapId) => { - // 通过mapId获取stream - const stream = service.getStreamByMap(mapId); - expect(stream).not.toBeNull(); - - // 通过stream反向获取mapId - const retrievedMapId = service.getMapIdByStream(stream!); - expect(retrievedMapId).toBe(mapId); - - // 通过stream获取配置 - const config = service.getMapConfigByStream(stream!); - expect(config).not.toBeNull(); - expect(config!.mapId).toBe(mapId); - } - ), - { numRuns: 50 } - ); - }, 30000); - - /** - * 属性测试: 交互对象查询的一致性 - */ - it('交互对象的不同查询方式应该返回一致的结果', async () => { - await fc.assert( - fc.asyncProperty( - fc.constantFrom('novice_village', 'tavern'), - async (mapId) => { - // 获取地图中的所有对象 - const objectsInMap = service.getObjectsInMap(mapId); - - for (const obj of objectsInMap) { - // 通过topic查找对象 - const objByTopic = service.findObjectByTopic(obj.zulipTopic); - expect(objByTopic).not.toBeNull(); - expect(objByTopic!.objectId).toBe(obj.objectId); - expect(objByTopic!.mapId).toBe(mapId); - - // 通过mapId和objectId获取topic - const topic = service.getTopicByObject(mapId, obj.objectId); - expect(topic).toBe(obj.zulipTopic); - } - } - ), - { numRuns: 50 } - ); - }, 30000); - - /** - * 属性测试: 配置验证的幂等性 - */ - 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); - }); }); diff --git a/src/core/zulip/services/config_manager.service.ts b/src/core/zulip/services/config_manager.service.ts index ecfb40b..5ee40d0 100644 --- a/src/core/zulip/services/config_manager.service.ts +++ b/src/core/zulip/services/config_manager.service.ts @@ -26,7 +26,7 @@ * 依赖模块: * - AppLoggerService: 日志记录服务 * - * @author angjustinl, moyin + * @author angjustinl * @version 1.0.0 * @since 2025-12-25 */ diff --git a/src/core/zulip/services/error_handler.service.spec.ts b/src/core/zulip/services/error_handler.service.spec.ts index b76c765..0baf916 100644 --- a/src/core/zulip/services/error_handler.service.spec.ts +++ b/src/core/zulip/services/error_handler.service.spec.ts @@ -8,7 +8,7 @@ * **Feature: zulip-integration, Property 9: 错误处理和服务降级** * **Validates: Requirements 8.1, 8.2, 8.3, 8.4** * - * @author angjustinl, moyin + * @author angjustinl * @version 1.0.0 * @since 2025-12-25 */ @@ -570,457 +570,4 @@ describe('ErrorHandlerService', () => { ); }, 30000); }); - - - // ==================== 补充测试用例 ==================== - - describe('错误统计测试', () => { - it('应该正确记录和获取错误统计', async () => { - // 触发几个不同类型的错误 - await service.handleZulipError({ code: 401, message: 'Unauthorized' }, 'auth'); - await service.handleZulipError({ code: 429, message: 'Rate limit' }, 'rate'); - await service.handleZulipError({ code: 401, message: 'Unauthorized' }, 'auth2'); - - const stats = service.getErrorStats(); - - expect(stats.serviceStatus).toBeDefined(); - expect(stats.errorCounts).toBeDefined(); - expect(stats.recentErrors).toBeDefined(); - - // 应该有认证错误和频率限制错误的记录 - expect(Object.keys(stats.errorCounts).length).toBeGreaterThan(0); - }); - - it('应该能够重置错误统计', async () => { - // 先产生一些错误 - await service.handleZulipError({ code: 500, message: 'Server error' }, 'test'); - - service.resetErrorStats(); - - const stats = service.getErrorStats(); - expect(Object.keys(stats.errorCounts)).toHaveLength(0); - expect(Object.keys(stats.recentErrors)).toHaveLength(0); - }); - }); - - describe('服务健康检查测试', () => { - it('应该返回完整的健康状态信息', async () => { - const health = await service.checkServiceHealth(); - - expect(health.status).toBeDefined(); - expect(health.details).toBeDefined(); - expect(health.details.serviceStatus).toBeDefined(); - expect(health.details.errorCounts).toBeDefined(); - expect(health.details.lastErrors).toBeDefined(); - }); - - it('应该在降级模式下返回正确状态', async () => { - await service.enableDegradedMode(); - - const health = await service.checkServiceHealth(); - - expect(health.status).toBe(ServiceStatus.DEGRADED); - expect(health.details.degradedModeStartTime).toBeDefined(); - }); - }); - - describe('配置获取测试', () => { - it('应该返回正确的配置信息', () => { - const config = service.getConfig(); - - expect(config.degradedModeEnabled).toBe(true); - expect(config.autoReconnectEnabled).toBe(true); - expect(config.maxReconnectAttempts).toBe(5); - expect(config.reconnectBaseDelay).toBe(1000); - expect(config.apiTimeout).toBe(30000); - expect(config.maxRetries).toBe(3); - expect(config.maxConnections).toBe(1000); - }); - - it('应该返回正确的单个配置项', () => { - expect(service.isDegradedModeEnabled()).toBe(true); - expect(service.isAutoReconnectEnabled()).toBe(true); - expect(service.getApiTimeout()).toBe(30000); - expect(service.getMaxRetries()).toBe(3); - expect(service.getMaxReconnectAttempts()).toBe(5); - expect(service.getReconnectBaseDelay()).toBe(1000); - }); - - it('应该返回默认重试配置', () => { - const retryConfig = service.getDefaultRetryConfig(); - - expect(retryConfig.maxRetries).toBe(3); - expect(retryConfig.baseDelay).toBe(1000); - expect(retryConfig.maxDelay).toBe(30000); - expect(retryConfig.backoffMultiplier).toBe(2); - }); - }); - - describe('状态检查方法测试', () => { - it('应该正确检查服务可用性', () => { - expect(service.isServiceAvailable()).toBe(true); - - // 设置为不可用状态(通过私有属性) - (service as any).serviceStatus = ServiceStatus.UNAVAILABLE; - expect(service.isServiceAvailable()).toBe(false); - }); - - it('应该正确检查降级模式状态', async () => { - expect(service.isDegradedMode()).toBe(false); - - await service.enableDegradedMode(); - expect(service.isDegradedMode()).toBe(true); - - await service.enableNormalMode(); - expect(service.isDegradedMode()).toBe(false); - }); - }); - - describe('连接数管理测试', () => { - it('应该能够设置最大连接数', () => { - service.setMaxConnections(500); - expect(service.getConfig().maxConnections).toBe(500); - }); - - it('应该正确处理负连接数变化', () => { - service.updateActiveConnections(100); - service.updateActiveConnections(-150); // 应该不会变成负数 - - // 活跃连接数不应该小于0 - const loadStatus = service.getLoadStatus(); - expect(loadStatus).toBeDefined(); - }); - - it('应该在连接数达到上限时限制新连接', () => { - service.setMaxConnections(100); - service.updateActiveConnections(100); - - expect(service.shouldLimitNewConnections()).toBe(true); - }); - }); - - describe('带超时和重试的操作执行测试', () => { - it('应该成功执行带超时和重试的操作', async () => { - const operation = jest.fn().mockResolvedValue('success'); - - const result = await service.executeWithTimeoutAndRetry( - operation, - { timeout: 1000, operation: 'test' }, - { maxRetries: 2, baseDelay: 10 } - ); - - expect(result).toBe('success'); - expect(operation).toHaveBeenCalledTimes(1); - }); - - it('应该在超时后重试', async () => { - let callCount = 0; - const operation = jest.fn().mockImplementation(() => { - callCount++; - if (callCount === 1) { - return new Promise(resolve => setTimeout(() => resolve('late'), 200)); - } - return Promise.resolve('success'); - }); - - const result = await service.executeWithTimeoutAndRetry( - operation, - { timeout: 50, operation: 'test' }, - { maxRetries: 2, baseDelay: 10 } - ); - - expect(result).toBe('success'); - expect(operation).toHaveBeenCalledTimes(2); - }); - }); - - describe('重连状态管理测试', () => { - it('应该正确获取重连状态', async () => { - const reconnectCallback = jest.fn().mockImplementation( - () => new Promise(resolve => setTimeout(() => resolve(false), 100)) - ); - - await service.scheduleReconnect({ - userId: 'user1', - reconnectCallback, - maxAttempts: 3, - baseDelay: 50, - }); - - const state = service.getReconnectState('user1'); - expect(state).toBeDefined(); - expect(state?.userId).toBe('user1'); - expect(state?.isReconnecting).toBe(true); - - // 清理 - service.cancelReconnect('user1'); - }); - - it('应该在重连失败达到最大次数后停止', async () => { - const reconnectCallback = jest.fn().mockResolvedValue(false); - - await service.scheduleReconnect({ - userId: 'user1', - reconnectCallback, - maxAttempts: 2, - baseDelay: 10, - }); - - // 等待重连尝试完成 - await new Promise(resolve => setTimeout(resolve, 200)); - - // 应该尝试了最大次数 - expect(reconnectCallback).toHaveBeenCalledTimes(2); - - // 重连状态应该被清理 - expect(service.getReconnectState('user1')).toBeNull(); - }); - - it('应该处理重连回调异常', async () => { - const reconnectCallback = jest.fn().mockRejectedValue(new Error('Reconnect failed')); - - await service.scheduleReconnect({ - userId: 'user1', - reconnectCallback, - maxAttempts: 2, - baseDelay: 10, - }); - - // 等待重连尝试完成 - await new Promise(resolve => setTimeout(resolve, 200)); - - // 应该尝试了最大次数 - expect(reconnectCallback).toHaveBeenCalledTimes(2); - - // 重连状态应该被清理 - expect(service.getReconnectState('user1')).toBeNull(); - }); - - it('应该在已有重连进行时跳过新的重连调度', async () => { - const reconnectCallback1 = jest.fn().mockImplementation( - () => new Promise(resolve => setTimeout(() => resolve(false), 200)) - ); - const reconnectCallback2 = jest.fn().mockResolvedValue(true); - - // 第一次调度 - const scheduled1 = await service.scheduleReconnect({ - userId: 'user1', - reconnectCallback: reconnectCallback1, - maxAttempts: 3, - baseDelay: 50, - }); - - // 第二次调度(应该被跳过) - const scheduled2 = await service.scheduleReconnect({ - userId: 'user1', - reconnectCallback: reconnectCallback2, - maxAttempts: 3, - baseDelay: 50, - }); - - expect(scheduled1).toBe(true); - expect(scheduled2).toBe(false); - expect(reconnectCallback2).not.toHaveBeenCalled(); - - // 清理 - service.cancelReconnect('user1'); - }); - }); - - describe('事件发射测试', () => { - it('应该在启用降级模式时发射事件', async () => { - const eventListener = jest.fn(); - service.on('degraded_mode_enabled', eventListener); - - await service.enableDegradedMode(); - - expect(eventListener).toHaveBeenCalledWith({ - startTime: expect.any(Date), - }); - }); - - it('应该在重连成功时发射事件', async () => { - const eventListener = jest.fn(); - service.on('reconnect_success', eventListener); - - const reconnectCallback = jest.fn().mockResolvedValue(true); - - await service.scheduleReconnect({ - userId: 'user1', - reconnectCallback, - maxAttempts: 3, - baseDelay: 10, - }); - - // 等待重连完成 - await new Promise(resolve => setTimeout(resolve, 100)); - - expect(eventListener).toHaveBeenCalledWith({ - userId: 'user1', - attempts: 1, - }); - }); - - it('应该在重连失败时发射事件', async () => { - const eventListener = jest.fn(); - service.on('reconnect_failed', eventListener); - - const reconnectCallback = jest.fn().mockResolvedValue(false); - - await service.scheduleReconnect({ - userId: 'user1', - reconnectCallback, - maxAttempts: 1, - baseDelay: 10, - }); - - // 等待重连完成 - await new Promise(resolve => setTimeout(resolve, 100)); - - expect(eventListener).toHaveBeenCalledWith({ - userId: 'user1', - attempts: 1, - }); - }); - }); - - describe('错误处理边界条件测试', () => { - it('应该处理空错误对象', async () => { - const result = await service.handleZulipError(null, 'test'); - - expect(result.success).toBe(false); - expect(result.shouldRetry).toBe(false); - }); - - it('应该处理没有code和message的错误', async () => { - const result = await service.handleZulipError({}, 'test'); - - expect(result.success).toBe(false); - expect(result.message).toBeDefined(); - }); - - it('应该处理错误处理过程中的异常', async () => { - // 模拟错误分类过程中的异常 - const originalClassifyError = (service as any).classifyError; - (service as any).classifyError = jest.fn().mockImplementation(() => { - throw new Error('Classification error'); - }); - - const result = await service.handleZulipError({ code: 500 }, 'test'); - - expect(result.success).toBe(false); - expect(result.message).toContain('错误处理失败'); - - // 恢复原方法 - (service as any).classifyError = originalClassifyError; - }); - }); - - describe('模块销毁测试', () => { - it('应该正确清理所有资源', async () => { - // 设置一些状态 - await service.enableDegradedMode(); - - const reconnectCallback = jest.fn().mockImplementation( - () => new Promise(resolve => setTimeout(() => resolve(false), 1000)) - ); - - await service.scheduleReconnect({ - userId: 'user1', - reconnectCallback, - maxAttempts: 5, - baseDelay: 100, - }); - - // 销毁模块 - await service.onModuleDestroy(); - - // 验证资源被清理 - expect(service.getReconnectState('user1')).toBeNull(); - }); - }); - - describe('并发操作测试', () => { - it('应该处理并发的错误处理请求', async () => { - const errors = [ - { code: 401, message: 'Unauthorized' }, - { code: 429, message: 'Rate limit' }, - { code: 500, message: 'Server error' }, - { code: 'ECONNREFUSED', message: 'Connection refused' }, - ]; - - const promises = errors.map((error, index) => - service.handleZulipError(error, `operation${index}`) - ); - - const results = await Promise.all(promises); - - // 所有请求都应该得到处理 - expect(results).toHaveLength(4); - results.forEach(result => { - expect(result).toBeDefined(); - expect(typeof result.success).toBe('boolean'); - expect(typeof result.shouldRetry).toBe('boolean'); - }); - }); - - it('应该处理并发的重连请求', async () => { - const users = ['user1', 'user2', 'user3']; - const reconnectCallback = jest.fn().mockResolvedValue(true); - - const promises = users.map(userId => - service.scheduleReconnect({ - userId, - reconnectCallback, - maxAttempts: 2, - baseDelay: 10, - }) - ); - - const results = await Promise.all(promises); - - // 所有重连都应该被调度 - expect(results.every(r => r === true)).toBe(true); - - // 等待重连完成 - await new Promise(resolve => setTimeout(resolve, 100)); - - // 验证所有用户的重连状态都被清理 - users.forEach(userId => { - expect(service.getReconnectState(userId)).toBeNull(); - }); - }); - }); - - describe('性能测试', () => { - it('应该能够处理大量错误而不影响性能', async () => { - const startTime = Date.now(); - - // 处理100个错误 - const promises = Array.from({ length: 100 }, (_, i) => - service.handleZulipError({ code: 500, message: `Error ${i}` }, `op${i}`) - ); - - await Promise.all(promises); - - const elapsed = Date.now() - startTime; - - // 应该在合理时间内完成(比如1秒) - expect(elapsed).toBeLessThan(1000); - }); - - it('应该能够处理大量连接数更新', () => { - const startTime = Date.now(); - - // 更新1000次连接数 - for (let i = 0; i < 1000; i++) { - service.updateActiveConnections(1); - } - - const elapsed = Date.now() - startTime; - - // 应该在合理时间内完成 - expect(elapsed).toBeLessThan(100); - }); - }); }); diff --git a/src/core/zulip/services/error_handler.service.ts b/src/core/zulip/services/error_handler.service.ts index 91c6a19..f1f12c2 100644 --- a/src/core/zulip/services/error_handler.service.ts +++ b/src/core/zulip/services/error_handler.service.ts @@ -24,7 +24,7 @@ * 依赖模块: * - AppLoggerService: 日志记录服务 * - * @author angjustinl, moyin + * @author angjustinl * @version 1.0.0 * @since 2025-12-25 */ @@ -239,8 +239,8 @@ export class ErrorHandlerService extends EventEmitter implements OnModuleDestroy this.logger.warn('处理Zulip API错误', { operation: 'handleZulipError', targetOperation: operation, - errorMessage: error?.message || 'Unknown error', - errorCode: error?.code || 'UNKNOWN', + errorMessage: error.message, + errorCode: error.code, timestamp: new Date().toISOString(), }); @@ -269,7 +269,7 @@ export class ErrorHandlerService extends EventEmitter implements OnModuleDestroy this.logger.error('错误处理过程中发生异常', { operation: 'handleZulipError', targetOperation: operation, - originalError: error?.message || 'Unknown error', + originalError: error.message, handlingError: err.message, timestamp: new Date().toISOString(), }, err.stack); @@ -438,7 +438,7 @@ export class ErrorHandlerService extends EventEmitter implements OnModuleDestroy this.logger.warn('处理连接错误', { operation: 'handleConnectionError', connectionType, - errorMessage: error?.message || 'Unknown connection error', + errorMessage: error.message, timestamp: new Date().toISOString(), }); @@ -632,16 +632,16 @@ export class ErrorHandlerService extends EventEmitter implements OnModuleDestroy case ErrorType.ZULIP_API_ERROR: return { success: false, - shouldRetry: error?.code >= 500, // 服务器错误可以重试 + shouldRetry: error.code >= 500, // 服务器错误可以重试 retryAfter: 2000, - message: `Zulip API错误: ${error?.message || 'Unknown API error'}`, + message: `Zulip API错误: ${error.message}`, }; default: return { success: false, shouldRetry: false, - message: `未知错误: ${error?.message || 'Unknown error'}`, + message: `未知错误: ${error.message}`, }; } } diff --git a/src/core/zulip/services/monitoring.service.spec.ts b/src/core/zulip/services/monitoring.service.spec.ts index 4ce1c3e..378b1bb 100644 --- a/src/core/zulip/services/monitoring.service.spec.ts +++ b/src/core/zulip/services/monitoring.service.spec.ts @@ -12,7 +12,7 @@ * **Feature: zulip-integration, Property 11: 系统监控和告警** * **Validates: Requirements 9.4** * - * @author angjustinl moyin + * @author angjustinl * @version 1.0.0 * @since 2025-12-25 */ @@ -730,362 +730,4 @@ describe('MonitoringService', () => { ); }, 30000); }); - - // ==================== 补充测试用例 ==================== - - describe('边界条件和错误处理测试', () => { - it('应该正确处理活跃连接数不会变成负数', () => { - // 先断开一个不存在的连接 - service.logConnection({ - socketId: 'socket1', - eventType: ConnectionEventType.DISCONNECTED, - timestamp: new Date(), - }); - - const stats = service.getStats(); - expect(stats.connections.active).toBe(0); // 不应该是负数 - }); - - it('应该正确处理空的最近告警列表', () => { - const alerts = service.getRecentAlerts(5); - expect(alerts).toEqual([]); - }); - - it('应该正确处理超出限制的最近告警请求', () => { - // 添加一些告警 - for (let i = 0; i < 5; i++) { - service.sendAlert({ - id: `alert-${i}`, - level: AlertLevel.INFO, - title: `Alert ${i}`, - message: `Message ${i}`, - component: 'test', - timestamp: new Date(), - }); - } - - // 请求超过实际数量的告警 - const alerts = service.getRecentAlerts(10); - expect(alerts.length).toBe(5); - }); - - it('应该正确处理零除法情况', () => { - // 在没有任何API调用时获取统计 - const stats = service.getStats(); - expect(stats.apiCalls.avgResponseTime).toBe(0); // 应该是0而不是NaN - }); - - it('应该正确处理消息延迟统计的零除法', () => { - // 在没有任何消息时获取统计 - const stats = service.getStats(); - expect(stats.messages.avgLatency).toBe(0); // 应该是0而不是NaN - }); - - it('应该正确处理不同级别的告警日志', () => { - const logSpy = jest.spyOn(Logger.prototype, 'log'); - const warnSpy = jest.spyOn(Logger.prototype, 'warn'); - const errorSpy = jest.spyOn(Logger.prototype, 'error'); - - // INFO级别 - service.sendAlert({ - id: 'info-alert', - level: AlertLevel.INFO, - title: 'Info Alert', - message: 'Info message', - component: 'test', - timestamp: new Date(), - }); - - // WARNING级别 - service.sendAlert({ - id: 'warn-alert', - level: AlertLevel.WARNING, - title: 'Warning Alert', - message: 'Warning message', - component: 'test', - timestamp: new Date(), - }); - - // ERROR级别 - service.sendAlert({ - id: 'error-alert', - level: AlertLevel.ERROR, - title: 'Error Alert', - message: 'Error message', - component: 'test', - timestamp: new Date(), - }); - - // CRITICAL级别 - service.sendAlert({ - id: 'critical-alert', - level: AlertLevel.CRITICAL, - title: 'Critical Alert', - message: 'Critical message', - component: 'test', - timestamp: new Date(), - }); - - expect(logSpy).toHaveBeenCalled(); // INFO - expect(warnSpy).toHaveBeenCalled(); // WARNING - expect(errorSpy).toHaveBeenCalledTimes(2); // ERROR + CRITICAL - }); - - it('应该正确处理健康检查中的降级状态', async () => { - // 先添加一些正常连接 - for (let i = 0; i < 10; i++) { - service.logConnection({ - socketId: `socket-normal-${i}`, - eventType: ConnectionEventType.CONNECTED, - timestamp: new Date(), - }); - } - - // 然后添加一些错误来触发降级状态 - for (let i = 0; i < 5; i++) { - service.logConnection({ - socketId: `socket-error-${i}`, - eventType: ConnectionEventType.ERROR, - timestamp: new Date(), - }); - } - - const health = await service.checkSystemHealth(); - - // 错误率应该是 5/10 = 50%,超过阈值 10%,应该是不健康状态 - expect(health.components.websocket.status).toMatch(/^(degraded|unhealthy)$/); - }); - - it('应该正确处理API调用的降级状态', async () => { - // 先添加一些成功的API调用 - for (let i = 0; i < 10; i++) { - service.logApiCall({ - operation: 'test', - userId: 'user1', - result: ApiCallResult.SUCCESS, - responseTime: 100, - timestamp: new Date(), - }); - } - - // 然后模拟大量失败的API调用 - for (let i = 0; i < 5; i++) { - service.logApiCall({ - operation: 'test', - userId: 'user1', - result: ApiCallResult.FAILURE, - responseTime: 100, - timestamp: new Date(), - }); - } - - const health = await service.checkSystemHealth(); - - // 错误率应该是 5/15 = 33%,超过阈值 10%,应该是不健康状态 - expect(health.components.zulipApi.status).toMatch(/^(degraded|unhealthy)$/); - }); - - it('应该正确处理慢API调用的降级状态', async () => { - // 模拟慢API调用 - for (let i = 0; i < 10; i++) { - service.logApiCall({ - operation: 'test', - userId: 'user1', - result: ApiCallResult.SUCCESS, - responseTime: 15000, // 超过阈值 - timestamp: new Date(), - }); - } - - const health = await service.checkSystemHealth(); - - // 应该检测到API组件降级 - expect(health.components.zulipApi.status).toMatch(/^(degraded|unhealthy)$/); - }); - - it('应该正确处理模块生命周期', () => { - // 测试模块初始化 - service.onModuleInit(); - - // 验证健康检查已启动(通过检查私有属性) - expect((service as any).healthCheckInterval).toBeDefined(); - - // 测试模块销毁 - service.onModuleDestroy(); - - // 验证健康检查已停止 - expect((service as any).healthCheckInterval).toBeNull(); - }); - - it('应该正确处理最近日志的大小限制', () => { - const maxLogs = (service as any).maxRecentLogs; - - // 添加超过限制的API调用日志 - for (let i = 0; i < maxLogs + 10; i++) { - service.logApiCall({ - operation: `test-${i}`, - userId: 'user1', - result: ApiCallResult.SUCCESS, - responseTime: 100, - timestamp: new Date(), - }); - } - - // 验证最近日志数量不超过限制 - const recentLogs = (service as any).recentApiCalls; - expect(recentLogs.length).toBeLessThanOrEqual(maxLogs); - }); - - it('应该正确处理最近告警的大小限制', () => { - const maxLogs = (service as any).maxRecentLogs; - - // 添加超过限制的告警 - for (let i = 0; i < maxLogs + 10; i++) { - service.sendAlert({ - id: `alert-${i}`, - level: AlertLevel.INFO, - title: `Alert ${i}`, - message: `Message ${i}`, - component: 'test', - timestamp: new Date(), - }); - } - - // 验证最近告警数量不超过限制 - const recentAlerts = (service as any).recentAlerts; - expect(recentAlerts.length).toBeLessThanOrEqual(maxLogs); - }); - - it('应该正确处理消息转发错误统计', () => { - service.logMessageForward({ - fromUserId: 'user1', - toUserIds: ['user2'], - stream: 'test-stream', - topic: 'test-topic', - direction: 'upstream', - success: false, // 失败的消息 - latency: 100, - error: 'Transfer failed', - timestamp: new Date(), - }); - - const stats = service.getStats(); - expect(stats.messages.errors).toBe(1); - }); - - it('应该正确处理带有元数据的连接日志', () => { - const eventHandler = jest.fn(); - service.on('connection_event', eventHandler); - - service.logConnection({ - socketId: 'socket1', - userId: 'user1', - eventType: ConnectionEventType.CONNECTED, - duration: 1000, - timestamp: new Date(), - metadata: { userAgent: 'test-agent', ip: '127.0.0.1' }, - }); - - expect(eventHandler).toHaveBeenCalledWith( - expect.objectContaining({ - metadata: { userAgent: 'test-agent', ip: '127.0.0.1' }, - }) - ); - - service.removeListener('connection_event', eventHandler); - }); - - it('应该正确处理带有元数据的API调用日志', () => { - const eventHandler = jest.fn(); - service.on('api_call', eventHandler); - - service.logApiCall({ - operation: 'sendMessage', - userId: 'user1', - result: ApiCallResult.SUCCESS, - responseTime: 100, - statusCode: 200, - timestamp: new Date(), - metadata: { endpoint: '/api/messages', method: 'POST' }, - }); - - expect(eventHandler).toHaveBeenCalledWith( - expect.objectContaining({ - metadata: { endpoint: '/api/messages', method: 'POST' }, - }) - ); - - service.removeListener('api_call', eventHandler); - }); - - it('应该正确处理带有消息ID的消息转发日志', () => { - const eventHandler = jest.fn(); - service.on('message_forward', eventHandler); - - service.logMessageForward({ - messageId: 12345, - fromUserId: 'user1', - toUserIds: ['user2', 'user3'], - stream: 'test-stream', - topic: 'test-topic', - direction: 'downstream', - success: true, - latency: 50, - timestamp: new Date(), - }); - - expect(eventHandler).toHaveBeenCalledWith( - expect.objectContaining({ - messageId: 12345, - }) - ); - - service.removeListener('message_forward', eventHandler); - }); - - it('应该正确处理带有详情的操作确认', () => { - const eventHandler = jest.fn(); - service.on('operation_confirmed', eventHandler); - - service.confirmOperation({ - operationId: 'op123', - operation: 'sendMessage', - userId: 'user1', - success: true, - timestamp: new Date(), - details: { messageId: 456, recipients: 3 }, - }); - - expect(eventHandler).toHaveBeenCalledWith( - expect.objectContaining({ - details: { messageId: 456, recipients: 3 }, - }) - ); - - service.removeListener('operation_confirmed', eventHandler); - }); - - it('应该正确处理带有元数据的告警', () => { - const eventHandler = jest.fn(); - service.on('alert', eventHandler); - - service.sendAlert({ - id: 'alert123', - level: AlertLevel.WARNING, - title: 'Test Alert', - message: 'Test message', - component: 'test-component', - timestamp: new Date(), - metadata: { threshold: 5000, actual: 7000 }, - }); - - expect(eventHandler).toHaveBeenCalledWith( - expect.objectContaining({ - metadata: { threshold: 5000, actual: 7000 }, - }) - ); - - service.removeListener('alert', eventHandler); - }); - }); }); diff --git a/src/main.ts b/src/main.ts index e12ee89..03bbfe6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -40,17 +40,10 @@ async function bootstrap() { logger: ['error', 'warn', 'log'], }); - // 允许前端后台(如Vite/React)跨域访问,包括WebSocket + // 允许前端后台(如Vite/React)跨域访问 app.enableCors({ - origin: [ - 'http://localhost:3000', - 'http://localhost:5173', // Vite默认端口 - 'https://whaletownend.xinghangee.icu', - /^https:\/\/.*\.xinghangee\.icu$/ - ], + origin: true, credentials: true, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], }); // 全局启用校验管道(核心配置) diff --git a/test_zulip.js b/test_zulip.js deleted file mode 100644 index d58f7db..0000000 --- a/test_zulip.js +++ /dev/null @@ -1,131 +0,0 @@ -const io = require('socket.io-client'); - -// 使用用户 API Key 测试 Zulip 集成 -async function testWithUserApiKey() { - console.log('🚀 使用用户 API Key 测试 Zulip 集成...'); - console.log('📡 用户 API Key: lCPWCPfGh7WU...pqNfGF8'); - console.log('📡 Zulip 服务器: https://zulip.xinghangee.icu/'); - console.log('📡 游戏服务器: https://whaletownend.xinghangee.icu/game'); - - const socket = io('wss://whaletownend.xinghangee.icu/game', { - transports: ['websocket', 'polling'], // WebSocket优先,polling备用 - timeout: 20000, - forceNew: true, - reconnection: true, - reconnectionAttempts: 3, - reconnectionDelay: 1000 - }); - - let testStep = 0; - - socket.on('connect', () => { - console.log('✅ WebSocket 连接成功'); - testStep = 1; - - // 使用包含用户 API Key 的 token - const loginMessage = { - type: 'login', - token: 'lCPWCPfGh7...fGF8_user_token' - }; - - console.log('📤 步骤 1: 发送登录消息(使用用户 API Key)'); - socket.emit('login', loginMessage); - }); - - socket.on('login_success', (data) => { - console.log('✅ 步骤 1 完成: 登录成功'); - console.log(' 会话ID:', data.sessionId); - console.log(' 用户ID:', data.userId); - console.log(' 用户名:', data.username); - console.log(' 当前地图:', data.currentMap); - testStep = 2; - - // 等待 Zulip 客户端初始化 - console.log('⏳ 等待 3 秒让 Zulip 客户端初始化...'); - setTimeout(() => { - const chatMessage = { - t: 'chat', - content: '🎮 【用户API Key测试】来自游戏的消息!\\n' + - '时间: ' + new Date().toLocaleString() + '\\n' + - '使用用户 API Key 发送此消息。', - scope: 'local' - }; - - console.log('📤 步骤 2: 发送消息到 Zulip(使用用户 API Key)'); - console.log(' 目标 Stream: Whale Port'); - socket.emit('chat', chatMessage); - }, 3000); - }); - - socket.on('chat_sent', (data) => { - console.log('✅ 步骤 2 完成: 消息发送成功'); - console.log(' 响应:', JSON.stringify(data, null, 2)); - - // 只在第一次收到 chat_sent 时发送第二条消息 - if (testStep === 2) { - testStep = 3; - - setTimeout(() => { - // 先切换到 Pumpkin Valley 地图 - console.log('📤 步骤 3: 切换到 Pumpkin Valley 地图'); - const positionUpdate = { - t: 'position', - x: 150, - y: 400, - mapId: 'pumpkin_valley' - }; - socket.emit('position_update', positionUpdate); - - // 等待位置更新后发送消息 - setTimeout(() => { - const chatMessage2 = { - t: 'chat', - content: '🎃 在南瓜谷发送的测试消息!', - scope: 'local' - }; - - console.log('📤 步骤 4: 在 Pumpkin Valley 发送消息'); - socket.emit('chat', chatMessage2); - }, 1000); - }, 2000); - } - }); - - socket.on('chat_render', (data) => { - console.log('📨 收到来自 Zulip 的消息:'); - console.log(' 发送者:', data.from); - console.log(' 内容:', data.txt); - console.log(' Stream:', data.stream || '未知'); - console.log(' Topic:', data.topic || '未知'); - }); - - socket.on('error', (error) => { - console.log('❌ 收到错误:', JSON.stringify(error, null, 2)); - }); - - socket.on('disconnect', () => { - console.log('🔌 WebSocket 连接已关闭'); - console.log(''); - console.log('📊 测试结果:'); - console.log(' 完成步骤:', testStep, '/ 4'); - if (testStep >= 3) { - console.log(' ✅ 核心功能正常!'); - console.log(' 💡 请检查 Zulip 中的 "Whale Port" 和 "Pumpkin Valley" Streams 查看消息'); - } - process.exit(0); - }); - - socket.on('connect_error', (error) => { - console.error('❌ 连接错误:', error.message); - process.exit(1); - }); - - // 20秒后自动关闭(给足够时间完成测试) - setTimeout(() => { - console.log('⏰ 测试时间到,关闭连接'); - socket.disconnect(); - }, 20000); -} - -console.log('🔧 准备测试环境...'); -testWithUserApiKey().catch(console.error); \ No newline at end of file