#!/usr/bin/env python3 """ HMS Health Management Platform -- Vital Signs to Alert Pipeline E2E API Test Correct route structure (derived from actual source code): - Patients: GET/POST /health/patients - Vital Signs: GET/POST /health/patients/{id}/vital-signs - Vital Signs DTO: { record_date, systolic_bp_morning, diastolic_bp_morning, heart_rate, ... } - Trends: GET /health/patients/{id}/trends/{indicator} - Lab Reports: GET/POST /health/patients/{id}/lab-reports - Lab DTO: { report_date, report_type, items: [{name, value, unit, reference_low, reference_high, is_abnormal}] } - Alerts: GET /health/alerts - Alert Rules: GET/POST /health/alert-rules - Critical Value Thresholds: GET /health/critical-value-thresholds - Critical Alerts: GET /health/critical-alerts """ import json import sys import time from datetime import datetime, timezone, date from urllib.request import Request, urlopen from urllib.error import HTTPError BASE_URL = "http://localhost:3000/api/v1" results = [] def log_test(category, test_name, passed, detail="", response_code=None, response_time_ms=None): status = "PASS" if passed else "FAIL" entry = {"category": category, "test_name": test_name, "status": status, "detail": detail} if response_code is not None: entry["http_code"] = response_code if response_time_ms is not None: entry["response_time_ms"] = round(response_time_ms, 1) results.append(entry) icon = "[PASS]" if passed else "[FAIL]" rt = f" ({response_time_ms:.0f}ms)" if response_time_ms else "" code = f" HTTP {response_code}" if response_code else "" print(f" {icon} {test_name}{rt}{code} -- {detail}") return passed def api_call(method, path, data=None, token=None): url = f"{BASE_URL}{path}" headers = {"Content-Type": "application/json"} if token: headers["Authorization"] = f"Bearer {token}" body = json.dumps(data).encode("utf-8") if data else None req = Request(url, data=body, headers=headers, method=method) start = time.time() try: resp = urlopen(req, timeout=30) elapsed = (time.time() - start) * 1000 status_code = resp.status resp_body = json.loads(resp.read().decode("utf-8")) except HTTPError as e: elapsed = (time.time() - start) * 1000 status_code = e.code try: resp_body = json.loads(e.read().decode("utf-8")) except Exception: resp_body = {"error": str(e)} except Exception as e: elapsed = (time.time() - start) * 1000 status_code = 0 resp_body = {"error": str(e)} return status_code, resp_body, elapsed def extract_items(data_node): """Extract items from paginated response (supports both data.data and data.items patterns)""" if isinstance(data_node, list): return data_node items = data_node.get("data", data_node.get("items", [])) return items if isinstance(items, list) else [] def extract_total(data_node): """Extract total from paginated response""" if isinstance(data_node, list): return len(data_node) return data_node.get("total", len(extract_items(data_node))) # ============================================================ # STEP 1: Authentication # ============================================================ print("\n" + "=" * 70) print("STEP 1: Authentication") print("=" * 70) code, resp, rt = api_call("POST", "/auth/login", {"username": "admin", "password": "Admin@2026"}) if code == 200 and resp.get("success") and resp.get("data", {}).get("access_token"): TOKEN = resp["data"]["access_token"] log_test("Auth", "Admin login", True, f"Token acquired ({len(TOKEN)} chars)", code, rt) else: log_test("Auth", "Admin login", False, f"Failed: {json.dumps(resp, ensure_ascii=False)[:200]}", code, rt) sys.exit(1) # ============================================================ # STEP 2: Get Patient ID # ============================================================ print("\n" + "=" * 70) print("STEP 2: Get test Patient ID") print("=" * 70) code, resp, rt = api_call("GET", "/health/patients?page=1&page_size=3", token=TOKEN) if code == 200 and resp.get("success"): items = extract_items(resp["data"]) total = extract_total(resp["data"]) PATIENT_ID = items[0]["id"] if items else None log_test("Patient", "Get patient list", True, f"Total {total}, using ID: {PATIENT_ID}", code, rt) else: log_test("Patient", "Get patient list", False, f"Failed: {json.dumps(resp, ensure_ascii=False)[:200]}", code, rt) PATIENT_ID = None if not PATIENT_ID: print("\nFATAL: No patient ID available, aborting") sys.exit(1) # ============================================================ # STEP 3: Vital Signs Recording # ============================================================ print("\n" + "=" * 70) print("STEP 3: Vital Signs Recording") print("=" * 70) today = date.today().isoformat() vs_path = f"/health/patients/{PATIENT_ID}/vital-signs" # 3.1 Normal BP 120/80 + HR 72 code1, resp1, rt1 = api_call("POST", vs_path, { "record_date": today, "systolic_bp_morning": 120, "diastolic_bp_morning": 80, "heart_rate": 72, "source": "manual" }, token=TOKEN) if code1 in (200, 201): s1 = resp1.get("success", False) log_test("VitalSigns", "Normal BP 120/80 + HR 72", s1, f"success={s1}", code1, rt1) else: log_test("VitalSigns", "Normal BP 120/80 + HR 72", False, f"HTTP {code1}: {json.dumps(resp1, ensure_ascii=False)[:200]}", code1, rt1) time.sleep(0.5) # 3.2 Abnormal BP 200/130 (should trigger alert) code2, resp2, rt2 = api_call("POST", vs_path, { "record_date": today, "systolic_bp_morning": 200, "diastolic_bp_morning": 130, "heart_rate": 85, "source": "manual" }, token=TOKEN) if code2 in (200, 201): s2 = resp2.get("success", False) log_test("VitalSigns", "Abnormal BP 200/130", s2, f"success={s2}", code2, rt2) else: log_test("VitalSigns", "Abnormal BP 200/130", False, f"HTTP {code2}: {json.dumps(resp2, ensure_ascii=False)[:200]}", code2, rt2) time.sleep(0.5) # 3.3 Abnormal HR 150 (should trigger alert) code3, resp3, rt3 = api_call("POST", vs_path, { "record_date": today, "systolic_bp_morning": 125, "diastolic_bp_morning": 82, "heart_rate": 150, "source": "manual" }, token=TOKEN) if code3 in (200, 201): s3 = resp3.get("success", False) log_test("VitalSigns", "Abnormal HR 150", s3, f"success={s3}", code3, rt3) else: log_test("VitalSigns", "Abnormal HR 150", False, f"HTTP {code3}: {json.dumps(resp3, ensure_ascii=False)[:200]}", code3, rt3) time.sleep(0.5) # 3.4 Abnormal blood sugar 20.0 code4, resp4, rt4 = api_call("POST", vs_path, { "record_date": today, "blood_sugar": 20.0, "blood_sugar_type": "fasting", "source": "manual" }, token=TOKEN) if code4 in (200, 201): s4 = resp4.get("success", False) log_test("VitalSigns", "Abnormal blood sugar 20.0", s4, f"success={s4}", code4, rt4) else: log_test("VitalSigns", "Abnormal blood sugar 20.0", False, f"HTTP {code4}: {json.dumps(resp4, ensure_ascii=False)[:200]}", code4, rt4) time.sleep(0.5) # 3.5 Validation -- missing required field (record_date) code5, resp5, rt5 = api_call("POST", vs_path, { "systolic_bp_morning": 120, "heart_rate": 72, }, token=TOKEN) validation_fail = (code5 == 400 or code5 == 422) log_test("VitalSigns", "Missing record_date rejected", validation_fail, f"HTTP {code5}", code5, rt5) time.sleep(0.5) # 3.6 Security -- no token code6, resp6, rt6 = api_call("POST", vs_path, { "record_date": today, "heart_rate": 80, }, token=None) auth_fail = (code6 == 401) log_test("VitalSigns", "No token returns 401", auth_fail, f"HTTP {code6} (expected 401)", code6, rt6) # ============================================================ # STEP 4: Query Vital Signs # ============================================================ print("\n" + "=" * 70) print("STEP 4: Query Vital Signs") print("=" * 70) time.sleep(1) # 4.1 List by patient code_q1, resp_q1, rt_q1 = api_call("GET", f"/health/patients/{PATIENT_ID}/vital-signs", token=TOKEN) if code_q1 == 200 and resp_q1.get("success"): total_vs = extract_total(resp_q1["data"]) items_vs = extract_items(resp_q1["data"]) log_test("VitalSigns Query", "List by patient", True, f"total={total_vs}, returned {len(items_vs)} records", code_q1, rt_q1) else: log_test("VitalSigns Query", "List by patient", False, f"HTTP {code_q1}: {json.dumps(resp_q1, ensure_ascii=False)[:200]}", code_q1, rt_q1) time.sleep(0.5) # 4.2 Trend data (management path) code_q2, resp_q2, rt_q2 = api_call("GET", f"/health/patients/{PATIENT_ID}/trends", token=TOKEN) if code_q2 == 200 and resp_q2.get("success"): trend_data = resp_q2.get("data", {}) log_test("VitalSigns Query", "Trend list", True, f"data type: {type(trend_data).__name__}", code_q2, rt_q2) else: log_test("VitalSigns Query", "Trend list", False, f"HTTP {code_q2}: {json.dumps(resp_q2, ensure_ascii=False)[:200]}", code_q2, rt_q2) time.sleep(0.5) # 4.3 Specific indicator timeseries code_q3, resp_q3, rt_q3 = api_call("GET", f"/health/patients/{PATIENT_ID}/trends/systolic_bp_morning", token=TOKEN) if code_q3 == 200 and resp_q3.get("success"): log_test("VitalSigns Query", "BP timeseries", True, f"success", code_q3, rt_q3) else: log_test("VitalSigns Query", "BP timeseries", False, f"HTTP {code_q3}: {json.dumps(resp_q3, ensure_ascii=False)[:200]}", code_q3, rt_q3) # ============================================================ # STEP 5: Lab Reports # ============================================================ print("\n" + "=" * 70) print("STEP 5: Lab Reports") print("=" * 70) time.sleep(0.5) # 5.1 Create lab report (with abnormal items) lab_path = f"/health/patients/{PATIENT_ID}/lab-reports" code_l1, resp_l1, rt_l1 = api_call("POST", lab_path, { "report_date": today, "report_type": "blood_routine", "items": [ {"name": "WBC", "value": "12.5", "unit": "10^9/L", "reference_low": "4.0", "reference_high": "10.0", "is_abnormal": True}, {"name": "Hemoglobin", "value": "135", "unit": "g/L", "reference_low": "120", "reference_high": "160", "is_abnormal": False} ] }, token=TOKEN) if code_l1 in (200, 201) and resp_l1.get("success"): lab_id = resp_l1.get("data", {}).get("id", "N/A") log_test("LabReport", "Create blood routine (with abnormal)", True, f"id={lab_id}", code_l1, rt_l1) else: log_test("LabReport", "Create blood routine (with abnormal)", False, f"HTTP {code_l1}: {json.dumps(resp_l1, ensure_ascii=False)[:200]}", code_l1, rt_l1) time.sleep(0.5) # 5.2 List lab reports code_l2, resp_l2, rt_l2 = api_call("GET", f"{lab_path}?page=1&page_size=10", token=TOKEN) if code_l2 == 200 and resp_l2.get("success"): total_lab = extract_total(resp_l2["data"]) log_test("LabReport", "List patient lab reports", True, f"total={total_lab}", code_l2, rt_l2) else: log_test("LabReport", "List patient lab reports", False, f"HTTP {code_l2}: {json.dumps(resp_l2, ensure_ascii=False)[:200]}", code_l2, rt_l2) # ============================================================ # STEP 6: Alert Management # ============================================================ print("\n" + "=" * 70) print("STEP 6: Alert Management") print("=" * 70) time.sleep(2) # Wait for alert engine processing # 6.1 Alert list code_a1, resp_a1, rt_a1 = api_call("GET", "/health/alerts?page=1&page_size=20", token=TOKEN) if code_a1 == 200 and resp_a1.get("success"): total_alerts = extract_total(resp_a1["data"]) alert_items = extract_items(resp_a1["data"]) log_test("Alerts", "Alert list query", True, f"total={total_alerts}, returned {len(alert_items)} records", code_a1, rt_a1) if total_alerts > 0: for a in alert_items[:3]: level = a.get("severity", a.get("level", "N/A")) status_a = a.get("status", "N/A") msg = a.get("message", a.get("title", "N/A")) print(f" -> Alert: [{level}] {status_a} -- {str(msg)[:60]}") else: print(" -> Note: No alerts generated. Check alert rules configuration.") else: log_test("Alerts", "Alert list query", False, f"HTTP {code_a1}: {json.dumps(resp_a1, ensure_ascii=False)[:200]}", code_a1, rt_a1) time.sleep(0.5) # 6.2 Alert rules list code_a2, resp_a2, rt_a2 = api_call("GET", "/health/alert-rules?page=1&page_size=20", token=TOKEN) if code_a2 == 200 and resp_a2.get("success"): total_rules = extract_total(resp_a2["data"]) rule_items = extract_items(resp_a2["data"]) log_test("Alerts", "Alert rules list", True, f"total={total_rules}, returned {len(rule_items)} records", code_a2, rt_a2) if total_rules > 0: for r in rule_items[:3]: rname = r.get("name", "N/A") renabled = r.get("is_enabled", r.get("enabled", "N/A")) rtype = r.get("indicator_type", r.get("type", "N/A")) print(f" -> Rule: {rname} | type: {rtype} | enabled: {renabled}") else: log_test("Alerts", "Alert rules list", False, f"HTTP {code_a2}: {json.dumps(resp_a2, ensure_ascii=False)[:200]}", code_a2, rt_a2) time.sleep(0.5) # 6.3 Critical value thresholds (may return list directly) code_a3, resp_a3, rt_a3 = api_call("GET", "/health/critical-value-thresholds?page=1&page_size=20", token=TOKEN) if code_a3 == 200 and resp_a3.get("success"): data_a3 = resp_a3.get("data", {}) if isinstance(data_a3, list): total_thresholds = len(data_a3) threshold_items = data_a3 else: total_thresholds = extract_total(data_a3) threshold_items = extract_items(data_a3) log_test("Alerts", "Critical value thresholds list", True, f"total={total_thresholds}, returned {len(threshold_items)} records", code_a3, rt_a3) if total_thresholds > 0: for t in threshold_items[:3]: tname = t.get("name", t.get("indicator_name", "N/A")) ttype = t.get("indicator_type", "N/A") thigh = t.get("high_threshold", t.get("threshold_high", "N/A")) tlow = t.get("low_threshold", t.get("threshold_low", "N/A")) print(f" -> Threshold: {tname} | type: {ttype} | high: {thigh} | low: {tlow}") else: log_test("Alerts", "Critical value thresholds list", False, f"HTTP {code_a3}: {json.dumps(resp_a3, ensure_ascii=False)[:200]}", code_a3, rt_a3) time.sleep(0.5) # 6.4 Critical alerts list code_a4, resp_a4, rt_a4 = api_call("GET", "/health/critical-alerts?page=1&page_size=20", token=TOKEN) if code_a4 == 200 and resp_a4.get("success"): total_ca = extract_total(resp_a4["data"]) ca_items = extract_items(resp_a4["data"]) log_test("Alerts", "Critical alerts list", True, f"total={total_ca}, returned {len(ca_items)} records", code_a4, rt_a4) else: log_test("Alerts", "Critical alerts list", False, f"HTTP {code_a4}: {json.dumps(resp_a4, ensure_ascii=False)[:200]}", code_a4, rt_a4) time.sleep(0.5) # 6.5 Security -- alerts without auth code_a5, resp_a5, rt_a5 = api_call("GET", "/health/alerts", token=None) auth_fail_alerts = (code_a5 == 401) log_test("Alerts", "No token on alerts returns 401", auth_fail_alerts, f"HTTP {code_a5} (expected 401)", code_a5, rt_a5) # ============================================================ # STEP 7: Performance Statistics # ============================================================ print("\n" + "=" * 70) print("STEP 7: Performance Statistics") print("=" * 70) response_times = [r["response_time_ms"] for r in results if r.get("response_time_ms")] if response_times: avg_rt = sum(response_times) / len(response_times) max_rt = max(response_times) min_rt = min(response_times) under_200ms = sum(1 for v in response_times if v < 200) pct = (under_200ms / len(response_times)) * 100 print(f" Average response time: {avg_rt:.0f}ms") print(f" Max response time: {max_rt:.0f}ms") print(f" Min response time: {min_rt:.0f}ms") print(f" Under 200ms: {pct:.0f}% ({under_200ms}/{len(response_times)})") print(f" SLA (< 200ms 95%): {'PASS' if pct >= 95 else 'FAIL'}") # ============================================================ # FINAL REPORT # ============================================================ print("\n" + "=" * 70) print("FINAL TEST REPORT") print("=" * 70) total_tests = len(results) passed = sum(1 for r in results if r["status"] == "PASS") failed = total_tests - passed pass_rate = (passed / total_tests * 100) if total_tests > 0 else 0 print(f"\n Total tests: {total_tests}") print(f" Passed: {passed}") print(f" Failed: {failed}") print(f" Pass rate: {pass_rate:.1f}%") print("\n Category summary:") categories = {} for r in results: cat = r["category"] if cat not in categories: categories[cat] = {"pass": 0, "fail": 0} if r["status"] == "PASS": categories[cat]["pass"] += 1 else: categories[cat]["fail"] += 1 for cat, counts in categories.items(): t = counts["pass"] + counts["fail"] print(f" {cat}: {counts['pass']}/{t} PASS ({counts['fail']} FAIL)") if failed > 0: print("\n Failed test details:") for r in results: if r["status"] == "FAIL": print(f" [FAIL] {r['category']} / {r['test_name']}") print(f" {r['detail'][:150]}") print(f"\n Overall: {'ALL PASS' if failed == 0 else 'HAS FAILURES'}") print("=" * 70) sys.exit(0 if failed == 0 else 1)