/** * 分享功能边界条件深度测试 * * 测试场景: * 1. 已过期的分享 * 2. 分享者被删除 * 3. 存储类型切换后的分享 * 4. 路径遍历攻击 * 5. 并发访问限流 */ const http = require('http'); const { db, ShareDB, UserDB } = require('./database'); const BASE_URL = process.env.TEST_BASE_URL || 'http://127.0.0.1:40001'; const results = { passed: 0, failed: 0, errors: [] }; // HTTP 请求工具 function request(method, path, data = null, headers = {}) { return new Promise((resolve, reject) => { const url = new URL(path, BASE_URL); const port = url.port ? parseInt(url.port, 10) : 80; const options = { hostname: url.hostname, port: port, path: url.pathname + url.search, method: method, headers: { 'Content-Type': 'application/json', ...headers } }; const req = http.request(options, (res) => { let body = ''; res.on('data', chunk => body += chunk); res.on('end', () => { try { const json = JSON.parse(body); resolve({ status: res.statusCode, data: json, headers: res.headers }); } catch (e) { resolve({ status: res.statusCode, data: body, headers: res.headers }); } }); }); req.on('error', reject); if (data) { req.write(JSON.stringify(data)); } req.end(); }); } function assert(condition, message) { if (condition) { results.passed++; console.log(` [PASS] ${message}`); } else { results.failed++; results.errors.push(message); console.log(` [FAIL] ${message}`); } } // ===== 测试用例 ===== async function testExpiredShare() { console.log('\n[测试] 已过期的分享...'); // 直接在数据库中创建一个已过期的分享 const expiredShareCode = 'expired_' + Date.now(); try { // 插入一个已过期的分享(过期时间设为昨天) const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); const expiresAt = yesterday.toISOString().replace('T', ' ').substring(0, 19); db.prepare(` INSERT INTO shares (user_id, share_code, share_path, share_type, expires_at) VALUES (?, ?, ?, ?, ?) `).run(1, expiredShareCode, '/expired-test.txt', 'file', expiresAt); console.log(` 创建过期分享: ${expiredShareCode}, 过期时间: ${expiresAt}`); // 尝试访问过期分享 const res = await request('POST', `/api/share/${expiredShareCode}/verify`, {}); assert(res.status === 404, `过期分享应返回 404, 实际: ${res.status}`); assert(res.data.message === '分享不存在', '应提示分享不存在'); // 清理测试数据 db.prepare('DELETE FROM shares WHERE share_code = ?').run(expiredShareCode); return true; } catch (error) { console.log(` [ERROR] ${error.message}`); results.failed++; // 清理 db.prepare('DELETE FROM shares WHERE share_code = ?').run(expiredShareCode); return false; } } async function testShareWithDeletedFile() { console.log('\n[测试] 分享的文件不存在...'); // 创建一个指向不存在文件的分享 const shareCode = 'nofile_' + Date.now(); try { db.prepare(` INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type) VALUES (?, ?, ?, ?, ?) `).run(1, shareCode, '/non_existent_file_xyz.txt', 'file', 'local'); console.log(` 创建分享: ${shareCode}, 路径: /non_existent_file_xyz.txt`); // 访问分享 const res = await request('POST', `/api/share/${shareCode}/verify`, {}); // 应该返回错误(文件不存在) // 注意:verify 接口在缓存未命中时会查询存储 if (res.status === 500) { assert(res.data.message && res.data.message.includes('不存在'), '应提示文件不存在'); } else if (res.status === 200) { // 如果成功返回,file 字段应该没有正确的文件信息 console.log(` [INFO] verify 返回 200,检查文件信息`); } // 清理 db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); return true; } catch (error) { console.log(` [ERROR] ${error.message}`); db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); return false; } } async function testShareByBannedUser() { console.log('\n[测试] 被封禁用户的分享...'); // 创建测试用户 let testUserId = null; const shareCode = 'banned_' + Date.now(); try { // 创建测试用户 testUserId = UserDB.create({ username: 'test_banned_' + Date.now(), email: `test_banned_${Date.now()}@test.com`, password: 'test123', is_verified: 1 }); // 创建分享 db.prepare(` INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type) VALUES (?, ?, ?, ?, ?) `).run(testUserId, shareCode, '/test.txt', 'file', 'local'); console.log(` 创建测试用户 ID: ${testUserId}`); console.log(` 创建分享: ${shareCode}`); // 封禁用户 UserDB.setBanStatus(testUserId, true); console.log(` 封禁用户: ${testUserId}`); // 访问分享 const res = await request('POST', `/api/share/${shareCode}/verify`, {}); // 当前实现:被封禁用户的分享仍然可以访问 // 如果需要阻止,应该在 ShareDB.findByCode 中检查用户状态 console.log(` 被封禁用户分享访问状态码: ${res.status}`); // 注意:这里可能是一个潜在的功能增强点 // 如果希望被封禁用户的分享也被禁止访问,需要修改代码 // 清理 db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); UserDB.delete(testUserId); assert(true, '被封禁用户分享测试完成'); return true; } catch (error) { console.log(` [ERROR] ${error.message}`); if (shareCode) db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); if (testUserId) UserDB.delete(testUserId); return false; } } async function testPathTraversalAttacks() { console.log('\n[测试] 路径遍历攻击防护...'); // 创建测试分享 const shareCode = 'traverse_' + Date.now(); try { db.prepare(` INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type) VALUES (?, ?, ?, ?, ?) `).run(1, shareCode, '/allowed-folder', 'directory', 'local'); // 测试各种路径遍历攻击 const attackPaths = [ '../../../etc/passwd', '..\\..\\..\\etc\\passwd', '%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd', '/allowed-folder/../../../etc/passwd', '/allowed-folder/./../../etc/passwd', '....//....//....//etc/passwd', '/allowed-folder%00.txt/../../../etc/passwd' ]; let blocked = 0; for (const attackPath of attackPaths) { const res = await request('GET', `/api/share/${shareCode}/download-url?path=${encodeURIComponent(attackPath)}`); if (res.status === 403 || res.status === 400) { blocked++; console.log(` [BLOCKED] ${attackPath.substring(0, 40)}...`); } else { console.log(` [WARN] 可能未阻止: ${attackPath}, 状态: ${res.status}`); } } assert(blocked >= attackPaths.length - 1, `路径遍历攻击应被阻止 (${blocked}/${attackPaths.length})`); // 清理 db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); return true; } catch (error) { console.log(` [ERROR] ${error.message}`); db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); return false; } } async function testSpecialCharactersInPath() { console.log('\n[测试] 特殊字符路径处理...'); // 测试创建包含特殊字符的分享 const specialPaths = [ '/文件夹/中文文件.txt', '/folder with spaces/file.txt', '/folder-with-dashes/file_underscore.txt', '/folder.with.dots/file.name.ext.txt', "/folder'with'quotes/file.txt" ]; let handled = 0; for (const path of specialPaths) { try { const res = await request('POST', '/api/share/create', { share_type: 'file', file_path: path }, { Cookie: authCookie }); if (res.status === 200 || res.status === 400) { handled++; console.log(` [OK] ${path.substring(0, 30)}... - 状态: ${res.status}`); // 如果创建成功,清理 if (res.data.share_code) { const myShares = await request('GET', '/api/share/my', null, { Cookie: authCookie }); const share = myShares.data.shares?.find(s => s.share_code === res.data.share_code); if (share) { await request('DELETE', `/api/share/${share.id}`, null, { Cookie: authCookie }); } } } } catch (error) { console.log(` [ERROR] ${path}: ${error.message}`); } } assert(handled === specialPaths.length, '特殊字符路径处理完成'); return true; } async function testConcurrentPasswordAttempts() { console.log('\n[测试] 并发密码尝试限流...'); // 创建一个带密码的分享 const shareCode = 'concurrent_' + Date.now(); try { // 使用 bcrypt 哈希密码 const bcrypt = require('bcryptjs'); const hashedPassword = bcrypt.hashSync('correct123', 10); db.prepare(` INSERT INTO shares (user_id, share_code, share_path, share_type, share_password, storage_type) VALUES (?, ?, ?, ?, ?, ?) `).run(1, shareCode, '/test.txt', 'file', hashedPassword, 'local'); // 发送大量并发错误密码请求 const promises = []; for (let i = 0; i < 20; i++) { promises.push(request('POST', `/api/share/${shareCode}/verify`, { password: 'wrong' + i })); } const results = await Promise.all(promises); // 检查是否有请求被限流 const rateLimited = results.filter(r => r.status === 429).length; const unauthorized = results.filter(r => r.status === 401).length; console.log(` 并发请求: 20, 限流: ${rateLimited}, 401错误: ${unauthorized}`); // 注意:限流是否触发取决于配置 if (rateLimited > 0) { assert(true, '限流机制生效'); } else { console.log(' [INFO] 限流未触发(可能配置较宽松)'); assert(true, '并发测试完成'); } // 清理 db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); return true; } catch (error) { console.log(` [ERROR] ${error.message}`); db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); return false; } } async function testShareStatistics() { console.log('\n[测试] 分享统计功能...'); const shareCode = 'stats_' + Date.now(); try { db.prepare(` INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type, view_count, download_count) VALUES (?, ?, ?, ?, ?, ?, ?) `).run(1, shareCode, '/test.txt', 'file', 'local', 0, 0); // 验证多次(增加查看次数) for (let i = 0; i < 3; i++) { await request('POST', `/api/share/${shareCode}/verify`, {}); } // 记录下载次数 for (let i = 0; i < 2; i++) { await request('POST', `/api/share/${shareCode}/download`, {}); } // 检查统计数据 const share = db.prepare('SELECT view_count, download_count FROM shares WHERE share_code = ?').get(shareCode); assert(share.view_count === 3, `查看次数应为 3, 实际: ${share.view_count}`); assert(share.download_count === 2, `下载次数应为 2, 实际: ${share.download_count}`); console.log(` 查看次数: ${share.view_count}, 下载次数: ${share.download_count}`); // 清理 db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); return true; } catch (error) { console.log(` [ERROR] ${error.message}`); db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode); return false; } } async function testShareCodeUniqueness() { console.log('\n[测试] 分享码唯一性...'); try { // 创建多个分享,检查分享码是否唯一 const codes = new Set(); for (let i = 0; i < 10; i++) { const code = ShareDB.generateShareCode(); if (codes.has(code)) { console.log(` [WARN] 发现重复分享码: ${code}`); } codes.add(code); } assert(codes.size === 10, `应生成 10 个唯一分享码, 实际: ${codes.size}`); console.log(` 生成了 ${codes.size} 个唯一分享码`); // 检查分享码长度和字符 const sampleCode = ShareDB.generateShareCode(); assert(sampleCode.length === 8, `分享码长度应为 8, 实际: ${sampleCode.length}`); assert(/^[a-zA-Z0-9]+$/.test(sampleCode), '分享码应只包含字母数字'); return true; } catch (error) { console.log(` [ERROR] ${error.message}`); return false; } } async function testExpiryTimeFormat() { console.log('\n[测试] 过期时间格式...'); try { // 测试不同的过期天数 const testDays = [1, 7, 30, 365]; for (const days of testDays) { const result = ShareDB.create(1, { share_type: 'file', file_path: `/test_${days}_days.txt`, expiry_days: days }); const share = db.prepare('SELECT expires_at FROM shares WHERE share_code = ?').get(result.share_code); // 验证过期时间格式 const expiresAt = new Date(share.expires_at); const now = new Date(); const diffDays = Math.round((expiresAt - now) / (1000 * 60 * 60 * 24)); // 允许1天的误差(由于时区等因素) assert(Math.abs(diffDays - days) <= 1, `${days}天过期应正确设置, 实际差异: ${diffDays}天`); // 清理 db.prepare('DELETE FROM shares WHERE share_code = ?').run(result.share_code); } return true; } catch (error) { console.log(` [ERROR] ${error.message}`); return false; } } // 全局认证 Cookie let authCookie = ''; async function login() { console.log('\n[准备] 登录获取认证...'); try { const res = await request('POST', '/api/login', { username: 'admin', password: 'admin123' }); if (res.status === 200 && res.data.success) { const setCookie = res.headers['set-cookie']; if (setCookie) { authCookie = setCookie.map(c => c.split(';')[0]).join('; '); console.log(' 认证成功'); return true; } } console.log(' 认证失败'); return false; } catch (error) { console.log(` [ERROR] ${error.message}`); return false; } } // ===== 主测试流程 ===== async function runTests() { console.log('========================================'); console.log(' 分享功能边界条件深度测试'); console.log('========================================'); // 登录 const loggedIn = await login(); if (!loggedIn) { console.log('\n[WARN] 登录失败,部分测试可能无法执行'); } // 运行测试 await testExpiredShare(); await testShareWithDeletedFile(); await testShareByBannedUser(); await testPathTraversalAttacks(); await testSpecialCharactersInPath(); await testConcurrentPasswordAttempts(); await testShareStatistics(); await testShareCodeUniqueness(); await testExpiryTimeFormat(); // 结果统计 console.log('\n========================================'); console.log(' 测试结果统计'); console.log('========================================'); console.log(`通过: ${results.passed}`); console.log(`失败: ${results.failed}`); if (results.errors.length > 0) { console.log('\n失败的测试:'); results.errors.forEach((err, i) => { console.log(` ${i + 1}. ${err}`); }); } console.log('\n========================================'); process.exit(results.failed > 0 ? 1 : 0); } runTests().catch(error => { console.error('测试执行失败:', error); process.exit(1); });