api-format-validation-test.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. const axios = require('axios');
  2. const colors = require('colors');
  3. // API格式验证测试脚本
  4. // 用于验证所有接口是否符合统一响应格式规范
  5. // 获取Token
  6. async function getToken() {
  7. try {
  8. const { getToken } = require('./quick-token-getter');
  9. return await getToken();
  10. } catch (error) {
  11. console.log('❌ 获取Token失败'.red);
  12. return null;
  13. }
  14. }
  15. // 验证响应格式
  16. function validateResponseFormat(response, apiName, expectedType) {
  17. const issues = [];
  18. // 检查基础字段
  19. if (typeof response.code !== 'number') {
  20. issues.push('❌ code字段缺失或类型错误(应为number)');
  21. }
  22. if (typeof response.msg !== 'string') {
  23. issues.push('❌ msg字段缺失或类型错误(应为string)');
  24. }
  25. if (!response.hasOwnProperty('data')) {
  26. issues.push('❌ data字段缺失');
  27. }
  28. // 检查成功状态码
  29. if (response.code === 200) {
  30. // 根据期望类型验证字段
  31. switch (expectedType) {
  32. case 'paginated':
  33. // 分页接口直接在顶层包含total和rows字段
  34. if (typeof response.total !== 'number') {
  35. issues.push('❌ 分页接口缺少total字段(应为number)');
  36. }
  37. if (!Array.isArray(response.rows)) {
  38. issues.push('❌ 分页接口缺少rows字段(应为array)');
  39. }
  40. break;
  41. case 'array':
  42. if (!Array.isArray(response.data)) {
  43. issues.push('❌ 列表接口data应为数组');
  44. }
  45. break;
  46. case 'object':
  47. if (
  48. response.data === null ||
  49. typeof response.data !== 'object' ||
  50. Array.isArray(response.data)
  51. ) {
  52. issues.push('❌ 单条数据接口data应为对象(非null)');
  53. }
  54. break;
  55. case 'null':
  56. if (response.data !== null) {
  57. issues.push('❌ 操作类接口data应为null');
  58. }
  59. break;
  60. }
  61. }
  62. return issues;
  63. }
  64. // API测试用例
  65. const apiTestCases = [
  66. {
  67. name: '报修工单列表(分页)',
  68. url: '/pm/repairOrder/list',
  69. method: 'GET',
  70. data: { pageNum: 1, pageSize: 10 },
  71. expectedType: 'paginated',
  72. permission: 'pm:repairOrder:list',
  73. },
  74. {
  75. name: '维修分类下拉选项',
  76. url: '/pm/repairCategory/options',
  77. method: 'GET',
  78. data: null,
  79. expectedType: 'array',
  80. permission: 'pm:category:options',
  81. },
  82. {
  83. name: '报修统计数据',
  84. url: '/pm/repairOrder/statistics',
  85. method: 'GET',
  86. data: null,
  87. expectedType: 'object',
  88. permission: 'pm:repairOrder:statistics',
  89. },
  90. {
  91. name: '系统用户信息',
  92. url: '/system/user/getInfo',
  93. method: 'GET',
  94. data: null,
  95. expectedType: 'object',
  96. permission: 'system:user:query',
  97. },
  98. {
  99. name: '系统字典数据',
  100. url: '/system/dict/data/list',
  101. method: 'GET',
  102. data: { pageNum: 1, pageSize: 10 },
  103. expectedType: 'paginated',
  104. permission: 'system:dict:list',
  105. },
  106. ];
  107. // 执行API格式验证测试
  108. async function runApiFormatValidation() {
  109. console.log('🔍 API格式验证测试'.rainbow.bold);
  110. console.log('='.repeat(80).rainbow);
  111. console.log('📋 验证规范:统一响应格式 {code, msg, data}');
  112. console.log('🎯 测试目标:确保所有接口符合新的格式规范');
  113. console.log('');
  114. // 获取Token
  115. console.log('🔐 获取认证Token...'.magenta);
  116. const token = await getToken();
  117. if (!token) {
  118. console.log('❌ 无法获取Token,测试终止'.red);
  119. return;
  120. }
  121. console.log('✅ Token获取成功'.green);
  122. console.log('');
  123. const results = [];
  124. let totalTests = 0;
  125. let passedTests = 0;
  126. // 逐个测试API
  127. for (const testCase of apiTestCases) {
  128. totalTests++;
  129. console.log(`📝 测试 ${totalTests}: ${testCase.name}`.cyan.bold);
  130. console.log(` 📍 接口: ${testCase.method} ${testCase.url}`);
  131. console.log(` 🔑 权限: ${testCase.permission}`);
  132. console.log(` 📊 期望格式: ${testCase.expectedType}`);
  133. try {
  134. // 构建请求配置
  135. const config = {
  136. method: testCase.method.toLowerCase(),
  137. url: `http://localhost/dev-api${testCase.url}`,
  138. headers: {
  139. 'Content-Type': 'application/json;charset=utf-8',
  140. clientid: 'e5cd7e4891bf95d1d19206ce24a7b32e',
  141. 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
  142. referer: 'http://localhost/login?redirect=%252Findex',
  143. Authorization: `Bearer ${token}`,
  144. },
  145. timeout: 10000,
  146. };
  147. if (testCase.data) {
  148. if (testCase.method === 'GET') {
  149. config.params = testCase.data;
  150. } else {
  151. config.data = testCase.data;
  152. }
  153. }
  154. // 发送请求
  155. const response = await axios(config);
  156. // 验证响应格式
  157. const issues = validateResponseFormat(
  158. response.data,
  159. testCase.name,
  160. testCase.expectedType,
  161. );
  162. if (issues.length === 0) {
  163. console.log(' ✅ 格式验证通过'.green);
  164. passedTests++;
  165. // 显示响应数据统计
  166. if (response.data.code === 200) {
  167. if (testCase.expectedType === 'paginated') {
  168. console.log(
  169. ` 📊 数据统计: total=${response.data.total}, rows=${
  170. response.data.rows?.length || 0
  171. }`,
  172. );
  173. } else if (
  174. testCase.expectedType === 'array' &&
  175. Array.isArray(response.data.data)
  176. ) {
  177. console.log(` 📊 数据统计: 数组长度=${response.data.data.length}`);
  178. } else if (testCase.expectedType === 'object' && response.data.data) {
  179. const keys = Object.keys(response.data.data);
  180. console.log(` 📊 数据统计: 对象字段=${keys.length}个`);
  181. }
  182. }
  183. } else {
  184. console.log(' ❌ 格式验证失败'.red);
  185. issues.forEach((issue) => {
  186. console.log(` ${issue}`.red);
  187. });
  188. }
  189. results.push({
  190. name: testCase.name,
  191. url: testCase.url,
  192. method: testCase.method,
  193. success: issues.length === 0,
  194. issues: issues,
  195. response: {
  196. code: response.data.code,
  197. msg: response.data.msg,
  198. hasData: response.data.hasOwnProperty('data'),
  199. dataType:
  200. testCase.expectedType === 'paginated'
  201. ? 'paginated'
  202. : Array.isArray(response.data.data)
  203. ? 'array'
  204. : response.data.data === null
  205. ? 'null'
  206. : typeof response.data.data,
  207. total: response.data.total,
  208. rowsCount: response.data.rows?.length,
  209. },
  210. });
  211. } catch (error) {
  212. console.log(' ❌ 请求失败'.red);
  213. console.log(` 错误信息: ${error.response?.data?.msg || error.message}`.red);
  214. results.push({
  215. name: testCase.name,
  216. url: testCase.url,
  217. method: testCase.method,
  218. success: false,
  219. issues: [`请求失败: ${error.response?.data?.msg || error.message}`],
  220. response: null,
  221. });
  222. }
  223. console.log('');
  224. }
  225. // 输出测试总结
  226. console.log('📊 测试结果汇总'.rainbow.bold);
  227. console.log('='.repeat(80).rainbow);
  228. console.log(`🎯 总测试数: ${totalTests}`);
  229. console.log(`✅ 通过测试: ${passedTests}`.green);
  230. console.log(`❌ 失败测试: ${totalTests - passedTests}`.red);
  231. console.log(`📈 通过率: ${((passedTests / totalTests) * 100).toFixed(1)}%`);
  232. console.log('');
  233. // 详细结果表格
  234. console.log('📋 详细测试结果'.blue.bold);
  235. console.log('─'.repeat(100));
  236. results.forEach((result, index) => {
  237. const status = result.success ? '✅ 通过'.green : '❌ 失败'.red;
  238. console.log(
  239. `${(index + 1).toString().padStart(2)}. ${result.name.padEnd(
  240. 20,
  241. )} ${result.method.padEnd(6)} ${status}`,
  242. );
  243. if (!result.success && result.issues.length > 0) {
  244. result.issues.forEach((issue) => {
  245. console.log(` ${issue}`.red);
  246. });
  247. }
  248. if (result.response) {
  249. console.log(
  250. ` 响应: code=${result.response.code}, msg="${result.response.msg}", data类型=${result.response.dataType}`,
  251. );
  252. }
  253. console.log('');
  254. });
  255. // 规范建议
  256. if (passedTests < totalTests) {
  257. console.log('💡 规范建议'.yellow.bold);
  258. console.log('─'.repeat(50));
  259. console.log('1. 检查请求头是否包含Bearer Token格式的Authorization');
  260. console.log('2. 分页接口必须直接返回TableDataInfo格式(total、rows、code、msg)');
  261. console.log('3. 非分页接口必须使用R<T>类型包装返回结果');
  262. console.log('4. 确保包含clientid、Content-Type等必要请求头');
  263. console.log('5. 验证响应格式是否符合新的统一规范');
  264. console.log('');
  265. }
  266. // 成功提示
  267. if (passedTests === totalTests) {
  268. console.log('🎉 恭喜!所有接口都符合统一响应格式规范!'.green.bold);
  269. }
  270. return {
  271. total: totalTests,
  272. passed: passedTests,
  273. failed: totalTests - passedTests,
  274. successRate: ((passedTests / totalTests) * 100).toFixed(1),
  275. };
  276. }
  277. // 主函数
  278. async function main() {
  279. try {
  280. const result = await runApiFormatValidation();
  281. console.log('\n🏁 API格式验证测试完成'.rainbow.bold);
  282. // 返回测试结果供其他脚本使用
  283. return result;
  284. } catch (error) {
  285. console.log('❌ 测试执行失败:'.red, error.message);
  286. process.exit(1);
  287. }
  288. }
  289. // 如果直接运行此脚本
  290. if (require.main === module) {
  291. main().then((result) => {
  292. if (result.failed > 0) {
  293. process.exit(1); // 有失败用例时退出码为1
  294. }
  295. });
  296. }
  297. module.exports = { runApiFormatValidation };