#!/usr/bin/env python3 """ e2e_full_scenario.py — E2E API test driver for every user type. Hits the live Odoo API at http://localhost:8069 with each demo account and exercises the key flows for that role: admin → AI agents config, list agents/tools, run grader, branding teacher → list courses, course plans, create approval-bound exam approver → list pending approval requests, peek into a request student → list assigned exams, fetch exam, list course materials corporate → corporate dashboard / users in entity mastercorporate → master (multi-entity) overview agent → agent surface area developer → developer / superuser-ish API access Exit code 0 when every targeted endpoint either returns 2xx or returns a known graceful 4xx; non-zero if any unexpected failure happens. """ import json import os import sys import time from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen BASE = os.environ.get("BASE", "http://localhost:8069") TIMEOUT = 30 PASS, FAIL, WARN = 0, 0, 0 RESULTS = [] # list of (role, label, ok, detail) def colour(s, code): return f"\033[{code}m{s}\033[0m" GREEN = lambda s: colour(s, 32) RED = lambda s: colour(s, 31) YELL = lambda s: colour(s, 33) DIM = lambda s: colour(s, 90) BOLD = lambda s: colour(s, "1;36") def http(method, path, *, token=None, body=None): url = f"{BASE}{path}" data = json.dumps(body).encode() if body is not None else None req = Request(url, data=data, method=method) req.add_header("Accept", "application/json") if data is not None: req.add_header("Content-Type", "application/json") if token: req.add_header("Authorization", f"Bearer {token}") try: with urlopen(req, timeout=TIMEOUT) as resp: raw = resp.read().decode("utf-8", "replace") return resp.status, _try_json(raw), raw except HTTPError as e: raw = e.read().decode("utf-8", "replace") return e.code, _try_json(raw), raw except URLError as e: return 0, None, str(e) def _try_json(raw): try: return json.loads(raw) except Exception: return None def login(email, password): code, payload, _ = http("POST", "/api/login", body={"login": email, "password": password}) if code == 200 and isinstance(payload, dict) and payload.get("access_token"): return payload["access_token"], payload.get("user", {}) return None, None def record(role, label, ok, detail="", soft=False): global PASS, FAIL, WARN RESULTS.append((role, label, ok, detail, soft)) if ok: PASS += 1 print(f" {GREEN('PASS')} {label} {DIM(detail)}") else: if soft: WARN += 1 print(f" {YELL('SKIP')} {label} {DIM(detail)}") else: FAIL += 1 print(f" {RED('FAIL')} {label} {RED(detail)}") def check(role, label, code, payload, *, ok_codes=(200,), allow_codes=(), key_required=None): """Generic assertion: 2xx is pass, codes in allow_codes are soft-skip.""" ok = code in ok_codes if ok and key_required: if isinstance(payload, dict) and key_required in payload: ok = True else: ok = False detail = f"HTTP {code}" if not ok and isinstance(payload, dict): msg = payload.get("error") or payload.get("message") if msg: detail += f" — {str(msg)[:80]}" soft = code in allow_codes record(role, label, ok, detail, soft=soft) return ok def banner(text): print("\n" + BOLD("━" * 72)) print(BOLD(f" {text}")) print(BOLD("━" * 72)) # ───────────────────────────────────────────────────────────────────────── # Test definitions per role # ───────────────────────────────────────────────────────────────────────── ACCOUNTS = [ ("admin", "admin@encoach.test", "admin123"), ("teacher", "khalid@encoach.test", "teacher123"), ("approver", "approver@encoach.test", "approver123"), ("student", "sarah@encoach.test", "student123"), ("corporate", "corporate@encoach.test", "corporate123"), ("mastercorporate", "master@encoach.test", "master123"), ("agent", "agent@encoach.test", "agent123"), ("developer", "dev@encoach.test", "dev123"), ] def whoami(role, token): code, _, _ = http("GET", "/api/user", token=token) check(role, "GET /api/user (current profile)", code, None, ok_codes=(200,)) def test_admin(token): role = "admin" whoami(role, token) # AI agents config (the new LangGraph surface) code, payload, _ = http("GET", "/api/ai/agents", token=token) check(role, "GET /api/ai/agents (LangGraph agents list)", code, payload, key_required="items") agent_id = None if isinstance(payload, dict): items = payload.get("items") or [] agent_id = next((a["id"] for a in items if a.get("key") == "writing_grader"), None) code, payload, _ = http("GET", "/api/ai/agents/tools", token=token) check(role, "GET /api/ai/agents/tools (tool registry)", code, payload, key_required="items") if agent_id: code, payload, _ = http("GET", f"/api/ai/agents/{agent_id}", token=token) check(role, f"GET /api/ai/agents/{agent_id} (writing_grader detail)", code, payload, key_required="key") body = { "variables": {"language": "en"}, "payload": { "rubric": "Task achievement (0-9), coherence (0-9), lexical (0-9), grammar (0-9).", "task": "Describe a place you visited recently. Min 80 words.", "response": "Last weekend I visited Sharjah Aquarium. The building is modern and clean. There were many fish in big tanks. I went there with my family and we taked photos. The price was good.", }, } t0 = time.time() code, payload, _ = http("POST", f"/api/ai/agents/{agent_id}/test", token=token, body=body) dt = int((time.time() - t0) * 1000) ok = (code == 200 and isinstance(payload, dict) and not payload.get("error") and isinstance(payload.get("output"), dict)) record(role, "POST /api/ai/agents/{id}/test (writing_grader live LangGraph)", ok, detail=f"HTTP {code} in {dt}ms band={payload.get('output',{}).get('overall_band') if isinstance(payload, dict) else '?'}") # Branding admin (per-entity) code, _, _ = http("GET", "/api/entity/1/branding", token=token) check(role, "GET /api/entity/1/branding", code, None, ok_codes=(200, 404), allow_codes=(404,)) # AI prompts library code, _, _ = http("GET", "/api/ai/prompts", token=token) check(role, "GET /api/ai/prompts", code, None, ok_codes=(200,)) # Approval workflows code, payload, _ = http("GET", "/api/approval-workflows", token=token) check(role, "GET /api/approval-workflows", code, payload, ok_codes=(200,)) # Approval users (the picker that lists potential approvers) code, _, _ = http("GET", "/api/approval-users", token=token) check(role, "GET /api/approval-users", code, None, ok_codes=(200,)) # Reports code, _, _ = http("GET", "/api/reports/student-performance", token=token) check(role, "GET /api/reports/student-performance", code, None, ok_codes=(200,)) # Users code, _, _ = http("GET", "/api/users/list", token=token) check(role, "GET /api/users/list", code, None, ok_codes=(200,)) def test_teacher(token): role = "teacher" whoami(role, token) # Course plans (correct path: /api/ai/course-plan) code, payload, _ = http("GET", "/api/ai/course-plan", token=token) plan_id = None if check(role, "GET /api/ai/course-plan (list)", code, payload, ok_codes=(200,)): items = (isinstance(payload, dict) and (payload.get("items") or payload.get("plans"))) or [] plan_id = items[0]["id"] if items else None if plan_id: code, _, _ = http("GET", f"/api/ai/course-plan/{plan_id}", token=token) check(role, f"GET /api/ai/course-plan/{plan_id} (full plan with weeks)", code, None, ok_codes=(200,)) code, _, _ = http("GET", f"/api/ai/course-plan/{plan_id}/weeks/1/materials", token=token) check(role, f"GET /api/ai/course-plan/{plan_id}/weeks/1/materials", code, None, ok_codes=(200,)) # Courses the teacher can see code, _, _ = http("GET", "/api/courses", token=token) check(role, "GET /api/courses", code, None, ok_codes=(200,)) # Exam structures + schedules code, _, _ = http("GET", "/api/exam-structures", token=token) check(role, "GET /api/exam-structures", code, None, ok_codes=(200,)) code, _, _ = http("GET", "/api/exam-schedules", token=token) check(role, "GET /api/exam-schedules", code, None, ok_codes=(200,)) # Approval requests this teacher raised (requester) and pending for them code, _, _ = http("GET", "/api/approval-requests", token=token) check(role, "GET /api/approval-requests", code, None, ok_codes=(200,)) def test_approver(token): role = "approver" whoami(role, token) # Pending approvals (filter handled server-side via current user) code, payload, _ = http("GET", "/api/approval-requests", token=token) check(role, "GET /api/approval-requests", code, payload, ok_codes=(200,)) # Exam-review queue (used by the legacy approver page) code, _, _ = http("GET", "/api/exam/review/queue", token=token) check(role, "GET /api/exam/review/queue", code, None, ok_codes=(200,)) def test_student(token): role = "student" whoami(role, token) # Assigned exams (correct path) code, _, _ = http("GET", "/api/student/my-exams", token=token) check(role, "GET /api/student/my-exams", code, None, ok_codes=(200,)) # Enrolled courses code, _, _ = http("GET", "/api/student/my-courses", token=token) check(role, "GET /api/student/my-courses", code, None, ok_codes=(200,)) # LMS tutor (lms_tutor agent under the hood) code, _, _ = http("POST", "/api/coach/chat", token=token, body={"message": "Give me one B1 example using present continuous."}) check(role, "POST /api/coach/chat (LMS tutor agent)", code, None, ok_codes=(200,)) # Quick tip + writing help calls (also AI-coach surface) code, _, _ = http("GET", "/api/coach/tip", token=token) check(role, "GET /api/coach/tip", code, None, ok_codes=(200,)) def test_corporate(token): role = "corporate" whoami(role, token) # Reports — corporate stats code, _, _ = http("GET", "/api/reports/stats-corporate", token=token) check(role, "GET /api/reports/stats-corporate", code, None, ok_codes=(200,)) # User listing (entity-scoped) code, _, _ = http("GET", "/api/users/list", token=token) check(role, "GET /api/users/list", code, None, ok_codes=(200, 403), allow_codes=(403,)) def test_mastercorporate(token): role = "mastercorporate" whoami(role, token) code, _, _ = http("GET", "/api/reports/stats-corporate", token=token) check(role, "GET /api/reports/stats-corporate (multi-entity)", code, None, ok_codes=(200,)) code, _, _ = http("GET", "/api/users/list", token=token) check(role, "GET /api/users/list", code, None, ok_codes=(200, 403), allow_codes=(403,)) def test_agent(token): role = "agent" whoami(role, token) code, _, _ = http("GET", "/api/courses", token=token) check(role, "GET /api/courses", code, None, ok_codes=(200, 403), allow_codes=(403,)) def test_developer(token): role = "developer" whoami(role, token) code, _, _ = http("GET", "/api/ai/agents", token=token) check(role, "GET /api/ai/agents", code, None, ok_codes=(200,)) code, _, _ = http("GET", "/api/metrics", token=token) check(role, "GET /api/metrics", code, None, ok_codes=(200,)) HANDLERS = { "admin": test_admin, "teacher": test_teacher, "approver": test_approver, "student": test_student, "corporate": test_corporate, "mastercorporate": test_mastercorporate, "agent": test_agent, "developer": test_developer, } def main(): print(BOLD("\nEnCoach — End-to-end role smoke test")) print(DIM(f"Target: {BASE}")) for role, login_email, password in ACCOUNTS: banner(f"{role.upper()} ({login_email})") token, user = login(login_email, password) if not token: record(role, "POST /api/login", False, f"login failed for {login_email}") continue record(role, "POST /api/login", True, f"user_id={user.get('id')} type={user.get('user_type')}") try: HANDLERS[role](token) except Exception as e: record(role, f"{role}-handler raised", False, str(e)[:100]) print("\n" + BOLD("━" * 72)) print(BOLD(f" Summary: {GREEN(str(PASS) + ' PASS')} " f"{RED(str(FAIL) + ' FAIL')} " f"{YELL(str(WARN) + ' SKIP (endpoint absent)')}")) print(BOLD("━" * 72)) # Compact per-role rollup by_role = {} for role, label, ok, detail, soft in RESULTS: agg = by_role.setdefault(role, {"pass": 0, "fail": 0, "skip": 0}) if ok: agg["pass"] += 1 elif soft: agg["skip"] += 1 else: agg["fail"] += 1 print() for role, agg in by_role.items(): line = (f" {role:<18} " f"{GREEN(str(agg['pass']) + ' pass')} " f"{RED(str(agg['fail']) + ' fail')} " f"{YELL(str(agg['skip']) + ' skip')}") print(line) print() sys.exit(0 if FAIL == 0 else 1) if __name__ == "__main__": main()