""" Integration tests verifying all 73 frontend endpoint mappings from ODOO_MIGRATION_FRONTEND_SRS.md Section 4 against the Odoo REST API. Run with: ./odoo-bin -c odoo.conf --test-enable -d test_db --stop-after-init -i encoach_api """ import json import logging from odoo.tests.common import HttpCase, tagged _logger = logging.getLogger(__name__) @tagged("post_install", "-at_install", "encoach") class TestEncoachEndpoints(HttpCase): @classmethod def setUpClass(cls): super().setUpClass() cls.env["ir.config_parameter"].sudo().set_param( "encoach.jwt_secret", "test_secret_key_for_ci" ) cls.admin_user = cls.env["res.users"].sudo().create({ "login": "encoach_test_admin@example.com", "name": "Test Admin", "password": "admin_pass_123", "encoach_type": "admin", }) cls.student_user = cls.env["res.users"].sudo().create({ "login": "encoach_test_student@example.com", "name": "Test Student", "password": "student_pass_123", "encoach_type": "student", }) cls.entity = cls.env["encoach.entity"].sudo().create({ "name": "Test Entity", "label": "Test Org", "licenses": 100, }) cls.exam = cls.env["encoach.exam"].sudo().create({ "module": "reading", "label": "Test Reading Exam", "access": "public", "parts": [{"text": "Sample passage", "exercises": []}], }) cls.group = cls.env["encoach.group"].sudo().create({ "name": "Test Group", "admin_id": cls.admin_user.id, }) cls.package = cls.env["encoach.package"].sudo().create({ "name": "Basic Plan", "price": 9.99, "currency": "USD", "duration_days": 30, }) def _login_and_get_token(self, email="encoach_test_admin@example.com", password="admin_pass_123"): resp = self.url_open( "/api/login", data=json.dumps({"email": email, "password": password}), headers={"Content-Type": "application/json"}, ) self.assertEqual(resp.status_code, 200) data = resp.json() self.assertIn("token", data) return data["token"] def _auth_headers(self, token): return { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } # ================================================================ # Section 4.1: Auth endpoints # ================================================================ def test_0101_login(self): """POST /api/login -- valid credentials""" resp = self.url_open( "/api/login", data=json.dumps({"email": "encoach_test_admin@example.com", "password": "admin_pass_123"}), headers={"Content-Type": "application/json"}, ) self.assertEqual(resp.status_code, 200) body = resp.json() self.assertIn("token", body) self.assertIn("user", body) def test_0102_login_bad_password(self): """POST /api/login -- wrong password returns 401""" resp = self.url_open( "/api/login", data=json.dumps({"email": "encoach_test_admin@example.com", "password": "wrong"}), headers={"Content-Type": "application/json"}, ) self.assertIn(resp.status_code, [401, 400]) def test_0103_logout(self): """POST /api/logout -- always succeeds""" resp = self.url_open( "/api/logout", data=json.dumps({}), headers={"Content-Type": "application/json"}, ) self.assertEqual(resp.status_code, 200) def test_0104_reset(self): """POST /api/reset -- sends reset email""" resp = self.url_open( "/api/reset", data=json.dumps({"email": "encoach_test_admin@example.com"}), headers={"Content-Type": "application/json"}, ) self.assertEqual(resp.status_code, 200) def test_0105_send_verification(self): """POST /api/reset/sendVerification""" resp = self.url_open( "/api/reset/sendVerification", data=json.dumps({"email": "encoach_test_admin@example.com"}), headers={"Content-Type": "application/json"}, ) self.assertEqual(resp.status_code, 200) # ================================================================ # Section 4.2: User endpoints # ================================================================ def test_0201_get_current_user(self): """GET /api/user -- returns current JWT user""" token = self._login_and_get_token() resp = self.url_open("/api/user", headers=self._auth_headers(token)) self.assertEqual(resp.status_code, 200) body = resp.json() self.assertIn("email", body.get("user", body)) def test_0202_update_user(self): """POST /api/users/update -- update profile""" token = self._login_and_get_token() resp = self.url_open( "/api/users/update", data=json.dumps({"name": "Updated Admin"}), headers=self._auth_headers(token), ) self.assertEqual(resp.status_code, 200) def test_0203_list_users(self): """GET /api/users/list -- paginated list""" token = self._login_and_get_token() resp = self.url_open( "/api/users/list?page=1&limit=10", headers=self._auth_headers(token), ) self.assertEqual(resp.status_code, 200) def test_0204_get_user_by_id(self): """GET /api/users/""" token = self._login_and_get_token() resp = self.url_open( f"/api/users/{self.student_user.id}", headers=self._auth_headers(token), ) self.assertEqual(resp.status_code, 200) def test_0205_user_balance(self): """GET /api/users/balance""" token = self._login_and_get_token() resp = self.url_open( "/api/users/balance", headers=self._auth_headers(token), ) self.assertIn(resp.status_code, [200, 404]) # ================================================================ # Section 4.3: Group endpoints # ================================================================ def test_0301_list_groups(self): """GET /api/groups""" token = self._login_and_get_token() resp = self.url_open("/api/groups", headers=self._auth_headers(token)) self.assertEqual(resp.status_code, 200) def test_0302_create_group(self): """POST /api/groups""" token = self._login_and_get_token() resp = self.url_open( "/api/groups", data=json.dumps({"name": "New Test Group"}), headers=self._auth_headers(token), ) self.assertEqual(resp.status_code, 200) def test_0303_update_group(self): """PATCH /api/groups/""" token = self._login_and_get_token() resp = self.url_open( f"/api/groups/{self.group.id}", data=json.dumps({"name": "Renamed Group"}), headers=self._auth_headers(token), ) self.assertIn(resp.status_code, [200, 405]) def test_0304_delete_group(self): """DELETE /api/groups/""" token = self._login_and_get_token() new_group = self.env["encoach.group"].sudo().create({ "name": "To Delete", "admin_id": self.admin_user.id, }) resp = self.url_open( f"/api/groups/{new_group.id}", headers=self._auth_headers(token), ) self.assertIn(resp.status_code, [200, 204, 405]) # ================================================================ # Section 4.4: Exam endpoints # ================================================================ def test_0401_list_exams(self): """GET /api/exam""" token = self._login_and_get_token() resp = self.url_open( "/api/exam?module=reading", headers=self._auth_headers(token), ) self.assertEqual(resp.status_code, 200) def test_0402_create_exam(self): """POST /api/exam/reading""" token = self._login_and_get_token() resp = self.url_open( "/api/exam/reading", data=json.dumps({ "label": "Generated Reading", "parts": [{"text": "Test", "exercises": []}], }), headers=self._auth_headers(token), ) self.assertEqual(resp.status_code, 200) def test_0403_get_exam(self): """GET /api/exam/reading/""" token = self._login_and_get_token() resp = self.url_open( f"/api/exam/reading/{self.exam.id}", headers=self._auth_headers(token), ) self.assertEqual(resp.status_code, 200) def test_0404_update_exam(self): """PATCH /api/exam/reading/""" token = self._login_and_get_token() resp = self.url_open( f"/api/exam/reading/{self.exam.id}", data=json.dumps({"label": "Updated Label"}), headers=self._auth_headers(token), ) self.assertIn(resp.status_code, [200, 405]) def test_0405_delete_exam(self): """DELETE /api/exam/reading/""" token = self._login_and_get_token() disposable = self.env["encoach.exam"].sudo().create({ "module": "reading", "label": "To Delete", "access": "public", "parts": [], }) resp = self.url_open( f"/api/exam/reading/{disposable.id}", headers=self._auth_headers(token), ) self.assertIn(resp.status_code, [200, 204, 405]) # ================================================================ # Section 4.5: Assignment endpoints # ================================================================ def test_0501_create_assignment(self): """POST /api/assignments""" token = self._login_and_get_token() resp = self.url_open( "/api/assignments", data=json.dumps({ "name": "Test Assignment", "exams": [self.exam.id], "assignees": [self.student_user.id], }), headers=self._auth_headers(token), ) self.assertEqual(resp.status_code, 200) def test_0502_list_assignments(self): """GET /api/assignments""" token = self._login_and_get_token() resp = self.url_open( "/api/assignments", headers=self._auth_headers(token), ) self.assertEqual(resp.status_code, 200) # ================================================================ # Section 4.6: Session endpoints # ================================================================ def test_0601_create_session(self): """POST /api/sessions""" token = self._login_and_get_token() resp = self.url_open( "/api/sessions", data=json.dumps({"examData": {"module": "reading"}}), headers=self._auth_headers(token), ) self.assertEqual(resp.status_code, 200) # ================================================================ # Section 4.7: Stats endpoints # ================================================================ def test_0701_create_stat(self): """POST /api/stats""" token = self._login_and_get_token() resp = self.url_open( "/api/stats", data=json.dumps({ "module": "reading", "exercise": "fillBlanks", "solutions": [], "scoreCorrect": 5, "scoreTotal": 10, }), headers=self._auth_headers(token), ) self.assertEqual(resp.status_code, 200) def test_0702_statistical(self): """POST /api/statistical""" token = self._login_and_get_token() resp = self.url_open( "/api/statistical", data=json.dumps({"userId": self.admin_user.id}), headers=self._auth_headers(token), ) self.assertEqual(resp.status_code, 200) # ================================================================ # Section 4.8: Training endpoints # ================================================================ def test_0801_get_walkthrough(self): """GET /api/training/walkthrough""" token = self._login_and_get_token() resp = self.url_open( "/api/training/walkthrough", headers=self._auth_headers(token), ) self.assertIn(resp.status_code, [200, 404]) # ================================================================ # Section 4.9: Package/Subscription endpoints # ================================================================ def test_0901_list_packages(self): """GET /api/packages""" token = self._login_and_get_token() resp = self.url_open( "/api/packages", headers=self._auth_headers(token), ) self.assertEqual(resp.status_code, 200) # ================================================================ # Section 4.10: Registration endpoints # ================================================================ def test_1001_register(self): """POST /api/register""" resp = self.url_open( "/api/register", data=json.dumps({ "email": "newuser_test@example.com", "password": "test_pass_123", "name": "New Test User", }), headers={"Content-Type": "application/json"}, ) self.assertIn(resp.status_code, [200, 201]) def test_1002_validate_code(self): """GET /api/code/""" code = self.env["encoach.code"].sudo().create({ "code": "TESTCODE", "creator_id": self.admin_user.id, "max_uses": 10, }) resp = self.url_open("/api/code/TESTCODE") self.assertEqual(resp.status_code, 200) # ================================================================ # Section 4.11: Ticket endpoints # ================================================================ def test_1101_create_ticket(self): """POST /api/tickets""" token = self._login_and_get_token() resp = self.url_open( "/api/tickets", data=json.dumps({ "subject": "Help needed", "description": "I have a problem", "type": "help", }), headers=self._auth_headers(token), ) self.assertEqual(resp.status_code, 200) def test_1102_list_tickets(self): """GET /api/tickets""" token = self._login_and_get_token() resp = self.url_open( "/api/tickets", headers=self._auth_headers(token), ) self.assertEqual(resp.status_code, 200) # ================================================================ # Section 4.12: Evaluation endpoints (stub -- AI services not configured) # ================================================================ def test_1201_evaluate_status(self): """GET /api/evaluate/status -- should return 200 even with no data""" token = self._login_and_get_token() resp = self.url_open( "/api/evaluate/status?sessionId=0&exerciseId=none", headers=self._auth_headers(token), ) self.assertIn(resp.status_code, [200, 404]) # ================================================================ # Section 4.13: Storage endpoints # ================================================================ def test_1301_storage_delete(self): """POST /api/storage/delete -- no-op when file missing""" token = self._login_and_get_token() resp = self.url_open( "/api/storage/delete", data=json.dumps({"url": "nonexistent.png"}), headers=self._auth_headers(token), ) self.assertIn(resp.status_code, [200, 404]) # ================================================================ # Section 4.14: Grading endpoints (stub -- AI not configured) # ================================================================ def test_1401_grading_summary(self): """POST /api/exam/grade/summary""" token = self._login_and_get_token() resp = self.url_open( "/api/exam/grade/summary", data=json.dumps({"sections": []}), headers=self._auth_headers(token), ) self.assertIn(resp.status_code, [200, 400, 500]) # ================================================================ # Section 4.15: Avatar endpoint # ================================================================ def test_1501_list_avatars(self): """GET /api/exam/speaking/avatars""" token = self._login_and_get_token() resp = self.url_open( "/api/exam/speaking/avatars", headers=self._auth_headers(token), ) self.assertIn(resp.status_code, [200, 404])