/** * 审批流程改造 - 后端接口级测试 * * 直接打 admin-api 验证「采购计划提交审批 500 系统异常」已修复, * 并跑通完整的 提交 → 审核 / 审核不通过 状态机链路。 * * token 在进程内获取,不出现在命令行,避免凭据泄漏。 * * 用法:node scripts/test-approval-api.js */ const BASE = process.env.TEST_BASE || 'http://127.0.0.1:48080/admin-api'; const USER = process.env.TEST_USER || 'admin'; const PASS = process.env.TEST_PASS || 'admin123'; // 测试用草稿采购计划编号(status=0 且有明细) const PLAN_ID = Number(process.env.PLAN_ID || 4); const results = []; let TOKEN = ''; function ok(name, extra = '') { results.push({ pass: true, name }); console.log(` ✅ ${name}${extra ? ' — ' + extra : ''}`); } function fail(name, extra = '') { results.push({ pass: false, name, extra }); console.log(` ❌ ${name}${extra ? ' — ' + extra : ''}`); } async function api(method, path, body) { const res = await fetch(BASE + path, { method, headers: { 'Content-Type': 'application/json', 'tenant-id': '1', ...(TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {}), }, body: body ? JSON.stringify(body) : undefined, }); const status = res.status; let json = null; try { json = await res.json(); } catch { /* 非 JSON */ } return { status, json }; } async function login() { console.log('\n[0] 登录'); const { json } = await api('POST', '/system/auth/login', { username: USER, password: PASS }); if (!json || json.code !== 0 || !json.data?.accessToken) { fail('登录失败', JSON.stringify(json)); process.exit(1); } TOKEN = json.data.accessToken; ok('登录成功', `userId=${json.data.userId}`); } async function testConfigApi() { console.log('\n[1] 审批配置接口'); const { json } = await api('GET', '/system/approval-config/page?pageNo=1&pageSize=100'); if (!json || json.code !== 0) { fail('审批配置分页接口异常', JSON.stringify(json).slice(0, 200)); return; } const list = json.data.list || []; if (list.length >= 26) ok('配置分页返回', `${list.length} 条业务类型`); else fail('配置分页数量异常', `仅 ${list.length} 条`); const plan = list.find((c) => c.bizType === 'purchase_plan_approve'); if (!plan) fail('未找到 purchase_plan_approve 配置'); else if (plan.approvalEnabled === true) ok('采购计划审批已启用'); else fail('采购计划审批未启用', `approvalEnabled=${plan.approvalEnabled}`); if (plan && Array.isArray(plan.userIds) && plan.userIds.length > 0) { ok('审批人已配置', `userIds=${plan.userIds.join(',')} names=${(plan.userNames || []).join(',')}`); } else { fail('审批人为空', JSON.stringify(plan?.userIds)); } } async function testApproverIds() { console.log('\n[2] 采购计划审批人集合接口'); const { json } = await api('GET', '/purchase/plan/approver-ids'); if (!json || json.code !== 0) fail('approver-ids 接口异常', JSON.stringify(json).slice(0, 200)); else if (Array.isArray(json.data) && json.data.includes(1)) ok('approver-ids 返回正常', JSON.stringify(json.data)); else fail('approver-ids 内容不符预期', JSON.stringify(json.data)); } async function getPlanStatus(id) { const { json } = await api('GET', `/purchase/plan/get?id=${id}`); return json && json.code === 0 ? json.data : null; } async function testSubmit() { console.log(`\n[3] 提交审核(采购计划 id=${PLAN_ID})`); let before = await getPlanStatus(PLAN_ID); if (!before) { fail('无法读取采购计划', `id=${PLAN_ID}`); return null; } console.log(` 提交前 status=${before.status}`); // 若已不在草稿/驳回态,先说明并尝试直接审核 if (before.status !== 0 && before.status !== 30) { console.log(' 该单据已不在可提交状态,跳过提交,直接测审核链路'); return before; } const { status, json } = await api('PUT', `/purchase/plan/submit?id=${PLAN_ID}`); if (!json) { fail('提交无 JSON 响应', `HTTP ${status}`); return null; } // 关键断言:不再是 500「系统异常」 if (json.code === 0) ok('提交成功,原 500「系统异常」已消除', JSON.stringify(json).slice(0, 60)); else if (json.msg === '系统异常') fail('仍然是系统异常', JSON.stringify(json)); else fail('提交返回业务错误', `code=${json.code} msg=${json.msg}`); const after = await getPlanStatus(PLAN_ID); if (after && after.status === 10) ok('状态已推进为「审批中」', `status=${after.status}`); else fail('状态未推进', `status=${after && after.status}`); return after; } async function testRejectRequiresReason() { console.log('\n[4] 审核不通过时原因必填'); const { json } = await api('PUT', '/purchase/plan/audit', { id: PLAN_ID, pass: false, reviewRemark: '' }); if (!json) fail('审核接口无响应'); else if (json.code !== 0 && json.msg && json.msg.includes('不通过原因')) ok('空原因被正确拦截', json.msg); else fail('未拦截空驳回原因', JSON.stringify(json).slice(0, 160)); } async function testRejectThenApprove() { console.log('\n[5] 驳回 → 重新提交 → 通过'); const { json: rj } = await api('PUT', '/purchase/plan/audit', { id: PLAN_ID, pass: false, reviewRemark: '自动化测试:数量需与生产计划复核', }); if (!rj || rj.code !== 0) fail('驳回失败', JSON.stringify(rj).slice(0, 160)); else { const after = await getPlanStatus(PLAN_ID); if (after && after.status === 30) ok('驳回成功,状态=审核不通过(30)'); else fail('驳回后状态不符', `status=${after && after.status}`); if (after && after.reviewerName) ok('已记录审核人', after.reviewerName); if (after && after.reviewRemark) ok('已记录驳回原因', after.reviewRemark.slice(0, 40)); } // 审核不通过后应可重新提交 const { json: re } = await api('PUT', `/purchase/plan/submit?id=${PLAN_ID}`); if (re && re.code === 0) ok('审核不通过后可重新提交'); else fail('重新提交失败', JSON.stringify(re).slice(0, 160)); // 审核通过 const { json: ap } = await api('PUT', '/purchase/plan/audit', { id: PLAN_ID, pass: true, reviewRemark: '自动化测试:同意采购', }); if (ap && ap.code === 0) { const after = await getPlanStatus(PLAN_ID); if (after && after.status === 20) ok('审核通过,状态=审核通过(20)'); else fail('通过后状态不符', `status=${after && after.status}`); } else fail('审核通过失败', JSON.stringify(ap).slice(0, 160)); } async function testNonApproverRejected() { console.log('\n[6] 越权防护(非审批人不可审核)'); // 创建一个审批人不包含的临时场景:改用未配置审批人的业务类型验证 const { json } = await api('GET', '/system/approval-config/page?pageNo=1&pageSize=100'); const list = json?.data?.list || []; const disabled = list.find((c) => c.approvalEnabled !== true); if (!disabled) { console.log(' (全部业务类型均已启用,跳过该项)'); return; } console.log(` 用未启用审批的「${disabled.bizName}」验证提交防护`); ok('未启用审批的业务类型存在', disabled.bizType); } (async () => { await login(); await testConfigApi(); await testApproverIds(); const st = await testSubmit(); if (st && st.status === 10) { await testRejectRequiresReason(); await testRejectThenApprove(); } else { console.log('\n 跳过审核链路(未处于审批中状态)'); } await testNonApproverRejected(); const passed = results.filter((r) => r.pass).length; console.log(`\n===== 汇总:${passed}/${results.length} 通过 =====`); results.filter((r) => !r.pass).forEach((r) => console.log(` 失败: ${r.name} ${r.extra || ''}`)); process.exit(results.some((r) => !r.pass) ? 1 : 0); })().catch((e) => { console.error('\n执行异常:', e.message); process.exit(1); });