liu
2 天以前 e347fd6e8b523b75e7eb28627ba3597cf9e40f6a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
/**
 * 审批流程改造 - 后端接口级测试
 *
 * 直接打 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);
});