""" Nexus full-site API coverage (read-mostly + isolated CRUD with cleanup). Runs after test_api.py patterns; safe on production when CRUD uses e2e-* names only. Usage: python tests/test_full_site_api.py Env: same as tests/test_api.py (loads .env NEXUS_TEST_*) """ from __future__ import annotations import json import os import sys import urllib.error import urllib.request sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import test_api as base # noqa: E402 base._load_credentials_from_env_file() def _headers(): h = {"Content-Type": "application/json"} if base.ACCESS_TOKEN: h["Authorization"] = f"Bearer {base.ACCESS_TOKEN}" return h PASS = 0 FAIL = 0 def call(name: str, method: str, path: str, body=None, expect=200): global PASS, FAIL url = f"{base.BASE}{path}" data = json.dumps(body).encode() if body is not None else None req = urllib.request.Request(url, data=data, method=method, headers=_headers()) try: with urllib.request.urlopen(req, timeout=30) as resp: code = resp.getcode() raw = resp.read() if code == expect: PASS += 1 print(f" [PASS] {name}") if not raw: return {} return json.loads(raw) FAIL += 1 print(f" [FAIL] {name}: expected {expect}, got {code}") except urllib.error.HTTPError as e: if e.code == expect: PASS += 1 print(f" [PASS] {name}") body_b = e.read() return json.loads(body_b) if body_b else {} FAIL += 1 print(f" [FAIL] {name}: HTTP {e.code} — {e.read().decode()[:180]}") except Exception as ex: FAIL += 1 print(f" [FAIL] {name}: {ex}") return None def main() -> int: global PASS, FAIL print("=" * 50) print("Nexus Full-Site API Tests") print(f" Target: {base.BASE}") print("=" * 50) base._load_credentials_from_env_file() base.api_test_plain_text("GET /health", "GET", "/health", expect_body="ok") login = base.api_test("POST /api/auth/login", "POST", "/api/auth/login", body={ "username": base.ADMIN_USER, "password": base.ADMIN_PASSWORD, }, expect_code=200) if not login or not login.get("access_token"): print("FATAL: login failed") return 1 base.ACCESS_TOKEN = login["access_token"] base.REFRESH_TOKEN = login.get("refresh_token", "") print("\n[Search & Alerts]") call("GET /api/search/", "GET", "/api/search/?q=test") call("GET /api/alert-history/", "GET", "/api/alert-history/?limit=5") call("GET /api/alert-history/stats", "GET", "/api/alert-history/stats") print("\n[Assets]") call("GET /api/assets/platforms", "GET", "/api/assets/platforms") call("GET /api/assets/nodes", "GET", "/api/assets/nodes") call("GET /api/assets/nodes/tree", "GET", "/api/assets/nodes/tree") call("GET /api/assets/ssh-sessions", "GET", "/api/assets/ssh-sessions?limit=5") call("GET /api/assets/command-logs", "GET", "/api/assets/command-logs?limit=5") plat = call("POST /api/assets/platforms (e2e)", "POST", "/api/assets/platforms", body={ "name": "e2e-platform-full", "category": "test", "type": "linux", }, expect=201) plat_id = plat.get("id") if plat else None if plat_id: call(f"PUT /api/assets/platforms/{plat_id}", "PUT", f"/api/assets/platforms/{plat_id}", body={ "description": "updated", }) call(f"DELETE /api/assets/platforms/{plat_id}", "DELETE", f"/api/assets/platforms/{plat_id}", expect=204) print("\n[Servers meta]") call("GET /api/servers/categories", "GET", "/api/servers/categories") call("GET /api/servers/logs", "GET", "/api/servers/logs?per_page=5") call("GET /api/servers/meta/api_base_url", "GET", "/api/servers/meta/api_base_url") servers = call("GET /api/servers/", "GET", "/api/servers/?per_page=5") first_id = None if servers: items = servers.get("items") or servers.get("servers") or [] if items and isinstance(items[0], dict): first_id = items[0].get("id") if first_id: call(f"GET /api/servers/{first_id}/logs", "GET", f"/api/servers/{first_id}/logs?limit=3") print("\n[Scripts & credentials]") call("GET /api/scripts/exec-config", "GET", "/api/scripts/exec-config") call("GET /api/scripts/executions", "GET", "/api/scripts/executions?limit=5") call("GET /api/scripts/credentials", "GET", "/api/scripts/credentials") call("GET /api/ssh-key-presets/", "GET", "/api/ssh-key-presets/") print("\n[Sync read-only / safe]") call("POST /api/sync/validate-source-path", "POST", "/api/sync/validate-source-path", body={ "path": "/tmp", }) if first_id: call("POST /api/sync/browse (real server)", "POST", "/api/sync/browse", body={ "server_id": first_id, "path": "/tmp", }) call("POST /api/auth/webssh-token", "POST", "/api/auth/webssh-token", body={ "server_id": first_id, }) print("\n[Settings & health detail]") call("GET /api/settings/", "GET", "/api/settings/") call("GET /api/settings/ip-allowlist", "GET", "/api/settings/ip-allowlist") call("GET /api/settings/bing-wallpapers", "GET", "/api/settings/bing-wallpapers") call("GET /health/detail", "GET", "/health/detail") print("\n[Terminal]") call("GET /api/terminal/quick-commands", "GET", "/api/terminal/quick-commands") print("\n[Auth refresh]") if base.REFRESH_TOKEN: call("POST /api/auth/refresh", "POST", "/api/auth/refresh", body={ "refresh_token": base.REFRESH_TOKEN, }) print(f"\n{'=' * 50}") print(f"Full-site API: {PASS}/{PASS + FAIL} passed, {FAIL} failed") return 0 if FAIL == 0 else 1 if __name__ == "__main__": sys.exit(main())