From a7767b0383526452ac57dcf24e569f5f757b0315 Mon Sep 17 00:00:00 2001 From: snowgitea Date: Wed, 29 Apr 2026 02:11:50 +0800 Subject: [PATCH] chore: remove test-bookkeeping-full.js --- test-bookkeeping-full.js | 853 --------------------------------------- 1 file changed, 853 deletions(-) delete mode 100644 test-bookkeeping-full.js diff --git a/test-bookkeeping-full.js b/test-bookkeeping-full.js deleted file mode 100644 index 3287268..0000000 --- a/test-bookkeeping-full.js +++ /dev/null @@ -1,853 +0,0 @@ -#!/usr/bin/env node -/** - * 记账功能全面测试脚本 - QA自动化测试 - * 测试覆盖:6个支出类别 + 6个收入类别 - * 验证点:账单明细、首页、预算、统计页面、API、数据库一致性 - * - * 执行方式: node test-bookkeeping-full.js - */ - -const http = require('http'); - -const API_BASE = 'http://localhost:3001'; -const USER_ID = 1; -let accountId = 1; // 默认使用第一个账户 - -// =========================== -// 测试基础设施 -// =========================== -const results = { - total: 0, - passed: 0, - failed: 0, - warnings: 0, - tests: [], - bugs: [] -}; - -function log(level, message) { - const prefix = { - 'INFO': '\x1b[36m[INFO]\x1b[0m', - 'PASS': '\x1b[32m[PASS]\x1b[0m', - 'FAIL': '\x1b[31m[FAIL]\x1b[0m', - 'WARN': '\x1b[33m[WARN]\x1b[0m', - 'TEST': '\x1b[35m[TEST]\x1b[0m', - 'SUMMARY': '\x1b[1m\x1b[37m[SUMMARY]\x1b[0m', - }[level] || `[${level}]`; - console.log(`${prefix} ${message}`); -} - -function recordTest(name, status, detail = '', bug = null) { - results.total++; - if (status === 'pass') results.passed++; - else if (status === 'fail') results.failed++; - else if (status === 'warn') results.warnings++; - - results.tests.push({ name, status, detail }); - if (bug) results.bugs.push(bug); - - log(status === 'pass' ? 'PASS' : status === 'fail' ? 'FAIL' : 'WARN', - `${name}${detail ? ' - ' + detail : ''}`); -} - -function assertEqual(actual, expected, message) { - results.total++; - - // 字符串类型直接比较 - if (typeof expected === 'string' && typeof actual === 'string') { - if (actual === expected) { - results.passed++; - log('PASS', message); - return true; - } else { - results.failed++; - log('FAIL', `${message} - 期望: "${expected}", 实际: "${actual}"`); - return false; - } - } - - // 数值类型比较(处理浮点数) - const actualNum = typeof actual === 'number' ? actual : parseFloat(actual); - const expectedNum = typeof expected === 'number' ? expected : parseFloat(expected); - - if (Math.abs(actualNum - expectedNum) < 0.01) { - results.passed++; - log('PASS', message); - return true; - } else { - results.failed++; - log('FAIL', `${message} - 期望: ${expected}, 实际: ${actual}`); - return false; - } -} - -function assertContains(actual, expected, message) { - results.total++; - if (actual && JSON.stringify(actual).includes(JSON.stringify(expected))) { - results.passed++; - log('PASS', message); - return true; - } else { - results.failed++; - log('FAIL', `${message} - 未找到预期数据`); - return false; - } -} - -// =========================== -// HTTP 请求封装 -// =========================== -function apiRequest(method, path, body = null) { - return new Promise((resolve, reject) => { - const url = new URL(path, API_BASE); - const options = { - hostname: url.hostname, - port: url.port, - path: url.pathname + url.search, - method, - headers: { - 'Content-Type': 'application/json', - }, - }; - - const req = http.request(options, (res) => { - let data = ''; - res.on('data', chunk => data += chunk); - res.on('end', () => { - try { - resolve({ status: res.statusCode, data: JSON.parse(data) }); - } catch (e) { - resolve({ status: res.statusCode, data: data }); - } - }); - }); - - req.on('error', reject); - if (body) req.write(JSON.stringify(body)); - req.end(); - }); -} - -async function apiGet(path) { - return apiRequest('GET', path); -} - -async function apiPost(path, body) { - return apiRequest('POST', path, body); -} - -async function apiDelete(path) { - return apiRequest('DELETE', path); -} - -// =========================== -// 测试阶段 0: 环境检查 -// =========================== -async function testEnvironmentCheck() { - log('TEST', '========== 阶段0: 环境检查 =========='); - - try { - const healthRes = await apiGet('/health'); - recordTest('后端健康检查', healthRes.status === 200 ? 'pass' : 'fail', - `状态码: ${healthRes.status}`); - - const apiRes = await apiGet('/api'); - recordTest('API可用性', apiRes.status === 200 && apiRes.data.success ? 'pass' : 'fail', - `API版本: ${apiRes.data?.data?.version}`); - - const usersRes = await apiGet(`/api/users`); - recordTest('用户列表API', usersRes.status === 200 && usersRes.data.success ? 'pass' : 'fail'); - - const accountsRes = await apiGet(`/api/accounts?userId=${USER_ID}`); - if (accountsRes.status === 200 && accountsRes.data.success && accountsRes.data.data.length > 0) { - accountId = accountsRes.data.data[0].id; - recordTest('账户可用性', 'pass', `使用账户ID: ${accountId}, 账户名: ${accountsRes.data.data[0].name}`); - } else { - recordTest('账户可用性', 'fail', '没有可用账户'); - process.exit(1); - } - - // 记录初始状态 - const recordsRes = await apiGet(`/api/records?userId=${USER_ID}`); - log('INFO', `当前已有 ${recordsRes.data.data.length} 条交易记录`); - - } catch (err) { - recordTest('环境检查', 'fail', err.message); - log('FAIL', '环境检查失败,请确保后端服务正在运行'); - process.exit(1); - } -} - -// =========================== -// 测试阶段 1: 6个支出类别测试 -// =========================== -async function testExpenseCategories() { - log('TEST', '========== 阶段1: 6个支出类别添加测试 =========='); - - const expenseCategories = [ - { category: '餐饮', amount: 35.50, description: '测试-午餐' }, - { category: '交通', amount: 28.00, description: '测试-地铁充值' }, - { category: '购物', amount: 199.99, description: '测试-日用品' }, - { category: '娱乐', amount: 88.00, description: '测试-电影票' }, - { category: '医疗', amount: 156.80, description: '测试-药品' }, - { category: '其他', amount: 50.00, description: '测试-杂项' }, - ]; - - const createdIds = []; - - for (const item of expenseCategories) { - log('TEST', `--- 支出类别: ${item.category}, 金额: ¥${item.amount} ---`); - - const now = new Date(); - const recordData = { - userId: USER_ID, - accountId: accountId, - type: 'expense', - amount: item.amount, - category: item.category, - description: item.description, - date: now.toISOString(), - }; - - // 1. 测试创建记录 - const createRes = await apiPost('/api/records', recordData); - if (createRes.status === 200 && createRes.data.success) { - const recordId = createRes.data.data.id; - createdIds.push(recordId); - recordTest(`支出创建-${item.category}`, 'pass', `记录ID: ${recordId}`); - - // 2. 验证API返回的数据 - assertEqual(createRes.data.data.type, 'expense', `${item.category} type字段`); - assertEqual(createRes.data.data.category, item.category, `${item.category} category字段`); - assertEqual(createRes.data.data.amount, item.amount, `${item.category} amount字段`); - assertEqual(createRes.data.data.userId, USER_ID, `${item.category} userId字段`); - recordTest(`支出数据完整性-${item.category}`, 'pass'); - - // 3. 验证单条记录查询 - const getRes = await apiGet(`/api/records/${recordId}`); - if (getRes.status === 200 && getRes.data.success) { - assertEqual(getRes.data.data.id, recordId, `${item.category} 单条查询ID`); - recordTest(`支出单条查询-${item.category}`, 'pass'); - } else { - recordTest(`支出单条查询-${item.category}`, 'fail', '无法查询到刚创建的记录'); - } - - } else { - recordTest(`支出创建-${item.category}`, 'fail', - createRes.data?.message || `HTTP ${createRes.status}`); - - const bug = { - id: `BUG-EXP-${item.category}`, - severity: 'P0', - title: `【高危】支出类别"${item.category}"创建失败`, - environment: `后端: ${API_BASE}, 数据库: SQLite, 用户ID: ${USER_ID}`, - steps: [ - `调用 POST /api/records`, - `请求体: ${JSON.stringify(recordData)}`, - '返回错误' - ], - expected: '返回 success: true, 包含创建记录ID', - actual: `success: false, message: ${createRes.data?.message || 'unknown'}`, - suggestion: '检查后端records创建接口参数校验逻辑' - }; - results.bugs.push(bug); - } - - // 等待100ms避免时间戳冲突 - await new Promise(r => setTimeout(r, 100)); - } - - // 验证所有支出记录都能查到 - const allRecordsRes = await apiGet(`/api/records?userId=${USER_ID}`); - if (allRecordsRes.data.success) { - const expenseRecords = allRecordsRes.data.data.filter(r => r.type === 'expense'); - const todayExpenseCount = expenseRecords.filter(r => { - const d = new Date(r.date); - const now = new Date(); - return d.toDateString() === now.toDateString(); - }).length; - - log('INFO', `今日支出记录数: ${todayExpenseCount} (预期至少6条新增)`); - if (todayExpenseCount >= 6) { - recordTest('支出记录总数验证', 'pass', `今日支出: ${todayExpenseCount}条`); - } else { - recordTest('支出记录总数验证', 'warn', `今日支出: ${todayExpenseCount}条 (可能包含之前已有的记录)`); - } - } - - return createdIds; -} - -// =========================== -// 测试阶段 2: 6个收入类别测试 -// =========================== -async function testIncomeCategories() { - log('TEST', '========== 阶段2: 6个收入类别添加测试 =========='); - - const incomeCategories = [ - { category: '工资', amount: 15000.00, description: '测试-月工资' }, - { category: '奖金', amount: 2000.00, description: '测试-季度奖金' }, - { category: '投资', amount: 800.00, description: '测试-股票收益' }, - { category: '兼职', amount: 500.00, description: '测试-兼职收入' }, - { category: '理财', amount: 350.00, description: '测试-理财收益' }, - { category: '其他', amount: 100.00, description: '测试-其他收入' }, - ]; - - const createdIds = []; - - for (const item of incomeCategories) { - log('TEST', `--- 收入类别: ${item.category}, 金额: ¥${item.amount} ---`); - - const now = new Date(); - const recordData = { - userId: USER_ID, - accountId: accountId, - type: 'income', - amount: item.amount, - category: item.category, - description: item.description, - date: now.toISOString(), - }; - - const createRes = await apiPost('/api/records', recordData); - if (createRes.status === 200 && createRes.data.success) { - const recordId = createRes.data.data.id; - createdIds.push(recordId); - recordTest(`收入创建-${item.category}`, 'pass', `记录ID: ${recordId}`); - - // 验证API返回数据 - assertEqual(createRes.data.data.type, 'income', `${item.category} type字段`); - assertEqual(createRes.data.data.category, item.category, `${item.category} category字段`); - assertEqual(createRes.data.data.amount, item.amount, `${item.category} amount字段`); - recordTest(`收入数据完整性-${item.category}`, 'pass'); - - } else { - recordTest(`收入创建-${item.category}`, 'fail', - createRes.data?.message || `HTTP ${createRes.status}`); - } - - await new Promise(r => setTimeout(r, 100)); - } - - return createdIds; -} - -// =========================== -// 测试阶段 3: 账单明细页筛选验证 -// =========================== -async function testRecordFilters() { - log('TEST', '========== 阶段3: 账单明细筛选功能测试 =========='); - - // 测试全部筛选 - const allRes = await apiGet(`/api/records?userId=${USER_ID}`); - if (allRes.data.success) { - recordTest('账单-全部筛选', 'pass', `总记录数: ${allRes.data.data.length}`); - } else { - recordTest('账单-全部筛选', 'fail'); - } - - // 测试支出筛选 - const expenseRes = await apiGet(`/api/records?userId=${USER_ID}&type=expense`); - if (expenseRes.data.success) { - const allExpense = expenseRes.data.data.every(r => r.type === 'expense'); - if (allExpense) { - recordTest('账单-支出筛选', 'pass', `支出记录: ${expenseRes.data.data.length}条`); - } else { - recordTest('账单-支出筛选', 'fail', '返回数据中包含非支出记录'); - } - } else { - recordTest('账单-支出筛选', 'fail'); - } - - // 测试收入筛选 - const incomeRes = await apiGet(`/api/records?userId=${USER_ID}&type=income`); - if (incomeRes.data.success) { - const allIncome = incomeRes.data.data.every(r => r.type === 'income'); - if (allIncome) { - recordTest('账单-收入筛选', 'pass', `收入记录: ${incomeRes.data.data.length}条`); - } else { - recordTest('账单-收入筛选', 'fail', '返回数据中包含非收入记录'); - } - } else { - recordTest('账单-收入筛选', 'fail'); - } - - // 测试类别筛选 - const categoryRes = await apiGet(`/api/records?userId=${USER_ID}&category=餐饮`); - if (categoryRes.data.success) { - const allDining = categoryRes.data.data.every(r => r.category === '餐饮'); - if (allDining) { - recordTest('账单-类别筛选(餐饮)', 'pass', `餐饮记录: ${categoryRes.data.data.length}条`); - } else { - recordTest('账单-类别筛选(餐饮)', 'fail', '返回数据中包含非餐饮记录'); - } - } else { - recordTest('账单-类别筛选(餐饮)', 'fail'); - } -} - -// =========================== -// 测试阶段 4: 首页数据一致性验证 -// =========================== -async function testDashboardConsistency() { - log('TEST', '========== 阶段4: 首页数据一致性验证 =========='); - - const dashboardRes = await apiGet(`/api/dashboard/summary?userId=${USER_ID}`); - if (!dashboardRes.data.success) { - recordTest('首页Dashboard API', 'fail', '获取失败'); - return; - } - - const summary = dashboardRes.data.data; - recordTest('首页Dashboard API', 'pass', - `余额: ¥${summary.totalBalance}, 月收: ¥${summary.monthIncome}, 月支: ¥${summary.monthExpense}`); - - // 验证余额是否为各账户余额之和 - const accountBalanceSum = summary.accounts.reduce((sum, a) => sum + parseFloat(a.balance), 0); - assertEqual(summary.totalBalance, accountBalanceSum, '首页余额 = 各账户余额之和'); - - // 手动计算本月收支 - const now = new Date(); - const month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; - const recordsRes = await apiGet(`/api/records?userId=${USER_ID}`); - - if (recordsRes.data.success) { - let calcIncome = 0, calcExpense = 0; - recordsRes.data.data.forEach(r => { - const d = new Date(r.date); - const rMonth = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; - if (rMonth === month) { - if (r.type === 'income') calcIncome += parseFloat(r.amount); - else calcExpense += parseFloat(r.amount); - } - }); - - assertEqual(summary.monthIncome, calcIncome, `本月收入一致性 (API=${summary.monthIncome}, 计算=${calcIncome.toFixed(2)})`); - assertEqual(summary.monthExpense, calcExpense, `本月支出一致性 (API=${summary.monthExpense}, 计算=${calcExpense.toFixed(2)})`); - } - - // 验证预算使用情况 - if (summary.budgetUsage && summary.budgetUsage.length > 0) { - recordTest('首页预算进度展示', 'pass', `预算类别数: ${summary.budgetUsage.length}`); - - for (const budget of summary.budgetUsage) { - const pct = (budget.spent / budget.amount) * 100; - assertEqual(Math.round(budget.percentage), Math.round(Math.min(pct, 100)), - `${budget.category}预算百分比 (API=${budget.percentage}%, 计算=${pct.toFixed(1)}%)`); - } - } else { - recordTest('首页预算进度展示', 'warn', '当前没有预算数据'); - } -} - -// =========================== -// 测试阶段 5: 预算页数据验证 -// =========================== -async function testBudgetConsistency() { - log('TEST', '========== 阶段5: 预算页数据一致性验证 =========='); - - const now = new Date(); - const month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; - - const budgetsRes = await apiGet(`/api/budgets?userId=${USER_ID}&month=${month}`); - if (!budgetsRes.data.success) { - recordTest('预算API', 'fail'); - return; - } - - const budgets = budgetsRes.data.data; - recordTest('预算API', 'pass', `预算数: ${budgets.length}`); - - // 获取所有支出记录 - const expenseRes = await apiGet(`/api/records?userId=${USER_ID}&type=expense`); - if (expenseRes.data.success) { - const expenses = expenseRes.data.data; - - // 按类别汇总本月支出 - const spendingByCategory = {}; - expenses.forEach(r => { - const d = new Date(r.date); - const rMonth = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; - if (rMonth === month) { - spendingByCategory[r.category] = (spendingByCategory[r.category] || 0) + parseFloat(r.amount); - } - }); - - // 验证预算使用计算 - for (const budget of budgets) { - const spent = spendingByCategory[budget.category] || 0; - log('INFO', `预算类别 "${budget.category}": 预算=¥${budget.amount}, 已花=¥${spent.toFixed(2)}, 使用率=${(spent/budget.amount*100).toFixed(1)}%`); - recordTest(`预算-${budget.category}`, 'pass', - `已花: ¥${spent.toFixed(2)}, 预算: ¥${budget.amount}`); - } - - // 检查新增的支出类别是否有对应预算 - const budgetCategories = budgets.map(b => b.category); - const expenseCategories = [...new Set(expenses.map(r => r.category))]; - const missingBudgets = expenseCategories.filter(c => !budgetCategories.includes(c)); - - if (missingBudgets.length > 0) { - recordTest('预算覆盖率', 'warn', `以下支出类别未设置预算: ${missingBudgets.join(', ')}`); - } else { - recordTest('预算覆盖率', 'pass', '所有支出类别都有预算'); - } - } -} - -// =========================== -// 测试阶段 6: 统计页数据验证 -// =========================== -async function testStatisticsConsistency() { - log('TEST', '========== 阶段6: 统计页数据一致性验证 =========='); - - const now = new Date(); - const month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; - - // 月度统计API - const statsRes = await apiGet(`/api/statistics/monthly?userId=${USER_ID}&month=${month}`); - if (!statsRes.data.success) { - recordTest('统计月度API', 'fail'); - return; - } - - const stats = statsRes.data.data; - recordTest('统计月度API', 'pass', - `月收: ¥${stats.totalIncome}, 月支: ¥${stats.totalExpense}, 结余: ¥${stats.balance}`); - - // 手动计算验证 - const recordsRes = await apiGet(`/api/records?userId=${USER_ID}`); - if (recordsRes.data.success) { - let calcIncome = 0, calcExpense = 0; - const categoryStats = {}; - - recordsRes.data.data.forEach(r => { - const d = new Date(r.date); - const rMonth = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; - if (rMonth === month) { - const amt = parseFloat(r.amount); - if (r.type === 'income') calcIncome += amt; - else { - calcExpense += amt; - categoryStats[r.category] = (categoryStats[r.category] || 0) + amt; - } - } - }); - - assertEqual(stats.totalIncome, calcIncome, '统计-总收入一致性'); - assertEqual(stats.totalExpense, calcExpense, '统计-总支出一致性'); - assertEqual(stats.balance, calcIncome - calcExpense, '统计-结余一致性'); - - // 验证分类统计 - const apiCategoryMap = {}; - stats.categoryStats.forEach(c => apiCategoryMap[c.category] = c.amount); - - let categoryMatch = true; - for (const [cat, amount] of Object.entries(categoryStats)) { - const apiAmount = apiCategoryMap[cat] || 0; - if (Math.abs(apiAmount - amount) > 0.01) { - recordTest(`统计-分类(${cat})`, 'fail', `API=${apiAmount}, 计算=${amount.toFixed(2)}`); - categoryMatch = false; - } else { - recordTest(`统计-分类(${cat})`, 'pass', `¥${amount.toFixed(2)}`); - } - } - - if (categoryMatch) { - recordTest('统计-全部分类一致性', 'pass', '所有分类金额一致'); - } - } - - // 趋势统计API - const trendRes = await apiGet(`/api/statistics/trend?userId=${USER_ID}&startDate=${month}-01&endDate=${month}-31`); - if (trendRes.data.success) { - recordTest('统计趋势API', 'pass', `趋势数据点: ${trendRes.data.data.length}`); - } else { - // 深入诊断趋势API失败原因 - const noDateTrendRes = await apiGet(`/api/statistics/trend?userId=${USER_ID}`); - if (noDateTrendRes.data.success) { - recordTest('统计趋势API', 'fail', '带日期参数时返回500错误'); - results.bugs.push({ - id: 'BUG-TREND-001', - severity: 'P1', - title: '【高危】统计趋势API在带日期范围参数时返回500错误', - environment: `后端: ${API_BASE}, 数据库: SQLite (Prisma), 月份: ${month}`, - steps: [ - 'GET /api/statistics/trend?userId=1&startDate=2026-04-01&endDate=2026-04-30', - '后端在records.forEach中调用r.date.toISOString()时出错', - 'SQLite返回的date字段是字符串而非Date对象' - ], - expected: '返回按日期分组的收入/支出统计数据', - actual: `success: false, message: "获取趋势统计失败"`, - suggestion: '修复 index.js 趋势API第543行: 将 r.date.toISOString() 改为 (r.date instanceof Date ? r.date.toISOString() : new Date(r.date).toISOString())' - }); - } else { - recordTest('统计趋势API', 'fail', '无条件查询也失败'); - } - } -} - -// =========================== -// 测试阶段 7: 账户余额联动验证 -// =========================== -async function testAccountBalanceConsistency() { - log('TEST', '========== 阶段7: 账户余额联动验证 =========='); - - const accountsRes = await apiGet(`/api/accounts?userId=${USER_ID}`); - if (!accountsRes.data.success) { - recordTest('账户余额查询', 'fail'); - return; - } - - for (const account of accountsRes.data.data) { - // 计算该账户的所有交易对余额的影响 - const recordsRes = await apiGet(`/api/records?userId=${USER_ID}&accountId=${account.id}`); - - if (recordsRes.data.success) { - let balanceChange = 0; - recordsRes.data.data.forEach(r => { - if (r.type === 'income') balanceChange += parseFloat(r.amount); - else balanceChange -= parseFloat(r.amount); - }); - - // 注意:初始余额未知,这里只验证余额不为负数(正常情况) - const balance = parseFloat(account.balance); - if (balance >= -0.01) { - recordTest(`账户余额-${account.name}`, 'pass', `余额: ¥${balance}`); - } else { - recordTest(`账户余额-${account.name}`, 'warn', `余额: ¥${balance} (可能为负)`); - } - } - } -} - -// =========================== -// 测试阶段 8: 边界条件测试 -// =========================== -async function testEdgeCases() { - log('TEST', '========== 阶段8: 边界条件测试 =========='); - - // 测试必填字段缺失 - const missingFieldRes = await apiPost('/api/records', { - userId: USER_ID, - accountId: accountId, - type: 'expense', - // 缺少 amount 和 category - }); - if (missingFieldRes.status === 400 && !missingFieldRes.data.success) { - recordTest('边界-必填字段校验', 'pass', '正确返回400错误'); - } else { - recordTest('边界-必填字段校验', 'fail', - `预期400, 实际${missingFieldRes.status}, success=${missingFieldRes.data?.success}`); - - results.bugs.push({ - id: 'BUG-VALIDATION-001', - severity: 'P1', - title: '【中危】Records创建接口缺少必填字段校验', - environment: `后端: ${API_BASE}`, - steps: ['POST /api/records', '不传amount和category字段'], - expected: '返回400错误,提示必填字段缺失', - actual: `返回${missingFieldRes.status}, success=${missingFieldRes.data?.success}`, - suggestion: '在后端添加必填字段校验逻辑' - }); - } - - // 测试负数金额 - const negativeAmountRes = await apiPost('/api/records', { - userId: USER_ID, - accountId: accountId, - type: 'expense', - amount: -100, - category: '餐饮', - description: '测试负数金额', - date: new Date().toISOString(), - }); - if (negativeAmountRes.status === 200 && negativeAmountRes.data.success) { - recordTest('边界-负数金额', 'warn', '系统接受负数金额,可能导致数据异常'); - results.bugs.push({ - id: 'BUG-NEGATIVE-001', - severity: 'P1', - title: '【中危】Records接口未校验金额不能为负数', - environment: `后端: ${API_BASE}`, - steps: ['POST /api/records', 'amount=-100'], - expected: '拒绝负数金额,返回400错误', - actual: `接受负数金额,创建成功,ID=${negativeAmountRes.data.data.id}`, - suggestion: '添加 amount > 0 的校验' - }); - } else { - recordTest('边界-负数金额', 'pass', '正确拒绝负数金额'); - } - - // 测试金额为0 - const zeroAmountRes = await apiPost('/api/records', { - userId: USER_ID, - accountId: accountId, - type: 'expense', - amount: 0, - category: '餐饮', - description: '测试0金额', - date: new Date().toISOString(), - }); - if (zeroAmountRes.status === 200 && zeroAmountRes.data.success) { - recordTest('边界-零金额', 'warn', '系统接受0金额记录'); - } else { - recordTest('边界-零金额', 'pass', '拒绝0金额'); - } -} - -// =========================== -// 测试阶段 9: 数据删除验证 -// =========================== -async function testDeleteRecords(createdIds) { - log('TEST', '========== 阶段9: 删除记录+余额恢复验证 =========='); - - if (createdIds.length === 0) { - log('WARN', '没有需要删除的记录'); - return; - } - - // 获取删除前账户余额 - const accountsBeforeRes = await apiGet(`/api/accounts?userId=${USER_ID}`); - const accountBefore = accountsBeforeRes.data.data.find(a => a.id === accountId); - const balanceBefore = parseFloat(accountBefore.balance); - - // 删除第一条创建的记录 - const deleteId = createdIds[0]; - const deleteRes = await apiDelete(`/api/records/${deleteId}`); - if (deleteRes.status === 200 && deleteRes.data.success) { - recordTest('删除记录', 'pass', `删除记录ID: ${deleteId}`); - } else { - recordTest('删除记录', 'fail', `HTTP ${deleteRes.status}`); - } - - // 验证记录已删除 - const getDeletedRes = await apiGet(`/api/records/${deleteId}`); - if (getDeletedRes.status === 404 || !getDeletedRes.data.success) { - recordTest('删除记录验证', 'pass', '记录已被正确删除'); - } else { - recordTest('删除记录验证', 'fail', '删除后仍能查询到记录'); - } - - // 验证账户余额恢复 - const accountsAfterRes = await apiGet(`/api/accounts?userId=${USER_ID}`); - const accountAfter = accountsAfterRes.data.data.find(a => a.id === accountId); - const balanceAfter = parseFloat(accountAfter.balance); - - log('INFO', `账户余额变化: ¥${balanceBefore} -> ¥${balanceAfter}`); - recordTest('删除后余额恢复', 'pass', `余额差值: ¥${(balanceAfter - balanceBefore).toFixed(2)}`); -} - -// =========================== -// 生成测试报告 -// =========================== -function generateReport() { - log('TEST', '\n'); - log('SUMMARY', '========== 测试报告 =========='); - console.log(''); - - const passRate = results.total > 0 ? ((results.passed / results.total) * 100).toFixed(1) : 0; - - console.log(`\x1b[1m总测试数: ${results.total}\x1b[0m`); - console.log(`\x1b[32m通过: ${results.passed}\x1b[0m`); - console.log(`\x1b[31m失败: ${results.failed}\x1b[0m`); - console.log(`\x1b[33m警告: ${results.warnings}\x1b[0m`); - console.log(`\x1b[1m通过率: ${passRate}%\x1b[0m`); - console.log(`\x1b[1m发现Bug: ${results.bugs.length}\x1b[0m`); - - if (results.bugs.length > 0) { - console.log('\n\x1b[1m\x1b[31m--- Bug 列表 ---\x1b[0m'); - results.bugs.forEach((bug, i) => { - console.log(`\n${i + 1}. [${bug.severity}] ${bug.id}: ${bug.title}`); - console.log(` 环境: ${bug.environment}`); - console.log(` 步骤: ${bug.steps.join(' -> ')}`); - console.log(` 预期: ${bug.expected}`); - console.log(` 实际: ${bug.actual}`); - console.log(` 建议: ${bug.suggestion}`); - }); - } - - // 生成JSON报告 - const report = { - timestamp: new Date().toISOString(), - summary: { - total: results.total, - passed: results.passed, - failed: results.failed, - warnings: results.warnings, - passRate: passRate + '%', - }, - bugs: results.bugs, - tests: results.tests, - }; - - const fs = require('fs'); - const path = require('path'); - const reportPath = path.join(__dirname, 'test-report-bookkeeping.json'); - fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8'); - console.log(`\n详细报告已保存至: ${reportPath}`); - - // 判断是否通过 - if (results.failed === 0) { - console.log('\n\x1b[32m===========================================\x1b[0m'); - console.log('\x1b[32m 测试通过! 所有核心用例执行成功\x1b[0m'); - console.log('\x1b[32m===========================================\x1b[0m'); - } else { - console.log('\n\x1b[31m===========================================\x1b[0m'); - console.log('\x1b[31m 测试失败! 存在 ' + results.failed + ' 个失败项\x1b[0m'); - console.log('\x1b[31m===========================================\x1b[0m'); - } -} - -// =========================== -// 主流程 -// =========================== -async function main() { - log('TEST', '========================================'); - log('TEST', ' 记账功能全面自动化测试'); - log('TEST', ' 测试时间: ' + new Date().toLocaleString('zh-CN')); - log('TEST', ' 后端地址: ' + API_BASE); - log('TEST', '========================================\n'); - - try { - // 阶段0: 环境检查 - await testEnvironmentCheck(); - - // 阶段1: 支出类别测试 - const expenseIds = await testExpenseCategories(); - - // 阶段2: 收入类别测试 - const incomeIds = await testIncomeCategories(); - - const allCreatedIds = [...expenseIds, ...incomeIds]; - - // 阶段3: 账单明细筛选 - await testRecordFilters(); - - // 阶段4: 首页数据一致性 - await testDashboardConsistency(); - - // 阶段5: 预算页数据 - await testBudgetConsistency(); - - // 阶段6: 统计页数据 - await testStatisticsConsistency(); - - // 阶段7: 账户余额联动 - await testAccountBalanceConsistency(); - - // 阶段8: 边界条件 - await testEdgeCases(); - - // 阶段9: 删除记录验证 - await testDeleteRecords(allCreatedIds); - - // 生成报告 - generateReport(); - - } catch (err) { - log('FAIL', '测试执行异常: ' + err.message); - console.error(err); - generateReport(); - } -} - -main();