#!/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())