test: update E2E test suite for v6 API + JWT auth
- Login first to acquire JWT token for all protected routes - Use correct v6 API paths (/api/presets/, /api/schedules/, /api/scripts/) - Add tests for: scripts CRUD, schedules toggle, settings, audit, sync browse - Remove obsolete password-presets and retry-jobs paths Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+96
-41
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Nexus API End-to-End Tests
|
||||
Usage: python tests/test_api.py
|
||||
Prerequisites: Python backend must be running (python -m server.main)
|
||||
Prerequisites: Python backend must be running (python -m uvicorn server.main:app)
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
@@ -9,20 +9,32 @@ import urllib.request
|
||||
import urllib.error
|
||||
|
||||
BASE = "http://127.0.0.1:8600"
|
||||
API_KEY = ""
|
||||
PASS = 0
|
||||
FAIL = 0
|
||||
|
||||
# ── Auth (login first to get JWT) ──
|
||||
ACCESS_TOKEN = ""
|
||||
REFRESH_TOKEN = ""
|
||||
|
||||
def test(name: str, method: str, path: str, body=None, expect_code=200):
|
||||
"""执行一个 API 测试并报告结果。返回响应 body(含可能的新建 ID)。"""
|
||||
|
||||
def _get_auth_headers():
|
||||
return {"Authorization": f"Bearer {ACCESS_TOKEN}"} if ACCESS_TOKEN else {}
|
||||
|
||||
|
||||
def 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")
|
||||
if API_KEY:
|
||||
req.add_header("X-API-Key", API_KEY)
|
||||
# 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()
|
||||
@@ -38,7 +50,11 @@ def test(name: str, method: str, path: str, body=None, expect_code=200):
|
||||
code = e.code
|
||||
if code == expect_code:
|
||||
PASS += 1
|
||||
print(f" [PASS] {name} (HTTP {code} as expected)")
|
||||
result_text = e.read().decode()[:200]
|
||||
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]}")
|
||||
@@ -48,27 +64,36 @@ def test(name: str, method: str, path: str, body=None, expect_code=200):
|
||||
return None
|
||||
|
||||
|
||||
# ================================================================
|
||||
# Test Suite
|
||||
# ================================================================
|
||||
print("=" * 50)
|
||||
print("Nexus API End-to-End Tests")
|
||||
print(f" Target: {BASE}")
|
||||
print("=" * 50)
|
||||
|
||||
# --- Health Endpoints (no API_KEY needed) ---
|
||||
print("\n[1] Health Endpoints")
|
||||
test("GET /", "GET", "/")
|
||||
# --- Health (no auth) ---
|
||||
print("\n[1] Health")
|
||||
test("GET /health", "GET", "/health")
|
||||
test("GET /health/services", "GET", "/health/services")
|
||||
|
||||
# --- Auth: Login ---
|
||||
print("\n[2] Auth")
|
||||
login_result = test("POST /api/auth/login", "POST", "/api/auth/login", body={
|
||||
"username": "admin",
|
||||
"password": "admin",
|
||||
}, 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")
|
||||
|
||||
test("GET /api/auth/me", "GET", "/api/auth/me")
|
||||
|
||||
# --- Server CRUD ---
|
||||
print("\n[2] Server CRUD")
|
||||
|
||||
# 创建测试服务器(记录 ID 以便后续操作和清理)
|
||||
print("\n[3] Server CRUD")
|
||||
created = test("POST /api/servers/ (create)", "POST", "/api/servers/", body={
|
||||
"name": "test-server-e2e",
|
||||
"domain": "127.0.0.1",
|
||||
"domain": "192.168.1.100",
|
||||
"port": 22,
|
||||
"username": "root",
|
||||
"auth_method": "password",
|
||||
@@ -78,43 +103,73 @@ created = test("POST /api/servers/ (create)", "POST", "/api/servers/", body={
|
||||
})
|
||||
server_id = created.get("id") if created and isinstance(created, dict) else 1
|
||||
|
||||
# 列表
|
||||
test("GET /api/servers/ (list)", "GET", "/api/servers/")
|
||||
|
||||
# 获取刚创建的
|
||||
test("GET /api/servers/stats", "GET", "/api/servers/stats")
|
||||
test(f"GET /api/servers/{server_id}", "GET", f"/api/servers/{server_id}")
|
||||
|
||||
# 更新
|
||||
test(f"PUT /api/servers/{server_id} (update)", "PUT", f"/api/servers/{server_id}", body={
|
||||
"description": "E2E test server",
|
||||
})
|
||||
|
||||
# 清理:删除测试服务器
|
||||
test(f"DELETE /api/servers/{server_id}", "DELETE", f"/api/servers/{server_id}")
|
||||
|
||||
# --- Logs ---
|
||||
print("\n[3] Push Logs")
|
||||
test("GET /api/servers/logs", "GET", "/api/servers/logs")
|
||||
test("GET /api/servers/logs?limit=5", "GET", "/api/servers/logs?limit=5")
|
||||
# --- Scripts ---
|
||||
print("\n[4] Scripts")
|
||||
script = test("POST /api/scripts/ (create)", "POST", "/api/scripts/", body={
|
||||
"name": "test-script",
|
||||
"category": "ops",
|
||||
"content": "echo hello",
|
||||
})
|
||||
script_id = script.get("id") if script and isinstance(script, dict) else 1
|
||||
test("GET /api/scripts/ (list)", "GET", "/api/scripts/")
|
||||
test(f"DELETE /api/scripts/{script_id}", "DELETE", f"/api/scripts/{script_id}")
|
||||
|
||||
# --- Password Presets ---
|
||||
print("\n[4] Password Presets")
|
||||
test("GET /api/servers/password-presets (list)", "GET", "/api/servers/password-presets")
|
||||
preset = test("POST /api/servers/password-presets (create)", "POST", "/api/servers/password-presets", body={
|
||||
# --- Schedules ---
|
||||
print("\n[5] Schedules")
|
||||
sched = test("POST /api/schedules/ (create)", "POST", "/api/schedules/", body={
|
||||
"name": "test-schedule",
|
||||
"source_path": "/www/wwwroot/",
|
||||
"cron_expr": "0 2 * * *",
|
||||
"server_ids": "[]",
|
||||
"enabled": True,
|
||||
})
|
||||
sched_id = sched.get("id") if sched and isinstance(sched, dict) else 1
|
||||
test("GET /api/schedules/ (list)", "GET", "/api/schedules/")
|
||||
test(f"PUT /api/schedules/{sched_id} (disable)", "PUT", f"/api/schedules/{sched_id}", body={
|
||||
"enabled": False,
|
||||
})
|
||||
test(f"DELETE /api/schedules/{sched_id}", "DELETE", f"/api/schedules/{sched_id}")
|
||||
|
||||
# --- Presets ---
|
||||
print("\n[6] Password Presets")
|
||||
preset = test("POST /api/presets/ (create)", "POST", "/api/presets/", body={
|
||||
"name": "test-preset",
|
||||
"password": "test-e2e-123",
|
||||
"encrypted_pw": "test-password-123",
|
||||
})
|
||||
preset_id = preset.get("id") if preset and isinstance(preset, dict) else 1
|
||||
test(f"POST /api/servers/password-presets/{preset_id}/reveal", "POST", f"/api/servers/password-presets/{preset_id}/reveal")
|
||||
# 清理:删除测试预设
|
||||
test(f"DELETE /api/servers/password-presets/{preset_id}", "DELETE", f"/api/servers/password-presets/{preset_id}")
|
||||
test("GET /api/presets/ (list)", "GET", "/api/presets/")
|
||||
test(f"DELETE /api/presets/{preset_id}", "DELETE", f"/api/presets/{preset_id}")
|
||||
|
||||
# --- Retry Queue ---
|
||||
print("\n[5] Retry Queue")
|
||||
test("GET /api/servers/retry-jobs", "GET", "/api/servers/retry-jobs")
|
||||
# --- Settings ---
|
||||
print("\n[7] Settings")
|
||||
test("GET /api/settings/ (list)", "GET", "/api/settings/")
|
||||
test("PUT /api/settings/system_name", "PUT", "/api/settings/system_name", body={
|
||||
"value": "Nexus",
|
||||
})
|
||||
|
||||
# --- Audit ---
|
||||
print("\n[8] Audit")
|
||||
test("GET /api/audit/ (list)", "GET", "/api/audit/?limit=10")
|
||||
|
||||
# --- Retries ---
|
||||
print("\n[9] Retry Queue")
|
||||
test("GET /api/retries/ (list)", "GET", "/api/retries/")
|
||||
|
||||
# --- Sync ---
|
||||
print("\n[10] Sync")
|
||||
test("POST /api/sync/browse (no server)", "POST", "/api/sync/browse", body={
|
||||
"server_id": 99999,
|
||||
"path": "/tmp",
|
||||
}, expect_code=404)
|
||||
|
||||
# ================================================================
|
||||
# Report
|
||||
# ================================================================
|
||||
total = PASS + FAIL
|
||||
print(f"\n{'='*50}")
|
||||
|
||||
Reference in New Issue
Block a user