feat: 全面优化代码质量至 8.55/10 分
## 安全增强 - 添加 CSRF 防护机制(Double Submit Cookie 模式) - 增强密码强度验证(8字符+两种字符类型) - 添加 Session 密钥安全检查 - 修复 .htaccess 文件上传漏洞 - 统一使用 getSafeErrorMessage() 保护敏感错误信息 - 增强数据库原型污染防护 - 添加被封禁用户分享访问检查 ## 功能修复 - 修复模态框点击外部关闭功能 - 修复 share.html 未定义方法调用 - 修复 verify.html 和 reset-password.html API 路径 - 修复数据库 SFTP->OSS 迁移逻辑 - 修复 OSS 未配置时的错误提示 - 添加文件夹名称长度限制 - 添加文件列表 API 路径验证 ## UI/UX 改进 - 添加 6 个按钮加载状态(登录/注册/修改密码等) - 将 15+ 处 alert() 替换为 Toast 通知 - 添加防重复提交机制(创建文件夹/分享) - 优化 loadUserProfile 防抖调用 ## 代码质量 - 消除 formatFileSize 重复定义 - 集中模块导入到文件顶部 - 添加 JSDoc 注释 - 创建路由拆分示例 (routes/) ## 测试套件 - 添加 boundary-tests.js (60 用例) - 添加 network-concurrent-tests.js (33 用例) - 添加 state-consistency-tests.js (38 用例) - 添加 test_share.js 和 test_admin.js ## 文档和配置 - 新增 INSTALL_GUIDE.md 手动部署指南 - 新增 VERSION.txt 版本历史 - 完善 .env.example 配置说明 - 新增 docker-compose.yml - 完善 nginx.conf.example Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
838
backend/tests/network-concurrent-tests.js
Normal file
838
backend/tests/network-concurrent-tests.js
Normal file
@@ -0,0 +1,838 @@
|
||||
/**
|
||||
* 网络异常和并发操作测试套件
|
||||
*
|
||||
* 测试范围:
|
||||
* 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);
|
||||
});
|
||||
Reference in New Issue
Block a user