test: update admin/share edge scripts for cookie+csrf auth
This commit is contained in:
@@ -1,69 +1,163 @@
|
|||||||
/**
|
/**
|
||||||
* 管理员功能完整性测试脚本
|
* 管理员功能完整性测试脚本(Cookie + CSRF 认证模型)
|
||||||
* 测试范围:
|
*
|
||||||
* 1. 用户管理 - 用户列表、搜索、封禁/解封、删除、修改存储权限、查看用户文件
|
* 覆盖范围:
|
||||||
* 2. 系统设置 - SMTP邮件配置、存储配置、注册开关、主题设置
|
* 1. 鉴权与权限校验
|
||||||
* 3. 分享管理 - 查看所有分享、删除分享
|
* 2. 用户管理(列表、封禁/删除自保护、存储权限)
|
||||||
* 4. 系统监控 - 健康检查、存储统计、操作日志
|
* 3. 系统设置(获取/更新/参数校验)
|
||||||
* 5. 安全检查 - 管理员权限验证、敏感操作确认
|
* 4. 分享管理
|
||||||
|
* 5. 系统监控(健康、存储、日志)
|
||||||
|
* 6. 上传工具接口
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
|
const https = require('https');
|
||||||
|
const { UserDB } = require('./database');
|
||||||
|
|
||||||
const BASE_URL = 'http://localhost:40001';
|
const BASE_URL = process.env.TEST_BASE_URL || 'http://127.0.0.1:40001';
|
||||||
let adminToken = '';
|
|
||||||
let testUserId = null;
|
const state = {
|
||||||
let testShareId = null;
|
adminSession: {
|
||||||
|
cookies: {},
|
||||||
|
csrfToken: ''
|
||||||
|
},
|
||||||
|
adminUserId: null,
|
||||||
|
testUserId: null,
|
||||||
|
latestSettings: null
|
||||||
|
};
|
||||||
|
|
||||||
// 测试结果收集
|
|
||||||
const testResults = {
|
const testResults = {
|
||||||
passed: [],
|
passed: [],
|
||||||
failed: [],
|
failed: [],
|
||||||
warnings: []
|
warnings: []
|
||||||
};
|
};
|
||||||
|
|
||||||
// 辅助函数:发送HTTP请求
|
function makeCookieHeader(cookies) {
|
||||||
function request(method, path, data = null, token = null) {
|
return Object.entries(cookies || {})
|
||||||
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
|
.join('; ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeSetCookies(session, setCookieHeader) {
|
||||||
|
if (!session || !setCookieHeader) return;
|
||||||
|
const list = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
|
||||||
|
for (const raw of list) {
|
||||||
|
const first = String(raw || '').split(';')[0];
|
||||||
|
const idx = first.indexOf('=');
|
||||||
|
if (idx <= 0) continue;
|
||||||
|
const key = first.slice(0, idx).trim();
|
||||||
|
const value = first.slice(idx + 1).trim();
|
||||||
|
session.cookies[key] = value;
|
||||||
|
if (key === 'csrf_token') {
|
||||||
|
session.csrfToken = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSafeMethod(method) {
|
||||||
|
const upper = String(method || '').toUpperCase();
|
||||||
|
return upper === 'GET' || upper === 'HEAD' || upper === 'OPTIONS';
|
||||||
|
}
|
||||||
|
|
||||||
|
function request(method, path, options = {}) {
|
||||||
|
const {
|
||||||
|
data = null,
|
||||||
|
session = null,
|
||||||
|
headers = {},
|
||||||
|
requireCsrf = true
|
||||||
|
} = options;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const url = new URL(path, BASE_URL);
|
const url = new URL(path, BASE_URL);
|
||||||
const options = {
|
const transport = url.protocol === 'https:' ? https : http;
|
||||||
|
|
||||||
|
const requestHeaders = { ...headers };
|
||||||
|
if (session) {
|
||||||
|
const cookieHeader = makeCookieHeader(session.cookies);
|
||||||
|
if (cookieHeader) {
|
||||||
|
requestHeaders.Cookie = cookieHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireCsrf && !isSafeMethod(method)) {
|
||||||
|
const csrfToken = session.csrfToken || session.cookies.csrf_token;
|
||||||
|
if (csrfToken) {
|
||||||
|
requestHeaders['X-CSRF-Token'] = csrfToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload = null;
|
||||||
|
if (data !== null && data !== undefined) {
|
||||||
|
payload = JSON.stringify(data);
|
||||||
|
requestHeaders['Content-Type'] = 'application/json';
|
||||||
|
requestHeaders['Content-Length'] = Buffer.byteLength(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = transport.request({
|
||||||
|
protocol: url.protocol,
|
||||||
hostname: url.hostname,
|
hostname: url.hostname,
|
||||||
port: url.port,
|
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
||||||
path: url.pathname + url.search,
|
path: url.pathname + url.search,
|
||||||
method: method,
|
method,
|
||||||
headers: {
|
headers: requestHeaders
|
||||||
'Content-Type': 'application/json'
|
}, (res) => {
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
options.headers['Authorization'] = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const req = http.request(options, (res) => {
|
|
||||||
let body = '';
|
let body = '';
|
||||||
res.on('data', chunk => body += chunk);
|
res.on('data', chunk => {
|
||||||
|
body += chunk;
|
||||||
|
});
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
try {
|
if (session) {
|
||||||
const json = JSON.parse(body);
|
storeSetCookies(session, res.headers['set-cookie']);
|
||||||
resolve({ status: res.statusCode, data: json });
|
|
||||||
} catch (e) {
|
|
||||||
resolve({ status: res.statusCode, data: body });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let parsed = body;
|
||||||
|
try {
|
||||||
|
parsed = body ? JSON.parse(body) : {};
|
||||||
|
} catch (e) {
|
||||||
|
// keep raw text
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
status: res.statusCode,
|
||||||
|
data: parsed,
|
||||||
|
headers: res.headers
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('error', reject);
|
req.on('error', reject);
|
||||||
|
|
||||||
if (data) {
|
if (payload) {
|
||||||
req.write(JSON.stringify(data));
|
req.write(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.end();
|
req.end();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试函数包装器
|
async function initCsrf(session) {
|
||||||
|
const res = await request('GET', '/api/csrf-token', {
|
||||||
|
session,
|
||||||
|
requireCsrf: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data && res.data.csrfToken) {
|
||||||
|
session.csrfToken = res.data.csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assert(condition, message) {
|
||||||
|
if (!condition) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function warn(message) {
|
||||||
|
testResults.warnings.push(message);
|
||||||
|
console.log(`[WARN] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
async function test(name, fn) {
|
async function test(name, fn) {
|
||||||
try {
|
try {
|
||||||
await fn();
|
await fn();
|
||||||
@@ -75,20 +169,43 @@ async function test(name, fn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 警告记录
|
function ensureTestUser() {
|
||||||
function warn(message) {
|
if (state.testUserId) {
|
||||||
testResults.warnings.push(message);
|
return;
|
||||||
console.log(`[WARN] ${message}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 断言函数
|
const suffix = Date.now();
|
||||||
function assert(condition, message) {
|
const username = `admin_test_user_${suffix}`;
|
||||||
if (!condition) {
|
const email = `${username}@test.local`;
|
||||||
throw new Error(message);
|
const password = `AdminTest#${suffix}`;
|
||||||
}
|
|
||||||
|
const id = UserDB.create({
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
is_verified: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
UserDB.update(id, {
|
||||||
|
is_active: 1,
|
||||||
|
is_banned: 0,
|
||||||
|
storage_permission: 'user_choice',
|
||||||
|
current_storage_type: 'oss'
|
||||||
|
});
|
||||||
|
|
||||||
|
state.testUserId = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 测试用例 ============
|
function cleanupTestUser() {
|
||||||
|
if (!state.testUserId) return;
|
||||||
|
try {
|
||||||
|
UserDB.delete(state.testUserId);
|
||||||
|
} catch (error) {
|
||||||
|
warn(`清理测试用户失败: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
state.testUserId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. 安全检查:未认证访问应被拒绝
|
// 1. 安全检查:未认证访问应被拒绝
|
||||||
async function testUnauthorizedAccess() {
|
async function testUnauthorizedAccess() {
|
||||||
@@ -96,411 +213,311 @@ async function testUnauthorizedAccess() {
|
|||||||
assert(res.status === 401, `未认证访问应返回401,实际返回: ${res.status}`);
|
assert(res.status === 401, `未认证访问应返回401,实际返回: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 管理员登录
|
// 2. 安全检查:无效 Token 应被拒绝
|
||||||
async function testAdminLogin() {
|
async function testInvalidTokenAccess() {
|
||||||
const res = await request('POST', '/api/login', {
|
const res = await request('GET', '/api/admin/users', {
|
||||||
username: 'admin',
|
headers: {
|
||||||
password: 'admin123',
|
Authorization: 'Bearer invalid-token'
|
||||||
captcha: '' // 开发环境可能不需要验证码
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 登录可能因为验证码失败,这是预期的
|
|
||||||
if (res.status === 400 && res.data.message && res.data.message.includes('验证码')) {
|
|
||||||
warn('登录需要验证码,跳过登录测试,使用模拟token');
|
|
||||||
// 使用JWT库生成一个测试token(需要知道JWT_SECRET)
|
|
||||||
// 或者直接查询数据库
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.data.success) {
|
|
||||||
adminToken = res.data.token;
|
|
||||||
console.log(' - 获取到管理员token');
|
|
||||||
} else {
|
|
||||||
throw new Error(`登录失败: ${res.data.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 用户列表获取
|
|
||||||
async function testGetUsers() {
|
|
||||||
if (!adminToken) {
|
|
||||||
warn('无admin token,跳过用户列表测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('GET', '/api/admin/users', null, adminToken);
|
|
||||||
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
|
||||||
assert(res.data.success === true, '应返回success: true');
|
|
||||||
assert(Array.isArray(res.data.users), 'users应为数组');
|
|
||||||
|
|
||||||
// 记录测试用户ID
|
|
||||||
if (res.data.users.length > 1) {
|
|
||||||
const nonAdminUser = res.data.users.find(u => !u.is_admin);
|
|
||||||
if (nonAdminUser) {
|
|
||||||
testUserId = nonAdminUser.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 系统设置获取
|
|
||||||
async function testGetSettings() {
|
|
||||||
if (!adminToken) {
|
|
||||||
warn('无admin token,跳过系统设置测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('GET', '/api/admin/settings', null, adminToken);
|
|
||||||
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
|
||||||
assert(res.data.success === true, '应返回success: true');
|
|
||||||
assert(res.data.settings !== undefined, '应包含settings对象');
|
|
||||||
assert(res.data.settings.smtp !== undefined, '应包含smtp配置');
|
|
||||||
assert(res.data.settings.global_theme !== undefined, '应包含全局主题设置');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 更新系统设置
|
|
||||||
async function testUpdateSettings() {
|
|
||||||
if (!adminToken) {
|
|
||||||
warn('无admin token,跳过更新系统设置测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('POST', '/api/admin/settings', {
|
|
||||||
global_theme: 'dark',
|
|
||||||
max_upload_size: 10737418240
|
|
||||||
}, adminToken);
|
|
||||||
|
|
||||||
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
|
||||||
assert(res.data.success === true, '应返回success: true');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 健康检查
|
|
||||||
async function testHealthCheck() {
|
|
||||||
if (!adminToken) {
|
|
||||||
warn('无admin token,跳过健康检查测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('GET', '/api/admin/health-check', null, adminToken);
|
|
||||||
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
|
||||||
assert(res.data.success === true, '应返回success: true');
|
|
||||||
assert(res.data.checks !== undefined, '应包含checks数组');
|
|
||||||
assert(res.data.overallStatus !== undefined, '应包含overallStatus');
|
|
||||||
assert(res.data.summary !== undefined, '应包含summary');
|
|
||||||
|
|
||||||
// 检查各项检测项目
|
|
||||||
const checkNames = res.data.checks.map(c => c.name);
|
|
||||||
assert(checkNames.includes('JWT密钥'), '应包含JWT密钥检查');
|
|
||||||
assert(checkNames.includes('数据库连接'), '应包含数据库连接检查');
|
|
||||||
assert(checkNames.includes('存储目录'), '应包含存储目录检查');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. 存储统计
|
|
||||||
async function testStorageStats() {
|
|
||||||
if (!adminToken) {
|
|
||||||
warn('无admin token,跳过存储统计测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('GET', '/api/admin/storage-stats', null, adminToken);
|
|
||||||
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
|
||||||
assert(res.data.success === true, '应返回success: true');
|
|
||||||
assert(res.data.stats !== undefined, '应包含stats对象');
|
|
||||||
assert(typeof res.data.stats.totalDisk === 'number', 'totalDisk应为数字');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. 系统日志获取
|
|
||||||
async function testGetLogs() {
|
|
||||||
if (!adminToken) {
|
|
||||||
warn('无admin token,跳过系统日志测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('GET', '/api/admin/logs?page=1&pageSize=10', null, adminToken);
|
|
||||||
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
|
||||||
assert(res.data.success === true, '应返回success: true');
|
|
||||||
assert(Array.isArray(res.data.logs), 'logs应为数组');
|
|
||||||
assert(typeof res.data.total === 'number', 'total应为数字');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 9. 日志统计
|
|
||||||
async function testLogStats() {
|
|
||||||
if (!adminToken) {
|
|
||||||
warn('无admin token,跳过日志统计测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('GET', '/api/admin/logs/stats', null, adminToken);
|
|
||||||
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
|
||||||
assert(res.data.success === true, '应返回success: true');
|
|
||||||
assert(res.data.stats !== undefined, '应包含stats对象');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 10. 分享列表获取
|
|
||||||
async function testGetShares() {
|
|
||||||
if (!adminToken) {
|
|
||||||
warn('无admin token,跳过分享列表测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('GET', '/api/admin/shares', null, adminToken);
|
|
||||||
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
|
||||||
assert(res.data.success === true, '应返回success: true');
|
|
||||||
assert(Array.isArray(res.data.shares), 'shares应为数组');
|
|
||||||
|
|
||||||
// 记录测试分享ID
|
|
||||||
if (res.data.shares.length > 0) {
|
|
||||||
testShareId = res.data.shares[0].id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 11. 安全检查:普通用户不能访问管理员API
|
|
||||||
async function testNonAdminAccess() {
|
|
||||||
// 使用一个无效的token模拟普通用户
|
|
||||||
const fakeToken = 'invalid-token';
|
|
||||||
const res = await request('GET', '/api/admin/users', null, fakeToken);
|
|
||||||
assert(res.status === 401, `无效token应返回401,实际: ${res.status}`);
|
assert(res.status === 401, `无效token应返回401,实际: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 12. 安全检查:不能封禁自己
|
// 3. 管理员登录(基于 Cookie)
|
||||||
|
async function testAdminLogin() {
|
||||||
|
await initCsrf(state.adminSession);
|
||||||
|
|
||||||
|
const res = await request('POST', '/api/login', {
|
||||||
|
data: {
|
||||||
|
username: 'admin',
|
||||||
|
password: 'admin123'
|
||||||
|
},
|
||||||
|
session: state.adminSession,
|
||||||
|
requireCsrf: false
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(res.status === 200, `登录应返回200,实际: ${res.status}`);
|
||||||
|
assert(res.data && res.data.success === true, `登录失败: ${res.data?.message || 'unknown'}`);
|
||||||
|
assert(!!state.adminSession.cookies.token, '登录后应写入 token Cookie');
|
||||||
|
|
||||||
|
await initCsrf(state.adminSession);
|
||||||
|
|
||||||
|
const profileRes = await request('GET', '/api/user/profile', {
|
||||||
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(profileRes.status === 200, `读取profile应返回200,实际: ${profileRes.status}`);
|
||||||
|
assert(profileRes.data?.success === true, '读取profile应成功');
|
||||||
|
assert(profileRes.data?.user?.is_admin === 1, '登录账号应为管理员');
|
||||||
|
|
||||||
|
state.adminUserId = profileRes.data.user.id;
|
||||||
|
assert(Number.isInteger(state.adminUserId) && state.adminUserId > 0, '应获取管理员ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 用户列表获取(分页)
|
||||||
|
async function testGetUsers() {
|
||||||
|
const res = await request('GET', '/api/admin/users?paged=1&page=1&pageSize=20&sort=created_desc', {
|
||||||
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
||||||
|
assert(res.data?.success === true, '应返回success: true');
|
||||||
|
assert(Array.isArray(res.data?.users), 'users应为数组');
|
||||||
|
assert(!!res.data?.pagination, '分页模式应返回pagination');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 系统设置获取
|
||||||
|
async function testGetSettings() {
|
||||||
|
const res = await request('GET', '/api/admin/settings', {
|
||||||
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
||||||
|
assert(res.data?.success === true, '应返回success: true');
|
||||||
|
assert(res.data?.settings && typeof res.data.settings === 'object', '应包含settings对象');
|
||||||
|
assert(res.data?.settings?.smtp && typeof res.data.settings.smtp === 'object', '应包含smtp配置');
|
||||||
|
assert(res.data?.settings?.global_theme !== undefined, '应包含全局主题设置');
|
||||||
|
|
||||||
|
state.latestSettings = res.data.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 更新系统设置(写回当前值,避免影响测试环境)
|
||||||
|
async function testUpdateSettings() {
|
||||||
|
const current = state.latestSettings || {};
|
||||||
|
const payload = {
|
||||||
|
global_theme: current.global_theme || 'dark',
|
||||||
|
max_upload_size: Number(current.max_upload_size || 10737418240)
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await request('POST', '/api/admin/settings', {
|
||||||
|
data: payload,
|
||||||
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
||||||
|
assert(res.data?.success === true, '应返回success: true');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 健康检查
|
||||||
|
async function testHealthCheck() {
|
||||||
|
const res = await request('GET', '/api/admin/health-check', {
|
||||||
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
||||||
|
assert(res.data?.success === true, '应返回success: true');
|
||||||
|
assert(Array.isArray(res.data?.checks), '应包含checks数组');
|
||||||
|
assert(res.data?.summary && typeof res.data.summary === 'object', '应包含summary');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 存储统计
|
||||||
|
async function testStorageStats() {
|
||||||
|
const res = await request('GET', '/api/admin/storage-stats', {
|
||||||
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
||||||
|
assert(res.data?.success === true, '应返回success: true');
|
||||||
|
assert(res.data?.stats && typeof res.data.stats === 'object', '应包含stats对象');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 系统日志获取
|
||||||
|
async function testGetLogs() {
|
||||||
|
const res = await request('GET', '/api/admin/logs?page=1&pageSize=10', {
|
||||||
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
||||||
|
assert(res.data?.success === true, '应返回success: true');
|
||||||
|
assert(Array.isArray(res.data?.logs), 'logs应为数组');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. 日志统计
|
||||||
|
async function testLogStats() {
|
||||||
|
const res = await request('GET', '/api/admin/logs/stats', {
|
||||||
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
||||||
|
assert(res.data?.success === true, '应返回success: true');
|
||||||
|
assert(res.data?.stats && typeof res.data.stats === 'object', '应包含stats对象');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 11. 分享列表获取
|
||||||
|
async function testGetShares() {
|
||||||
|
const res = await request('GET', '/api/admin/shares', {
|
||||||
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
||||||
|
assert(res.data?.success === true, '应返回success: true');
|
||||||
|
assert(Array.isArray(res.data?.shares), 'shares应为数组');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. 不能封禁自己
|
||||||
async function testCannotBanSelf() {
|
async function testCannotBanSelf() {
|
||||||
if (!adminToken) {
|
assert(state.adminUserId, '管理员ID未初始化');
|
||||||
warn('无admin token,跳过封禁自己测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当前管理员ID
|
const res = await request('POST', `/api/admin/users/${state.adminUserId}/ban`, {
|
||||||
const usersRes = await request('GET', '/api/admin/users', null, adminToken);
|
data: { banned: true },
|
||||||
const adminUser = usersRes.data.users.find(u => u.is_admin);
|
session: state.adminSession
|
||||||
|
});
|
||||||
if (!adminUser) {
|
|
||||||
warn('未找到管理员用户');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('POST', `/api/admin/users/${adminUser.id}/ban`, {
|
|
||||||
banned: true
|
|
||||||
}, adminToken);
|
|
||||||
|
|
||||||
assert(res.status === 400, `封禁自己应返回400,实际: ${res.status}`);
|
assert(res.status === 400, `封禁自己应返回400,实际: ${res.status}`);
|
||||||
assert(res.data.message.includes('不能封禁自己'), '应提示不能封禁自己');
|
assert(String(res.data?.message || '').includes('不能封禁自己'), '应提示不能封禁自己');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 13. 安全检查:不能删除自己
|
// 13. 不能删除自己
|
||||||
async function testCannotDeleteSelf() {
|
async function testCannotDeleteSelf() {
|
||||||
if (!adminToken) {
|
assert(state.adminUserId, '管理员ID未初始化');
|
||||||
warn('无admin token,跳过删除自己测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const usersRes = await request('GET', '/api/admin/users', null, adminToken);
|
const res = await request('DELETE', `/api/admin/users/${state.adminUserId}`, {
|
||||||
const adminUser = usersRes.data.users.find(u => u.is_admin);
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
if (!adminUser) {
|
|
||||||
warn('未找到管理员用户');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('DELETE', `/api/admin/users/${adminUser.id}`, null, adminToken);
|
|
||||||
assert(res.status === 400, `删除自己应返回400,实际: ${res.status}`);
|
assert(res.status === 400, `删除自己应返回400,实际: ${res.status}`);
|
||||||
assert(res.data.message.includes('不能删除自己'), '应提示不能删除自己');
|
assert(String(res.data?.message || '').includes('不能删除自己'), '应提示不能删除自己');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 14. 参数验证:无效用户ID
|
// 14. 参数验证:无效用户ID
|
||||||
async function testInvalidUserId() {
|
async function testInvalidUserId() {
|
||||||
if (!adminToken) {
|
|
||||||
warn('无admin token,跳过无效用户ID测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('POST', '/api/admin/users/invalid/ban', {
|
const res = await request('POST', '/api/admin/users/invalid/ban', {
|
||||||
banned: true
|
data: { banned: true },
|
||||||
}, adminToken);
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
assert(res.status === 400, `无效用户ID应返回400,实际: ${res.status}`);
|
assert(res.status === 400, `无效用户ID应返回400,实际: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 15. 参数验证:无效分享ID
|
// 15. 参数验证:无效分享ID
|
||||||
async function testInvalidShareId() {
|
async function testInvalidShareId() {
|
||||||
if (!adminToken) {
|
const res = await request('DELETE', '/api/admin/shares/invalid', {
|
||||||
warn('无admin token,跳过无效分享ID测试');
|
session: state.adminSession
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('DELETE', '/api/admin/shares/invalid', null, adminToken);
|
|
||||||
assert(res.status === 400, `无效分享ID应返回400,实际: ${res.status}`);
|
assert(res.status === 400, `无效分享ID应返回400,实际: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 16. 存储权限设置
|
// 16. 存储权限设置
|
||||||
async function testSetStoragePermission() {
|
async function testSetStoragePermission() {
|
||||||
if (!adminToken || !testUserId) {
|
ensureTestUser();
|
||||||
warn('无admin token或测试用户,跳过存储权限测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('POST', `/api/admin/users/${testUserId}/storage-permission`, {
|
const res = await request('POST', `/api/admin/users/${state.testUserId}/storage-permission`, {
|
||||||
|
data: {
|
||||||
storage_permission: 'local_only',
|
storage_permission: 'local_only',
|
||||||
local_storage_quota: 2147483648 // 2GB
|
local_storage_quota: 2147483648,
|
||||||
}, adminToken);
|
download_traffic_quota: 3221225472
|
||||||
|
},
|
||||||
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
||||||
assert(res.data.success === true, '应返回success: true');
|
assert(res.data?.success === true, '应返回success: true');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 17. 参数验证:无效的存储权限值
|
// 17. 参数验证:无效的存储权限值
|
||||||
async function testInvalidStoragePermission() {
|
async function testInvalidStoragePermission() {
|
||||||
if (!adminToken || !testUserId) {
|
ensureTestUser();
|
||||||
warn('无admin token或测试用户,跳过无效存储权限测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('POST', `/api/admin/users/${testUserId}/storage-permission`, {
|
const res = await request('POST', `/api/admin/users/${state.testUserId}/storage-permission`, {
|
||||||
storage_permission: 'invalid_permission'
|
data: { storage_permission: 'invalid_permission' },
|
||||||
}, adminToken);
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
assert(res.status === 400, `无效存储权限应返回400,实际: ${res.status}`);
|
assert(res.status === 400, `无效存储权限应返回400,实际: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 18. 主题设置验证
|
// 18. 主题设置验证
|
||||||
async function testInvalidTheme() {
|
async function testInvalidTheme() {
|
||||||
if (!adminToken) {
|
|
||||||
warn('无admin token,跳过无效主题测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('POST', '/api/admin/settings', {
|
const res = await request('POST', '/api/admin/settings', {
|
||||||
global_theme: 'invalid_theme'
|
data: { global_theme: 'invalid_theme' },
|
||||||
}, adminToken);
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
assert(res.status === 400, `无效主题应返回400,实际: ${res.status}`);
|
assert(res.status === 400, `无效主题应返回400,实际: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 19. 日志清理测试
|
// 19. 日志清理测试
|
||||||
async function testLogCleanup() {
|
async function testLogCleanup() {
|
||||||
if (!adminToken) {
|
|
||||||
warn('无admin token,跳过日志清理测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('POST', '/api/admin/logs/cleanup', {
|
const res = await request('POST', '/api/admin/logs/cleanup', {
|
||||||
keepDays: 90
|
data: { keepDays: 90 },
|
||||||
}, adminToken);
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
||||||
assert(res.data.success === true, '应返回success: true');
|
assert(res.data?.success === true, '应返回success: true');
|
||||||
assert(typeof res.data.deletedCount === 'number', 'deletedCount应为数字');
|
assert(typeof res.data?.deletedCount === 'number', 'deletedCount应为数字');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 20. SMTP测试(预期失败因为未配置)
|
// 20. SMTP测试(未配置时返回400,配置后可能200/500)
|
||||||
async function testSmtpTest() {
|
async function testSmtpTest() {
|
||||||
if (!adminToken) {
|
|
||||||
warn('无admin token,跳过SMTP测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('POST', '/api/admin/settings/test-smtp', {
|
const res = await request('POST', '/api/admin/settings/test-smtp', {
|
||||||
to: 'test@example.com'
|
data: { to: 'test@example.com' },
|
||||||
}, adminToken);
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
// SMTP未配置时应返回400
|
if (res.status === 400 && String(res.data?.message || '').includes('SMTP未配置')) {
|
||||||
if (res.status === 400 && res.data.message && res.data.message.includes('SMTP未配置')) {
|
|
||||||
console.log(' - SMTP未配置,这是预期的');
|
console.log(' - SMTP未配置,这是预期的');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果SMTP已配置,可能成功或失败
|
|
||||||
assert(res.status === 200 || res.status === 500, `应返回200或500,实际: ${res.status}`);
|
assert(res.status === 200 || res.status === 500, `应返回200或500,实际: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 21. 上传工具检查
|
// 21. 上传工具配置生成(替代已移除的 /api/admin/check-upload-tool)
|
||||||
async function testCheckUploadTool() {
|
async function testGenerateUploadToolConfig() {
|
||||||
if (!adminToken) {
|
const res = await request('POST', '/api/upload/generate-tool', {
|
||||||
warn('无admin token,跳过上传工具检查测试');
|
data: {},
|
||||||
return;
|
session: state.adminSession
|
||||||
}
|
});
|
||||||
|
|
||||||
const res = await request('GET', '/api/admin/check-upload-tool', null, adminToken);
|
|
||||||
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
assert(res.status === 200, `应返回200,实际: ${res.status}`);
|
||||||
assert(res.data.success === true, '应返回success: true');
|
assert(res.data?.success === true, '应返回success: true');
|
||||||
assert(typeof res.data.exists === 'boolean', 'exists应为布尔值');
|
assert(res.data?.config && typeof res.data.config === 'object', '应包含config对象');
|
||||||
|
assert(typeof res.data?.config?.api_key === 'string' && res.data.config.api_key.length > 0, '应返回有效api_key');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 22. 用户文件查看 - 无效用户ID验证
|
// 22. 用户文件查看 - 无效用户ID验证
|
||||||
async function testInvalidUserIdForFiles() {
|
async function testInvalidUserIdForFiles() {
|
||||||
if (!adminToken) {
|
const res = await request('GET', '/api/admin/users/invalid/files', {
|
||||||
warn('无admin token,跳过用户文件查看无效ID测试');
|
session: state.adminSession
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('GET', '/api/admin/users/invalid/files', null, adminToken);
|
|
||||||
assert(res.status === 400, `无效用户ID应返回400,实际: ${res.status}`);
|
assert(res.status === 400, `无效用户ID应返回400,实际: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 23. 删除用户 - 无效用户ID验证
|
// 23. 删除用户 - 无效用户ID验证
|
||||||
async function testInvalidUserIdForDelete() {
|
async function testInvalidUserIdForDelete() {
|
||||||
if (!adminToken) {
|
const res = await request('DELETE', '/api/admin/users/invalid', {
|
||||||
warn('无admin token,跳过删除用户无效ID测试');
|
session: state.adminSession
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('DELETE', '/api/admin/users/invalid', null, adminToken);
|
|
||||||
assert(res.status === 400, `无效用户ID应返回400,实际: ${res.status}`);
|
assert(res.status === 400, `无效用户ID应返回400,实际: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 24. 存储权限设置 - 无效用户ID验证
|
// 24. 存储权限设置 - 无效用户ID验证
|
||||||
async function testInvalidUserIdForPermission() {
|
async function testInvalidUserIdForPermission() {
|
||||||
if (!adminToken) {
|
|
||||||
warn('无admin token,跳过存储权限无效ID测试');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await request('POST', '/api/admin/users/invalid/storage-permission', {
|
const res = await request('POST', '/api/admin/users/invalid/storage-permission', {
|
||||||
|
data: {
|
||||||
storage_permission: 'local_only'
|
storage_permission: 'local_only'
|
||||||
}, adminToken);
|
},
|
||||||
|
session: state.adminSession
|
||||||
|
});
|
||||||
|
|
||||||
assert(res.status === 400, `无效用户ID应返回400,实际: ${res.status}`);
|
assert(res.status === 400, `无效用户ID应返回400,实际: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主测试函数
|
|
||||||
async function runTests() {
|
async function runTests() {
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
console.log('管理员功能完整性测试');
|
console.log('管理员功能完整性测试(Cookie + CSRF)');
|
||||||
console.log('========================================\n');
|
console.log('========================================\n');
|
||||||
|
|
||||||
// 先尝试直接使用数据库获取token
|
|
||||||
try {
|
|
||||||
const jwt = require('jsonwebtoken');
|
|
||||||
const { UserDB } = require('./database');
|
|
||||||
require('dotenv').config();
|
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
|
|
||||||
const adminUser = UserDB.findByUsername('admin');
|
|
||||||
|
|
||||||
if (adminUser) {
|
|
||||||
adminToken = jwt.sign(
|
|
||||||
{
|
|
||||||
id: adminUser.id,
|
|
||||||
username: adminUser.username,
|
|
||||||
is_admin: adminUser.is_admin,
|
|
||||||
type: 'access'
|
|
||||||
},
|
|
||||||
JWT_SECRET,
|
|
||||||
{ expiresIn: '2h' }
|
|
||||||
);
|
|
||||||
console.log('[INFO] 已通过数据库直接生成管理员token\n');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('[INFO] 无法直接生成token,将尝试登录: ' + e.message + '\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 安全检查测试
|
|
||||||
console.log('\n--- 安全检查 ---');
|
console.log('\n--- 安全检查 ---');
|
||||||
await test('未认证访问应被拒绝', testUnauthorizedAccess);
|
await test('未认证访问应被拒绝', testUnauthorizedAccess);
|
||||||
await test('无效token应被拒绝', testNonAdminAccess);
|
await test('无效token应被拒绝', testInvalidTokenAccess);
|
||||||
|
|
||||||
// 如果还没有token,尝试登录
|
|
||||||
if (!adminToken) {
|
|
||||||
await test('管理员登录', testAdminLogin);
|
await test('管理员登录', testAdminLogin);
|
||||||
}
|
|
||||||
|
|
||||||
// 用户管理测试
|
|
||||||
console.log('\n--- 用户管理 ---');
|
console.log('\n--- 用户管理 ---');
|
||||||
await test('获取用户列表', testGetUsers);
|
await test('获取用户列表', testGetUsers);
|
||||||
await test('不能封禁自己', testCannotBanSelf);
|
await test('不能封禁自己', testCannotBanSelf);
|
||||||
@@ -509,19 +526,16 @@ async function runTests() {
|
|||||||
await test('设置存储权限', testSetStoragePermission);
|
await test('设置存储权限', testSetStoragePermission);
|
||||||
await test('无效存储权限验证', testInvalidStoragePermission);
|
await test('无效存储权限验证', testInvalidStoragePermission);
|
||||||
|
|
||||||
// 系统设置测试
|
|
||||||
console.log('\n--- 系统设置 ---');
|
console.log('\n--- 系统设置 ---');
|
||||||
await test('获取系统设置', testGetSettings);
|
await test('获取系统设置', testGetSettings);
|
||||||
await test('更新系统设置', testUpdateSettings);
|
await test('更新系统设置', testUpdateSettings);
|
||||||
await test('无效主题验证', testInvalidTheme);
|
await test('无效主题验证', testInvalidTheme);
|
||||||
await test('SMTP测试', testSmtpTest);
|
await test('SMTP测试', testSmtpTest);
|
||||||
|
|
||||||
// 分享管理测试
|
|
||||||
console.log('\n--- 分享管理 ---');
|
console.log('\n--- 分享管理 ---');
|
||||||
await test('获取分享列表', testGetShares);
|
await test('获取分享列表', testGetShares);
|
||||||
await test('无效分享ID验证', testInvalidShareId);
|
await test('无效分享ID验证', testInvalidShareId);
|
||||||
|
|
||||||
// 系统监控测试
|
|
||||||
console.log('\n--- 系统监控 ---');
|
console.log('\n--- 系统监控 ---');
|
||||||
await test('健康检查', testHealthCheck);
|
await test('健康检查', testHealthCheck);
|
||||||
await test('存储统计', testStorageStats);
|
await test('存储统计', testStorageStats);
|
||||||
@@ -529,17 +543,16 @@ async function runTests() {
|
|||||||
await test('日志统计', testLogStats);
|
await test('日志统计', testLogStats);
|
||||||
await test('日志清理', testLogCleanup);
|
await test('日志清理', testLogCleanup);
|
||||||
|
|
||||||
// 其他功能测试
|
|
||||||
console.log('\n--- 其他功能 ---');
|
console.log('\n--- 其他功能 ---');
|
||||||
await test('上传工具检查', testCheckUploadTool);
|
await test('上传工具配置生成', testGenerateUploadToolConfig);
|
||||||
|
|
||||||
// 参数验证增强测试
|
|
||||||
console.log('\n--- 参数验证增强 ---');
|
console.log('\n--- 参数验证增强 ---');
|
||||||
await test('用户文件查看无效ID验证', testInvalidUserIdForFiles);
|
await test('用户文件查看无效ID验证', testInvalidUserIdForFiles);
|
||||||
await test('删除用户无效ID验证', testInvalidUserIdForDelete);
|
await test('删除用户无效ID验证', testInvalidUserIdForDelete);
|
||||||
await test('存储权限设置无效ID验证', testInvalidUserIdForPermission);
|
await test('存储权限设置无效ID验证', testInvalidUserIdForPermission);
|
||||||
|
|
||||||
// 输出测试结果
|
cleanupTestUser();
|
||||||
|
|
||||||
console.log('\n========================================');
|
console.log('\n========================================');
|
||||||
console.log('测试结果汇总');
|
console.log('测试结果汇总');
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
@@ -549,26 +562,25 @@ async function runTests() {
|
|||||||
|
|
||||||
if (testResults.failed.length > 0) {
|
if (testResults.failed.length > 0) {
|
||||||
console.log('\n失败的测试:');
|
console.log('\n失败的测试:');
|
||||||
testResults.failed.forEach(f => {
|
for (const f of testResults.failed) {
|
||||||
console.log(` - ${f.name}: ${f.error}`);
|
console.log(` - ${f.name}: ${f.error}`);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (testResults.warnings.length > 0) {
|
if (testResults.warnings.length > 0) {
|
||||||
console.log('\n警告:');
|
console.log('\n警告:');
|
||||||
testResults.warnings.forEach(w => {
|
for (const w of testResults.warnings) {
|
||||||
console.log(` - ${w}`);
|
console.log(` - ${w}`);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\n========================================');
|
console.log('\n========================================');
|
||||||
|
|
||||||
// 返回退出码
|
|
||||||
process.exit(testResults.failed.length > 0 ? 1 : 0);
|
process.exit(testResults.failed.length > 0 ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 运行测试
|
runTests().catch((error) => {
|
||||||
runTests().catch(err => {
|
cleanupTestUser();
|
||||||
console.error('测试执行错误:', err);
|
console.error('测试执行异常:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* 分享功能边界条件深度测试
|
* 分享功能边界条件深度测试(兼容 Cookie + CSRF)
|
||||||
*
|
*
|
||||||
* 测试场景:
|
* 测试场景:
|
||||||
* 1. 已过期的分享
|
* 1. 已过期的分享
|
||||||
* 2. 分享者被删除
|
* 2. 分享文件不存在
|
||||||
* 3. 存储类型切换后的分享
|
* 3. 被封禁用户的分享
|
||||||
* 4. 路径遍历攻击
|
* 4. 路径遍历攻击
|
||||||
* 5. 并发访问限流
|
* 5. 特殊字符路径
|
||||||
|
* 6. 并发密码尝试
|
||||||
|
* 7. 分享统计
|
||||||
|
* 8. 分享码唯一性
|
||||||
|
* 9. 过期时间格式
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
|
const https = require('https');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
const { db, ShareDB, UserDB } = require('./database');
|
const { db, ShareDB, UserDB } = require('./database');
|
||||||
|
|
||||||
const BASE_URL = process.env.TEST_BASE_URL || 'http://127.0.0.1:40001';
|
const BASE_URL = process.env.TEST_BASE_URL || 'http://127.0.0.1:40001';
|
||||||
@@ -20,45 +26,129 @@ const results = {
|
|||||||
errors: []
|
errors: []
|
||||||
};
|
};
|
||||||
|
|
||||||
// HTTP 请求工具
|
const adminSession = {
|
||||||
function request(method, path, data = null, headers = {}) {
|
cookies: {},
|
||||||
return new Promise((resolve, reject) => {
|
csrfToken: ''
|
||||||
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 adminUserId = 1;
|
||||||
let body = '';
|
|
||||||
res.on('data', chunk => body += chunk);
|
function makeCookieHeader(cookies) {
|
||||||
res.on('end', () => {
|
return Object.entries(cookies || {})
|
||||||
try {
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
const json = JSON.parse(body);
|
.join('; ');
|
||||||
resolve({ status: res.statusCode, data: json, headers: res.headers });
|
|
||||||
} catch (e) {
|
|
||||||
resolve({ status: res.statusCode, data: body, headers: res.headers });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function storeSetCookies(session, setCookieHeader) {
|
||||||
|
if (!session || !setCookieHeader) return;
|
||||||
|
const list = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
|
||||||
|
for (const raw of list) {
|
||||||
|
const first = String(raw || '').split(';')[0];
|
||||||
|
const idx = first.indexOf('=');
|
||||||
|
if (idx <= 0) continue;
|
||||||
|
const key = first.slice(0, idx).trim();
|
||||||
|
const value = first.slice(idx + 1).trim();
|
||||||
|
session.cookies[key] = value;
|
||||||
|
if (key === 'csrf_token') {
|
||||||
|
session.csrfToken = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSafeMethod(method) {
|
||||||
|
const upper = String(method || '').toUpperCase();
|
||||||
|
return upper === 'GET' || upper === 'HEAD' || upper === 'OPTIONS';
|
||||||
|
}
|
||||||
|
|
||||||
|
function request(method, path, options = {}) {
|
||||||
|
const {
|
||||||
|
data = null,
|
||||||
|
session = null,
|
||||||
|
headers = {},
|
||||||
|
requireCsrf = true
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = new URL(path, BASE_URL);
|
||||||
|
const transport = url.protocol === 'https:' ? https : http;
|
||||||
|
|
||||||
|
const requestHeaders = { ...headers };
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
const cookieHeader = makeCookieHeader(session.cookies);
|
||||||
|
if (cookieHeader) {
|
||||||
|
requestHeaders.Cookie = cookieHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireCsrf && !isSafeMethod(method)) {
|
||||||
|
const csrfToken = session.csrfToken || session.cookies.csrf_token;
|
||||||
|
if (csrfToken) {
|
||||||
|
requestHeaders['X-CSRF-Token'] = csrfToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload = null;
|
||||||
|
if (data !== null && data !== undefined) {
|
||||||
|
payload = JSON.stringify(data);
|
||||||
|
requestHeaders['Content-Type'] = 'application/json';
|
||||||
|
requestHeaders['Content-Length'] = Buffer.byteLength(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = transport.request({
|
||||||
|
protocol: url.protocol,
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
||||||
|
path: url.pathname + url.search,
|
||||||
|
method,
|
||||||
|
headers: requestHeaders
|
||||||
|
}, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', chunk => {
|
||||||
|
body += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', () => {
|
||||||
|
if (session) {
|
||||||
|
storeSetCookies(session, res.headers['set-cookie']);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed = body;
|
||||||
|
try {
|
||||||
|
parsed = body ? JSON.parse(body) : {};
|
||||||
|
} catch (e) {
|
||||||
|
// keep raw text
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
status: res.statusCode,
|
||||||
|
data: parsed,
|
||||||
|
headers: res.headers
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('error', reject);
|
req.on('error', reject);
|
||||||
|
|
||||||
if (data) {
|
if (payload) {
|
||||||
req.write(JSON.stringify(data));
|
req.write(payload);
|
||||||
}
|
}
|
||||||
req.end();
|
req.end();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function initCsrf(session) {
|
||||||
|
const res = await request('GET', '/api/csrf-token', {
|
||||||
|
session,
|
||||||
|
requireCsrf: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200 && res.data && res.data.csrfToken) {
|
||||||
|
session.csrfToken = res.data.csrfToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
function assert(condition, message) {
|
function assert(condition, message) {
|
||||||
if (condition) {
|
if (condition) {
|
||||||
results.passed++;
|
results.passed++;
|
||||||
@@ -70,41 +160,48 @@ function assert(condition, message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 测试用例 =====
|
function createValidShareCode(prefix = '') {
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
const generated = ShareDB.generateShareCode();
|
||||||
|
const code = (prefix + generated).replace(/[^A-Za-z0-9]/g, '').slice(0, 16);
|
||||||
|
if (!code || code.length < 6) continue;
|
||||||
|
const exists = db.prepare('SELECT 1 FROM shares WHERE share_code = ?').get(code);
|
||||||
|
if (!exists) return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ShareDB.generateShareCode();
|
||||||
|
}
|
||||||
|
|
||||||
async function testExpiredShare() {
|
async function testExpiredShare() {
|
||||||
console.log('\n[测试] 已过期的分享...');
|
console.log('\n[测试] 已过期的分享...');
|
||||||
|
|
||||||
// 直接在数据库中创建一个已过期的分享
|
const expiredShareCode = createValidShareCode('E');
|
||||||
const expiredShareCode = 'expired_' + Date.now();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 插入一个已过期的分享(过期时间设为昨天)
|
|
||||||
const yesterday = new Date();
|
const yesterday = new Date();
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
const expiresAt = yesterday.toISOString().replace('T', ' ').substring(0, 19);
|
const expiresAt = yesterday.toISOString().replace('T', ' ').substring(0, 19);
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO shares (user_id, share_code, share_path, share_type, expires_at)
|
INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type, expires_at)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`).run(1, expiredShareCode, '/expired-test.txt', 'file', expiresAt);
|
`).run(adminUserId, expiredShareCode, '/expired-test.txt', 'file', 'local', expiresAt);
|
||||||
|
|
||||||
console.log(` 创建过期分享: ${expiredShareCode}, 过期时间: ${expiresAt}`);
|
console.log(` 创建过期分享: ${expiredShareCode}, 过期时间: ${expiresAt}`);
|
||||||
|
|
||||||
// 尝试访问过期分享
|
const res = await request('POST', `/api/share/${expiredShareCode}/verify`, {
|
||||||
const res = await request('POST', `/api/share/${expiredShareCode}/verify`, {});
|
data: {},
|
||||||
|
requireCsrf: false
|
||||||
|
});
|
||||||
|
|
||||||
assert(res.status === 404, `过期分享应返回 404, 实际: ${res.status}`);
|
assert(res.status === 404, `过期分享应返回 404, 实际: ${res.status}`);
|
||||||
assert(res.data.message === '分享不存在', '应提示分享不存在');
|
assert(res.data && res.data.message === '分享不存在', '应提示分享不存在');
|
||||||
|
|
||||||
// 清理测试数据
|
|
||||||
db.prepare('DELETE FROM shares WHERE share_code = ?').run(expiredShareCode);
|
db.prepare('DELETE FROM shares WHERE share_code = ?').run(expiredShareCode);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(` [ERROR] ${error.message}`);
|
console.log(` [ERROR] ${error.message}`);
|
||||||
results.failed++;
|
results.failed++;
|
||||||
// 清理
|
|
||||||
db.prepare('DELETE FROM shares WHERE share_code = ?').run(expiredShareCode);
|
db.prepare('DELETE FROM shares WHERE share_code = ?').run(expiredShareCode);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -113,32 +210,27 @@ async function testExpiredShare() {
|
|||||||
async function testShareWithDeletedFile() {
|
async function testShareWithDeletedFile() {
|
||||||
console.log('\n[测试] 分享的文件不存在...');
|
console.log('\n[测试] 分享的文件不存在...');
|
||||||
|
|
||||||
// 创建一个指向不存在文件的分享
|
const shareCode = createValidShareCode('N');
|
||||||
const shareCode = 'nofile_' + Date.now();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type)
|
INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
`).run(1, shareCode, '/non_existent_file_xyz.txt', 'file', 'local');
|
`).run(adminUserId, shareCode, '/non_existent_file_xyz.txt', 'file', 'local');
|
||||||
|
|
||||||
console.log(` 创建分享: ${shareCode}, 路径: /non_existent_file_xyz.txt`);
|
console.log(` 创建分享: ${shareCode}, 路径: /non_existent_file_xyz.txt`);
|
||||||
|
|
||||||
// 访问分享
|
const res = await request('POST', `/api/share/${shareCode}/verify`, {
|
||||||
const res = await request('POST', `/api/share/${shareCode}/verify`, {});
|
data: {},
|
||||||
|
requireCsrf: false
|
||||||
|
});
|
||||||
|
|
||||||
// 应该返回错误(文件不存在)
|
assert(res.status === 500 || res.status === 200, `应返回500或200,实际: ${res.status}`);
|
||||||
// 注意:verify 接口在缓存未命中时会查询存储
|
|
||||||
if (res.status === 500) {
|
if (res.status === 500) {
|
||||||
assert(res.data.message && res.data.message.includes('不存在'), '应提示文件不存在');
|
assert(!!res.data?.message, '500时应返回错误消息');
|
||||||
} else if (res.status === 200) {
|
|
||||||
// 如果成功返回,file 字段应该没有正确的文件信息
|
|
||||||
console.log(` [INFO] verify 返回 200,检查文件信息`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理
|
|
||||||
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
|
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(` [ERROR] ${error.message}`);
|
console.log(` [ERROR] ${error.message}`);
|
||||||
@@ -150,20 +242,17 @@ async function testShareWithDeletedFile() {
|
|||||||
async function testShareByBannedUser() {
|
async function testShareByBannedUser() {
|
||||||
console.log('\n[测试] 被封禁用户的分享...');
|
console.log('\n[测试] 被封禁用户的分享...');
|
||||||
|
|
||||||
// 创建测试用户
|
|
||||||
let testUserId = null;
|
let testUserId = null;
|
||||||
const shareCode = 'banned_' + Date.now();
|
const shareCode = createValidShareCode('B');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建测试用户
|
|
||||||
testUserId = UserDB.create({
|
testUserId = UserDB.create({
|
||||||
username: 'test_banned_' + Date.now(),
|
username: `test_banned_${Date.now()}`,
|
||||||
email: `test_banned_${Date.now()}@test.com`,
|
email: `test_banned_${Date.now()}@test.com`,
|
||||||
password: 'test123',
|
password: 'test123',
|
||||||
is_verified: 1
|
is_verified: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
// 创建分享
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type)
|
INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
@@ -172,21 +261,16 @@ async function testShareByBannedUser() {
|
|||||||
console.log(` 创建测试用户 ID: ${testUserId}`);
|
console.log(` 创建测试用户 ID: ${testUserId}`);
|
||||||
console.log(` 创建分享: ${shareCode}`);
|
console.log(` 创建分享: ${shareCode}`);
|
||||||
|
|
||||||
// 封禁用户
|
|
||||||
UserDB.setBanStatus(testUserId, true);
|
UserDB.setBanStatus(testUserId, true);
|
||||||
console.log(` 封禁用户: ${testUserId}`);
|
console.log(` 封禁用户: ${testUserId}`);
|
||||||
|
|
||||||
// 访问分享
|
const res = await request('POST', `/api/share/${shareCode}/verify`, {
|
||||||
const res = await request('POST', `/api/share/${shareCode}/verify`, {});
|
data: {},
|
||||||
|
requireCsrf: false
|
||||||
|
});
|
||||||
|
|
||||||
// 当前实现:被封禁用户的分享仍然可以访问
|
|
||||||
// 如果需要阻止,应该在 ShareDB.findByCode 中检查用户状态
|
|
||||||
console.log(` 被封禁用户分享访问状态码: ${res.status}`);
|
console.log(` 被封禁用户分享访问状态码: ${res.status}`);
|
||||||
|
|
||||||
// 注意:这里可能是一个潜在的功能增强点
|
|
||||||
// 如果希望被封禁用户的分享也被禁止访问,需要修改代码
|
|
||||||
|
|
||||||
// 清理
|
|
||||||
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
|
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
|
||||||
UserDB.delete(testUserId);
|
UserDB.delete(testUserId);
|
||||||
|
|
||||||
@@ -203,16 +287,14 @@ async function testShareByBannedUser() {
|
|||||||
async function testPathTraversalAttacks() {
|
async function testPathTraversalAttacks() {
|
||||||
console.log('\n[测试] 路径遍历攻击防护...');
|
console.log('\n[测试] 路径遍历攻击防护...');
|
||||||
|
|
||||||
// 创建测试分享
|
const shareCode = createValidShareCode('T');
|
||||||
const shareCode = 'traverse_' + Date.now();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type)
|
INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
`).run(1, shareCode, '/allowed-folder', 'directory', 'local');
|
`).run(adminUserId, shareCode, '/allowed-folder', 'directory', 'local');
|
||||||
|
|
||||||
// 测试各种路径遍历攻击
|
|
||||||
const attackPaths = [
|
const attackPaths = [
|
||||||
'../../../etc/passwd',
|
'../../../etc/passwd',
|
||||||
'..\\..\\..\\etc\\passwd',
|
'..\\..\\..\\etc\\passwd',
|
||||||
@@ -225,7 +307,10 @@ async function testPathTraversalAttacks() {
|
|||||||
|
|
||||||
let blocked = 0;
|
let blocked = 0;
|
||||||
for (const attackPath of attackPaths) {
|
for (const attackPath of attackPaths) {
|
||||||
const res = await request('POST', `/api/share/${shareCode}/download-url`, { path: attackPath });
|
const res = await request('POST', `/api/share/${shareCode}/download-url`, {
|
||||||
|
data: { path: attackPath },
|
||||||
|
requireCsrf: false
|
||||||
|
});
|
||||||
|
|
||||||
if (res.status === 403 || res.status === 400) {
|
if (res.status === 403 || res.status === 400) {
|
||||||
blocked++;
|
blocked++;
|
||||||
@@ -237,9 +322,7 @@ async function testPathTraversalAttacks() {
|
|||||||
|
|
||||||
assert(blocked >= attackPaths.length - 1, `路径遍历攻击应被阻止 (${blocked}/${attackPaths.length})`);
|
assert(blocked >= attackPaths.length - 1, `路径遍历攻击应被阻止 (${blocked}/${attackPaths.length})`);
|
||||||
|
|
||||||
// 清理
|
|
||||||
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
|
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(` [ERROR] ${error.message}`);
|
console.log(` [ERROR] ${error.message}`);
|
||||||
@@ -251,7 +334,12 @@ async function testPathTraversalAttacks() {
|
|||||||
async function testSpecialCharactersInPath() {
|
async function testSpecialCharactersInPath() {
|
||||||
console.log('\n[测试] 特殊字符路径处理...');
|
console.log('\n[测试] 特殊字符路径处理...');
|
||||||
|
|
||||||
// 测试创建包含特殊字符的分享
|
if (!adminSession.cookies.token) {
|
||||||
|
console.log(' [WARN] 未登录,跳过特殊字符路径测试');
|
||||||
|
assert(true, '未登录时跳过特殊字符路径测试');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const specialPaths = [
|
const specialPaths = [
|
||||||
'/文件夹/中文文件.txt',
|
'/文件夹/中文文件.txt',
|
||||||
'/folder with spaces/file.txt',
|
'/folder with spaces/file.txt',
|
||||||
@@ -262,28 +350,35 @@ async function testSpecialCharactersInPath() {
|
|||||||
|
|
||||||
let handled = 0;
|
let handled = 0;
|
||||||
|
|
||||||
for (const path of specialPaths) {
|
for (const virtualPath of specialPaths) {
|
||||||
try {
|
try {
|
||||||
const res = await request('POST', '/api/share/create', {
|
const createRes = await request('POST', '/api/share/create', {
|
||||||
|
data: {
|
||||||
share_type: 'file',
|
share_type: 'file',
|
||||||
file_path: path
|
file_path: virtualPath
|
||||||
}, { Cookie: authCookie });
|
},
|
||||||
|
session: adminSession
|
||||||
|
});
|
||||||
|
|
||||||
if (res.status === 200 || res.status === 400) {
|
if (createRes.status === 200 || createRes.status === 400) {
|
||||||
handled++;
|
handled++;
|
||||||
console.log(` [OK] ${path.substring(0, 30)}... - 状态: ${res.status}`);
|
console.log(` [OK] ${virtualPath.substring(0, 30)}... - 状态: ${createRes.status}`);
|
||||||
|
|
||||||
// 如果创建成功,清理
|
if (createRes.status === 200 && createRes.data?.share_code) {
|
||||||
if (res.data.share_code) {
|
const mySharesRes = await request('GET', '/api/share/my', {
|
||||||
const myShares = await request('GET', '/api/share/my', null, { Cookie: authCookie });
|
session: adminSession
|
||||||
const share = myShares.data.shares?.find(s => s.share_code === res.data.share_code);
|
});
|
||||||
|
|
||||||
|
const share = mySharesRes.data?.shares?.find(s => s.share_code === createRes.data.share_code);
|
||||||
if (share) {
|
if (share) {
|
||||||
await request('DELETE', `/api/share/${share.id}`, null, { Cookie: authCookie });
|
await request('DELETE', `/api/share/${share.id}`, {
|
||||||
|
session: adminSession
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(` [ERROR] ${path}: ${error.message}`);
|
console.log(` [ERROR] ${virtualPath}: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,36 +389,33 @@ async function testSpecialCharactersInPath() {
|
|||||||
async function testConcurrentPasswordAttempts() {
|
async function testConcurrentPasswordAttempts() {
|
||||||
console.log('\n[测试] 并发密码尝试限流...');
|
console.log('\n[测试] 并发密码尝试限流...');
|
||||||
|
|
||||||
// 创建一个带密码的分享
|
const shareCode = createValidShareCode('C');
|
||||||
const shareCode = 'concurrent_' + Date.now();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用 bcrypt 哈希密码
|
|
||||||
const bcrypt = require('bcryptjs');
|
|
||||||
const hashedPassword = bcrypt.hashSync('correct123', 10);
|
const hashedPassword = bcrypt.hashSync('correct123', 10);
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO shares (user_id, share_code, share_path, share_type, share_password, storage_type)
|
INSERT INTO shares (user_id, share_code, share_path, share_type, share_password, storage_type)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`).run(1, shareCode, '/test.txt', 'file', hashedPassword, 'local');
|
`).run(adminUserId, shareCode, '/test.txt', 'file', hashedPassword, 'local');
|
||||||
|
|
||||||
// 发送大量并发错误密码请求
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (let i = 0; i < 20; i++) {
|
for (let i = 0; i < 20; i++) {
|
||||||
promises.push(request('POST', `/api/share/${shareCode}/verify`, {
|
promises.push(
|
||||||
password: 'wrong' + i
|
request('POST', `/api/share/${shareCode}/verify`, {
|
||||||
}));
|
data: { password: `wrong${i}` },
|
||||||
|
requireCsrf: false
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
const responses = await Promise.all(promises);
|
||||||
|
|
||||||
// 检查是否有请求被限流
|
const rateLimited = responses.filter(r => r.status === 429).length;
|
||||||
const rateLimited = results.filter(r => r.status === 429).length;
|
const unauthorized = responses.filter(r => r.status === 401).length;
|
||||||
const unauthorized = results.filter(r => r.status === 401).length;
|
|
||||||
|
|
||||||
console.log(` 并发请求: 20, 限流: ${rateLimited}, 401错误: ${unauthorized}`);
|
console.log(` 并发请求: 20, 限流: ${rateLimited}, 401错误: ${unauthorized}`);
|
||||||
|
|
||||||
// 注意:限流是否触发取决于配置
|
|
||||||
if (rateLimited > 0) {
|
if (rateLimited > 0) {
|
||||||
assert(true, '限流机制生效');
|
assert(true, '限流机制生效');
|
||||||
} else {
|
} else {
|
||||||
@@ -331,9 +423,7 @@ async function testConcurrentPasswordAttempts() {
|
|||||||
assert(true, '并发测试完成');
|
assert(true, '并发测试完成');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理
|
|
||||||
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
|
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(` [ERROR] ${error.message}`);
|
console.log(` [ERROR] ${error.message}`);
|
||||||
@@ -345,25 +435,28 @@ async function testConcurrentPasswordAttempts() {
|
|||||||
async function testShareStatistics() {
|
async function testShareStatistics() {
|
||||||
console.log('\n[测试] 分享统计功能...');
|
console.log('\n[测试] 分享统计功能...');
|
||||||
|
|
||||||
const shareCode = 'stats_' + Date.now();
|
const shareCode = createValidShareCode('S');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type, view_count, download_count)
|
INSERT INTO shares (user_id, share_code, share_path, share_type, storage_type, view_count, download_count)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
`).run(1, shareCode, '/test.txt', 'file', 'local', 0, 0);
|
`).run(adminUserId, shareCode, '/test.txt', 'file', 'local', 0, 0);
|
||||||
|
|
||||||
// 验证多次(增加查看次数)
|
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
await request('POST', `/api/share/${shareCode}/verify`, {});
|
await request('POST', `/api/share/${shareCode}/verify`, {
|
||||||
|
data: {},
|
||||||
|
requireCsrf: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录下载次数
|
|
||||||
for (let i = 0; i < 2; i++) {
|
for (let i = 0; i < 2; i++) {
|
||||||
await request('POST', `/api/share/${shareCode}/download`, {});
|
await request('POST', `/api/share/${shareCode}/download`, {
|
||||||
|
data: {},
|
||||||
|
requireCsrf: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查统计数据
|
|
||||||
const share = db.prepare('SELECT view_count, download_count FROM shares WHERE share_code = ?').get(shareCode);
|
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.view_count === 3, `查看次数应为 3, 实际: ${share.view_count}`);
|
||||||
@@ -371,9 +464,7 @@ async function testShareStatistics() {
|
|||||||
|
|
||||||
console.log(` 查看次数: ${share.view_count}, 下载次数: ${share.download_count}`);
|
console.log(` 查看次数: ${share.view_count}, 下载次数: ${share.download_count}`);
|
||||||
|
|
||||||
// 清理
|
|
||||||
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
|
db.prepare('DELETE FROM shares WHERE share_code = ?').run(shareCode);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(` [ERROR] ${error.message}`);
|
console.log(` [ERROR] ${error.message}`);
|
||||||
@@ -386,12 +477,10 @@ async function testShareCodeUniqueness() {
|
|||||||
console.log('\n[测试] 分享码唯一性...');
|
console.log('\n[测试] 分享码唯一性...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建多个分享,检查分享码是否唯一
|
|
||||||
const codes = new Set();
|
const codes = new Set();
|
||||||
|
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const code = ShareDB.generateShareCode();
|
const code = ShareDB.generateShareCode();
|
||||||
|
|
||||||
if (codes.has(code)) {
|
if (codes.has(code)) {
|
||||||
console.log(` [WARN] 发现重复分享码: ${code}`);
|
console.log(` [WARN] 发现重复分享码: ${code}`);
|
||||||
}
|
}
|
||||||
@@ -401,7 +490,6 @@ async function testShareCodeUniqueness() {
|
|||||||
assert(codes.size === 10, `应生成 10 个唯一分享码, 实际: ${codes.size}`);
|
assert(codes.size === 10, `应生成 10 个唯一分享码, 实际: ${codes.size}`);
|
||||||
console.log(` 生成了 ${codes.size} 个唯一分享码`);
|
console.log(` 生成了 ${codes.size} 个唯一分享码`);
|
||||||
|
|
||||||
// 检查分享码长度和字符
|
|
||||||
const sampleCode = ShareDB.generateShareCode();
|
const sampleCode = ShareDB.generateShareCode();
|
||||||
assert(sampleCode.length === 8, `分享码长度应为 8, 实际: ${sampleCode.length}`);
|
assert(sampleCode.length === 8, `分享码长度应为 8, 实际: ${sampleCode.length}`);
|
||||||
assert(/^[a-zA-Z0-9]+$/.test(sampleCode), '分享码应只包含字母数字');
|
assert(/^[a-zA-Z0-9]+$/.test(sampleCode), '分享码应只包含字母数字');
|
||||||
@@ -417,11 +505,10 @@ async function testExpiryTimeFormat() {
|
|||||||
console.log('\n[测试] 过期时间格式...');
|
console.log('\n[测试] 过期时间格式...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 测试不同的过期天数
|
|
||||||
const testDays = [1, 7, 30, 365];
|
const testDays = [1, 7, 30, 365];
|
||||||
|
|
||||||
for (const days of testDays) {
|
for (const days of testDays) {
|
||||||
const result = ShareDB.create(1, {
|
const result = ShareDB.create(adminUserId, {
|
||||||
share_type: 'file',
|
share_type: 'file',
|
||||||
file_path: `/test_${days}_days.txt`,
|
file_path: `/test_${days}_days.txt`,
|
||||||
expiry_days: days
|
expiry_days: days
|
||||||
@@ -429,15 +516,12 @@ async function testExpiryTimeFormat() {
|
|||||||
|
|
||||||
const share = db.prepare('SELECT expires_at FROM shares WHERE share_code = ?').get(result.share_code);
|
const share = db.prepare('SELECT expires_at FROM shares WHERE share_code = ?').get(result.share_code);
|
||||||
|
|
||||||
// 验证过期时间格式
|
|
||||||
const expiresAt = new Date(share.expires_at);
|
const expiresAt = new Date(share.expires_at);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diffDays = Math.round((expiresAt - now) / (1000 * 60 * 60 * 24));
|
const diffDays = Math.round((expiresAt - now) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
// 允许1天的误差(由于时区等因素)
|
|
||||||
assert(Math.abs(diffDays - days) <= 1, `${days}天过期应正确设置, 实际差异: ${diffDays}天`);
|
assert(Math.abs(diffDays - days) <= 1, `${days}天过期应正确设置, 实际差异: ${diffDays}天`);
|
||||||
|
|
||||||
// 清理
|
|
||||||
db.prepare('DELETE FROM shares WHERE share_code = ?').run(result.share_code);
|
db.prepare('DELETE FROM shares WHERE share_code = ?').run(result.share_code);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,28 +532,37 @@ async function testExpiryTimeFormat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局认证 Cookie
|
|
||||||
let authCookie = '';
|
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
console.log('\n[准备] 登录获取认证...');
|
console.log('\n[准备] 登录获取认证...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await initCsrf(adminSession);
|
||||||
|
|
||||||
const res = await request('POST', '/api/login', {
|
const res = await request('POST', '/api/login', {
|
||||||
|
data: {
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
password: 'admin123'
|
password: 'admin123'
|
||||||
|
},
|
||||||
|
session: adminSession,
|
||||||
|
requireCsrf: false
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data.success) {
|
if (res.status === 200 && res.data?.success && adminSession.cookies.token) {
|
||||||
const setCookie = res.headers['set-cookie'];
|
await initCsrf(adminSession);
|
||||||
if (setCookie) {
|
|
||||||
authCookie = setCookie.map(c => c.split(';')[0]).join('; ');
|
const profileRes = await request('GET', '/api/user/profile', {
|
||||||
|
session: adminSession
|
||||||
|
});
|
||||||
|
|
||||||
|
if (profileRes.status === 200 && profileRes.data?.user?.id) {
|
||||||
|
adminUserId = profileRes.data.user.id;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(' 认证成功');
|
console.log(' 认证成功');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' 认证失败');
|
console.log(` 认证失败: status=${res.status}, message=${res.data?.message || 'unknown'}`);
|
||||||
return false;
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(` [ERROR] ${error.message}`);
|
console.log(` [ERROR] ${error.message}`);
|
||||||
@@ -477,20 +570,16 @@ async function login() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 主测试流程 =====
|
|
||||||
|
|
||||||
async function runTests() {
|
async function runTests() {
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
console.log(' 分享功能边界条件深度测试');
|
console.log(' 分享功能边界条件深度测试(Cookie + CSRF)');
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
|
|
||||||
// 登录
|
|
||||||
const loggedIn = await login();
|
const loggedIn = await login();
|
||||||
if (!loggedIn) {
|
if (!loggedIn) {
|
||||||
console.log('\n[WARN] 登录失败,部分测试可能无法执行');
|
console.log('\n[WARN] 登录失败,部分测试可能无法执行');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 运行测试
|
|
||||||
await testExpiredShare();
|
await testExpiredShare();
|
||||||
await testShareWithDeletedFile();
|
await testShareWithDeletedFile();
|
||||||
await testShareByBannedUser();
|
await testShareByBannedUser();
|
||||||
@@ -501,7 +590,6 @@ async function runTests() {
|
|||||||
await testShareCodeUniqueness();
|
await testShareCodeUniqueness();
|
||||||
await testExpiryTimeFormat();
|
await testExpiryTimeFormat();
|
||||||
|
|
||||||
// 结果统计
|
|
||||||
console.log('\n========================================');
|
console.log('\n========================================');
|
||||||
console.log(' 测试结果统计');
|
console.log(' 测试结果统计');
|
||||||
console.log('========================================');
|
console.log('========================================');
|
||||||
|
|||||||
Reference in New Issue
Block a user