347 lines
12 KiB
Python
347 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Test all routes in the IRT Bank Soal application.
|
|
Tests each endpoint and checks for Internal Server Errors.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from urllib.parse import urlparse
|
|
|
|
import httpx
|
|
|
|
BASE_URL = "http://localhost:8000"
|
|
|
|
# All routes from OpenAPI spec
|
|
API_ROUTES = [
|
|
# Root endpoints
|
|
("GET", "/"),
|
|
("GET", "/health"),
|
|
# Session endpoints
|
|
("POST", "/api/v1/session/"),
|
|
("GET", "/api/v1/session/{session_id}"),
|
|
("POST", "/api/v1/session/{session_id}/complete"),
|
|
("GET", "/api/v1/session/{session_id}/next_item"),
|
|
("POST", "/api/v1/session/{session_id}/submit_answer"),
|
|
# Tryout endpoints
|
|
("GET", "/api/v1/tryout/"),
|
|
("GET", "/api/v1/tryout/{tryout_id}/config"),
|
|
("PUT", "/api/v1/tryout/{tryout_id}/normalization"),
|
|
("GET", "/api/v1/tryout/{tryout_id}/calibration-status"),
|
|
("POST", "/api/v1/tryout/{tryout_id}/calibrate"),
|
|
("POST", "/api/v1/tryout/{tryout_id}/calibrate/{item_id}"),
|
|
# WordPress endpoints
|
|
("POST", "/api/v1/wordpress/sync_users"),
|
|
("POST", "/api/v1/wordpress/verify_session"),
|
|
("GET", "/api/v1/wordpress/website/{website_id}/users"),
|
|
("GET", "/api/v1/wordpress/website/{website_id}/user/{wp_user_id}"),
|
|
# Reports endpoints
|
|
("POST", "/api/v1/reports/schedule"),
|
|
("GET", "/api/v1/reports/schedule/{schedule_id}"),
|
|
("GET", "/api/v1/reports/schedules"),
|
|
("DELETE", "/api/v1/reports/schedule/{schedule_id}"),
|
|
("POST", "/api/v1/reports/schedule/{schedule_id}/export"),
|
|
("GET", "/api/v1/reports/student/performance"),
|
|
("GET", "/api/v1/reports/student/performance/export/{format}"),
|
|
("GET", "/api/v1/reports/items/analysis"),
|
|
("GET", "/api/v1/reports/items/analysis/export/{format}"),
|
|
("GET", "/api/v1/reports/calibration/status"),
|
|
("GET", "/api/v1/reports/calibration/status/export/{format}"),
|
|
("GET", "/api/v1/reports/tryout/comparison"),
|
|
("GET", "/api/v1/reports/tryout/comparison/export/{format}"),
|
|
("GET", "/api/v1/reports/export/{schedule_id}/{format}"),
|
|
# Import/Export endpoints
|
|
("POST", "/api/v1/import-export/preview"),
|
|
("POST", "/api/v1/import-export/questions"),
|
|
("GET", "/api/v1/import-export/export/questions"),
|
|
("POST", "/api/v1/import-export/tryout-json/preview"),
|
|
("POST", "/api/v1/import-export/tryout-json"),
|
|
# Admin AI endpoints
|
|
("POST", "/api/v1/admin/ai/generate-preview"),
|
|
("POST", "/api/v1/admin/ai/generate-save"),
|
|
("GET", "/api/v1/admin/ai/stats"),
|
|
("GET", "/api/v1/admin/ai/models"),
|
|
# Admin endpoints
|
|
("POST", "/api/v1/admin/{tryout_id}/calibrate"),
|
|
("POST", "/api/v1/admin/{tryout_id}/toggle-ai-generation"),
|
|
("POST", "/api/v1/admin/{tryout_id}/reset-normalization"),
|
|
# Admin CAT endpoints
|
|
("POST", "/api/v1/admin/cat/test"),
|
|
("GET", "/api/v1/admin/session/{session_id}/status"),
|
|
# Admin web routes (HTML pages)
|
|
("GET", "/admin"),
|
|
("GET", "/admin/login"),
|
|
("POST", "/admin/login"),
|
|
("POST", "/admin/logout"),
|
|
("GET", "/admin/password"),
|
|
("POST", "/admin/password"),
|
|
("GET", "/admin/dashboard"),
|
|
("GET", "/admin/questions"),
|
|
("GET", "/admin/questions/{item_id}"),
|
|
("GET", "/admin/questions/{item_id}/quality"),
|
|
("GET", "/admin/exams"),
|
|
("GET", "/admin/exams/{tryout_id}"),
|
|
("GET", "/admin/reports"),
|
|
("GET", "/admin/settings"),
|
|
("GET", "/admin/hierarchy"),
|
|
("GET", "/admin/websites"),
|
|
("POST", "/admin/websites"),
|
|
("GET", "/admin/websites/new"),
|
|
("GET", "/admin/websites/{website_id}"),
|
|
("POST", "/admin/websites/{website_id}"),
|
|
("POST", "/admin/websites/{website_id}/delete"),
|
|
("GET", "/admin/tryout-import"),
|
|
("GET", "/admin/tryout-import/preview"),
|
|
("POST", "/admin/tryout-import"),
|
|
("GET", "/admin/snapshot-questions"),
|
|
("POST", "/admin/snapshot-questions/promote-bulk"),
|
|
("GET", "/admin/calibration-status"),
|
|
("GET", "/admin/item-statistics"),
|
|
("GET", "/admin/sessions"),
|
|
("GET", "/admin/basis-items"),
|
|
("GET", "/admin/basis-items/{item_id}"),
|
|
("POST", "/admin/basis-items/{item_id}/generate"),
|
|
("POST", "/admin/basis-items/{item_id}/generate/review-bulk"),
|
|
("GET", "/admin/basis-items/{item_id}/generate/variants/{variant_id}"),
|
|
]
|
|
|
|
# Placeholder values for path parameters
|
|
PLACEHOLDERS = {
|
|
"{session_id}": "test-session-123",
|
|
"{tryout_id}": "test-tryout-123",
|
|
"{item_id}": "1",
|
|
"{website_id}": "1",
|
|
"{wp_user_id}": "123",
|
|
"{schedule_id}": "test-schedule-123",
|
|
"{format}": "xlsx",
|
|
"{variant_id}": "test-variant-123",
|
|
}
|
|
|
|
# Minimal request bodies for POST endpoints
|
|
REQUEST_BODIES = {
|
|
"/api/v1/session/": {
|
|
"session_id": "test",
|
|
"tryout_id": "test",
|
|
"wp_user_id": "123",
|
|
"website_id": 1,
|
|
"scoring_mode": "ctt",
|
|
},
|
|
"/api/v1/session/{session_id}/complete": {
|
|
"end_time": "2024-01-01T00:00:00Z",
|
|
"user_answers": [],
|
|
},
|
|
"/api/v1/session/{session_id}/submit_answer": {
|
|
"item_id": 1,
|
|
"response": "A",
|
|
"time_spent": 10,
|
|
},
|
|
"/api/v1/tryout/{tryout_id}/normalization": {
|
|
"normalization_mode": "static",
|
|
"static_rataan": 500,
|
|
"static_sb": 100,
|
|
},
|
|
"/api/v1/wordpress/sync_users": {}, # Requires proper auth header
|
|
"/api/v1/wordpress/verify_session": {
|
|
"website_id": 1,
|
|
"wp_user_id": "123",
|
|
"token": "test",
|
|
},
|
|
"/api/v1/reports/schedule": {
|
|
"tryout_id": "test",
|
|
"report_type": "student_performance",
|
|
},
|
|
"/api/v1/admin/ai/generate-preview": {
|
|
"basis_item_id": 1,
|
|
"target_level": "sulit",
|
|
"ai_model": "qwen/qwen2.5-32b-instruct",
|
|
},
|
|
"/api/v1/admin/ai/generate-save": {
|
|
"stem": "Test?",
|
|
"options": {"A": "a", "B": "b", "C": "c", "D": "d"},
|
|
"correct": "A",
|
|
"tryout_id": "test",
|
|
"website_id": 1,
|
|
"basis_item_id": 1,
|
|
"slot": 1,
|
|
"level": "sulit",
|
|
"ai_model": "qwen/qwen2.5-32b-instruct",
|
|
},
|
|
"/api/v1/admin/cat/test": {"tryout_id": "test", "website_id": 1},
|
|
"/api/v1/admin/{tryout_id}/calibrate": {},
|
|
"/api/v1/admin/{tryout_id}/toggle-ai-generation": {},
|
|
"/api/v1/admin/{tryout_id}/reset-normalization": {},
|
|
"/api/v1/import-export/preview": None, # Requires file upload
|
|
"/api/v1/import-export/questions": None, # Requires file upload
|
|
"/api/v1/import-export/tryout-json/preview": None, # Requires file upload
|
|
"/api/v1/import-export/tryout-json": None, # Requires file upload
|
|
}
|
|
|
|
|
|
def expand_route(method: str, route: str) -> list:
|
|
"""Expand route with placeholders."""
|
|
expanded = []
|
|
test_route = route
|
|
for placeholder, value in PLACEHOLDERS.items():
|
|
if placeholder in test_route:
|
|
test_route = test_route.replace(placeholder, value)
|
|
expanded.append((method, test_route))
|
|
return expanded
|
|
|
|
|
|
def test_route(client: httpx.Client, method: str, route: str) -> dict:
|
|
"""Test a single route."""
|
|
# Expand placeholders
|
|
expanded = expand_route(method, route)
|
|
if not expanded:
|
|
return {
|
|
"route": route,
|
|
"method": method,
|
|
"error": "Could not expand route",
|
|
"status_code": None,
|
|
}
|
|
|
|
method, test_route = expanded[0]
|
|
|
|
# Determine request body
|
|
body = None
|
|
request_body = REQUEST_BODIES.get(route, REQUEST_BODIES.get(test_route, {}))
|
|
if request_body is not None:
|
|
body = request_body
|
|
|
|
# Determine query params
|
|
params = {}
|
|
if "export/questions" in route:
|
|
params = {"tryout_id": "test"}
|
|
|
|
headers = {"X-Website-ID": "1"}
|
|
|
|
try:
|
|
response = client.request(
|
|
method=method,
|
|
url=BASE_URL + test_route,
|
|
json=body if body and method in ["POST", "PUT", "PATCH"] else None,
|
|
params=params,
|
|
headers=headers,
|
|
timeout=10.0,
|
|
follow_redirects=True,
|
|
)
|
|
|
|
is_500 = response.status_code == 500
|
|
is_ise = "Internal Server Error" in response.text
|
|
|
|
return {
|
|
"route": route,
|
|
"method": method,
|
|
"expanded_route": test_route,
|
|
"status_code": response.status_code,
|
|
"has_500": is_500,
|
|
"has_ise": is_ise,
|
|
"response_preview": response.text[:200] if response.text else "",
|
|
"error": None,
|
|
}
|
|
except httpx.TimeoutException:
|
|
return {
|
|
"route": route,
|
|
"method": method,
|
|
"expanded_route": test_route,
|
|
"status_code": None,
|
|
"has_500": False,
|
|
"has_ise": False,
|
|
"response_preview": "",
|
|
"error": "Timeout",
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"route": route,
|
|
"method": method,
|
|
"expanded_route": test_route,
|
|
"status_code": None,
|
|
"has_500": False,
|
|
"has_ise": False,
|
|
"response_preview": "",
|
|
"error": str(e),
|
|
}
|
|
|
|
|
|
def main():
|
|
print("=" * 80)
|
|
print("Testing all IRT Bank Soal routes for Internal Server Errors")
|
|
print("=" * 80)
|
|
print()
|
|
|
|
results = []
|
|
has_errors = False
|
|
|
|
with httpx.Client(timeout=30.0) as client:
|
|
for method, route in API_ROUTES:
|
|
result = test_route(client, method, route)
|
|
results.append(result)
|
|
|
|
status = result["status_code"]
|
|
error_marker = ""
|
|
|
|
if result["error"]:
|
|
error_marker = f" [ERROR: {result['error']}]"
|
|
has_errors = True
|
|
elif status and status >= 500:
|
|
error_marker = f" [INTERNAL SERVER ERROR!]"
|
|
has_errors = True
|
|
elif status and status == 500:
|
|
error_marker = f" [500 - INTERNAL SERVER ERROR!]"
|
|
has_errors = True
|
|
elif "Internal Server Error" in str(result.get("response_preview", "")):
|
|
error_marker = " [500 - INTERNAL SERVER ERROR!]"
|
|
has_errors = True
|
|
|
|
status_str = str(status) if status else "N/A"
|
|
print(f"{method:6} {route:<60} -> {status_str}{error_marker}")
|
|
|
|
print()
|
|
print("=" * 80)
|
|
print("SUMMARY")
|
|
print("=" * 80)
|
|
|
|
total = len(results)
|
|
successful = sum(1 for r in results if r["status_code"] and r["status_code"] < 500)
|
|
client_errors = sum(
|
|
1 for r in results if r["status_code"] and 400 <= r["status_code"] < 500
|
|
)
|
|
server_errors = sum(
|
|
1 for r in results if r["status_code"] and r["status_code"] >= 500
|
|
)
|
|
timeouts = sum(1 for r in results if r["error"] == "Timeout")
|
|
exceptions = sum(1 for r in results if r["error"] and r["error"] != "Timeout")
|
|
ise_errors = sum(1 for r in results if r.get("has_ise") or r.get("has_500"))
|
|
|
|
print(f"Total routes tested: {total}")
|
|
print(f"Successful (2xx): {successful}")
|
|
print(f"Client errors (4xx): {client_errors}")
|
|
print(f"Server errors (5xx): {server_errors}")
|
|
print(f"Timeouts: {timeouts}")
|
|
print(f"Exceptions: {exceptions}")
|
|
print(f"Internal Server Errors: {ise_errors}")
|
|
print()
|
|
|
|
if has_errors:
|
|
print("Routes with issues:")
|
|
for r in results:
|
|
if r["status_code"] and r["status_code"] >= 500:
|
|
print(f" - {r['method']} {r['route']} -> {r['status_code']}")
|
|
elif r["error"]:
|
|
print(f" - {r['method']} {r['route']} -> ERROR: {r['error']}")
|
|
elif r.get("has_ise"):
|
|
print(f" - {r['method']} {r['route']} -> Internal Server Error")
|
|
|
|
print()
|
|
if ise_errors == 0 and exceptions == 0:
|
|
print("✅ All routes passed! No Internal Server Errors detected.")
|
|
return 0
|
|
else:
|
|
print("❌ Some routes have issues. Please review the output above.")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|