Files
vue-driven-cloud-storage/backend/test_admin.js

587 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 管理员功能完整性测试脚本Cookie + CSRF 认证模型)
*
* 覆盖范围:
* 1. 鉴权与权限校验
* 2. 用户管理(列表、封禁/删除自保护、存储权限)
* 3. 系统设置(获取/更新/参数校验)
* 4. 分享管理
* 5. 系统监控(健康、存储、日志)
* 6. 上传工具接口
*/
const http = require('http');
const https = require('https');
const { UserDB } = require('./database');
const BASE_URL = process.env.TEST_BASE_URL || 'http://127.0.0.1:40001';
const state = {
adminSession: {
cookies: {},
csrfToken: ''
},
adminUserId: null,
testUserId: null,
latestSettings: null
};
const testResults = {
passed: [],
failed: [],
warnings: []
};
function makeCookieHeader(cookies) {
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) => {
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);
if (payload) {
req.write(payload);
}
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) {
try {
await fn();
testResults.passed.push(name);
console.log(`[PASS] ${name}`);
} catch (error) {
testResults.failed.push({ name, error: error.message });
console.log(`[FAIL] ${name}: ${error.message}`);
}
}
function ensureTestUser() {
if (state.testUserId) {
return;
}
const suffix = Date.now();
const username = `admin_test_user_${suffix}`;
const email = `${username}@test.local`;
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. 安全检查:未认证访问应被拒绝
async function testUnauthorizedAccess() {
const res = await request('GET', '/api/admin/users');
assert(res.status === 401, `未认证访问应返回401实际返回: ${res.status}`);
}
// 2. 安全检查:无效 Token 应被拒绝
async function testInvalidTokenAccess() {
const res = await request('GET', '/api/admin/users', {
headers: {
Authorization: 'Bearer invalid-token'
}
});
assert(res.status === 401, `无效token应返回401实际: ${res.status}`);
}
// 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() {
assert(state.adminUserId, '管理员ID未初始化');
const res = await request('POST', `/api/admin/users/${state.adminUserId}/ban`, {
data: { banned: true },
session: state.adminSession
});
assert(res.status === 400, `封禁自己应返回400实际: ${res.status}`);
assert(String(res.data?.message || '').includes('不能封禁自己'), '应提示不能封禁自己');
}
// 13. 不能删除自己
async function testCannotDeleteSelf() {
assert(state.adminUserId, '管理员ID未初始化');
const res = await request('DELETE', `/api/admin/users/${state.adminUserId}`, {
session: state.adminSession
});
assert(res.status === 400, `删除自己应返回400实际: ${res.status}`);
assert(String(res.data?.message || '').includes('不能删除自己'), '应提示不能删除自己');
}
// 14. 参数验证无效用户ID
async function testInvalidUserId() {
const res = await request('POST', '/api/admin/users/invalid/ban', {
data: { banned: true },
session: state.adminSession
});
assert(res.status === 400, `无效用户ID应返回400实际: ${res.status}`);
}
// 15. 参数验证无效分享ID
async function testInvalidShareId() {
const res = await request('DELETE', '/api/admin/shares/invalid', {
session: state.adminSession
});
assert(res.status === 400, `无效分享ID应返回400实际: ${res.status}`);
}
// 16. 存储权限设置
async function testSetStoragePermission() {
ensureTestUser();
const res = await request('POST', `/api/admin/users/${state.testUserId}/storage-permission`, {
data: {
storage_permission: 'local_only',
local_storage_quota: 2147483648,
download_traffic_quota: 3221225472
},
session: state.adminSession
});
assert(res.status === 200, `应返回200实际: ${res.status}`);
assert(res.data?.success === true, '应返回success: true');
}
// 17. 参数验证:无效的存储权限值
async function testInvalidStoragePermission() {
ensureTestUser();
const res = await request('POST', `/api/admin/users/${state.testUserId}/storage-permission`, {
data: { storage_permission: 'invalid_permission' },
session: state.adminSession
});
assert(res.status === 400, `无效存储权限应返回400实际: ${res.status}`);
}
// 18. 主题设置验证
async function testInvalidTheme() {
const res = await request('POST', '/api/admin/settings', {
data: { global_theme: 'invalid_theme' },
session: state.adminSession
});
assert(res.status === 400, `无效主题应返回400实际: ${res.status}`);
}
// 19. 日志清理测试
async function testLogCleanup() {
const res = await request('POST', '/api/admin/logs/cleanup', {
data: { keepDays: 90 },
session: state.adminSession
});
assert(res.status === 200, `应返回200实际: ${res.status}`);
assert(res.data?.success === true, '应返回success: true');
assert(typeof res.data?.deletedCount === 'number', 'deletedCount应为数字');
}
// 20. SMTP测试未配置时返回400配置后可能200/500
async function testSmtpTest() {
const res = await request('POST', '/api/admin/settings/test-smtp', {
data: { to: 'test@example.com' },
session: state.adminSession
});
if (res.status === 400 && String(res.data?.message || '').includes('SMTP未配置')) {
console.log(' - SMTP未配置这是预期的');
return;
}
assert(res.status === 200 || res.status === 500, `应返回200或500实际: ${res.status}`);
}
// 21. 上传工具配置生成(替代已移除的 /api/admin/check-upload-tool
async function testGenerateUploadToolConfig() {
const res = await request('POST', '/api/upload/generate-tool', {
data: {},
session: state.adminSession
});
assert(res.status === 200, `应返回200实际: ${res.status}`);
assert(res.data?.success === true, '应返回success: true');
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验证
async function testInvalidUserIdForFiles() {
const res = await request('GET', '/api/admin/users/invalid/files', {
session: state.adminSession
});
assert(res.status === 400, `无效用户ID应返回400实际: ${res.status}`);
}
// 23. 删除用户 - 无效用户ID验证
async function testInvalidUserIdForDelete() {
const res = await request('DELETE', '/api/admin/users/invalid', {
session: state.adminSession
});
assert(res.status === 400, `无效用户ID应返回400实际: ${res.status}`);
}
// 24. 存储权限设置 - 无效用户ID验证
async function testInvalidUserIdForPermission() {
const res = await request('POST', '/api/admin/users/invalid/storage-permission', {
data: {
storage_permission: 'local_only'
},
session: state.adminSession
});
assert(res.status === 400, `无效用户ID应返回400实际: ${res.status}`);
}
async function runTests() {
console.log('========================================');
console.log('管理员功能完整性测试Cookie + CSRF');
console.log('========================================\n');
console.log('\n--- 安全检查 ---');
await test('未认证访问应被拒绝', testUnauthorizedAccess);
await test('无效token应被拒绝', testInvalidTokenAccess);
await test('管理员登录', testAdminLogin);
console.log('\n--- 用户管理 ---');
await test('获取用户列表', testGetUsers);
await test('不能封禁自己', testCannotBanSelf);
await test('不能删除自己', testCannotDeleteSelf);
await test('无效用户ID验证', testInvalidUserId);
await test('设置存储权限', testSetStoragePermission);
await test('无效存储权限验证', testInvalidStoragePermission);
console.log('\n--- 系统设置 ---');
await test('获取系统设置', testGetSettings);
await test('更新系统设置', testUpdateSettings);
await test('无效主题验证', testInvalidTheme);
await test('SMTP测试', testSmtpTest);
console.log('\n--- 分享管理 ---');
await test('获取分享列表', testGetShares);
await test('无效分享ID验证', testInvalidShareId);
console.log('\n--- 系统监控 ---');
await test('健康检查', testHealthCheck);
await test('存储统计', testStorageStats);
await test('获取系统日志', testGetLogs);
await test('日志统计', testLogStats);
await test('日志清理', testLogCleanup);
console.log('\n--- 其他功能 ---');
await test('上传工具配置生成', testGenerateUploadToolConfig);
console.log('\n--- 参数验证增强 ---');
await test('用户文件查看无效ID验证', testInvalidUserIdForFiles);
await test('删除用户无效ID验证', testInvalidUserIdForDelete);
await test('存储权限设置无效ID验证', testInvalidUserIdForPermission);
cleanupTestUser();
console.log('\n========================================');
console.log('测试结果汇总');
console.log('========================================');
console.log(`通过: ${testResults.passed.length}`);
console.log(`失败: ${testResults.failed.length}`);
console.log(`警告: ${testResults.warnings.length}`);
if (testResults.failed.length > 0) {
console.log('\n失败的测试:');
for (const f of testResults.failed) {
console.log(` - ${f.name}: ${f.error}`);
}
}
if (testResults.warnings.length > 0) {
console.log('\n警告:');
for (const w of testResults.warnings) {
console.log(` - ${w}`);
}
}
console.log('\n========================================');
process.exit(testResults.failed.length > 0 ? 1 : 0);
}
runTests().catch((error) => {
cleanupTestUser();
console.error('测试执行异常:', error);
process.exit(1);
});