run_e2e_tests.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import sys
  2. sys.path.insert(0, ".")
  3. from datetime import datetime
  4. from fastapi.testclient import TestClient
  5. from app.main import app
  6. client = TestClient(app)
  7. START = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  8. results = {}
  9. def j(r):
  10. return r.json()
  11. def tc(module, name, fn):
  12. try:
  13. ok, detail = fn()
  14. ds = str(detail)[:120]
  15. results.setdefault(module, []).append((name, ok, ds))
  16. print(f" [{'PASS' if ok else 'FAIL'}] {module}/{name}: {ds[:100]}")
  17. except Exception as e:
  18. results.setdefault(module, []).append((name, False, str(e)[:120]))
  19. print(f" [ERROR] {module}/{name}: {str(e)[:100]}")
  20. def login(username, password):
  21. r = client.post("/api/auth/login", json={"username": username, "password": password})
  22. if r.status_code == 200:
  23. token = r.json()["data"]["access_token"]
  24. return {"Authorization": f"Bearer {token}"}
  25. return None
  26. print("=" * 60)
  27. print("报销系统 多角色端到端测试")
  28. print("=" * 60)
  29. # ===================== 1. 认证模块 =====================
  30. print("\n=== 1. 认证模块 ===")
  31. H_admin = login("admin", "admin123")
  32. tc("认证", "管理员登录", lambda: (H_admin is not None, "admin"))
  33. tc("认证", "获取当前用户信息", lambda: (
  34. client.get("/api/auth/me", headers=H_admin).status_code == 200,
  35. j(client.get("/api/auth/me", headers=H_admin)).get("data", {}).get("username", "")
  36. ))
  37. tc("认证", "错误密码返回401", lambda: (
  38. client.post("/api/auth/login", json={"username": "admin", "password": "wrong"}).status_code == 401,
  39. "401"
  40. ))
  41. tc("认证", "未登录访问返回401", lambda: (
  42. client.get("/api/users/?page=1&pageSize=10").status_code == 401,
  43. "401"
  44. ))
  45. H_zhangsan = login("zhangsan", "test123")
  46. tc("认证", "员工(zhangsan)登录", lambda: (H_zhangsan is not None, "zhangsan"))
  47. H_lisi = login("lisi", "test123")
  48. tc("认证", "总经理(lisi)登录", lambda: (H_lisi is not None, "lisi"))
  49. H_wangwu = login("wangwu", "test123")
  50. tc("认证", "财务(wangwu)登录", lambda: (H_wangwu is not None, "wangwu"))
  51. # ===================== 2. 健康检查 =====================
  52. print("\n=== 2. 健康检查 ===")
  53. tc("系统", "健康检查", lambda: (client.get("/api/health").status_code == 200, "ok"))
  54. # ===================== 3. 基础数据管理 (admin) =====================
  55. print("\n=== 3. 基础数据管理 ===")
  56. tc("用户管理", "用户列表", lambda: (
  57. client.get("/api/users/?page=1&pageSize=10", headers=H_admin).status_code == 200,
  58. f"total={j(client.get('/api/users/?page=1&pageSize=10', headers=H_admin))['data']['total']}"
  59. ))
  60. tc("用户管理", "用户分页", lambda: (
  61. j(client.get("/api/users/?page=1&pageSize=2", headers=H_admin))["data"]["pageSize"] == 2,
  62. "pageSize=2"
  63. ))
  64. tc("角色管理", "角色列表", lambda: (
  65. client.get("/api/roles/?page=1&pageSize=10", headers=H_admin).status_code == 200,
  66. f"total={j(client.get('/api/roles/?page=1&pageSize=10', headers=H_admin))['data']['total']}"
  67. ))
  68. tc("角色管理", "角色详情", lambda: (
  69. client.get("/api/roles/1", headers=H_admin).status_code == 200,
  70. j(client.get("/api/roles/1", headers=H_admin))["data"]["roleName"]
  71. ))
  72. tc("角色管理", "角色详情-不存在", lambda: (
  73. client.get("/api/roles/99999", headers=H_admin).status_code == 404,
  74. "404"
  75. ))
  76. tc("菜单管理", "菜单树", lambda: (
  77. client.get("/api/menus/tree", headers=H_admin).status_code == 200,
  78. f"roots={len(j(client.get('/api/menus/tree', headers=H_admin))['data'])}"
  79. ))
  80. tc("菜单管理", "按角色查菜单", lambda: (
  81. client.get("/api/menus/role/1", headers=H_admin).status_code == 200,
  82. "ok"
  83. ))
  84. tc("费用项目", "费用项目树", lambda: (
  85. client.get("/api/expense-items/tree", headers=H_admin).status_code == 200,
  86. f"roots={len(j(client.get('/api/expense-items/tree', headers=H_admin))['data'])}"
  87. ))
  88. tc("费用项目", "费用项目详情", lambda: (
  89. client.get("/api/expense-items/1", headers=H_admin).status_code == 200,
  90. j(client.get("/api/expense-items/1", headers=H_admin))["data"]["itemName"]
  91. ))
  92. tc("工作流配置", "工作流列表", lambda: (
  93. client.get("/api/workflows/?page=1&pageSize=10", headers=H_admin).status_code == 200,
  94. f"total={j(client.get('/api/workflows/?page=1&pageSize=10', headers=H_admin))['data']['total']}"
  95. ))
  96. tc("工作流配置", "按biz_type查询-备用金", lambda: (
  97. client.get("/api/workflows/biz/advance", headers=H_admin).status_code == 200,
  98. j(client.get("/api/workflows/biz/advance", headers=H_admin))["data"]["wfName"]
  99. ))
  100. tc("工作流配置", "按biz_type查询-报销", lambda: (
  101. client.get("/api/workflows/biz/reimburse", headers=H_admin).status_code == 200,
  102. j(client.get("/api/workflows/biz/reimburse", headers=H_admin))["data"]["wfName"]
  103. ))
  104. wf_res = j(client.get("/api/workflows/1", headers=H_admin))
  105. if wf_res.get("code") == 200:
  106. nodes = wf_res["data"].get("nodes", [])
  107. has_approvers = all(n.get("approver_id") for n in nodes)
  108. tc("工作流配置", "备用金工作流节点有审批人", lambda: (has_approvers, f"nodes={len(nodes)}, all_have_approver={has_approvers}"))
  109. # ===================== 4. 备用金完整流程 =====================
  110. print("\n=== 4. 备用金完整流程 (员工申请 → 总经理审批 → 财务复核 → 总经理审批 → 发放) ===")
  111. r_adv = client.post("/api/advances/", json={"reason": "出差备用金-E2E测试", "purpose": "北京出差", "amount": 8000}, headers=H_zhangsan)
  112. adv_id = r_adv.json()["data"]["id"] if r_adv.status_code == 200 else 0
  113. tc("备用金流程", "员工创建备用金申请", lambda: (r_adv.status_code == 200, f"id={adv_id}"))
  114. tc("备用金流程", "备用金状态为draft", lambda: (
  115. j(client.get(f"/api/advances/{adv_id}", headers=H_zhangsan))["data"]["status"] == "draft",
  116. f"status={j(client.get(f'/api/advances/{adv_id}', headers=H_zhangsan))['data']['status']}"
  117. ))
  118. r_sub = client.post(f"/api/advances/{adv_id}/submit", headers=H_zhangsan)
  119. tc("备用金流程", "员工提交备用金", lambda: (r_sub.status_code == 200, f"status={r_sub.json()['data']['status']}"))
  120. r_appr_list = client.get(f"/api/advances/{adv_id}/approvals", headers=H_zhangsan)
  121. tc("备用金流程", "获取审批记录", lambda: (r_appr_list.status_code == 200, f"count={len(r_appr_list.json()['data'])}"))
  122. tc("备用金流程", "我的备用金列表包含新申请", lambda: (
  123. any(r["id"] == adv_id for r in j(client.get("/api/advances/my?page=1&pageSize=50", headers=H_zhangsan))["data"]["rows"]),
  124. f"found id={adv_id}"
  125. ))
  126. r_approve_1 = client.post(f"/api/advances/{adv_id}/approve", json={"approvalStatus": "approved", "approval_opinion": "同意-总经理"}, headers=H_lisi)
  127. tc("备用金流程", "总经理通过(节点1)", lambda: (r_approve_1.status_code == 200, f"status after={r_approve_1.json()['data']['status']}"))
  128. r_approve_2 = client.post(f"/api/advances/{adv_id}/approve", json={"approvalStatus": "approved", "approval_opinion": "复核通过-财务"}, headers=H_wangwu)
  129. tc("备用金流程", "财务复核通过(节点2)", lambda: (r_approve_2.status_code == 200, f"status after={r_approve_2.json()['data']['status']}"))
  130. detail_after = j(client.get(f"/api/advances/{adv_id}", headers=H_zhangsan))
  131. current_status = detail_after["data"]["status"]
  132. current_node = detail_after["data"].get("currentNode", "")
  133. tc("备用金流程", "审批中状态为pending且有当前节点", lambda: (
  134. current_status == "pending" and bool(current_node),
  135. f"status={current_status}, node={current_node}"
  136. ))
  137. r_approve_3 = client.post(f"/api/advances/{adv_id}/approve", json={"approvalStatus": "approved", "approval_opinion": "最终批准-总经理"}, headers=H_lisi)
  138. tc("备用金流程", "总经理最终审批通过(节点3)", lambda: (r_approve_3.status_code == 200, f"status={r_approve_3.json()['data']['status']}"))
  139. detail_final = j(client.get(f"/api/advances/{adv_id}", headers=H_zhangsan))
  140. final_status = detail_final["data"]["status"]
  141. tc("备用金流程", "备用金最终状态为approved", lambda: (final_status == "approved", f"status={final_status}"))
  142. # ===================== 5. 备用金发放 =====================
  143. print("\n=== 5. 备用金发放 (财务人员) ===")
  144. r_issues = client.get("/api/payments/advance-issues?page=1&pageSize=10", headers=H_wangwu)
  145. tc("备用金发放", "查看待发放列表(财务角色)", lambda: (r_issues.status_code == 200, f"total={j(r_issues)['data']['total']}"))
  146. r_issue_proof = client.post(
  147. "/api/attachments/upload",
  148. headers=H_wangwu,
  149. files={"file": ("issue_proof.txt", b"voucher", "text/plain")},
  150. params={"biz_type": "advance_issue", "biz_id": adv_id},
  151. )
  152. tc("备用金发放", "上传发放附件(发放前必传)", lambda: (r_issue_proof.status_code == 200, f"status={r_issue_proof.status_code}"))
  153. r_issue = client.post(f"/api/payments/advance/{adv_id}/issue", json={"issue_remark": "已发放到银行卡"}, headers=H_wangwu)
  154. tc("备用金发放", "发放备用金", lambda: (r_issue.status_code == 200, f"ok"))
  155. # ===================== 6. 报销完整流程 =====================
  156. print("\n=== 6. 报销完整流程 (员工申请 → 总经理审批 → 财务复核 → 总经理审批) ===")
  157. r_reim = client.post("/api/reimburses/", json={"title": "差旅报销-E2E测试", "expenseItemId": 9, "amount": 3500}, headers=H_zhangsan)
  158. reim_id = r_reim.json()["data"]["id"] if r_reim.status_code == 200 else 0
  159. tc("报销流程", "员工创建报销申请", lambda: (r_reim.status_code == 200, f"id={reim_id}"))
  160. r_reim_sub = client.post(f"/api/reimburses/{reim_id}/submit", headers=H_zhangsan)
  161. tc("报销流程", "员工提交报销", lambda: (r_reim_sub.status_code == 200, f"status={r_reim_sub.json()['data']['status']}"))
  162. r_reim_appr_1 = client.post(f"/api/reimburses/{reim_id}/approve", json={"approvalStatus": "approved", "approval_opinion": "同意-总经理"}, headers=H_lisi)
  163. tc("报销流程", "总经理通过(节点1)", lambda: (r_reim_appr_1.status_code == 200, f"ok"))
  164. r_reim_appr_2 = client.post(f"/api/reimburses/{reim_id}/approve", json={"approvalStatus": "approved", "approval_opinion": "复核通过-财务"}, headers=H_wangwu)
  165. tc("报销流程", "财务复核通过(节点2)", lambda: (r_reim_appr_2.status_code == 200, f"ok"))
  166. r_reim_appr_3 = client.post(f"/api/reimburses/{reim_id}/approve", json={"approvalStatus": "approved", "approval_opinion": "最终批准-总经理"}, headers=H_lisi)
  167. tc("报销流程", "总经理最终审批通过(节点3)", lambda: (r_reim_appr_3.status_code == 200, f"ok"))
  168. reim_final = j(client.get(f"/api/reimburses/{reim_id}", headers=H_zhangsan))
  169. tc("报销流程", "报销最终状态为approved", lambda: (reim_final["data"]["status"] == "approved", f"status={reim_final['data']['status']}"))
  170. # ===================== 7. 驳回流程 =====================
  171. print("\n=== 7. 驳回流程测试 ===")
  172. r_adv2 = client.post("/api/advances/", json={"reason": "测试驳回", "amount": 1000}, headers=H_zhangsan)
  173. adv2_id = r_adv2.json()["data"]["id"] if r_adv2.status_code == 200 else 0
  174. tc("驳回流程", "员工创建备用金", lambda: (r_adv2.status_code == 200, f"id={adv2_id}"))
  175. if adv2_id:
  176. client.post(f"/api/advances/{adv2_id}/submit", headers=H_zhangsan)
  177. r_rej = client.post(f"/api/advances/{adv2_id}/reject", json={"approvalStatus": "rejected", "approval_opinion": "金额不合理"}, headers=H_lisi)
  178. tc("驳回流程", "总经理驳回", lambda: (r_rej.status_code == 200, f"status={r_rej.json()['data']['status']}"))
  179. rej_detail = j(client.get(f"/api/advances/{adv2_id}", headers=H_zhangsan))
  180. tc("驳回流程", "状态为rejected", lambda: (rej_detail["data"]["status"] == "rejected", f"status={rej_detail['data']['status']}"))
  181. # ===================== 8. 各角色权限 =====================
  182. print("\n=== 8. 角色权限测试 ===")
  183. tc("权限", "员工能访问我的申请", lambda: (
  184. client.get("/api/advances/my?page=1&pageSize=10", headers=H_zhangsan).status_code == 200,
  185. "ok"
  186. ))
  187. tc("权限", "员工能创建备用金", lambda: (
  188. client.post("/api/advances/", json={"reason": "权限测试", "amount": 500}, headers=H_zhangsan).status_code == 200,
  189. "ok"
  190. ))
  191. tc("权限", "总经理能查看所有备用金", lambda: (
  192. client.get("/api/advances/?page=1&pageSize=10", headers=H_lisi).status_code == 200,
  193. "ok"
  194. ))
  195. tc("权限", "财务能查看待支付", lambda: (
  196. client.get("/api/payments/pending?page=1&pageSize=10", headers=H_wangwu).status_code == 200,
  197. "ok"
  198. ))
  199. tc("权限", "财务能查看备用金发放列表", lambda: (
  200. client.get("/api/payments/advance-issues?page=1&pageSize=10", headers=H_wangwu).status_code == 200,
  201. "ok"
  202. ))
  203. tc("权限", "员工不能查看待支付(无FINANCE角色)", lambda: (
  204. client.get("/api/payments/pending?page=1&pageSize=10", headers=H_zhangsan).status_code == 403,
  205. "403"
  206. ))
  207. # ===================== 9. 列表/筛选/分页 =====================
  208. print("\n=== 9. 列表筛选测试 ===")
  209. tc("列表筛选", "备用金列表按状态筛选(pending)", lambda: (
  210. client.get("/api/advances/?page=1&pageSize=10&status=pending", headers=H_admin).status_code == 200,
  211. "ok"
  212. ))
  213. tc("列表筛选", "备用金列表按状态筛选(approved)", lambda: (
  214. client.get("/api/advances/?page=1&pageSize=10&status=approved", headers=H_admin).status_code == 200,
  215. "ok"
  216. ))
  217. tc("列表筛选", "报销列表分页", lambda: (
  218. j(client.get("/api/reimburses/?page=1&pageSize=2", headers=H_admin))["data"]["pageSize"] == 2,
  219. "pageSize=2"
  220. ))
  221. # ===================== 10. 移动端API兼容性 =====================
  222. print("\n=== 10. 移动端API兼容性 ===")
  223. tc("移动端", "登录接口返回access_token", lambda: (
  224. "access_token" in j(client.post("/api/auth/login", json={"username": "zhangsan", "password": "test123"})).get("data", {}),
  225. "has_token"
  226. ))
  227. tc("移动端", "me接口返回角色信息", lambda: (
  228. isinstance(j(client.get("/api/auth/me", headers=H_zhangsan))["data"].get("roleList"), list),
  229. "has_roleList"
  230. ))
  231. tc("移动端", "备用金创建返回id", lambda: (
  232. "id" in j(client.post("/api/advances/", json={"reason": "移动端测试", "amount": 200}, headers=H_zhangsan)).get("data", {}),
  233. "has_id"
  234. ))
  235. tc("移动端", "备用金详情包含全部字段", lambda: (
  236. all(k in j(client.get(f"/api/advances/{adv_id}", headers=H_admin))["data"] for k in ["id", "reason", "amount", "status", "applyCode", "currentNode"]),
  237. "all_fields_present"
  238. ))
  239. # ===================== SUMMARY =====================
  240. print("\n" + "=" * 60)
  241. print("测试汇总")
  242. print("=" * 60)
  243. total_pass = total_fail = 0
  244. for mod, cases in results.items():
  245. p = sum(1 for _, ok, _ in cases if ok)
  246. f = len(cases) - p
  247. total_pass += p
  248. total_fail += f
  249. print(f" {mod}: {p}/{len(cases)} passed")
  250. rate = total_pass / (total_pass + total_fail) * 100 if (total_pass + total_fail) > 0 else 0
  251. print(f"\n TOTAL: {total_pass}/{total_pass + total_fail} passed ({rate:.1f}%)")
  252. END = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  253. report = f"# 多角色端到端测试报告\n\n> 执行时间: {START} ~ {END}\n\n"
  254. report += f"## 汇总\n\n总用例: {total_pass + total_fail}, 通过: {total_pass}, 失败: {total_fail}, 通过率: {rate:.1f}%\n\n"
  255. report += "| 模块 | 通过/总数 | 通过率 |\n|------|----------|-------|\n"
  256. for mod, cases in results.items():
  257. p = sum(1 for _, ok, _ in cases if ok)
  258. r = p / len(cases) * 100 if cases else 0
  259. report += f"| {mod} | {p}/{len(cases)} | {r:.0f}% |\n"
  260. report += "\n## 详细结果\n\n"
  261. for mod, cases in results.items():
  262. report += f"\n### {mod}\n\n| 用例 | 状态 | 结果 |\n|------|------|------|\n"
  263. for name, ok, detail in cases:
  264. s = "✅" if ok else "❌"
  265. report += f"| {name} | {s} | {detail[:100]} |\n"
  266. report += "\n## 测试用户\n\n| 用户名 | 密码 | 角色 |\n|--------|------|------|\n"
  267. report += "| zhangsan | test123 | 员工(EMPLOYEE) |\n"
  268. report += "| lisi | test123 | 总经理(GENERAL_MANAGER) |\n"
  269. report += "| wangwu | test123 | 财务(FINANCE) |\n"
  270. report += "| admin | admin123 | 管理员(ADMIN) |\n"
  271. report += "\n## 测试覆盖范围\n\n"
  272. report += "- 认证登录(多角色)\n"
  273. report += "- 基础数据管理(用户/角色/菜单/费用项目/工作流)\n"
  274. report += "- 备用金完整流程:创建→提交→审批(3节点)→发放\n"
  275. report += "- 报销完整流程:创建→提交→审批(3节点)\n"
  276. report += "- 驳回流程\n"
  277. report += "- 角色权限(员工/总经理/财务)\n"
  278. report += "- 列表筛选分页\n"
  279. report += "- 移动端API兼容性\n"
  280. with open("/Users/wuzilong/work/00code/shanxia/bx/报销业务/多角色端到端测试报告.md", "w") as f:
  281. f.write(report)
  282. print(f"\n报告已写入: 报销业务/多角色端到端测试报告.md")