test_business_api.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. from io import BytesIO
  2. import re
  3. from fastapi.testclient import TestClient
  4. from openpyxl import load_workbook
  5. from app.core.config import settings
  6. from app.db.repositories import document as document_repo
  7. from app.db.init_db import init_db
  8. from app.db.seed import seed_demo_data
  9. from app.db.session import SessionLocal
  10. from app.main import app
  11. from app.services.document_business_templates import resolve_document_template
  12. def test_seed_dashboard_diagnosis_and_evaluation(monkeypatch) -> None:
  13. init_db()
  14. with SessionLocal() as db:
  15. seed_demo_data(db)
  16. matched = resolve_document_template(
  17. db,
  18. title="上湾煤矿2025年7月份配风计划7.15(1)",
  19. )
  20. assert matched.code == "monthly_air_distribution_plan"
  21. capacity = resolve_document_template(
  22. db,
  23. title="上湾煤矿2025年通风能力核定",
  24. )
  25. assert capacity.code == "ventilation_capacity_verification_report"
  26. dust_plan = resolve_document_template(
  27. db,
  28. title="附件:2025年采掘作业场所粉尘分散度、游离二氧化硅含量监测",
  29. )
  30. assert dust_plan.code == "dust_dispersion_free_silica_monitoring_ledger"
  31. air_door_record = resolve_document_template(
  32. db,
  33. title="上湾煤矿行车风门检查记录",
  34. )
  35. assert air_door_record.code == "vehicle_air_door_inspection_record"
  36. with TestClient(app) as client:
  37. mines = client.get("/api/v1/mines")
  38. assert mines.status_code == 200
  39. mine_id = mines.json()[0]["id"]
  40. dashboard = client.get("/api/v1/dashboard/overview", params={"mine_id": mine_id})
  41. assert dashboard.status_code == 200
  42. assert dashboard.json()["mine_name"] == "上湾煤矿"
  43. documents = client.get(
  44. "/api/v1/documents",
  45. params={"mine_id": mine_id, "page": 1, "page_size": 10},
  46. )
  47. assert documents.status_code == 200
  48. document_id = documents.json()["items"][0]["id"]
  49. async def fake_process_document_run(run_id: str) -> None:
  50. return None
  51. monkeypatch.setattr(
  52. "app.api.v1.endpoints.documents.process_document_run",
  53. fake_process_document_run,
  54. )
  55. template_id = client.get("/api/v1/documents/templates").json()[0]["id"]
  56. reprocess = client.post(
  57. f"/api/v1/documents/{document_id}/process",
  58. json={
  59. "force": True,
  60. "parse_mode": "lightweight",
  61. "template_id": template_id,
  62. },
  63. )
  64. assert reprocess.status_code == 202
  65. assert reprocess.json()["document_id"] == document_id
  66. assert reprocess.json()["status"] == "pending"
  67. processing_tasks = client.get(
  68. "/api/v1/documents/processing-tasks",
  69. params={"mine_id": mine_id, "limit": 10},
  70. )
  71. assert processing_tasks.status_code == 200
  72. assert "processing_seconds" in processing_tasks.json()[0]
  73. assert "started_at" in processing_tasks.json()[0]
  74. assert any(
  75. item["document_id"] == document_id
  76. for item in processing_tasks.json()
  77. )
  78. with SessionLocal() as db:
  79. document_repo.create_processing_run(
  80. db,
  81. {
  82. "document_id": document_id,
  83. "status": "completed",
  84. "stage": "completed",
  85. "progress": 100,
  86. "document_type": "",
  87. "parser_engine": "pytest",
  88. "parsed_markdown_path": "",
  89. "parsed_markdown": "PYTEST_FULLTEXT_TOKEN 用于验证解析全文检索。",
  90. "model_output": {},
  91. "error": "",
  92. "metadata_json": {},
  93. },
  94. )
  95. fulltext_search = client.get(
  96. "/api/v1/documents/search",
  97. params={"mine_id": mine_id, "q": "PYTEST_FULLTEXT_TOKEN"},
  98. )
  99. assert fulltext_search.status_code == 200
  100. fulltext_search = fulltext_search.json()
  101. assert fulltext_search["total"] >= 1
  102. assert fulltext_search["items"][0]["document"]["id"] == document_id
  103. assert any(
  104. snippet["source"] == "parsed_text"
  105. for snippet in fulltext_search["items"][0]["snippets"]
  106. )
  107. export = client.get(
  108. "/api/v1/documents/export",
  109. params={"mine_id": mine_id},
  110. )
  111. assert export.status_code == 200
  112. assert export.content.startswith(b"PK")
  113. assert "spreadsheetml" in export.headers["content-type"]
  114. assert "filename*=UTF-8''" in export.headers["content-disposition"]
  115. workbook = load_workbook(BytesIO(export.content), data_only=True)
  116. sheet = workbook["资料清单"]
  117. assert "上湾煤矿业务资料清单" in str(sheet["A1"].value)
  118. assert sheet["A3"].value == "序号"
  119. assert sheet["B3"].value == "资料名称"
  120. assert sheet["E3"].value == "资料子类"
  121. assert all(
  122. sheet.cell(row_index, 5).value
  123. for row_index in range(4, sheet.max_row + 1)
  124. )
  125. document_types = [
  126. sheet.cell(row_index, 6).value
  127. for row_index in range(4, sheet.max_row + 1)
  128. ]
  129. assert all(
  130. not value or re.fullmatch(r"[a-z0-9_]+", str(value)) is None
  131. for value in document_types
  132. )
  133. workbook.close()
  134. preview_file = settings.upload_path / "pytest_preview_source.csv"
  135. preview_file.parent.mkdir(parents=True, exist_ok=True)
  136. preview_file.write_text("column_a,column_b\nvalue_1,value_2\n", encoding="utf-8")
  137. preview_document = client.post(
  138. "/api/v1/documents",
  139. json={
  140. "mine_id": mine_id,
  141. "title": "pytest preview document",
  142. "category": "TEST",
  143. "file_name": "pytest_preview_source.csv",
  144. "file_path": str(preview_file),
  145. "file_ext": ".csv",
  146. "file_size": preview_file.stat().st_size,
  147. },
  148. )
  149. assert preview_document.status_code == 201
  150. preview_document_id = preview_document.json()["id"]
  151. preview = client.get(f"/api/v1/documents/{preview_document_id}/preview")
  152. assert preview.status_code == 200
  153. preview_data = preview.json()
  154. assert preview_data["preview_type"] == "text"
  155. assert preview_data["source_available"] is True
  156. assert preview_data["can_download"] is True
  157. assert "column_a,column_b" in preview_data["text"]
  158. inline_file = client.get(f"/api/v1/documents/{preview_document_id}/file")
  159. assert inline_file.status_code == 200
  160. assert "inline" in inline_file.headers["content-disposition"]
  161. assert inline_file.content == preview_file.read_bytes()
  162. download_file = client.get(
  163. f"/api/v1/documents/{preview_document_id}/file",
  164. params={"download": "true"},
  165. )
  166. assert download_file.status_code == 200
  167. assert "attachment" in download_file.headers["content-disposition"]
  168. assert client.delete(f"/api/v1/documents/{preview_document_id}").status_code == 200
  169. statistics = client.get(
  170. "/api/v1/documents/statistics",
  171. params={"mine_id": mine_id},
  172. )
  173. assert statistics.status_code == 200
  174. statistics_data = statistics.json()
  175. assert statistics_data["total_documents"] >= 1
  176. assert statistics_data["extracted_fields"] >= 2
  177. assert 0 <= statistics_data["field_completeness"] <= 100
  178. assert 0 <= statistics_data["extraction_accuracy"] <= 100
  179. assert statistics_data["pending_tasks"] >= 1
  180. assert statistics_data["estimated_minutes"] >= 0
  181. assert statistics_data["storage_used_bytes"] >= 0
  182. assert statistics_data["storage_capacity_bytes"] > 0
  183. assert 0 <= statistics_data["storage_usage_percent"] <= 100
  184. assert statistics_data["expected_fields"] >= statistics_data["extracted_fields"]
  185. assert (
  186. statistics_data["valid_fields"]
  187. + statistics_data["invalid_fields"]
  188. + statistics_data["missing_fields"]
  189. == statistics_data["expected_fields"]
  190. )
  191. expected_completeness = round(
  192. statistics_data["valid_fields"]
  193. / max(statistics_data["expected_fields"], 1)
  194. * 100,
  195. 1,
  196. )
  197. assert statistics_data["field_completeness"] == expected_completeness
  198. document_detail = client.get(f"/api/v1/documents/{document_id}")
  199. assert document_detail.status_code == 200
  200. assert document_detail.json()["id"] == document_id
  201. metadata_update = client.patch(
  202. f"/api/v1/documents/{document_id}",
  203. json={
  204. "title": "更新后的资料名称",
  205. "code": "UPDATED-001",
  206. "category": "瓦斯防治",
  207. },
  208. )
  209. assert metadata_update.status_code == 200
  210. assert metadata_update.json()["title"] == "更新后的资料名称"
  211. assert metadata_update.json()["code"] == "UPDATED-001"
  212. assert metadata_update.json()["category"] == "瓦斯防治"
  213. extraction = client.post(
  214. f"/api/v1/documents/{document_id}/extractions",
  215. json={
  216. "document_id": document_id,
  217. "field_key": "test_field",
  218. "field_name": "测试字段",
  219. "field_value": "原始值",
  220. "normalized_value": "原始值",
  221. },
  222. )
  223. assert extraction.status_code == 201
  224. extraction = extraction.json()
  225. assert extraction["edited"] is False
  226. assert extraction["extraction_correct"] is True
  227. updated_extraction = client.patch(
  228. f"/api/v1/documents/{document_id}/extractions/{extraction['id']}",
  229. json={
  230. "field_value": "前端编辑值",
  231. "normalized_value": "前端编辑值",
  232. },
  233. )
  234. assert updated_extraction.status_code == 200
  235. updated_extraction = updated_extraction.json()
  236. assert updated_extraction["normalized_value"] == "前端编辑值"
  237. assert updated_extraction["edited"] is True
  238. assert updated_extraction["original_value"] == "原始值"
  239. assert updated_extraction["edit_judgement_status"] == "pending"
  240. assert updated_extraction["extraction_correct"] is True
  241. statistics_after_edit = client.get(
  242. "/api/v1/documents/statistics",
  243. params={"mine_id": mine_id},
  244. )
  245. assert statistics_after_edit.status_code == 200
  246. statistics_after_edit = statistics_after_edit.json()
  247. assert statistics_after_edit["edited_fields"] >= 1
  248. assert statistics_after_edit["incorrect_fields"] >= 1
  249. reverted_extraction = client.patch(
  250. f"/api/v1/documents/{document_id}/extractions/{extraction['id']}",
  251. json={
  252. "field_value": "原始值",
  253. "normalized_value": "原始值",
  254. },
  255. )
  256. assert reverted_extraction.status_code == 200
  257. reverted_extraction = reverted_extraction.json()
  258. assert reverted_extraction["edited"] is False
  259. assert reverted_extraction["edit_judgement_status"] == "completed"
  260. assert reverted_extraction["edit_judgement_source"] == "reverted"
  261. assert reverted_extraction["extraction_correct"] is True
  262. statistics_after_revert = client.get(
  263. "/api/v1/documents/statistics",
  264. params={"mine_id": mine_id},
  265. )
  266. assert statistics_after_revert.status_code == 200
  267. statistics_after_revert = statistics_after_revert.json()
  268. assert statistics_after_revert["edited_fields"] == 0
  269. assert statistics_after_revert["incorrect_fields"] == 0
  270. deleted = client.delete(f"/api/v1/documents/{document_id}")
  271. assert deleted.status_code == 200
  272. assert client.get(f"/api/v1/documents/{document_id}").status_code == 404
  273. templates = client.get("/api/v1/documents/templates")
  274. assert templates.status_code == 200
  275. assert {item["code"] for item in templates.json()} == {
  276. "ground_main_fan_inspection_report",
  277. "main_fan_external_leakage_rate_report",
  278. "monthly_air_distribution_plan",
  279. "mine_ventilation_resistance_report",
  280. "ventilation_capacity_verification_report",
  281. "airflow_measurement_decadal_report",
  282. "gas_grade_appraisal_report",
  283. "coal_dust_explosion_spontaneous_combustion_report",
  284. "safety_monitoring_system_ledger",
  285. "dust_dispersion_free_silica_monitoring_ledger",
  286. "vehicle_air_door_inspection_record",
  287. }
  288. diagnosis = client.post(
  289. "/api/v1/diagnosis/run",
  290. json={"mine_id": mine_id, "scene": "all", "requested_by": "pytest"},
  291. )
  292. assert diagnosis.status_code == 201
  293. assert diagnosis.json()["status"] == "completed"
  294. evaluation = client.post(
  295. "/api/v1/evaluations/run",
  296. json={"mine_id": mine_id, "requested_by": "pytest"},
  297. )
  298. assert evaluation.status_code == 201
  299. assert evaluation.json()["grade"] in {"A", "B", "C", "D"}