Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 247 additions & 1 deletion lms/djangoapps/instructor/tests/test_api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
from rest_framework.test import APIClient, APITestCase

from common.djangoapps.course_modes.tests.factories import CourseModeFactory
from common.djangoapps.student.models.course_enrollment import CourseEnrollment
from common.djangoapps.student.models import ManualEnrollmentAudit
from common.djangoapps.student.models.course_enrollment import CourseEnrollment, CourseEnrollmentAllowed
from common.djangoapps.student.roles import CourseBetaTesterRole, CourseDataResearcherRole, CourseInstructorRole
from common.djangoapps.student.tests.factories import (
AdminFactory,
Expand Down Expand Up @@ -2049,3 +2050,248 @@ def test_filter_beta_testers_with_search(self):
data = response.data
self.assertEqual(data['count'], 1)
self.assertTrue(data['results'][0]['is_beta_tester'])


class EnrollmentStatusViewTest(SharedModuleStoreTestCase):
"""Tests for the EnrollmentStatusView v2 GET endpoint."""

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.course = CourseFactory.create()

def setUp(self):
super().setUp()
self.client = APIClient()
self.instructor = InstructorFactory(course_key=self.course.id)
self.url = reverse(
'instructor_api_v2:enrollment_status',
kwargs={'course_id': str(self.course.id)}
)

def test_unauthenticated_returns_401(self):
response = self.client.get(self.url, {'email_or_username': 'test'})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_student_returns_403(self):
student = UserFactory()
self.client.force_authenticate(user=student)
response = self.client.get(self.url, {'email_or_username': 'test'})
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_missing_identifier_returns_400(self):
self.client.force_authenticate(user=self.instructor)
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_empty_identifier_returns_400(self):
self.client.force_authenticate(user=self.instructor)
response = self.client.get(self.url, {'email_or_username': ''})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_active_enrollment(self):
learner = UserFactory()
CourseEnrollmentFactory(user=learner, course_id=self.course.id, is_active=True)
self.client.force_authenticate(user=self.instructor)
response = self.client.get(self.url, {'email_or_username': learner.email})
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.data
self.assertEqual(data['enrollment_status'], 'active')
self.assertIsNotNone(data['mode'])
self.assertEqual(data['course_id'], str(self.course.id))
self.assertEqual(data['email_or_username'], learner.email)
self.assertIn('active', data['message'])

def test_inactive_enrollment(self):
learner = UserFactory()
CourseEnrollmentFactory(user=learner, course_id=self.course.id, is_active=False)
self.client.force_authenticate(user=self.instructor)
response = self.client.get(self.url, {'email_or_username': learner.username})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['enrollment_status'], 'inactive')

def test_pending_enrollment(self):
email = 'pending_learner@example.com'
CourseEnrollmentAllowed.objects.create(
email=email,
course_id=self.course.id,
)
self.client.force_authenticate(user=self.instructor)
response = self.client.get(self.url, {'email_or_username': email})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['enrollment_status'], 'pending')
self.assertIsNone(response.data['mode'])

def test_never_enrolled_existing_user(self):
learner = UserFactory()
self.client.force_authenticate(user=self.instructor)
response = self.client.get(self.url, {'email_or_username': learner.username})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['enrollment_status'], 'never_enrolled')
self.assertIsNone(response.data['mode'])

def test_never_enrolled_nonexistent_user(self):
self.client.force_authenticate(user=self.instructor)
response = self.client.get(self.url, {'email_or_username': 'nobody@example.com'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['enrollment_status'], 'never_enrolled')

def test_lookup_by_username(self):
learner = UserFactory()
CourseEnrollmentFactory(user=learner, course_id=self.course.id, is_active=True)
self.client.force_authenticate(user=self.instructor)
response = self.client.get(self.url, {'email_or_username': learner.username})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['enrollment_status'], 'active')

def test_staff_can_access(self):
staff = StaffFactory(course_key=self.course.id)
learner = UserFactory()
CourseEnrollmentFactory(user=learner, course_id=self.course.id, is_active=True)
self.client.force_authenticate(user=staff)
response = self.client.get(self.url, {'email_or_username': learner.email})
self.assertEqual(response.status_code, status.HTTP_200_OK)


class ChangeEnrollmentModeViewTest(SharedModuleStoreTestCase):
"""Tests for the ChangeEnrollmentModeView v2 POST endpoint."""

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.course = CourseFactory.create()

def setUp(self):
super().setUp()
self.client = APIClient()
self.instructor = InstructorFactory(course_key=self.course.id)
self.url = reverse(
'instructor_api_v2:change_enrollment_mode',
kwargs={'course_id': str(self.course.id)}
)
# Ensure audit and verified modes exist
CourseModeFactory(course_id=self.course.id, mode_slug='audit')
CourseModeFactory(course_id=self.course.id, mode_slug='verified')

def test_unauthenticated_returns_401(self):
response = self.client.post(self.url, {'email_or_username': 'x', 'mode': 'verified'})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_student_returns_403(self):
student = UserFactory()
self.client.force_authenticate(user=student)
response = self.client.post(self.url, {'email_or_username': 'x', 'mode': 'verified'})
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_missing_fields_returns_400(self):
self.client.force_authenticate(user=self.instructor)
response = self.client.post(self.url, {})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_nonexistent_user_returns_404(self):
self.client.force_authenticate(user=self.instructor)
response = self.client.post(self.url, {
'email_or_username': 'nobody@example.com',
'mode': 'verified',
})
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

def test_user_not_enrolled_returns_404(self):
learner = UserFactory()
self.client.force_authenticate(user=self.instructor)
response = self.client.post(self.url, {
'email_or_username': learner.email,
'mode': 'verified',
})
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

def test_inactive_enrollment_returns_404(self):
learner = UserFactory()
CourseEnrollmentFactory(user=learner, course_id=self.course.id, is_active=False)
self.client.force_authenticate(user=self.instructor)
response = self.client.post(self.url, {
'email_or_username': learner.email,
'mode': 'verified',
})
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

def test_invalid_mode_returns_400(self):
learner = UserFactory()
CourseEnrollmentFactory(user=learner, course_id=self.course.id, mode='audit')
self.client.force_authenticate(user=self.instructor)
response = self.client.post(self.url, {
'email_or_username': learner.email,
'mode': 'nonexistent_mode',
})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('Invalid mode', response.data['error'])

def test_same_mode_returns_400(self):
learner = UserFactory()
CourseEnrollmentFactory(user=learner, course_id=self.course.id, mode='audit')
self.client.force_authenticate(user=self.instructor)
response = self.client.post(self.url, {
'email_or_username': learner.email,
'mode': 'audit',
})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('already enrolled', response.data['error'])

def test_successful_mode_change(self):
learner = UserFactory()
CourseEnrollmentFactory(user=learner, course_id=self.course.id, mode='audit')
self.client.force_authenticate(user=self.instructor)
response = self.client.post(self.url, {
'email_or_username': learner.email,
'mode': 'verified',
})
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = response.data
self.assertEqual(data['old_mode'], 'audit')
self.assertEqual(data['new_mode'], 'verified')
self.assertEqual(data['course_id'], str(self.course.id))
self.assertEqual(data['email_or_username'], learner.email)

# Verify the enrollment was actually changed
enrollment = CourseEnrollment.get_enrollment(learner, self.course.id)
self.assertEqual(enrollment.mode, 'verified')

def test_audit_trail_created(self):
learner = UserFactory()
CourseEnrollmentFactory(user=learner, course_id=self.course.id, mode='audit')
self.client.force_authenticate(user=self.instructor)
response = self.client.post(self.url, {
'email_or_username': learner.email,
'mode': 'verified',
'reason': 'Student upgraded',
})
self.assertEqual(response.status_code, status.HTTP_200_OK)

enrollment = CourseEnrollment.get_enrollment(learner, self.course.id)
audit = ManualEnrollmentAudit.objects.filter(enrollment=enrollment).first()
self.assertIsNotNone(audit)
self.assertEqual(audit.enrolled_email, learner.email)
self.assertIn('mode changed from audit to verified', audit.state_transition)
self.assertEqual(audit.reason, 'Student upgraded')

def test_lookup_by_username(self):
learner = UserFactory()
CourseEnrollmentFactory(user=learner, course_id=self.course.id, mode='audit')
self.client.force_authenticate(user=self.instructor)
response = self.client.post(self.url, {
'email_or_username': learner.username,
'mode': 'verified',
})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['new_mode'], 'verified')

def test_staff_can_access(self):
staff = StaffFactory(course_key=self.course.id)
learner = UserFactory()
CourseEnrollmentFactory(user=learner, course_id=self.course.id, mode='audit')
self.client.force_authenticate(user=staff)
response = self.client.post(self.url, {
'email_or_username': learner.email,
'mode': 'verified',
})
self.assertEqual(response.status_code, status.HTTP_200_OK)
10 changes: 10 additions & 0 deletions lms/djangoapps/instructor/views/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@
api_v2.CourseEnrollmentsView.as_view(),
name='course_enrollments'
),
re_path(
rf'^courses/{COURSE_ID_PATTERN}/enrollment_status$',
api_v2.EnrollmentStatusView.as_view(),
name='enrollment_status'
),
re_path(
rf'^courses/{COURSE_ID_PATTERN}/change_enrollment_mode$',
api_v2.ChangeEnrollmentModeView.as_view(),
name='change_enrollment_mode'
),
]

urlpatterns = [
Expand Down
Loading
Loading