You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

296 lines
13 KiB

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="[email protected]",
password="AdminPassword123!",
fullname="Dr. Mohammad Razavi",
user_type=User.UserType.SUPER_ADMIN,
is_staff=True,
)
self.student_user = User.objects.create_user(
email="[email protected]",
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)