#!/usr/bin/env python3 """ HMS 健康管理平台 - 随访管理 / 咨询管理 / 积分商城 端到端 API 测试 使用 subprocess + curl 避免Windows urllib兼容问题 """ import json import subprocess import sys import time from datetime import datetime, timedelta BASE = "http://localhost:3000/api/v1" results = { "follow_up": [], "consultation": [], "points": [], } def api(method, path, body=None, token=None): """通过 curl 发送 HTTP 请求""" url = f"{BASE}{path}" cmd = ["curl", "-s", "-X", method, url, "-H", "Content-Type: application/json"] if token: cmd += ["-H", f"Authorization: Bearer {token}"] if body: cmd += ["-d", json.dumps(body)] try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=15) text = result.stdout.strip() if not text: return {"success": False, "error": f"Empty response (stderr: {result.stderr[:100]})"}, 0 data = json.loads(text) # Infer status code from response status = 200 if data.get("success") is False: error_msg = data.get("error", "") or data.get("message", "") if "not found" in error_msg.lower() or "404" in str(data.get("status", "")): status = 404 elif "unauthorized" in error_msg.lower() or "authentication" in error_msg.lower(): status = 401 elif "validation" in error_msg.lower() or "required" in error_msg.lower(): status = 400 elif "too many" in error_msg.lower() or "frequent" in error_msg.lower(): status = 429 elif "forbidden" in error_msg.lower(): status = 403 else: status = 400 return data, status except subprocess.TimeoutExpired: return {"success": False, "error": "Request timeout"}, 0 except json.JSONDecodeError as e: return {"success": False, "error": f"JSON decode error: {e}"}, 0 except Exception as e: return {"success": False, "error": str(e)}, 0 def api_raw(method, path, body=None, token=None): """通过 curl 发送请求并获取原始 HTTP 状态码,带重试""" url = f"{BASE}{path}" max_retries = 3 for attempt in range(max_retries): cmd = ["curl", "-s", "-w", "\n%{http_code}", "-X", method, url, "-H", "Content-Type: application/json"] if token: cmd += ["-H", f"Authorization: Bearer {token}"] if body: cmd += ["-d", json.dumps(body)] try: result = subprocess.run(cmd, capture_output=True, timeout=15) output = result.stdout.decode("utf-8", errors="replace").strip() if not output: return {"success": False, "error": "Empty response"}, 0 # Split last line as status code lines = output.rsplit("\n", 1) if len(lines) == 2 and lines[-1].strip().isdigit(): body_text = lines[0] status = int(lines[-1].strip()) else: body_text = output status = 200 if not body_text: return {"success": False, "error": "Empty body"}, status try: parsed = json.loads(body_text) except json.JSONDecodeError: return {"success": False, "error": body_text[:200]}, status # Retry on 503 (Redis fail-close transient) if status == 503 and attempt < max_retries - 1: time.sleep(5) continue return parsed, status except subprocess.TimeoutExpired: return {"success": False, "error": "Request timeout"}, 0 except Exception as e: return {"success": False, "error": str(e)}, 0 return {"success": False, "error": "Max retries exceeded (503)"}, 503 def record(section, name, response, status_code, expect_success=True): """记录测试结果""" success = response.get("success") == expect_success entry = { "name": name, "status_code": status_code, "success": success, "response_success": response.get("success"), "error": response.get("error") or response.get("message", ""), "detail": "", } data = response.get("data", {}) if isinstance(data, dict): if "total" in data: entry["detail"] = f"total={data['total']}" if "id" in data: entry["detail"] = f"id={data['id']}" results[section].append(entry) icon = "PASS" if success else "FAIL" detail_str = f" | {entry['detail']}" if entry["detail"] else "" err_str = f" | err={entry['error'][:60]}" if entry["error"] and not success else "" print(f" [{icon}] {name}: HTTP {status_code}{detail_str}{err_str}") return data # ============================================================ # 0. 认证 # ============================================================ print("=" * 70) print("0. Authentication") print("=" * 70) resp, code = api_raw("POST", "/auth/login", {"username": "admin", "password": "Admin@2026"}) # Retry login up to 5 times with delay for _ in range(5): if code == 200 and resp.get("success"): break print(f" Login attempt failed (HTTP {code}), retrying in 10s...") time.sleep(10) resp, code = api_raw("POST", "/auth/login", {"username": "admin", "password": "Admin@2026"}) if code != 200 or not resp.get("success"): print(f" [FAIL] Login failed after retries: HTTP {code}, {json.dumps(resp)[:200]}") sys.exit(1) TOKEN = resp["data"]["access_token"] print(f" [PASS] Login OK: HTTP {code}, token={TOKEN[:30]}...") # ============================================================ # Base data # ============================================================ print() print("=" * 70) print("Base Data Preparation") print("=" * 70) resp, code = api_raw("GET", "/health/patients", token=TOKEN) PATIENT_ID = None if resp.get("success") and resp.get("data", {}).get("items"): PATIENT_ID = resp["data"]["items"][0]["id"] print(f" Patients total: {resp['data']['total']}, selected ID: {PATIENT_ID}") else: # Create a test patient print(f" No patients found, creating test patient...") time.sleep(1) test_patient = { "name": "E2E Test Patient", "gender": "male", "birth_date": "1990-01-15", "phone": "13800000001", "id_card_number": "110101199001150011", } resp2, code2 = api_raw("POST", "/health/patients", test_patient, token=TOKEN) if resp2.get("success") and isinstance(resp2.get("data"), dict) and "id" in resp2["data"]: PATIENT_ID = resp2["data"]["id"] print(f" Created test patient: {PATIENT_ID}") else: print(f" [WARN] Failed to create test patient: HTTP {code2}, {resp2.get('error', resp2.get('message', ''))[:80]}") resp, code = api_raw("GET", "/health/doctors", token=TOKEN) DOCTOR_ID = None if resp.get("success") and resp.get("data", {}).get("items"): DOCTOR_ID = resp["data"]["items"][0]["id"] print(f" Doctors total: {resp['data']['total']}, selected ID: {DOCTOR_ID}") else: # Create a test doctor - needs user_id print(f" No doctors found, trying to create...") # List users to get a user_id for doctor time.sleep(1) resp_u, code_u = api_raw("GET", "/users", token=TOKEN) if resp_u.get("success") and resp_u.get("data", {}).get("items"): user_id = resp_u["data"]["items"][0]["id"] test_doctor = { "user_id": user_id, "name": "E2E Test Doctor", "department": "General", "title": "Attending Physician", "specialty": "Internal Medicine", } time.sleep(1) resp2, code2 = api_raw("POST", "/health/doctors", test_doctor, token=TOKEN) if resp2.get("success") and isinstance(resp2.get("data"), dict) and "id" in resp2["data"]: DOCTOR_ID = resp2["data"]["id"] print(f" Created test doctor: {DOCTOR_ID}") else: print(f" [WARN] Failed to create test doctor: HTTP {code2}, {resp2.get('error', resp2.get('message', ''))[:80]}") else: print(f" [WARN] No users to create doctor from") # ============================================================ # Part 1: Follow-up Management # ============================================================ print() print("=" * 70) print("Part 1: Follow-up Management") print("=" * 70) # 1.1 Task list print() print("1.1 List follow-up tasks") resp, code = api_raw("GET", "/health/follow-up-tasks", token=TOKEN) record("follow_up", "List follow-up tasks", resp, code) # 1.2 Create task print() print("1.2 Create follow-up task") if PATIENT_ID: time.sleep(1) next_week = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d") body = { "patient_id": PATIENT_ID, "follow_up_type": "phone", "planned_date": next_week, "content_template": "E2E test follow-up task", } resp, code = api_raw("POST", "/health/follow-up-tasks", body, token=TOKEN) task_data = record("follow_up", "Create follow-up task", resp, code) if resp.get("success") and isinstance(task_data, dict) and "id" in task_data: task_id = task_data["id"] task_version = task_data.get("version", 1) print() print("1.2a Get follow-up task detail") resp, code = api_raw("GET", f"/health/follow-up-tasks/{task_id}", token=TOKEN) record("follow_up", "Get follow-up task detail", resp, code) print() print("1.2b Update follow-up task") time.sleep(1) update_body = {"content_template": "E2E test updated", "status": "in_progress", "version": task_version} resp, code = api_raw("PUT", f"/health/follow-up-tasks/{task_id}", update_body, token=TOKEN) record("follow_up", "Update follow-up task", resp, code) print() print("1.2c Create follow-up record") time.sleep(1) today = datetime.now().strftime("%Y-%m-%d") record_body = { "task_id": task_id, "executed_date": today, "result": "contacted", "patient_condition": "stable", "medical_advice": "continue medication", } resp, code = api_raw("POST", f"/health/follow-up-tasks/{task_id}/records", record_body, token=TOKEN) record("follow_up", "Create follow-up record", resp, code) else: print(" [SKIP] No patient ID") # 1.3 Template list print() print("1.3 List follow-up templates") resp, code = api_raw("GET", "/health/follow-up-templates", token=TOKEN) record("follow_up", "List follow-up templates", resp, code) # 1.4 Record list print() print("1.4 List follow-up records") resp, code = api_raw("GET", "/health/follow-up-records", token=TOKEN) record("follow_up", "List follow-up records", resp, code) # 1.5 Auth check - no token print() print("1.5 Auth check - no token (expect 401)") resp, code = api_raw("GET", "/health/follow-up-tasks") if code == 401: print(f" [PASS] No token -> 401: HTTP {code}") results["follow_up"].append({ "name": "Auth check (no token)", "status_code": code, "success": True, "response_success": resp.get("success"), "error": "", "detail": "401 Unauthorized", }) else: print(f" [FAIL] Expected 401, got: HTTP {code}, body={json.dumps(resp)[:100]}") results["follow_up"].append({ "name": "Auth check (no token)", "status_code": code, "success": False, "response_success": resp.get("success"), "error": f"Expected 401, got {code}", "detail": "", }) # 1.6 Validation check - missing required fields print() print("1.6 Validation - missing required fields (expect 400/422)") body_invalid = {"follow_up_type": "phone"} resp, code = api_raw("POST", "/health/follow-up-tasks", body_invalid, token=TOKEN) is_validation_error = code in (400, 422) if is_validation_error: print(f" [PASS] Validation error: HTTP {code}, msg={resp.get('error', resp.get('message', ''))[:80]}") else: print(f" [FAIL] Expected 400/422, got: HTTP {code}, body={json.dumps(resp)[:100]}") results["follow_up"].append({ "name": "Validation (missing fields)", "status_code": code, "success": is_validation_error, "response_success": resp.get("success"), "error": resp.get("error", resp.get("message", ""))[:80], "detail": f"Expected 400/422, got {code}", }) # ============================================================ # Part 2: Consultation Management # ============================================================ print() print("=" * 70) print("Part 2: Consultation Management") print("=" * 70) # 2.1 Session list print() print("2.1 List consultation sessions") resp, code = api_raw("GET", "/health/consultation-sessions", token=TOKEN) record("consultation", "List consultation sessions", resp, code) # 2.2 Create session print() print("2.2 Create consultation session") if PATIENT_ID and DOCTOR_ID: time.sleep(1) body = { "patient_id": PATIENT_ID, "doctor_id": DOCTOR_ID, "subject": "E2E test consultation", } resp, code = api_raw("POST", "/health/consultation-sessions", body, token=TOKEN) consult_data = record("consultation", "Create consultation session", resp, code) if resp.get("success") and isinstance(consult_data, dict) and "id" in consult_data: consult_id = consult_data["id"] print() print("2.2a Get consultation detail") resp, code = api_raw("GET", f"/health/consultation-sessions/{consult_id}", token=TOKEN) record("consultation", "Get consultation detail", resp, code) print() print("2.2b Send consultation message") msg_body = { "session_id": consult_id, "content": "E2E test message", "message_type": "text", } resp, code = api_raw("POST", "/health/consultation-messages", msg_body, token=TOKEN) record("consultation", "Send consultation message", resp, code) print() print("2.2c List consultation messages") resp, code = api_raw("GET", f"/health/consultation-sessions/{consult_id}/messages", token=TOKEN) record("consultation", "List consultation messages", resp, code) print() print("2.2d Close consultation session") resp, code = api_raw("PUT", f"/health/consultation-sessions/{consult_id}/close", token=TOKEN) record("consultation", "Close consultation session", resp, code) print() print("2.2e Create follow-up from consultation") fu_body = { "follow_up_type": "phone", "planned_date": next_week if PATIENT_ID else "2026-06-01", "content_template": "Follow-up from E2E consultation", } resp, code = api_raw("POST", f"/health/consultation-sessions/{consult_id}/follow-up", fu_body, token=TOKEN) record("consultation", "Create follow-up from consultation", resp, code) else: print(f" [SKIP] Missing patient or doctor ID") # 2.3 Auth check print() print("2.3 Auth check - no token (expect 401)") resp, code = api_raw("GET", "/health/consultation-sessions") if code == 401: print(f" [PASS] No token -> 401: HTTP {code}") results["consultation"].append({ "name": "Auth check (no token)", "status_code": code, "success": True, "response_success": resp.get("success"), "error": "", "detail": "401 Unauthorized", }) else: print(f" [FAIL] Expected 401, got: HTTP {code}") results["consultation"].append({ "name": "Auth check (no token)", "status_code": code, "success": False, "response_success": resp.get("success"), "error": f"Expected 401, got {code}", "detail": "", }) # 2.4 Doctor dashboard print() print("2.4 Doctor dashboard") resp, code = api_raw("GET", "/health/doctor/dashboard", token=TOKEN) record("consultation", "Doctor dashboard", resp, code) # ============================================================ # Part 3: Points / Rewards # ============================================================ print() print("=" * 70) print("Part 3: Points / Rewards System") print("=" * 70) # 3.1 Rules print() print("3.1 List points rules (admin)") resp, code = api_raw("GET", "/health/admin/points/rules", token=TOKEN) record("points", "List points rules", resp, code) # 3.2 Products print() print("3.2 List points products (admin)") resp, code = api_raw("GET", "/health/admin/points/products", token=TOKEN) record("points", "List points products (admin)", resp, code) # 3.3 Orders print() print("3.3 List points orders (admin)") resp, code = api_raw("GET", "/health/admin/points/orders", token=TOKEN) record("points", "List points orders (admin)", resp, code) # 3.4 Account print() print("3.4 Get points account (patient)") resp, code = api_raw("GET", "/health/points/account", token=TOKEN) record("points", "Get points account", resp, code) # 3.5 Checkin print() print("3.5 Daily checkin") resp, code = api_raw("POST", "/health/points/checkin", token=TOKEN) is_checkin_ok = resp.get("success") == True or "already" in str(resp.get("error", "")).lower() or "already" in str(resp.get("message", "")).lower() print(f" [{'PASS' if is_checkin_ok else 'INFO'}] Checkin: HTTP {code}, success={resp.get('success')}, error={resp.get('error', resp.get('message', ''))}") results["points"].append({ "name": "Daily checkin", "status_code": code, "success": is_checkin_ok, "response_success": resp.get("success"), "error": resp.get("error", resp.get("message", "")), "detail": "Success or already checked in = PASS", }) # 3.6 Checkin status print() print("3.6 Checkin status") resp, code = api_raw("GET", "/health/points/checkin/status", token=TOKEN) record("points", "Checkin status", resp, code) # 3.7 Transactions print() print("3.7 List transactions") resp, code = api_raw("GET", "/health/points/transactions", token=TOKEN) record("points", "List transactions", resp, code) # 3.8 Products (patient view) print() print("3.8 List products (patient)") resp, code = api_raw("GET", "/health/points/products", token=TOKEN) record("points", "List products (patient)", resp, code) # 3.9 Statistics (admin) print() print("3.9 Points statistics (admin)") resp, code = api_raw("GET", "/health/admin/points/statistics", token=TOKEN) record("points", "Points statistics", resp, code) # 3.10 Offline events (patient) print() print("3.10 List offline events (patient)") resp, code = api_raw("GET", "/health/offline-events", token=TOKEN) record("points", "List offline events", resp, code) # 3.11 Auth check print() print("3.11 Auth check - no token (expect 401)") resp, code = api_raw("GET", "/health/admin/points/rules") if code == 401: print(f" [PASS] No token -> 401: HTTP {code}") results["points"].append({ "name": "Auth check (no token)", "status_code": code, "success": True, "response_success": resp.get("success"), "error": "", "detail": "401 Unauthorized", }) else: print(f" [FAIL] Expected 401, got: HTTP {code}") results["points"].append({ "name": "Auth check (no token)", "status_code": code, "success": False, "response_success": resp.get("success"), "error": f"Expected 401, got {code}", "detail": "", }) # ============================================================ # Summary Report # ============================================================ print() print("=" * 70) print("SUMMARY REPORT") print("=" * 70) total_pass = 0 total_fail = 0 for section_name, section_key in [("Follow-up", "follow_up"), ("Consultation", "consultation"), ("Points/Rewards", "points")]: items = results[section_key] passed = sum(1 for i in items if i["success"]) failed = sum(1 for i in items if not i["success"]) total_pass += passed total_fail += failed print() print(f"--- {section_name} ---") print(f" PASS: {passed}, FAIL: {failed}, Total: {len(items)}") for item in items: icon = "PASS" if item["success"] else "FAIL" detail = f" | {item['detail']}" if item["detail"] else "" print(f" [{icon}] {item['name']}: HTTP {item['status_code']}{detail}") total = total_pass + total_fail rate = (total_pass / total * 100) if total > 0 else 0 print() print("=" * 70) print(f"TOTAL: {total} tests, PASS {total_pass} ({rate:.1f}%), FAIL {total_fail}") print("=" * 70) if total_fail > 0: print() print("FAILED ITEMS:") for section_name, section_key in [("Follow-up", "follow_up"), ("Consultation", "consultation"), ("Points/Rewards", "points")]: for item in results[section_key]: if not item["success"]: print(f" - [{section_name}] {item['name']}: HTTP {item['status_code']}, error={item['error']}")