/** * 边界条件和异常处理测试套件 * * 测试范围: * 1. 输入边界测试(空字符串、超长字符串、特殊字符、SQL注入、XSS) * 2. 文件操作边界测试(空文件、超大文件、特殊字符文件名、深层目录) * 3. 网络异常测试(超时、断连、OSS连接失败) * 4. 并发操作测试(多文件上传、多文件删除、重复提交) * 5. 状态一致性测试(刷新恢复、Token过期、存储切换) */ const assert = require('assert'); const path = require('path'); const fs = require('fs'); // 主函数包装器(支持 async/await) async function runTests() { // 测试结果收集器 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}`); } } // ============================================================ // 1. 输入边界测试 // ============================================================ console.log('\n========== 1. 输入边界测试 ==========\n'); // 测试 sanitizeInput 函数 function testSanitizeInput() { console.log('--- 测试 XSS 过滤函数 sanitizeInput ---'); // 从 server.js 复制的 sanitizeInput 函数 function sanitizeInput(str) { if (typeof str !== 'string') return str; let sanitized = str .replace(/[&<>"']/g, (char) => { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return map[char]; }); sanitized = sanitized.replace(/(?:javascript|data|vbscript|expression|on\w+)\s*:/gi, ''); sanitized = sanitized.replace(/\x00/g, ''); return sanitized; } // 空字符串测试 test('空字符串输入应该返回空字符串', () => { assert.strictEqual(sanitizeInput(''), ''); }); // 超长字符串测试 test('超长字符串应该被正确处理', () => { const longStr = 'a'.repeat(100000); const result = sanitizeInput(longStr); assert.strictEqual(result.length, 100000); }); // 特殊字符测试 test('HTML 特殊字符应该被转义', () => { assert.strictEqual(sanitizeInput('', '', 'click', '
hover
', 'javascript:alert(1)', 'data:text/html,' ]; xssTests.forEach(xss => { const result = sanitizeInput(xss); assert.ok(!result.includes(''), false); }); test('合法 token 应该被接受', () => { assert.strictEqual(isValidTokenFormat('a'.repeat(48)), true); assert.strictEqual(isValidTokenFormat('abcdef123456'.repeat(4)), true); assert.strictEqual(isValidTokenFormat('ABCDEF123456'.repeat(4)), true); }); } testTokenValidation(); // ============================================================ // 5. 并发和竞态条件测试 // ============================================================ console.log('\n========== 5. 并发和竞态条件测试 ==========\n'); async function testRateLimiter() { 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 }; } 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); } getFailureCount(key) { const attemptInfo = this.attempts.get(key); if (!attemptInfo || Date.now() > attemptInfo.windowEnd) { return 0; } return attemptInfo.count; } } const limiter = new RateLimiter({ maxAttempts: 3, windowMs: 1000, blockDuration: 1000 }); await asyncTest('首次请求应该不被阻止', async () => { const result = limiter.recordFailure('test-ip-1'); assert.strictEqual(result.blocked, false); assert.strictEqual(result.remainingAttempts, 2); }); await asyncTest('达到限制后应该被阻止', async () => { const key = 'test-ip-2'; limiter.recordFailure(key); limiter.recordFailure(key); const result = limiter.recordFailure(key); assert.strictEqual(result.blocked, true); assert.strictEqual(limiter.isBlocked(key), true); }); await asyncTest('成功后应该清除计数', async () => { const key = 'test-ip-3'; limiter.recordFailure(key); limiter.recordFailure(key); limiter.recordSuccess(key); assert.strictEqual(limiter.getFailureCount(key), 0); assert.strictEqual(limiter.isBlocked(key), false); }); await asyncTest('阻止过期后应该自动解除', async () => { const key = 'test-ip-4'; limiter.recordFailure(key); limiter.recordFailure(key); limiter.recordFailure(key); // 模拟时间过期 const blockInfo = limiter.blockedKeys.get(key); if (blockInfo) { blockInfo.expiresAt = Date.now() - 1; } assert.strictEqual(limiter.isBlocked(key), false); }); } await testRateLimiter(); // ============================================================ // 6. 数据库操作边界测试 // ============================================================ console.log('\n========== 6. 数据库操作边界测试 ==========\n'); function testDatabaseFieldWhitelist() { console.log('--- 测试数据库字段白名单 ---'); const ALLOWED_FIELDS = [ 'username', 'email', 'password', 'oss_provider', 'oss_region', 'oss_access_key_id', 'oss_access_key_secret', 'oss_bucket', 'oss_endpoint', 'upload_api_key', 'is_admin', 'is_active', 'is_banned', 'has_oss_config', 'is_verified', 'verification_token', 'verification_expires_at', 'storage_permission', 'current_storage_type', 'local_storage_quota', 'local_storage_used', 'theme_preference' ]; function filterUpdates(updates) { const filtered = {}; for (const [key, value] of Object.entries(updates)) { if (ALLOWED_FIELDS.includes(key)) { filtered[key] = value; } } return filtered; } test('合法字段应该被保留', () => { const updates = { username: 'newname', email: 'new@email.com' }; const filtered = filterUpdates(updates); assert.strictEqual(filtered.username, 'newname'); assert.strictEqual(filtered.email, 'new@email.com'); }); test('非法字段应该被过滤', () => { const updates = { username: 'newname', id: 999, // 尝试修改 ID is_admin: 1, // 合法字段 sql_injection: "'; DROP TABLE users; --" // 非法字段 }; const filtered = filterUpdates(updates); assert.ok(!('id' in filtered)); assert.ok(!('sql_injection' in filtered)); assert.strictEqual(filtered.username, 'newname'); assert.strictEqual(filtered.is_admin, 1); }); test('原型污染尝试应该被阻止', () => { // 测试通过 JSON.parse 创建的包含 __proto__ 的对象 const maliciousJson = '{"username":"test","__proto__":{"isAdmin":true},"constructor":{"prototype":{}}}'; const updates = JSON.parse(maliciousJson); const filtered = filterUpdates(updates); // 即使 JSON.parse 创建了 __proto__ 属性,也不应该被处理 // 因为 Object.entries 不会遍历 __proto__ assert.strictEqual(filtered.username, 'test'); assert.ok(!('isAdmin' in filtered)); // 确保不会污染原型 assert.ok(!({}.isAdmin)); }); test('空对象应该返回空对象', () => { const filtered = filterUpdates({}); assert.strictEqual(Object.keys(filtered).length, 0); }); } testDatabaseFieldWhitelist(); // ============================================================ // 7. HTML 实体解码测试 // ============================================================ console.log('\n========== 7. HTML 实体解码测试 ==========\n'); function testHtmlEntityDecoding() { console.log('--- 测试 HTML 实体解码 ---'); function decodeHtmlEntities(str) { if (typeof str !== 'string') return str; const entityMap = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", '#x27': "'", '#x2F': '/', '#x60': '`' }; const decodeOnce = (input) => input.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z]+);/g, (match, code) => { if (code[0] === '#') { const isHex = code[1]?.toLowerCase() === 'x'; const num = isHex ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10); if (!Number.isNaN(num)) { return String.fromCharCode(num); } return match; } const mapped = entityMap[code]; return mapped !== undefined ? mapped : match; }); let output = str; let decoded = decodeOnce(output); while (decoded !== output) { output = decoded; decoded = decodeOnce(output); } return output; } test('基本 HTML 实体应该被解码', () => { assert.strictEqual(decodeHtmlEntities('<'), '<'); assert.strictEqual(decodeHtmlEntities('>'), '>'); assert.strictEqual(decodeHtmlEntities('&'), '&'); assert.strictEqual(decodeHtmlEntities('"'), '"'); }); test('数字实体应该被解码', () => { assert.strictEqual(decodeHtmlEntities('''), "'"); assert.strictEqual(decodeHtmlEntities('''), "'"); assert.strictEqual(decodeHtmlEntities('`'), '`'); }); test('嵌套实体应该被完全解码', () => { assert.strictEqual(decodeHtmlEntities('&#x60;'), '`'); assert.strictEqual(decodeHtmlEntities('&amp;'), '&'); }); test('普通文本应该保持不变', () => { assert.strictEqual(decodeHtmlEntities('hello world'), 'hello world'); assert.strictEqual(decodeHtmlEntities('test123'), 'test123'); }); test('非字符串输入应该原样返回', () => { assert.strictEqual(decodeHtmlEntities(null), null); assert.strictEqual(decodeHtmlEntities(undefined), undefined); assert.strictEqual(decodeHtmlEntities(123), 123); }); } testHtmlEntityDecoding(); // ============================================================ // 8. 分享路径权限测试 // ============================================================ console.log('\n========== 8. 分享路径权限测试 ==========\n'); function testSharePathAccess() { console.log('--- 测试分享路径访问权限 ---'); function isPathWithinShare(requestPath, share) { if (!requestPath || !share) { return false; } const normalizedRequest = path.normalize(requestPath).replace(/^(\.\.[\/\\])+/, '').replace(/\\/g, '/'); const normalizedShare = path.normalize(share.share_path).replace(/\\/g, '/'); if (share.share_type === 'file') { return normalizedRequest === normalizedShare; } else { const sharePrefix = normalizedShare.endsWith('/') ? normalizedShare : normalizedShare + '/'; return normalizedRequest === normalizedShare || normalizedRequest.startsWith(sharePrefix); } } test('单文件分享只允许访问该文件', () => { const share = { share_type: 'file', share_path: '/documents/secret.pdf' }; assert.strictEqual(isPathWithinShare('/documents/secret.pdf', share), true); assert.strictEqual(isPathWithinShare('/documents/other.pdf', share), false); assert.strictEqual(isPathWithinShare('/documents/secret.pdf.bak', share), false); }); test('目录分享允许访问子目录', () => { const share = { share_type: 'directory', share_path: '/shared' }; assert.strictEqual(isPathWithinShare('/shared', share), true); assert.strictEqual(isPathWithinShare('/shared/file.txt', share), true); assert.strictEqual(isPathWithinShare('/shared/sub/file.txt', share), true); }); test('目录分享不允许访问父目录', () => { const share = { share_type: 'directory', share_path: '/shared' }; assert.strictEqual(isPathWithinShare('/other', share), false); assert.strictEqual(isPathWithinShare('/shared_extra', share), false); assert.strictEqual(isPathWithinShare('/', share), false); }); test('路径遍历攻击应该被阻止', () => { const share = { share_type: 'directory', share_path: '/shared' }; assert.strictEqual(isPathWithinShare('/shared/../etc/passwd', share), false); assert.strictEqual(isPathWithinShare('/shared/../../root', share), false); }); test('空或无效输入应该返回 false', () => { assert.strictEqual(isPathWithinShare('', { share_type: 'file', share_path: '/test' }), false); assert.strictEqual(isPathWithinShare(null, { share_type: 'file', share_path: '/test' }), false); assert.strictEqual(isPathWithinShare('/test', null), false); }); } testSharePathAccess(); // ============================================================ // 测试总结 // ============================================================ 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 => { // 如果有失败,退出码为 1 process.exit(testResults.failed > 0 ? 1 : 0); }).catch(err => { console.error('测试执行错误:', err); process.exit(1); });