from django.test import TestCase from django.contrib.auth import get_user_model from rest_framework.test import APIClient from rest_framework import status from apps.profiles.models import Institution from apps.lms.models import ( Course, CourseModule, Lesson, QuizQuestion, CourseEnrollment, LessonProgress, DigitalCertificate, CourseCategory, CourseLevel, CourseLanguage, ) User = get_user_model() class Phase8LMSAPITests(TestCase): """ Automated test suite for Phase 8: - Course Catalog & Multi-Facet Filtering - Course Creation & Detail Retrieval (with syllabus) - Course Enrollment Flow - Lesson Details & Content - Lesson Completion & Progress Recalculation - Interactive Quiz Submission & Grading - Automatic Certificate Generation on 100% Course Completion - Public QR-Code Certificate Verification - User Certificates Listing """ def setUp(self): self.client = APIClient() # Users self.admin_user = User.objects.create_user( email="lms.admin@razaviacademy.org", password="AdminPassword123!", fullname="Dr. Mohammad Razavi", user_type=User.UserType.SUPER_ADMIN, is_staff=True, ) self.student_user = User.objects.create_user( email="student@shiahub.org", password="StudentPassword123!", fullname="Ali Rezaei", user_type=User.UserType.STUDENT, ) # Partner Institution self.institution = Institution.objects.create( name="Razavi International Islamic Academy", country="Iran", city="Mashhad", type=Institution.InstitutionType.INSTITUTE, ) # Create Course self.course = Course.objects.create( title="Strategic Cultural Diplomacy in the Muslim World", category=CourseCategory.CULTURAL_DIPLOMACY, language=CourseLanguage.EN, level=CourseLevel.INTERMEDIATE, instructor_name="Prof. Seyed Hassan", instructor_title="Senior Scholar in International Relations", institution=self.institution, organization_name="Razavi Academy", description="Comprehensive curriculum on international communication, interfaith diplomacy, and center leadership.", duration_hours=12.5, rating=4.9, reviews_count=24, is_published=True, is_featured=True, created_by=self.admin_user, ) # Module 1 self.module1 = CourseModule.objects.create( course=self.course, title="Foundations of Cultural Dialogue", description="Historical and theological principles of dialogue.", order=0, ) # Lesson 1 (Reading) self.lesson1 = Lesson.objects.create( module=self.module1, title="Historical Overview of Razavi Discourse", lesson_type=Lesson.LessonType.READING, duration_minutes=25, content="# Razavi Discourse\n\nPrinciples of rational dialogue with scholars of other traditions.", order=0, is_preview=True, ) # Lesson 2 (Quiz) self.lesson2 = Lesson.objects.create( module=self.module1, title="Module 1 Knowledge Assessment", lesson_type=Lesson.LessonType.QUIZ, duration_minutes=15, order=1, is_preview=False, ) # Quiz Questions for Lesson 2 self.q1 = QuizQuestion.objects.create( lesson=self.lesson2, question_text="What was a core characteristic of Imam Reza's (AS) debates in Merv?", options=[ "Use of shared textual references and rational evidence", "Dismissal of opposing perspectives without debate", "Limiting discussions exclusively to one language", "Avoiding dialogue with other faith traditions" ], correct_option_index=0, explanation="The Imam engaged scholars using their own scriptures and universal rational arguments.", order=0, ) self.q2 = QuizQuestion.objects.create( lesson=self.lesson2, question_text="In modern cultural diplomacy, what is primary objective of inter-center networking?", options=[ "Encouraging bilateral isolationism", "Collaborative synergy, knowledge sharing, and mutual support", "Eliminating local community autonomy", "Replacing all regional cultural expressions" ], correct_option_index=1, explanation="Synergy and cooperation empower global communities while preserving local identity.", order=1, ) def test_course_list_and_filters(self): # 1. General list response = self.client.get("/api/v1/lms/courses/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 1) self.assertEqual(response.data[0]["title"], self.course.title) self.assertEqual(response.data[0]["total_lessons"], 2) # 2. Filter by category res_cat = self.client.get(f"/api/v1/lms/courses/?category={CourseCategory.CULTURAL_DIPLOMACY}") self.assertEqual(res_cat.status_code, status.HTTP_200_OK) self.assertEqual(len(res_cat.data), 1) # 3. Filter by non-matching category res_empty = self.client.get(f"/api/v1/lms/courses/?category={CourseCategory.SPECIALIZED_LANGUAGES}") self.assertEqual(res_empty.status_code, status.HTTP_200_OK) self.assertEqual(len(res_empty.data), 0) # 4. Search query res_search = self.client.get("/api/v1/lms/courses/?search=Diplomacy") self.assertEqual(res_search.status_code, status.HTTP_200_OK) self.assertEqual(len(res_search.data), 1) def test_course_create_authorized(self): self.client.force_authenticate(user=self.admin_user) payload = { "title": "Modern Mosque & Center Administration", "category": "center_management", "language": "en", "level": "introductory", "instructor_name": "Dr. Ali Reza", "description": "Organizational governance, financial sustainability, and youth engagement programs.", "duration_hours": 8.0, "organization_name": "Razavi Academy", "is_published": True, "is_featured": False, } response = self.client.post("/api/v1/lms/courses/", payload, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data["title"], "Modern Mosque & Center Administration") self.assertTrue(Course.objects.filter(title="Modern Mosque & Center Administration").exists()) def test_course_detail_retrieval(self): response = self.client.get(f"/api/v1/lms/courses/{self.course.slug}/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["id"], self.course.id) self.assertEqual(len(response.data["modules"]), 1) self.assertEqual(response.data["modules"][0]["title"], "Foundations of Cultural Dialogue") self.assertEqual(len(response.data["modules"][0]["lessons"]), 2) self.assertFalse(response.data["is_enrolled"]) def test_course_enrollment_flow(self): self.client.force_authenticate(user=self.student_user) # 1. Enroll response = self.client.post(f"/api/v1/lms/courses/{self.course.id}/enroll/") self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data["course_id"], self.course.id) self.assertEqual(response.data["progress_percentage"], 0) self.assertTrue(CourseEnrollment.objects.filter(user=self.student_user, course=self.course).exists()) # 2. Duplicate enroll returns 200 response_dup = self.client.post(f"/api/v1/lms/courses/{self.course.id}/enroll/") self.assertEqual(response_dup.status_code, status.HTTP_200_OK) # 3. Check course detail shows is_enrolled=True detail_res = self.client.get(f"/api/v1/lms/courses/{self.course.id}/") self.assertEqual(detail_res.status_code, status.HTTP_200_OK) self.assertTrue(detail_res.data["is_enrolled"]) def test_lesson_detail_view(self): # Unauthenticated student view of preview lesson response = self.client.get(f"/api/v1/lms/lessons/{self.lesson1.id}/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["title"], self.lesson1.title) self.assertIn("# Razavi Discourse", response.data["content"]) # Quiz lesson view res_quiz = self.client.get(f"/api/v1/lms/lessons/{self.lesson2.id}/") self.assertEqual(res_quiz.status_code, status.HTTP_200_OK) self.assertEqual(len(res_quiz.data["quiz_questions"]), 2) # Student view shouldn't leak correct_option_index self.assertNotIn("correct_option_index", res_quiz.data["quiz_questions"][0]) def test_lesson_completion_and_progress_recalculation(self): self.client.force_authenticate(user=self.student_user) # Complete Lesson 1 response = self.client.post(f"/api/v1/lms/lessons/{self.lesson1.id}/complete/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue(response.data["is_completed"]) # Course has 2 lessons -> 1 completed = 50% self.assertEqual(response.data["course_progress_percentage"], 50) # Verify progress endpoint prog_res = self.client.get(f"/api/v1/lms/courses/{self.course.id}/progress/") self.assertEqual(prog_res.status_code, status.HTTP_200_OK) self.assertEqual(prog_res.data["progress_percentage"], 50) self.assertEqual(prog_res.data["completed_lessons_count"], 1) self.assertEqual(prog_res.data["total_lessons_count"], 2) self.assertFalse(prog_res.data["is_completed"]) def test_quiz_submission_scoring_and_certificate_issuance(self): self.client.force_authenticate(user=self.student_user) # 1. Complete Lesson 1 first (50%) self.client.post(f"/api/v1/lms/lessons/{self.lesson1.id}/complete/") # 2. Submit FAILING quiz (1 wrong, 1 correct -> 50% < 70%) fail_payload = { "answers": [ {"question_id": self.q1.id, "selected_option": 0}, # Correct {"question_id": self.q2.id, "selected_option": 0}, # Wrong ] } fail_res = self.client.post(f"/api/v1/lms/lessons/{self.lesson2.id}/submit-quiz/", fail_payload, format="json") self.assertEqual(fail_res.status_code, status.HTTP_200_OK) self.assertEqual(fail_res.data["score"], 50.0) self.assertFalse(fail_res.data["passed"]) self.assertFalse(fail_res.data["is_lesson_completed"]) self.assertEqual(fail_res.data["course_progress_percentage"], 50) # 3. Submit PASSING quiz (2 correct -> 100% >= 70%) pass_payload = { "answers": [ {"question_id": self.q1.id, "selected_option": 0}, # Correct {"question_id": self.q2.id, "selected_option": 1}, # Correct ] } pass_res = self.client.post(f"/api/v1/lms/lessons/{self.lesson2.id}/submit-quiz/", pass_payload, format="json") self.assertEqual(pass_res.status_code, status.HTTP_200_OK) self.assertEqual(pass_res.data["score"], 100.0) self.assertTrue(pass_res.data["passed"]) self.assertTrue(pass_res.data["is_lesson_completed"]) # All 2 lessons completed -> 100% course progress self.assertEqual(pass_res.data["course_progress_percentage"], 100) self.assertIsNotNone(pass_res.data["certificate_code"]) cert_code = pass_res.data["certificate_code"] self.assertTrue(cert_code.startswith("CERT-RAZAVI-")) # 4. Check DigitalCertificate model persisted cert = DigitalCertificate.objects.get(certificate_code=cert_code) self.assertEqual(cert.user, self.student_user) self.assertEqual(cert.course, self.course) self.assertEqual(cert.final_score, 100.0) # 5. Verify via Public Verification Endpoint (No auth required) anon_client = APIClient() verify_res = anon_client.get(f"/api/v1/lms/certificates/{cert_code}/") self.assertEqual(verify_res.status_code, status.HTTP_200_OK) self.assertTrue(verify_res.data["is_valid"]) self.assertEqual(verify_res.data["recipient_name"], "Ali Rezaei") self.assertEqual(verify_res.data["course_title"], self.course.title) self.assertEqual(verify_res.data["final_score"], 100.0) # 6. Verify via User Certificates Endpoint my_certs_res = self.client.get("/api/v1/lms/my-certificates/") self.assertEqual(my_certs_res.status_code, status.HTTP_200_OK) self.assertEqual(len(my_certs_res.data), 1) self.assertEqual(my_certs_res.data[0]["certificate_code"], cert_code)