Files
yellow-bank-soal/backend/test_all_routes.py
2026-06-20 01:43:39 +07:00

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