Spaces:
Running
Running
File size: 8,096 Bytes
9eebce3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | #!/usr/bin/env python3
"""
inspect_har_endpoints.py
Runs extract_openapi_spec() on every HAR file in hars/ and prints a full
summary of discovered endpoints β method, path, status code, auth, and a
snippet of the request/response body where available.
Usage:
python scripts/inspect_har_endpoints.py [--json]
Flags:
--json Emit machine-readable JSON instead of the human-readable table
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
# ---------------------------------------------------------------------------
# Path setup β make the package importable without installing
# ---------------------------------------------------------------------------
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT))
from server.tools.browser_agent import extract_openapi_spec # noqa: E402
# ---------------------------------------------------------------------------
# HAR files to inspect
# ---------------------------------------------------------------------------
HARS_DIR = REPO_ROOT / "hars"
HAR_FILES = {
"shopping": HARS_DIR / "shopping.har",
"shopping_admin": HARS_DIR / "shopping_admin.har",
"forum": HARS_DIR / "forum.har",
"wikipedia": HARS_DIR / "wikipedia.har",
}
# Fake base URLs β only used for pass-through in extract_openapi_spec
APP_BASE_URLS = {
"shopping": "http://localhost:7770",
"shopping_admin": "http://localhost:7780",
"forum": "http://localhost:9999",
"wikipedia": "http://localhost:8888",
}
# ---------------------------------------------------------------------------
# Pretty-print helpers
# ---------------------------------------------------------------------------
_COL_W = 80
def _hr(char: str = "β") -> None:
print(char * _COL_W)
def _body_snippet(value) -> str | None:
if value is None:
return None
if isinstance(value, str):
snippet = value[:120]
else:
snippet = json.dumps(value)[:120]
return snippet + ("β¦" if len(str(snippet)) >= 120 else "")
def _print_entry(idx: int, entry: dict) -> None:
auth_flag = "π AUTH" if entry["auth_observed"] else "open"
print(f" [{idx:>3}] {entry['method']:<7} {entry['path']}")
print(f" status={entry['status_code']} ct={entry['response_content_type'] or 'β'} {auth_flag}")
if entry.get("query_params"):
print(f" query: {entry['query_params'][:100]}")
req_snippet = _body_snippet(entry.get("request_body"))
if req_snippet:
print(f" req_body: {req_snippet}")
resp_snippet = _body_snippet(entry.get("response_body_sample"))
if resp_snippet:
print(f" resp_sample: {resp_snippet}")
def _method_counts(entries: list[dict]) -> dict[str, int]:
counts: dict[str, int] = {}
for e in entries:
counts[e["method"]] = counts.get(e["method"], 0) + 1
return dict(sorted(counts.items()))
def print_app_summary(app_name: str, entries: list[dict], raw_total: int | None = None) -> None:
_hr("β")
header = f" APP: {app_name.upper()} ({len(entries)} unique API endpoints"
if raw_total is not None:
header += f" extracted from {raw_total} raw HAR entries"
header += ")"
print(header)
counts = _method_counts(entries)
print(f" Methods: {counts}")
auth_count = sum(1 for e in entries if e["auth_observed"])
print(f" Auth-required endpoints: {auth_count}/{len(entries)}")
_hr()
if not entries:
print(" (no API-like entries survived filtering)")
for i, entry in enumerate(entries, 1):
_print_entry(i, entry)
print()
# ---------------------------------------------------------------------------
# JSON mode
# ---------------------------------------------------------------------------
def emit_json(results: dict) -> None:
# Convert to a JSON-safe structure
output = {}
for app_name, entries in results.items():
output[app_name] = {
"total": len(entries),
"method_counts": _method_counts(entries),
"endpoints": entries,
}
print(json.dumps(output, indent=2))
# ---------------------------------------------------------------------------
# Verification / assertion checks
# ---------------------------------------------------------------------------
# NOTE: These HAR files are sparse β each was recorded for a narrow task
# scenario, not as a full API crawl. The vast majority of HAR entries are
# static assets (/static/ prefix) that the extractor correctly filters out.
# Thresholds below reflect the actual usable API surface in each file.
SANITY_CHECKS: dict[str, dict] = {
"shopping": {
"min_endpoints": 1,
"expected_methods": {"GET"},
"note": "Sparse HAR β only checkout success page recorded; "
"213 total entries but 212 are /static/ assets.",
},
"shopping_admin": {
"min_endpoints": 2,
"expected_methods": {"GET", "POST"},
"note": "Sparse HAR β product save/edit + MUI JSON endpoint; "
"353 total entries but 350 are /static/ assets.",
},
"forum": {
"min_endpoints": 2,
"expected_methods": {"GET", "POST"},
"note": "Sparse HAR β one POST submission + one forum thread GET; "
"24 total entries but 22 are .js build files.",
},
"wikipedia": {
"min_endpoints": 0,
"expected_methods": set(),
"note": "Sparse HAR β only an article HTML page + /-/mw/ style/JS assets; "
"no XHR/REST traffic recorded.",
},
}
def run_checks(results: dict) -> bool:
print("\n" + "β" * _COL_W)
print("SANITY CHECKS (thresholds calibrated to actual HAR content)")
print("β" * _COL_W)
all_passed = True
for app_name, checks in SANITY_CHECKS.items():
entries = results.get(app_name, [])
methods_found = set(e["method"] for e in entries)
n = len(entries)
min_ok = n >= checks["min_endpoints"]
exp = checks["expected_methods"]
methods_ok = exp.issubset(methods_found) if exp else True
status = "PASS" if (min_ok and methods_ok) else "FAIL"
if status == "FAIL":
all_passed = False
print(f" {status} {app_name}")
print(f" endpoints : {n} (min={checks['min_endpoints']}) {'β' if min_ok else 'β'}")
if exp:
print(f" methods : {sorted(methods_found)} "
f"(expected β {sorted(exp)}) {'β' if methods_ok else 'β'}")
print(f" note : {checks['note']}")
print("β" * _COL_W)
print("Overall:", "ALL PASSED β" if all_passed else "SOME FAILED β")
return all_passed
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> int:
emit_json_mode = "--json" in sys.argv
results: dict[str, list[dict]] = {}
raw_totals: dict[str, int] = {}
missing: list[str] = []
for app_name, har_path in HAR_FILES.items():
if not har_path.exists():
print(f"[WARN] HAR not found: {har_path}", file=sys.stderr)
missing.append(app_name)
results[app_name] = []
raw_totals[app_name] = 0
continue
with open(har_path) as f:
har_data = json.load(f)
raw_totals[app_name] = len(har_data.get("log", {}).get("entries", []))
entries = extract_openapi_spec(har_data, APP_BASE_URLS[app_name])
results[app_name] = entries
if emit_json_mode:
emit_json(results)
return 0
# Human-readable output
for app_name, entries in results.items():
print_app_summary(app_name, entries, raw_totals.get(app_name))
passed = run_checks(results)
if missing:
print(f"\n[WARN] Missing HAR files for: {', '.join(missing)}")
return 0 if passed else 1
if __name__ == "__main__":
sys.exit(main())
|