""" Nexus API End-to-End Tests Usage: python tests/test_api.py Prerequisites: Python backend must be running (python -m uvicorn server.main:app) Env: NEXUS_TEST_BASE — default http://127.0.0.1:8600 NEXUS_TEST_ADMIN_USER / NEXUS_TEST_ADMIN_PASSWORD — login credentials """ import json import os import sys import urllib.request import urllib.error BASE = os.environ.get("NEXUS_TEST_BASE", "http://127.0.0.1:8600") ADMIN_USER = os.environ.get("NEXUS_TEST_ADMIN_USER", "admin") ADMIN_PASSWORD = os.environ.get("NEXUS_TEST_ADMIN_PASSWORD", "admin") def _load_credentials_from_env_file(): """Try to load test credentials from .env file (production deploy scenario). The gate check runs test_api.py automatically on the deploy server. Instead of hardcoding production passwords, read from .env variables: NEXUS_TEST_ADMIN_USER / NEXUS_TEST_ADMIN_PASSWORD These can be set in .env by the admin during installation. """ env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env") if not os.path.exists(env_path): return try: with open(env_path) as f: for line in f: line = line.strip() if line.startswith("#") or "=" not in line: continue key, _, value = line.partition("=") key = key.strip() value = value.strip().strip("\"'") if key == "NEXUS_TEST_ADMIN_USER" and not os.environ.get("NEXUS_TEST_ADMIN_USER"): os.environ["NEXUS_TEST_ADMIN_USER"] = value elif key == "NEXUS_TEST_ADMIN_PASSWORD" and not os.environ.get("NEXUS_TEST_ADMIN_PASSWORD"): os.environ["NEXUS_TEST_ADMIN_PASSWORD"] = value except Exception: pass # Load from .env before reading env vars _load_credentials_from_env_file() BASE = os.environ.get("NEXUS_TEST_BASE", "http://127.0.0.1:8600") ADMIN_USER = os.environ.get("NEXUS_TEST_ADMIN_USER", "admin") ADMIN_PASSWORD = os.environ.get("NEXUS_TEST_ADMIN_PASSWORD", "admin") PASS = 0 FAIL = 0 # ── Auth (login first to get JWT) ── ACCESS_TOKEN = "" REFRESH_TOKEN = "" def _get_auth_headers(): return {"Authorization": f"Bearer {ACCESS_TOKEN}"} if ACCESS_TOKEN else {} def api_test(name: str, method: str, path: str, body=None, expect_code=200, headers=None): """Execute an API test and report results.""" global PASS, FAIL url = f"{BASE}{path}" data = json.dumps(body).encode() if body else None req = urllib.request.Request(url, data=data, method=method) req.add_header("Content-Type", "application/json") # JWT auth for k, v in _get_auth_headers().items(): req.add_header(k, v) # Extra headers if headers: for k, v in headers.items(): req.add_header(k, v) try: resp = urllib.request.urlopen(req, timeout=10) code = resp.getcode() resp_body = resp.read() # Handle 204 No Content and empty responses if not resp_body or resp_body.strip() == b"": if code == expect_code: PASS += 1 print(f" [PASS] {name}") return {} else: FAIL += 1 print(f" [FAIL] {name}: expected {expect_code}, got {code}") return None result = json.loads(resp_body) if code == expect_code: PASS += 1 print(f" [PASS] {name}") return result else: FAIL += 1 print(f" [FAIL] {name}: expected {expect_code}, got {code}") except urllib.error.HTTPError as e: code = e.code if code == expect_code: PASS += 1 result_text = e.read().decode()[:200] if not result_text or result_text.strip() == "": print(f" [PASS] {name}") return {} try: return json.loads(result_text) except Exception: return {"detail": result_text} else: FAIL += 1 print(f" [FAIL] {name}: expected {expect_code}, got {code} — {e.read().decode()[:200]}") except Exception as e: FAIL += 1 print(f" [FAIL] {name}: {e}") return None def api_test_plain_text(name: str, method: str, path: str, expect_body: str, expect_code=200): """Health and other probes that return plain text, not JSON.""" global PASS, FAIL url = f"{BASE}{path}" req = urllib.request.Request(url, method=method) try: resp = urllib.request.urlopen(req, timeout=10) code = resp.getcode() body = resp.read().decode().strip() if code == expect_code and body == expect_body: PASS += 1 print(f" [PASS] {name}") return body FAIL += 1 print(f" [FAIL] {name}: expected {expect_code} body={expect_body!r}, got {code} body={body!r}") except urllib.error.HTTPError as e: FAIL += 1 print(f" [FAIL] {name}: HTTP {e.code} — {e.read().decode()[:200]}") except Exception as e: FAIL += 1 print(f" [FAIL] {name}: {e}") return None def run_tests() -> int: """Run all API e2e tests. Returns exit code.""" global PASS, FAIL, ACCESS_TOKEN, REFRESH_TOKEN PASS = 0 FAIL = 0 ACCESS_TOKEN = "" REFRESH_TOKEN = "" print("=" * 50) print("Nexus API End-to-End Tests") print(f" Target: {BASE}") print("=" * 50) # --- Health (no auth) — plain text per health.py (Shell guardian) --- print("\n[1] Health") api_test_plain_text("GET /health", "GET", "/health", expect_body="ok") print("\n[2] Auth") login_result = api_test("POST /api/auth/login", "POST", "/api/auth/login", body={ "username": ADMIN_USER, "password": ADMIN_PASSWORD, }, expect_code=200) if login_result and login_result.get("access_token"): ACCESS_TOKEN = login_result["access_token"] REFRESH_TOKEN = login_result.get("refresh_token", "") print(f" → JWT token acquired (expires_in={login_result.get('expires_in')}s)") else: print(" → WARNING: Login failed, subsequent tests will fail without JWT") api_test("GET /api/auth/me", "GET", "/api/auth/me") print("\n[3] Server CRUD") existing_servers = api_test("GET /api/servers/ (pre-cleanup)", "GET", "/api/servers/?search=test-server-e2e", expect_code=200) if existing_servers and isinstance(existing_servers, dict): for srv in existing_servers.get("items", existing_servers.get("servers", [])): if isinstance(srv, dict) and srv.get("name") == "test-server-e2e": api_test("DELETE leftover test server", "DELETE", f"/api/servers/{srv['id']}", expect_code=200) break created = api_test("POST /api/servers/ (create)", "POST", "/api/servers/", body={ "name": "test-server-e2e", "domain": "192.168.1.100", "port": 22, "username": "root", "auth_method": "password", "password": "test123", "target_path": "/tmp/nexus-test", "category": "test", }, expect_code=201) server_id = created.get("id") if created and isinstance(created, dict) else None api_test("GET /api/servers/ (list)", "GET", "/api/servers/") api_test("GET /api/servers/stats", "GET", "/api/servers/stats") if server_id: api_test(f"GET /api/servers/{server_id}", "GET", f"/api/servers/{server_id}") api_test(f"PUT /api/servers/{server_id} (update)", "PUT", f"/api/servers/{server_id}", body={ "description": "E2E test server", }) api_test(f"DELETE /api/servers/{server_id}", "DELETE", f"/api/servers/{server_id}", expect_code=204) else: print(" → SKIP: Server CRUD detail tests (no server_id from create)") print("\n[4] Scripts") script = api_test("POST /api/scripts/ (create)", "POST", "/api/scripts/", body={ "name": "test-script", "category": "ops", "content": "echo hello", }, expect_code=201) script_id = script.get("id") if script and isinstance(script, dict) else None api_test("GET /api/scripts/ (list)", "GET", "/api/scripts/") if script_id: api_test(f"DELETE /api/scripts/{script_id}", "DELETE", f"/api/scripts/{script_id}", expect_code=204) print("\n[5] Schedules") sched = api_test("POST /api/schedules/ (create)", "POST", "/api/schedules/", body={ "name": "test-schedule", "source_path": "/tmp/nexus-test", "run_mode": "cron", "cron_expr": "0 2 * * *", "server_ids": "all", "enabled": True, }, expect_code=201) sched_id = sched.get("id") if sched and isinstance(sched, dict) else None api_test("GET /api/schedules/ (list)", "GET", "/api/schedules/") if sched_id: api_test(f"PUT /api/schedules/{sched_id} (disable)", "PUT", f"/api/schedules/{sched_id}", body={ "enabled": False, }) api_test(f"DELETE /api/schedules/{sched_id}", "DELETE", f"/api/schedules/{sched_id}", expect_code=204) print("\n[6] Password Presets") preset = api_test("POST /api/presets/ (create)", "POST", "/api/presets/", body={ "name": "test-preset", "encrypted_pw": "test-password-123", }, expect_code=201) preset_id = preset.get("id") if preset and isinstance(preset, dict) else None api_test("GET /api/presets/ (list)", "GET", "/api/presets/") if preset_id: api_test(f"DELETE /api/presets/{preset_id}", "DELETE", f"/api/presets/{preset_id}", expect_code=204) print("\n[7] Settings") api_test("GET /api/settings/ (list)", "GET", "/api/settings/") api_test("PUT /api/settings/system_name", "PUT", "/api/settings/system_name", body={ "value": "Nexus", }) print("\n[8] Audit") api_test("GET /api/audit/ (list)", "GET", "/api/audit/?limit=10") print("\n[9] Retry Queue") api_test("GET /api/retries/ (list)", "GET", "/api/retries/") print("\n[10] Sync") api_test("POST /api/sync/browse (no server)", "POST", "/api/sync/browse", body={ "server_id": 99999, "path": "/tmp", }, expect_code=404) total = PASS + FAIL print(f"\n{'='*50}") print(f"Results: {PASS}/{total} passed, {FAIL}/{total} failed") if FAIL == 0: print("All tests passed!") else: print(f"{FAIL} api_test(s) failed") return 0 if FAIL == 0 else 1 if __name__ == "__main__": _load_credentials_from_env_file() sys.exit(run_tests())