/** * 网络异常和并发操作测试套件 * * 测试范围: * 1. 网络异常处理(超时、断连、OSS连接失败) * 2. 并发操作测试(多文件上传、多文件删除、重复提交) * 3. 防重复提交测试 */ const assert = require('assert'); const path = require('path'); const fs = require('fs'); // 测试结果收集器 const testResults = { passed: 0, failed: 0, errors: [] }; // 测试辅助函数 function test(name, fn) { try { fn(); testResults.passed++; console.log(` [PASS] ${name}`); } catch (error) { testResults.failed++; testResults.errors.push({ name, error: error.message }); console.log(` [FAIL] ${name}: ${error.message}`); } } async function asyncTest(name, fn) { try { await fn(); testResults.passed++; console.log(` [PASS] ${name}`); } catch (error) { testResults.failed++; testResults.errors.push({ name, error: error.message }); console.log(` [FAIL] ${name}: ${error.message}`); } } async function runTests() { // ============================================================ // 1. OSS 错误格式化测试 // ============================================================ console.log('\n========== 1. OSS 错误格式化测试 ==========\n'); function testOssErrorFormatting() { console.log('--- 测试 OSS 错误消息格式化 ---'); // 模拟 formatOssError 函数 function formatOssError(error, operation = '操作') { const errorMessages = { 'NoSuchBucket': 'OSS 存储桶不存在,请检查配置', 'AccessDenied': 'OSS 访问被拒绝,请检查权限配置', 'InvalidAccessKeyId': 'OSS Access Key 无效,请重新配置', 'SignatureDoesNotMatch': 'OSS 签名验证失败,请检查 Secret Key', 'NoSuchKey': '文件或目录不存在', 'EntityTooLarge': '文件过大,超过了 OSS 允许的最大大小', 'RequestTimeout': 'OSS 请求超时,请稍后重试', 'SlowDown': 'OSS 请求过于频繁,请稍后重试', 'ServiceUnavailable': 'OSS 服务暂时不可用,请稍后重试', 'InternalError': 'OSS 内部错误,请稍后重试' }; const networkErrors = { 'ECONNREFUSED': '无法连接到 OSS 服务,请检查网络', 'ENOTFOUND': 'OSS 服务地址无法解析,请检查 endpoint 配置', 'ETIMEDOUT': '连接 OSS 服务超时,请检查网络', 'ECONNRESET': '与 OSS 服务的连接被重置,请重试', 'EPIPE': '与 OSS 服务的连接中断,请重试', 'EHOSTUNREACH': '无法访问 OSS 服务主机,请检查网络' }; if (error.name && errorMessages[error.name]) { return new Error(`${operation}失败: ${errorMessages[error.name]}`); } if (error.code && networkErrors[error.code]) { return new Error(`${operation}失败: ${networkErrors[error.code]}`); } if (error.$metadata?.httpStatusCode) { const statusCode = error.$metadata.httpStatusCode; const statusMessages = { 400: '请求参数错误', 401: '认证失败,请检查 Access Key', 403: '没有权限执行此操作', 404: '资源不存在', 429: '请求过于频繁,请稍后重试', 500: 'OSS 服务内部错误', 503: 'OSS 服务暂时不可用' }; if (statusMessages[statusCode]) { return new Error(`${operation}失败: ${statusMessages[statusCode]}`); } } return new Error(`${operation}失败: ${error.message}`); } test('NoSuchBucket 错误应该被正确格式化', () => { const error = { name: 'NoSuchBucket', message: 'The specified bucket does not exist' }; const formatted = formatOssError(error, '列出文件'); assert.ok(formatted.message.includes('存储桶不存在')); }); test('AccessDenied 错误应该被正确格式化', () => { const error = { name: 'AccessDenied', message: 'Access Denied' }; const formatted = formatOssError(error, '上传文件'); assert.ok(formatted.message.includes('访问被拒绝')); }); test('网络超时错误应该被正确格式化', () => { const error = { code: 'ETIMEDOUT', message: 'connect ETIMEDOUT' }; const formatted = formatOssError(error, '连接'); assert.ok(formatted.message.includes('超时')); }); test('连接被拒绝错误应该被正确格式化', () => { const error = { code: 'ECONNREFUSED', message: 'connect ECONNREFUSED' }; const formatted = formatOssError(error, '连接'); assert.ok(formatted.message.includes('无法连接')); }); test('DNS 解析失败应该被正确格式化', () => { const error = { code: 'ENOTFOUND', message: 'getaddrinfo ENOTFOUND' }; const formatted = formatOssError(error, '连接'); assert.ok(formatted.message.includes('无法解析')); }); test('HTTP 401 错误应该被正确格式化', () => { const error = { message: 'Unauthorized', $metadata: { httpStatusCode: 401 } }; const formatted = formatOssError(error, '认证'); assert.ok(formatted.message.includes('认证失败')); }); test('HTTP 403 错误应该被正确格式化', () => { const error = { message: 'Forbidden', $metadata: { httpStatusCode: 403 } }; const formatted = formatOssError(error, '访问'); assert.ok(formatted.message.includes('没有权限')); }); test('HTTP 429 错误(限流)应该被正确格式化', () => { const error = { message: 'Too Many Requests', $metadata: { httpStatusCode: 429 } }; const formatted = formatOssError(error, '请求'); assert.ok(formatted.message.includes('过于频繁')); }); test('未知错误应该保留原始消息', () => { const error = { message: 'Unknown error occurred' }; const formatted = formatOssError(error, '操作'); assert.ok(formatted.message.includes('Unknown error occurred')); }); } testOssErrorFormatting(); // ============================================================ // 2. 并发限流测试 // ============================================================ console.log('\n========== 2. 并发限流测试 ==========\n'); async function testConcurrentRateLimiting() { console.log('--- 测试并发请求限流 ---'); // 简化版 RateLimiter class RateLimiter { constructor(options = {}) { this.maxAttempts = options.maxAttempts || 5; this.windowMs = options.windowMs || 15 * 60 * 1000; this.blockDuration = options.blockDuration || 30 * 60 * 1000; this.attempts = new Map(); this.blockedKeys = new Map(); } isBlocked(key) { const blockInfo = this.blockedKeys.get(key); if (!blockInfo) return false; if (Date.now() > blockInfo.expiresAt) { this.blockedKeys.delete(key); this.attempts.delete(key); return false; } return true; } recordFailure(key) { const now = Date.now(); if (this.isBlocked(key)) { return { blocked: true, remainingAttempts: 0 }; } let attemptInfo = this.attempts.get(key); if (!attemptInfo || now > attemptInfo.windowEnd) { attemptInfo = { count: 0, windowEnd: now + this.windowMs }; } attemptInfo.count++; this.attempts.set(key, attemptInfo); if (attemptInfo.count >= this.maxAttempts) { this.blockedKeys.set(key, { expiresAt: now + this.blockDuration }); return { blocked: true, remainingAttempts: 0 }; } return { blocked: false, remainingAttempts: this.maxAttempts - attemptInfo.count }; } recordSuccess(key) { this.attempts.delete(key); this.blockedKeys.delete(key); } getStats() { return { activeAttempts: this.attempts.size, blockedKeys: this.blockedKeys.size }; } } await asyncTest('并发失败请求应该正确累计', async () => { const limiter = new RateLimiter({ maxAttempts: 5, windowMs: 1000, blockDuration: 1000 }); const key = 'concurrent-test-1'; // 模拟并发请求 const promises = Array(5).fill().map(() => new Promise(resolve => { const result = limiter.recordFailure(key); resolve(result); }) ); const results = await Promise.all(promises); // 最后一个请求应该触发阻止 assert.ok(results.some(r => r.blocked), '应该有请求被阻止'); }); await asyncTest('不同 IP 的并发请求应该独立计数', async () => { const limiter = new RateLimiter({ maxAttempts: 3, windowMs: 1000, blockDuration: 1000 }); // 模拟来自不同 IP 的请求 const ips = ['192.168.1.1', '192.168.1.2', '192.168.1.3']; for (const ip of ips) { limiter.recordFailure(`login:ip:${ip}`); limiter.recordFailure(`login:ip:${ip}`); } // 每个 IP 都应该还有 1 次机会 for (const ip of ips) { const result = limiter.recordFailure(`login:ip:${ip}`); assert.strictEqual(result.blocked, true, `IP ${ip} 应该被阻止`); } }); await asyncTest('限流器统计应该正确反映状态', async () => { const limiter = new RateLimiter({ maxAttempts: 2, windowMs: 1000, blockDuration: 1000 }); limiter.recordFailure('key1'); limiter.recordFailure('key2'); limiter.recordFailure('key2'); // 这会阻止 key2 const stats = limiter.getStats(); assert.ok(stats.activeAttempts >= 1, '应该有活动的尝试记录'); assert.ok(stats.blockedKeys >= 1, '应该有被阻止的 key'); }); } await testConcurrentRateLimiting(); // ============================================================ // 3. 文件上传并发测试 // ============================================================ console.log('\n========== 3. 文件上传并发测试 ==========\n'); async function testConcurrentFileOperations() { console.log('--- 测试并发文件操作 ---'); // 模拟文件上传限流器 class UploadLimiter { constructor(maxConcurrent = 5, maxPerHour = 100) { this.maxConcurrent = maxConcurrent; this.maxPerHour = maxPerHour; this.currentUploads = new Map(); this.hourlyCount = new Map(); } canUpload(userId) { const now = Date.now(); const hourKey = `${userId}:${Math.floor(now / 3600000)}`; // 检查小时限制 const hourlyUsage = this.hourlyCount.get(hourKey) || 0; if (hourlyUsage >= this.maxPerHour) { return { allowed: false, reason: '每小时上传次数已达上限' }; } // 检查并发限制 const userUploads = this.currentUploads.get(userId) || 0; if (userUploads >= this.maxConcurrent) { return { allowed: false, reason: '并发上传数已达上限' }; } return { allowed: true }; } startUpload(userId) { const check = this.canUpload(userId); if (!check.allowed) { return check; } const now = Date.now(); const hourKey = `${userId}:${Math.floor(now / 3600000)}`; // 增加计数 this.currentUploads.set(userId, (this.currentUploads.get(userId) || 0) + 1); this.hourlyCount.set(hourKey, (this.hourlyCount.get(hourKey) || 0) + 1); return { allowed: true }; } endUpload(userId) { const current = this.currentUploads.get(userId) || 0; if (current > 0) { this.currentUploads.set(userId, current - 1); } } getStatus(userId) { const now = Date.now(); const hourKey = `${userId}:${Math.floor(now / 3600000)}`; return { concurrent: this.currentUploads.get(userId) || 0, hourlyUsed: this.hourlyCount.get(hourKey) || 0, maxConcurrent: this.maxConcurrent, maxPerHour: this.maxPerHour }; } } await asyncTest('并发上传限制应该生效', async () => { const limiter = new UploadLimiter(3, 100); const userId = 'user1'; // 开始 3 个上传 assert.ok(limiter.startUpload(userId).allowed); assert.ok(limiter.startUpload(userId).allowed); assert.ok(limiter.startUpload(userId).allowed); // 第 4 个应该被拒绝 const result = limiter.startUpload(userId); assert.strictEqual(result.allowed, false); assert.ok(result.reason.includes('并发')); }); await asyncTest('完成上传后应该释放并发槽位', async () => { const limiter = new UploadLimiter(2, 100); const userId = 'user2'; limiter.startUpload(userId); limiter.startUpload(userId); // 应该被拒绝 assert.strictEqual(limiter.startUpload(userId).allowed, false); // 完成一个上传 limiter.endUpload(userId); // 现在应该允许 assert.ok(limiter.startUpload(userId).allowed); }); await asyncTest('每小时上传限制应该生效', async () => { const limiter = new UploadLimiter(100, 5); // 最多 5 次每小时 const userId = 'user3'; // 上传 5 次 for (let i = 0; i < 5; i++) { limiter.startUpload(userId); limiter.endUpload(userId); } // 第 6 次应该被拒绝 const result = limiter.startUpload(userId); assert.strictEqual(result.allowed, false); assert.ok(result.reason.includes('小时')); }); await asyncTest('不同用户的限制应该独立', async () => { const limiter = new UploadLimiter(2, 100); // 用户 1 达到限制 limiter.startUpload('userA'); limiter.startUpload('userA'); assert.strictEqual(limiter.startUpload('userA').allowed, false); // 用户 2 应该不受影响 assert.ok(limiter.startUpload('userB').allowed); }); } await testConcurrentFileOperations(); // ============================================================ // 4. 防重复提交测试 // ============================================================ console.log('\n========== 4. 防重复提交测试 ==========\n'); async function testDuplicateSubmissionPrevention() { console.log('--- 测试防重复提交机制 ---'); // 简单的请求去重器 class RequestDeduplicator { constructor(windowMs = 1000) { this.windowMs = windowMs; this.pending = new Map(); } // 生成请求唯一标识 getRequestKey(userId, action, params) { return `${userId}:${action}:${JSON.stringify(params)}`; } // 检查是否是重复请求 isDuplicate(userId, action, params) { const key = this.getRequestKey(userId, action, params); const now = Date.now(); if (this.pending.has(key)) { const lastRequest = this.pending.get(key); if (now - lastRequest < this.windowMs) { return true; } } this.pending.set(key, now); return false; } // 清除过期记录 cleanup() { const now = Date.now(); for (const [key, timestamp] of this.pending.entries()) { if (now - timestamp > this.windowMs) { this.pending.delete(key); } } } } await asyncTest('快速重复提交应该被检测', async () => { const dedup = new RequestDeduplicator(100); const isDup1 = dedup.isDuplicate('user1', 'delete', { file: 'test.txt' }); assert.strictEqual(isDup1, false, '首次请求不应该是重复'); const isDup2 = dedup.isDuplicate('user1', 'delete', { file: 'test.txt' }); assert.strictEqual(isDup2, true, '立即重复应该被检测'); }); await asyncTest('不同参数的请求不应该被视为重复', async () => { const dedup = new RequestDeduplicator(100); dedup.isDuplicate('user1', 'delete', { file: 'test1.txt' }); const isDup = dedup.isDuplicate('user1', 'delete', { file: 'test2.txt' }); assert.strictEqual(isDup, false, '不同参数不应该是重复'); }); await asyncTest('超时后应该允许重新提交', async () => { const dedup = new RequestDeduplicator(50); dedup.isDuplicate('user1', 'create', { name: 'folder' }); // 等待超时 await new Promise(resolve => setTimeout(resolve, 60)); const isDup = dedup.isDuplicate('user1', 'create', { name: 'folder' }); assert.strictEqual(isDup, false, '超时后应该允许'); }); await asyncTest('不同用户的相同请求不应该冲突', async () => { const dedup = new RequestDeduplicator(100); dedup.isDuplicate('user1', 'share', { file: 'doc.pdf' }); const isDup = dedup.isDuplicate('user2', 'share', { file: 'doc.pdf' }); assert.strictEqual(isDup, false, '不同用户不应该冲突'); }); } await testDuplicateSubmissionPrevention(); // ============================================================ // 5. 缓存失效测试 // ============================================================ console.log('\n========== 5. 缓存失效测试 ==========\n'); async function testCacheInvalidation() { console.log('--- 测试缓存过期和失效 ---'); // TTL 缓存类 class TTLCache { constructor(defaultTTL = 3600000) { this.cache = new Map(); this.defaultTTL = defaultTTL; } set(key, value, ttl = this.defaultTTL) { const expiresAt = Date.now() + ttl; this.cache.set(key, { value, expiresAt }); } get(key) { const item = this.cache.get(key); if (!item) return undefined; if (Date.now() > item.expiresAt) { this.cache.delete(key); return undefined; } return item.value; } has(key) { return this.get(key) !== undefined; } delete(key) { return this.cache.delete(key); } size() { return this.cache.size; } cleanup() { const now = Date.now(); let cleaned = 0; for (const [key, item] of this.cache.entries()) { if (now > item.expiresAt) { this.cache.delete(key); cleaned++; } } return cleaned; } } await asyncTest('缓存应该在 TTL 内有效', async () => { const cache = new TTLCache(100); cache.set('key1', 'value1'); assert.strictEqual(cache.get('key1'), 'value1'); }); await asyncTest('缓存应该在 TTL 后过期', async () => { const cache = new TTLCache(50); cache.set('key2', 'value2'); await new Promise(resolve => setTimeout(resolve, 60)); assert.strictEqual(cache.get('key2'), undefined); }); await asyncTest('手动删除应该立即生效', async () => { const cache = new TTLCache(10000); cache.set('key3', 'value3'); cache.delete('key3'); assert.strictEqual(cache.get('key3'), undefined); }); await asyncTest('cleanup 应该清除所有过期项', async () => { const cache = new TTLCache(50); cache.set('a', 1); cache.set('b', 2); cache.set('c', 3); await new Promise(resolve => setTimeout(resolve, 60)); const cleaned = cache.cleanup(); assert.strictEqual(cleaned, 3); assert.strictEqual(cache.size(), 0); }); await asyncTest('不同 TTL 的项应该分别过期', async () => { const cache = new TTLCache(1000); cache.set('short', 'value', 30); cache.set('long', 'value', 1000); await new Promise(resolve => setTimeout(resolve, 50)); assert.strictEqual(cache.get('short'), undefined, '短 TTL 应该过期'); assert.strictEqual(cache.get('long'), 'value', '长 TTL 应该有效'); }); } await testCacheInvalidation(); // ============================================================ // 6. 超时处理测试 // ============================================================ console.log('\n========== 6. 超时处理测试 ==========\n'); async function testTimeoutHandling() { console.log('--- 测试请求超时处理 ---'); // 带超时的 Promise 包装器 function withTimeout(promise, ms, errorMessage = '操作超时') { let timeoutId; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(new Error(errorMessage)); }, ms); }); return Promise.race([promise, timeoutPromise]).finally(() => { clearTimeout(timeoutId); }); } await asyncTest('快速操作应该成功完成', async () => { const fastOperation = new Promise(resolve => { setTimeout(() => resolve('success'), 10); }); const result = await withTimeout(fastOperation, 100); assert.strictEqual(result, 'success'); }); await asyncTest('慢速操作应该触发超时', async () => { const slowOperation = new Promise(resolve => { setTimeout(() => resolve('success'), 200); }); try { await withTimeout(slowOperation, 50); assert.fail('应该抛出超时错误'); } catch (error) { assert.ok(error.message.includes('超时')); } }); await asyncTest('自定义超时消息应该正确显示', async () => { const slowOperation = new Promise(resolve => { setTimeout(() => resolve('success'), 200); }); try { await withTimeout(slowOperation, 50, 'OSS 连接超时'); } catch (error) { assert.ok(error.message.includes('OSS')); } }); await asyncTest('超时后原始 Promise 的完成不应该影响结果', async () => { let completed = false; const operation = new Promise(resolve => { setTimeout(() => { completed = true; resolve('done'); }, 100); }); try { await withTimeout(operation, 20); } catch (error) { // 超时了 } // 等待原始 Promise 完成 await new Promise(resolve => setTimeout(resolve, 150)); assert.ok(completed, '原始 Promise 应该完成'); }); } await testTimeoutHandling(); // ============================================================ // 7. 重试机制测试 // ============================================================ console.log('\n========== 7. 重试机制测试 ==========\n'); async function testRetryMechanism() { console.log('--- 测试操作重试机制 ---'); // 带重试的函数执行器 async function withRetry(fn, options = {}) { const { maxAttempts = 3, delayMs = 100, backoff = 1.5, shouldRetry = (error) => true } = options; let lastError; let delay = delayMs; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(); } catch (error) { lastError = error; if (attempt === maxAttempts || !shouldRetry(error)) { throw error; } await new Promise(resolve => setTimeout(resolve, delay)); delay *= backoff; } } throw lastError; } await asyncTest('成功操作不应该重试', async () => { let attempts = 0; const result = await withRetry(async () => { attempts++; return 'success'; }); assert.strictEqual(result, 'success'); assert.strictEqual(attempts, 1); }); await asyncTest('失败操作应该重试指定次数', async () => { let attempts = 0; try { await withRetry(async () => { attempts++; throw new Error('always fail'); }, { maxAttempts: 3, delayMs: 10 }); } catch (error) { // 预期会失败 } assert.strictEqual(attempts, 3); }); await asyncTest('重试后成功应该返回结果', async () => { let attempts = 0; const result = await withRetry(async () => { attempts++; if (attempts < 3) { throw new Error('not yet'); } return 'finally success'; }, { maxAttempts: 5, delayMs: 10 }); assert.strictEqual(result, 'finally success'); assert.strictEqual(attempts, 3); }); await asyncTest('shouldRetry 为 false 时不应该重试', async () => { let attempts = 0; try { await withRetry(async () => { attempts++; const error = new Error('fatal'); error.code = 'FATAL'; throw error; }, { maxAttempts: 5, delayMs: 10, shouldRetry: (error) => error.code !== 'FATAL' }); } catch (error) { // 预期会失败 } assert.strictEqual(attempts, 1, '不应该重试 FATAL 错误'); }); } await testRetryMechanism(); // ============================================================ // 测试总结 // ============================================================ console.log('\n========================================'); console.log('测试总结'); console.log('========================================'); console.log(`通过: ${testResults.passed}`); console.log(`失败: ${testResults.failed}`); if (testResults.errors.length > 0) { console.log('\n失败的测试:'); testResults.errors.forEach((e, i) => { console.log(` ${i + 1}. ${e.name}: ${e.error}`); }); } console.log('\n'); return testResults; } // 运行测试 runTests().then(testResults => { process.exit(testResults.failed > 0 ? 1 : 0); }).catch(err => { console.error('测试执行错误:', err); process.exit(1); });