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())